mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-14 15:11:32 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6920163026 | ||
|
|
e2dcf9a1f3 | ||
|
|
d2265e4a0d | ||
|
|
3e9f12e3ac | ||
|
|
2fec33b952 | ||
|
|
f8b87f7865 | ||
|
|
e192c5a94c | ||
|
|
c529fc15a8 | ||
|
|
25977a7320 | ||
|
|
c10f1a3e92 | ||
|
|
d8a78bae03 |
@@ -2,12 +2,27 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- device-framework
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
documentation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: bash -n scripts/*.sh
|
||||||
|
- run: ./scripts/check-docs.sh
|
||||||
|
|
||||||
compile-tests:
|
compile-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
@@ -20,4 +35,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
- run: python -m pip install --upgrade platformio==6.1.19
|
- run: python -m pip install --upgrade platformio==6.1.19
|
||||||
- run: ./scripts/compile-check.sh --platform ${{ matrix.environment }}
|
- run: ./scripts/test.sh compile --platform ${{ matrix.environment }}
|
||||||
|
- run: ./scripts/test.sh examples --platform ${{ matrix.environment }}
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
- run: python -m pip install --upgrade platformio==6.1.19
|
- 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/prepare-release.sh "$GITHUB_REF_NAME"
|
||||||
- run: gh release create "$GITHUB_REF_NAME" --generate-notes --title "$GITHUB_REF_NAME"
|
- run: ./scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/release-notes.md"
|
||||||
|
- run: >-
|
||||||
|
gh release create "$GITHUB_REF_NAME"
|
||||||
|
--title "WiFiManager $GITHUB_REF_NAME"
|
||||||
|
--notes-file "$RUNNER_TEMP/release-notes.md"
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
|||||||
+32
-1
@@ -1,10 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 3.0.3
|
## 3.2.2
|
||||||
|
|
||||||
|
- Align the direct ESP8266 and ESP32 test environments with the packaged DFTE 1.2.0 dependency.
|
||||||
|
|
||||||
|
## 3.2.1
|
||||||
|
|
||||||
|
- Ensure externally supplied station profiles remain authoritative on ESP32 by clearing stale SDK-owned credentials before opening the portal for an empty or invalid external profile set.
|
||||||
|
- Pin DFTE 1.2.0, the tested template-engine release used by the maintained portal.
|
||||||
|
|
||||||
|
## 3.2.0
|
||||||
|
|
||||||
|
- Refine the device-hosted portal with a startup Wi-Fi scan, stable loading overlays, clearer connection progress, and a resettable configuration timeout.
|
||||||
|
- Keep a successful portal-to-station hand-off reachable until the browser acknowledges its redirect, with a bounded fallback for captive or headless clients.
|
||||||
|
- Rename the presentation field `homeIntro` to `tagline` so portal identity and wording are clearer; update portal bootstrap contract to v3.
|
||||||
|
- Add focused Basic Portal, Branded Portal, Custom Portal Content, and Station Profiles examples for ESP8266 and ESP32, with real-hardware README captures.
|
||||||
|
- Strengthen release CI with documentation and clean-consumer/example compilation checks for both supported targets.
|
||||||
|
|
||||||
|
## 3.1.0
|
||||||
|
|
||||||
|
- Add an opt-in primary/fallback station-profile controller with bounded failover, reconnection, a durable consumer-supplied store, and profile-aware portal APIs. DeviceFramework uses this to persist verified WiFi profiles transactionally.
|
||||||
|
- Add portable portal branding and presentation hooks, including theme-aware shell and template rendering, without requiring DeviceFramework.
|
||||||
|
- Pin ESP32 tests to the Arduino 3-compatible pioarduino platform release and resolve DFTE 1.1.0.
|
||||||
|
|
||||||
|
## 3.0.6
|
||||||
|
|
||||||
|
- Correct async PlatformIO dependency owners to the registry's canonical lowercase identity, so a clean consumer builds WiFiManager and its ESP8266/ESP32 transport dependencies without duplicating them in `lib_deps`. Remove the superseded include-path bridge.
|
||||||
|
|
||||||
|
## 3.0.5
|
||||||
|
|
||||||
|
- Add a library-owned PlatformIO bridge for the asynchronous web/TCP include paths required when WiFiManager is nested beneath DeviceFramework.
|
||||||
|
|
||||||
## 3.0.4
|
## 3.0.4
|
||||||
|
|
||||||
- Set the package's deep PlatformIO dependency-discovery mode so WiFiManager's asynchronous web/TCP children remain available when it is consumed through DeviceFramework.
|
- Set the package's deep PlatformIO dependency-discovery mode so WiFiManager's asynchronous web/TCP children remain available when it is consumed through DeviceFramework.
|
||||||
|
|
||||||
|
## 3.0.3
|
||||||
|
|
||||||
- Declare the ESP8266 and ESP32 async TCP libraries explicitly in the PlatformIO package manifest so clean consumers resolve the headers required by the asynchronous portal.
|
- Declare the ESP8266 and ESP32 async TCP libraries explicitly in the PlatformIO package manifest so clean consumers resolve the headers required by the asynchronous portal.
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
# WiFiManager
|
# WiFiManager
|
||||||
|
|
||||||
This repository is a **breaking fork** of upstream [`tzapu/WiFiManager`](https://github.com/tzapu/WiFiManager).
|
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.
|
||||||
It is not a drop-in replacement for upstream behavior, APIs, templates, or portal customization patterns.
|
|
||||||
|
|
||||||
If you are evaluating this fork, assume that core web-portal architecture has changed and review the code before adopting it in an existing upstream-based project.
|
## See it on real hardware
|
||||||
|
|
||||||
**This repository:** [alexhopeoconnor/WiFiManager](https://github.com/alexhopeoconnor/WiFiManager)
|
| Brand the built-in portal | Combine Wi-Fi and application setup |
|
||||||
**Upstream:** [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager)
|
| --- | --- |
|
||||||
|
|  |  |
|
||||||
|
| Give each product its own title, identity, icon, and color theme without copying portal HTML. | Show live nearby networks and collect application values, such as an MQTT broker, in the same setup flow. |
|
||||||
|
|
||||||
## Basic provisioning
|
## Start with a working portal
|
||||||
|
|
||||||
For a standalone Arduino project, create one long-lived `WiFiManager` and call
|
|
||||||
`autoConnect` during setup. It first tries saved station credentials; when that
|
|
||||||
fails it starts the configuration portal with the supplied AP name and password.
|
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
@@ -22,256 +19,73 @@ WiFiManager wifi;
|
|||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
wifi.setConfigPortalTimeout(180); // seconds; 0 leaves it open
|
wifi.setConfigPortalTimeout(180);
|
||||||
|
wifi.autoConnect("Device Setup", "change-me");
|
||||||
if (!wifi.autoConnect("Device Setup", "change-me")) {
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Normal application work after WiFi is connected.
|
wifi.process();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The AP password must meet the Wi-Fi password requirements. For applications
|
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.
|
||||||
built on DeviceFramework, configure `setConfigDevicePassword(...)` instead:
|
|
||||||
DeviceFramework passes the same validated password to WiFiManager, Arduino OTA,
|
|
||||||
HTTP Basic authentication, and WebSerial.
|
|
||||||
|
|
||||||
|
## Make it yours
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
This fork intentionally modernizes and restructures the configuration portal.
|
|
||||||
Notable differences from upstream include:
|
|
||||||
|
|
||||||
- The portal now serves a **single HTML shell** from `GET /`.
|
|
||||||
- Client navigation is handled as a **SPA with hash routing**.
|
|
||||||
- Interactive behavior is exposed through **JSON APIs under `/api/...`** plus firmware upload at **`POST /u`**.
|
|
||||||
- Legacy multi-page portal routes and legacy root-template override paths have been removed.
|
|
||||||
- The portal rendering pipeline is built around **DFTE** instead of upstream's monolithic HTML string assembly.
|
|
||||||
- JSON endpoints are expected to be **data-first**, not derived from generated HTML fragments.
|
|
||||||
|
|
||||||
## Current Improvements And Modernization
|
|
||||||
|
|
||||||
This fork currently includes the following architectural improvements:
|
|
||||||
|
|
||||||
- A **single-shell portal architecture** with embedded bootstrap JSON and embedded application JS.
|
|
||||||
- A **SPA-based configuration UI** for WiFi setup, parameters, info views, device actions, and OTA flow.
|
|
||||||
- A **clean API surface** for WiFi scanning, WiFi save, parameters, info, status, restart, erase, portal exit, and captive-portal close behavior.
|
|
||||||
- **Captive portal redirect handling** retained while removing duplicate legacy UI architecture.
|
|
||||||
- **Data-first JSON generation** for portal APIs, including info/device/about data, instead of HTML-to-JSON parsing.
|
|
||||||
- **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state.
|
|
||||||
- **Portal bootstrap contract v2**: nested `brand`, `context`, `pages`, `actions`, `layout`, `extraHomeCards`; Wi-Fi meta params include `kind` (`field` | `html`) for first-class custom HTML parameters.
|
|
||||||
- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows.
|
|
||||||
- **Connect-on-save handoff**: when portal save triggers a station connect, `/api/wifi/connect-status` now reports a brief success state with `stationIp` and `redirectUrl` so the SPA can show the new address and redirect before the AP shuts down.
|
|
||||||
- **Request-scoped shell rendering**: the root portal page is built for each `GET /` from `WM_ROOT_SHELL_TEMPLATE` using a fresh placeholder registry. Shell inputs are `%PAGE_TITLE%`, `%STYLES%`, `%BOOTSTRAP_JSON%`, `%PORTAL_APP_JS%`, and `%PORTAL_APPEND_JS%` — filled in `WiFiManagerHandlers` from WiFiManager state and embedded assets (not from a server-wide template registry).
|
|
||||||
- **Customization via WiFiManager `portal*` APIs** (`portalSetBrandTitle`, `portalSetPageInfoVisible`, `portalSetLayoutParamsLocation`, `portalAddParameter`, asset hooks, etc.) and JSON under `/api/...`, not by exposing placeholder-registry mutation to consumers.
|
|
||||||
- A clearer separation between:
|
|
||||||
- shell rendering (handlers + SPA bootstrap)
|
|
||||||
- JSON API responses
|
|
||||||
- captive portal behavior
|
|
||||||
- OTA handling
|
|
||||||
- Updated tests focused on the **shell contract**, **bootstrap payloads**, and **API JSON shapes** rather than removed legacy portal pages.
|
|
||||||
|
|
||||||
## Portal Customization Boundary
|
|
||||||
|
|
||||||
Stable, supported portal customization is intentionally scoped:
|
|
||||||
|
|
||||||
- `portalSetBrand*` for title, intro text, and logo SVG, plus `portalSetContextIdentityText(...)` for the user-facing runtime identity string on the home view.
|
|
||||||
- `portalSetPage*`, `portalSetAction*`, `portalSetLayout*`, and `portalSetBehavior*` for built-in portal capabilities and runtime behavior.
|
|
||||||
- `portalAddParameter(...)` for first-class custom parameters, including raw HTML blocks inside parameter-rendering surfaces (`#/wifi` or `#/setup`).
|
|
||||||
- `portalAddInfoSection(...)` and `portalAddHomeCard(...)` for structured extra content rendered by the built-in SPA.
|
|
||||||
- `portalAppendCss(...)`, `portalOverrideCss(...)`, and `portalAppendJs(...)` for light theming and enhancement hooks.
|
|
||||||
|
|
||||||
Not part of the stable API:
|
|
||||||
|
|
||||||
- arbitrary HTML injection into home/info/nav/shell
|
|
||||||
- replacing built-in SPA routing or action flow
|
|
||||||
- depending on undocumented DOM IDs or route internals
|
|
||||||
- treating `include/templates/*` as a supported consumer override surface
|
|
||||||
|
|
||||||
Appended JS should enhance rather than replace the built-in SPA. The documented hook contract is:
|
|
||||||
|
|
||||||
- `wm:ready` with `detail.boot`
|
|
||||||
- `wm:view-changed` with `detail.route`
|
|
||||||
|
|
||||||
If a consumer needs custom live widgets, new primary navigation concepts, or new backend-to-frontend workflows, that is considered **fork territory** rather than portal customization.
|
|
||||||
|
|
||||||
## Portal Customization Example
|
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
WiFiManager wm;
|
const char kTitle[] PROGMEM = "Set up Temperature Monitor";
|
||||||
|
const char kBrand[] PROGMEM = "Example Devices";
|
||||||
|
const char kAccent[] PROGMEM = "#347a45";
|
||||||
|
|
||||||
wm.portalSetBrandTitle("Solar Battery Monitor Setup");
|
void setup() {
|
||||||
wm.portalSetContextIdentityText("Solar Battery Monitor");
|
WiFiManagerPortalConfig portalUI;
|
||||||
wm.portalSetBrandHomeIntro(
|
|
||||||
"Connect your monitor to WiFi, then review battery and inverter settings."
|
|
||||||
);
|
|
||||||
wm.portalSetBrandLogoSvg(
|
|
||||||
"<svg viewBox='0 0 24 24' aria-hidden='true'>"
|
|
||||||
"<path d='M12 2L4 12h5l-1 10 8-10h-5z'></path>"
|
|
||||||
"</svg>"
|
|
||||||
);
|
|
||||||
|
|
||||||
wm.portalSetPageInfoVisible(true);
|
// Leave fields unset to keep WiFiManager's built-in portal values.
|
||||||
wm.portalSetPageUpdateVisible(false);
|
portalUI.title = WiFiManagerPortalText::progmem(kTitle);
|
||||||
wm.portalSetActionEraseVisible(false);
|
portalUI.identityText = WiFiManagerPortalText::progmem(kBrand);
|
||||||
wm.portalSetActionRestartVisible(true);
|
|
||||||
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
|
|
||||||
|
|
||||||
wm.portalSetBehaviorCaptivePortalEnabled(true);
|
// Theme values customise the built-in styles; they do not replace the portal.
|
||||||
wm.portalSetBehaviorConnectOnSave(true);
|
portalUI.theme.accent = WiFiManagerPortalText::progmem(kAccent);
|
||||||
wm.portalSetBehaviorExitAllowed(true);
|
portalUI.theme.cornerRadiusPx = 10;
|
||||||
|
|
||||||
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Masked);
|
wifi.setPortalConfig(portalUI); // Apply before the portal starts.
|
||||||
wm.portalSetFieldStaticIpVisibility(PortalFieldVisibility::Auto);
|
wifi.autoConnect("Device Setup");
|
||||||
wm.portalSetFieldStaticDnsVisibility(PortalFieldVisibility::Auto);
|
}
|
||||||
|
|
||||||
WiFiManagerParameter mqttHost(
|
void loop() {
|
||||||
"mqtt_host",
|
wifi.process();
|
||||||
"MQTT host",
|
|
||||||
"broker.local",
|
|
||||||
64,
|
|
||||||
"placeholder='broker.local'"
|
|
||||||
);
|
|
||||||
wm.portalAddParameter(&mqttHost);
|
|
||||||
|
|
||||||
// Raw HTML remains first-class for custom parameters, but only inside the
|
|
||||||
// parameter-rendering surfaces (#/wifi or #/setup), not arbitrary portal regions.
|
|
||||||
WiFiManagerParameter gpsHelp(
|
|
||||||
"<div class='wm-callout wm-callout--info'>"
|
|
||||||
"<p>GPS options below apply only when a GPS module is connected.</p>"
|
|
||||||
"</div>"
|
|
||||||
);
|
|
||||||
wm.portalAddParameter(&gpsHelp);
|
|
||||||
|
|
||||||
PortalInfoSection battery;
|
|
||||||
battery.id = "battery";
|
|
||||||
battery.title = "Battery";
|
|
||||||
battery.items.push_back({"soc", "State of charge", "84%"});
|
|
||||||
battery.items.push_back({"voltage", "Voltage", "13.2V"});
|
|
||||||
wm.portalAddInfoSection(battery);
|
|
||||||
|
|
||||||
PortalHomeCard solar;
|
|
||||||
solar.id = "solar";
|
|
||||||
solar.title = "Solar summary";
|
|
||||||
solar.kind = PortalHomeCardKind::KeyValue;
|
|
||||||
solar.items.push_back({"pv", "PV input", "420W"});
|
|
||||||
solar.items.push_back({"load", "Load", "180W"});
|
|
||||||
wm.portalAddHomeCard(solar);
|
|
||||||
|
|
||||||
wm.portalAppendCss(
|
|
||||||
".wm-brand-logo svg{width:40px;height:40px;display:block;}"
|
|
||||||
".wm-hero-intro{max-width:42ch;}"
|
|
||||||
);
|
|
||||||
|
|
||||||
wm.portalAppendJs(
|
|
||||||
"document.addEventListener('wm:ready', function(e){"
|
|
||||||
" console.log('Portal booted', e.detail.boot);"
|
|
||||||
"});"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## WiFi Connect Status API
|
|
||||||
|
|
||||||
When `portalSetBehaviorConnectOnSave(true)` is enabled, saving WiFi credentials queues a station join and the SPA polls `GET /api/wifi/connect-status`.
|
|
||||||
|
|
||||||
Response shape:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"state": "waiting | success | failed",
|
|
||||||
"message": "human readable status",
|
|
||||||
"wifiStatus": "WL_CONNECTED",
|
|
||||||
"stationIp": "192.168.1.42",
|
|
||||||
"redirectUrl": "http://192.168.1.42/"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
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.
|
||||||
|
|
||||||
- `stationIp` and `redirectUrl` are present only after a successful station join.
|
## What it provides
|
||||||
- If the portal HTTP server is not on port `80`, `redirectUrl` includes the active port.
|
|
||||||
- On success, WiFiManager keeps the portal alive briefly so the client can read the success payload and navigate to the new device address before the captive AP is shut down.
|
|
||||||
- This improves the handoff on typical home networks, but it is not a universal guarantee: client captive-portal helpers, browser behavior, DHCP timing, and network isolation can still affect whether the redirect completes automatically.
|
|
||||||
|
|
||||||
## Dependencies
|
- **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.
|
||||||
|
|
||||||
This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**.
|
## Choose a guide
|
||||||
|
|
||||||
The published `library.json` resolves DFTE from the maintained, pinned
|
| Goal | Guide |
|
||||||
`v1.0.2` Git tag. No sibling checkout is required to consume WiFiManager.
|
| --- | --- |
|
||||||
When developing both libraries together, use a local `symlink://` or
|
| Brand the portal or add structured built-in content | [Portal UI](docs/PORTAL_UI.md) |
|
||||||
`file://` dependency in your own ignored PlatformIO override.
|
| 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 or flash a complete example | [Examples](examples/README.md) |
|
||||||
|
| Contribute to this fork or prepare a release | [Development](docs/DEVELOPMENT.md) |
|
||||||
|
|
||||||
|
## Install
|
||||||
## Installation
|
|
||||||
|
|
||||||
Use the release tag and list the asynchronous web/TCP packages at project level.
|
|
||||||
PlatformIO installs the manifest dependencies automatically, but its dependency
|
|
||||||
finder requires these headers to be direct project dependencies before it adds
|
|
||||||
their include paths:
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[common]
|
[common]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.0.4
|
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.2
|
||||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
|
||||||
|
|
||||||
[env:esp8266]
|
|
||||||
extends = common
|
|
||||||
platform = espressif8266
|
|
||||||
board = d1_mini
|
|
||||||
framework = arduino
|
|
||||||
lib_deps =
|
|
||||||
${common.lib_deps}
|
|
||||||
ESP32Async/ESPAsyncTCP@2.0.0
|
|
||||||
|
|
||||||
[env:esp32]
|
|
||||||
extends = common
|
|
||||||
lib_deps =
|
|
||||||
platform = espressif32
|
|
||||||
board = esp32dev
|
|
||||||
framework = arduino
|
|
||||||
${common.lib_deps}
|
|
||||||
ESP32Async/AsyncTCP@3.4.9
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The text after `#` is a Git ref. PlatformIO clones the repository and checks
|
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.
|
||||||
out that tag; it does not download a GitHub Release asset. A tag gives a
|
|
||||||
reproducible library input. Use a local checkout only while actively changing
|
|
||||||
the library:
|
|
||||||
|
|
||||||
```ini
|
See the [documentation index](docs/README.md), [examples](examples/README.md), [release history](CHANGELOG.md), and [licence](LICENSE).
|
||||||
lib_deps =
|
|
||||||
WiFiManager=file:///path/to/WiFiManager
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tests and releases
|
|
||||||
|
|
||||||
The CI workflow compiles a minimal clean consumer project for both supported
|
|
||||||
targets. These commands need no connected board:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/compile-check.sh --platform esp8266
|
|
||||||
./scripts/compile-check.sh --platform esp32
|
|
||||||
```
|
|
||||||
|
|
||||||
Before publishing a version, update `library.json`, `CHANGELOG.md`, and this
|
|
||||||
README, run both checks, then create the annotated tag:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
|
|
||||||
```
|
|
||||||
|
|
||||||
Push the branch and tag. GitHub Actions validates the PlatformIO package again
|
|
||||||
and creates the GitHub Release from that tag.
|
|
||||||
|
|
||||||
## AI Assistance Notice
|
|
||||||
|
|
||||||
Parts of this codebase have been developed and refactored with the aid of AI coding agents under human direction and review.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
See [LICENSE](LICENSE).
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Development and releases
|
||||||
|
|
||||||
|
Released consumers use the public Git tag. While changing WiFiManager and a sibling dependency together, point an ignored local PlatformIO override at a `symlink://` or `file://` checkout rather than changing tracked application dependencies.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
lib_deps =
|
||||||
|
WiFiManager=symlink:///path/to/WiFiManager
|
||||||
|
```
|
||||||
|
|
||||||
|
The ESP32 environments pin the PlatformIO-compatible pioarduino 51.03.05
|
||||||
|
platform package, which packages official Arduino-ESP32 3.0.5. This avoids the
|
||||||
|
known six-second asynchronous scan failure in the older 2.0.17 framework. Core
|
||||||
|
3 also requires the `SOC_WIFI_SUPPORTED`, `Network/src`, and ESP8266-transport
|
||||||
|
ignore settings shown in this repository `platformio.ini`; keep those settings
|
||||||
|
when adding an ESP32 environment.
|
||||||
|
|
||||||
|
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
|
||||||
|
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
|
||||||
|
```
|
||||||
|
|
||||||
|
Push the branch and annotated tag. GitHub Actions repeats the board-free compile checks, validates the package, and creates a GitHub Release using that version’s changelog section. The workflow does not publish to the PlatformIO Registry.
|
||||||
|
|
||||||
|
Back to [documentation](README.md) · [project overview](../README.md).
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Getting started
|
||||||
|
|
||||||
|
WiFiManager owns station credential recovery and a temporary configuration portal. Keep one instance for the life of the application.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
|
WiFiManager wifi;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
wifi.setConfigPortalTimeout(180);
|
||||||
|
wifi.autoConnect("Example Setup", "change-me");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
wifi.process(); // Keeps the portal responsive when setup is needed.
|
||||||
|
// Normal work begins after autoConnect succeeds.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`autoConnect()` first tries stored station credentials. If that cannot connect, it starts the AP and portal with the supplied name and password, then returns `false`; call `process()` from `loop()` so that portal can run. Use a Wi-Fi-valid AP password.
|
||||||
|
|
||||||
|
## PlatformIO dependency
|
||||||
|
|
||||||
|
```ini
|
||||||
|
lib_deps =
|
||||||
|
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.2
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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).
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Portal API
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## WiFi connect status
|
||||||
|
|
||||||
|
When `portalSetBehaviorConnectOnSave(true)` is enabled, saving credentials queues a station join. The SPA polls `GET /api/wifi/connect-status`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": "waiting | success | failed",
|
||||||
|
"message": "human readable status",
|
||||||
|
"wifiStatus": "WL_CONNECTED",
|
||||||
|
"stationIp": "192.168.1.42",
|
||||||
|
"redirectUrl": "http://192.168.1.42/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`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, 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
|
||||||
|
|
||||||
|
- Use JSON endpoint results for state and actions.
|
||||||
|
- Keep UI-visible capabilities in the portal bootstrap/API payloads.
|
||||||
|
- Add a documented endpoint contract before adding a new portal workflow.
|
||||||
|
- Do not derive state by parsing the rendered shell.
|
||||||
|
|
||||||
|
Back to [documentation](README.md) · [project overview](../README.md).
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Portal UI
|
||||||
|
|
||||||
|
`WiFiManagerPortalConfig` is the supported presentation API for the built-in provisioning portal. It changes identity and semantic visual tokens only. WiFiManager continues to own portal routes, forms, navigation, captive behavior, reset, and OTA views.
|
||||||
|
|
||||||
|
Apply the configuration before `autoConnect()`, `startConfigPortal()`, or `startWebPortal()`. The configuration is non-owning: text and SVG data must have static firmware lifetime and declare whether it is in RAM or PROGMEM. WiFiManager locks presentation after a portal starts, so active asynchronous responses cannot observe a partial configuration.
|
||||||
|
|
||||||
|
## Standalone branded portal
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
|
WiFiManager wifi;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char kTitle[] PROGMEM = "Set up Temperature Monitor";
|
||||||
|
const char kIdentity[] PROGMEM = "Example Devices";
|
||||||
|
const char kTagline[] PROGMEM = "Reliable setup for connected devices.";
|
||||||
|
const char kLogoAlt[] PROGMEM = "Example Devices";
|
||||||
|
const char kLogo[] PROGMEM = R"svg(<svg viewBox="0 0 64 64"><circle cx="32" cy="32" r="28"/></svg>)svg";
|
||||||
|
const char kPage[] PROGMEM = "#f4f7f3";
|
||||||
|
const char kSurface[] PROGMEM = "#ffffff";
|
||||||
|
const char kAccent[] PROGMEM = "#347a45";
|
||||||
|
const char kAccentText[] PROGMEM = "#ffffff";
|
||||||
|
|
||||||
|
WiFiManagerPortalConfig kPortalUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Set only the identity values this product needs.
|
||||||
|
kPortalUI.title = WiFiManagerPortalText::progmem(kTitle);
|
||||||
|
kPortalUI.identityText = WiFiManagerPortalText::progmem(kIdentity);
|
||||||
|
kPortalUI.tagline = WiFiManagerPortalText::progmem(kTagline);
|
||||||
|
kPortalUI.logo = WiFiManagerPortalAsset::svgFromProgmem(kLogo);
|
||||||
|
kPortalUI.logoAltText = WiFiManagerPortalText::progmem(kLogoAlt);
|
||||||
|
|
||||||
|
// Unset theme values retain the built-in style.
|
||||||
|
kPortalUI.theme.pageBackground = WiFiManagerPortalText::progmem(kPage);
|
||||||
|
kPortalUI.theme.surface = WiFiManagerPortalText::progmem(kSurface);
|
||||||
|
kPortalUI.theme.accent = WiFiManagerPortalText::progmem(kAccent);
|
||||||
|
kPortalUI.theme.accentText = WiFiManagerPortalText::progmem(kAccentText);
|
||||||
|
kPortalUI.theme.cornerRadiusPx = 10;
|
||||||
|
kPortalUI.theme.smallCornerRadiusPx = 6;
|
||||||
|
|
||||||
|
wifi.setPortalConfig(kPortalUI); // Configure before the portal starts.
|
||||||
|
wifi.autoConnect("Temperature Monitor");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() { wifi.process(); }
|
||||||
|
```
|
||||||
|
|
||||||
|
The complete buildable example is [BrandedPortal](../examples/BrandedPortal/BrandedPortal.ino). The compile fixture exercises this API on ESP8266 and ESP32.
|
||||||
|
|
||||||
|
## Configuration reference
|
||||||
|
|
||||||
|
Leave a text or colour value empty, or a radius at `0`, to retain the built-in stylesheet value. The default title is `WiFiManager`.
|
||||||
|
|
||||||
|
| Field | Used by |
|
||||||
|
| --- | --- |
|
||||||
|
| `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 |
|
||||||
|
| `success`, `danger`, `dangerHover` | Status and destructive actions |
|
||||||
|
| `cornerRadiusPx`, `smallCornerRadiusPx` | Card and compact-control corners, limited to 64 px |
|
||||||
|
|
||||||
|
Theme values accept only simple semantic CSS value syntax and are emitted once into a small `#wm-portal-theme` block. This is deliberately not a raw CSS or JavaScript injection API. An SVG is a trusted compiled firmware asset, never form, MQTT, or network input.
|
||||||
|
|
||||||
|
## Built-in portal capabilities
|
||||||
|
|
||||||
|
Presentation uses one configuration route: `setPortalConfig()`. Existing structured portal capabilities remain separate: `portalAddParameter()`, `portalAddInfoSection()`, `portalAddHomeCard()`, page visibility, and portal behavior configure documented built-in functionality rather than private markup. See [Portal API](PORTAL_API.md) and [Station profiles](STATION_PROFILES.md).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
Back to the [documentation index](README.md) or [project overview](../README.md).
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# WiFiManager documentation
|
||||||
|
|
||||||
|
| 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 or flash a complete example | [Examples](../examples/README.md) |
|
||||||
|
| Work on this fork or make a release | [Development and releases](DEVELOPMENT.md) |
|
||||||
|
|
||||||
|
Back to the [project overview](../README.md).
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Station profiles
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
A profile set has exactly two fixed slots:
|
||||||
|
|
||||||
|
- **Primary** (slot 0) is required whenever the controller is enabled.
|
||||||
|
- **Fallback** (slot 1) is optional.
|
||||||
|
- The last successful slot is tried first on the next connection cycle, then the remaining enabled slot.
|
||||||
|
|
||||||
|
The controller never treats the ESP SDK's saved single network as an additional source of truth. It begins one bounded connection attempt at a time, moves to the fallback after failure, and retries both profiles after a temporary loss of a previously working connection. If a new device has no valid profiles, it opens the normal configuration portal.
|
||||||
|
|
||||||
|
A candidate submitted by the portal is only committed after it connects and receives a usable IP address. A failed candidate leaves the last saved profile set intact.
|
||||||
|
|
||||||
|
## Direct WiFiManager use
|
||||||
|
|
||||||
|
Implement a small store appropriate to the application. The manager neither allocates nor owns it:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class MyProfileStore final : public WiFiManagerStationProfileStore {
|
||||||
|
public:
|
||||||
|
bool load(WiFiManagerStationProfiles& profiles) override;
|
||||||
|
bool save(const WiFiManagerStationProfiles& profiles) override;
|
||||||
|
bool clear() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
WiFiManager wifi;
|
||||||
|
MyProfileStore profiles;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
wifi.setStationProfileStore(&profiles);
|
||||||
|
wifi.setStationRecoveryInterval(30000);
|
||||||
|
wifi.startStationConnection("Example Setup", "setup-password");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
wifi.process();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The store must return a complete `WiFiManagerStationProfiles` value. Each enabled profile has a NUL-terminated SSID of at most 32 characters and an optional NUL-terminated password of at most 64 characters. Keep slot 0 enabled; set `hasPassword = false` for an open network.
|
||||||
|
|
||||||
|
To stage profiles supplied by another subsystem, use `startStationCandidate(candidate)`. It connects the primary/fallback set first and calls the store only after success. Use `saveStationProfiles(profiles)` only when deliberately saving without a connection check. `clearStationProfiles()` clears the supplied store and disconnects the station.
|
||||||
|
|
||||||
|
## Portal contract
|
||||||
|
|
||||||
|
In profile mode, the existing Wi-Fi page becomes a two-profile form. It remains driven by the same JSON endpoints:
|
||||||
|
|
||||||
|
- `GET /api/wifi/meta` returns `profiles`, `activeSlot`, and controller `state`.
|
||||||
|
- `POST /api/wifi/save` accepts `s0`/ `p0` for primary and `s1`/ `p1` for fallback. A blank submitted password preserves an existing password; send `clear0` or `clear1` for an intentional open network.
|
||||||
|
- A normal save verifies the candidate by connecting. Poll `GET /api/wifi/connect-status` for its result.
|
||||||
|
- Set `stationAction=save` only to store the submitted profiles for a later connection attempt.
|
||||||
|
|
||||||
|
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. 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).
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Testing
|
||||||
|
|
||||||
|
The clean-consumer check builds a project that declares only WiFiManager. It proves the package manifest resolves DFTE, ESPAsyncWebServer, and the correct ESP8266 or ESP32 TCP dependency without a sibling checkout or attached board. The runner removes a previous local package link before each
|
||||||
|
check, so dependency resolution uses the current manifest rather than a stale
|
||||||
|
`.pio` copy.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/test.sh compile --platform esp8266
|
||||||
|
./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 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,17 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
|
WiFiManager portal;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
portal.setConfigPortalTimeout(180);
|
||||||
|
|
||||||
|
if (portal.autoConnect("WiFiManager Basic", "example-pass")) {
|
||||||
|
Serial.println("Connected. Run your normal application here.");
|
||||||
|
} else {
|
||||||
|
Serial.println("Setup portal started at http://192.168.4.1/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() { portal.process(); }
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
|
WiFiManager wifi;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char kPortalTitle[] PROGMEM = "Set up Temperature Monitor";
|
||||||
|
const char kPortalIdentity[] PROGMEM = "Example Devices";
|
||||||
|
const char kPortalTagline[] PROGMEM = "Reliable setup for connected devices.";
|
||||||
|
const char kPortalLogoAlt[] PROGMEM = "Example Devices";
|
||||||
|
const char kExampleLogo[] PROGMEM = "<svg viewBox='0 0 64 64' aria-hidden='true'><circle cx='32' cy='32' r='28' fill='#347a45'/></svg>";
|
||||||
|
const char kPage[] PROGMEM = "#f4f7f3";
|
||||||
|
const char kSurface[] PROGMEM = "#ffffff";
|
||||||
|
const char kText[] PROGMEM = "#1c251e";
|
||||||
|
const char kAccent[] PROGMEM = "#347a45";
|
||||||
|
const char kAccentText[] PROGMEM = "#ffffff";
|
||||||
|
|
||||||
|
WiFiManagerPortalConfig kPortalUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
// Brand the built-in portal; unassigned fields retain their default values.
|
||||||
|
kPortalUI.title = WiFiManagerPortalText::progmem(kPortalTitle);
|
||||||
|
kPortalUI.identityText = WiFiManagerPortalText::progmem(kPortalIdentity);
|
||||||
|
kPortalUI.tagline = WiFiManagerPortalText::progmem(kPortalTagline);
|
||||||
|
kPortalUI.logo = WiFiManagerPortalAsset::svgFromProgmem(kExampleLogo);
|
||||||
|
kPortalUI.logoAltText = WiFiManagerPortalText::progmem(kPortalLogoAlt);
|
||||||
|
|
||||||
|
// Override only the theme tokens that define this product's appearance.
|
||||||
|
kPortalUI.theme.pageBackground = WiFiManagerPortalText::progmem(kPage);
|
||||||
|
kPortalUI.theme.surface = WiFiManagerPortalText::progmem(kSurface);
|
||||||
|
kPortalUI.theme.text = WiFiManagerPortalText::progmem(kText);
|
||||||
|
kPortalUI.theme.accent = WiFiManagerPortalText::progmem(kAccent);
|
||||||
|
kPortalUI.theme.accentText = WiFiManagerPortalText::progmem(kAccentText);
|
||||||
|
kPortalUI.theme.cornerRadiusPx = 10;
|
||||||
|
kPortalUI.theme.smallCornerRadiusPx = 6;
|
||||||
|
|
||||||
|
if (!wifi.setPortalConfig(kPortalUI)) {
|
||||||
|
Serial.println("Portal UI configuration was rejected");
|
||||||
|
}
|
||||||
|
wifi.setConfigPortalTimeout(180);
|
||||||
|
wifi.autoConnect("Temperature Monitor");
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* WiFiManager.h
|
* WiFiManager.h
|
||||||
*
|
*
|
||||||
* WiFiManager, a library for the ESP8266/Arduino platform
|
* WiFiManager, a library for the ESP8266/Arduino platform
|
||||||
* for configuration of WiFi credentials using a Captive Portal
|
* for configuration of WiFi credentials using a Captive Portal
|
||||||
*
|
*
|
||||||
* @author Creator tzapu
|
* @author Creator tzapu
|
||||||
* @author tablatronix
|
* @author tablatronix
|
||||||
* @author alexhopeoconnor
|
* @author alexhopeoconnor
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include "WiFiManagerParameter.h"
|
#include "WiFiManagerParameter.h"
|
||||||
#include "WiFiManagerLogLevel.h"
|
#include "WiFiManagerLogLevel.h"
|
||||||
@@ -39,13 +40,13 @@
|
|||||||
// #define WM_DFTE_LOGGING // opt-in: bridge DFTE logging into WiFiManager (see README)
|
// #define WM_DFTE_LOGGING // opt-in: bridge DFTE logging into WiFiManager (see README)
|
||||||
// #define WM_MDNS // includes MDNS, also set MDNS with sethostname
|
// #define WM_MDNS // includes MDNS, also set MDNS with sethostname
|
||||||
// #define WM_FIXERASECONFIG // use erase flash fix
|
// #define WM_FIXERASECONFIG // use erase flash fix
|
||||||
// #define WM_ERASE_NVS // esp32 erase(true) will erase NVS
|
// #define WM_ERASE_NVS // esp32 erase(true) will erase NVS
|
||||||
// #define WM_RTC // esp32 info page will include reset reasons
|
// #define WM_RTC // esp32 info page will include reset reasons
|
||||||
|
|
||||||
// #define WIFI_MANAGER_OVERRIDE_STRINGS // build flag for using own strings include
|
// #define WIFI_MANAGER_OVERRIDE_STRINGS // build flag for using own strings include
|
||||||
|
|
||||||
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
|
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
|
||||||
#warning "ARDUINO_ESP8266_RELEASE_2_3_0, some WM features disabled"
|
#warning "ARDUINO_ESP8266_RELEASE_2_3_0, some WM features disabled"
|
||||||
// @todo check failing on platform = espressif8266@1.7.3
|
// @todo check failing on platform = espressif8266@1.7.3
|
||||||
#define WM_NOASYNC // esp8266 no async scan wifi
|
#define WM_NOASYNC // esp8266 no async scan wifi
|
||||||
#define WM_NOCOUNTRY // esp8266 no country
|
#define WM_NOCOUNTRY // esp8266 no country
|
||||||
@@ -91,16 +92,16 @@
|
|||||||
#include <ESP8266mDNS.h>
|
#include <ESP8266mDNS.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define WIFI_getChipId() ESP.getChipId()
|
#define WIFI_getChipId() ESP.getChipId()
|
||||||
#define WM_WIFIOPEN ENC_TYPE_NONE
|
#define WM_WIFIOPEN ENC_TYPE_NONE
|
||||||
|
|
||||||
#elif defined(ESP32)
|
#elif defined(ESP32)
|
||||||
|
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_wifi.h>
|
#include <esp_wifi.h>
|
||||||
#include <Update.h>
|
#include <Update.h>
|
||||||
#include <AsyncTCP.h>
|
#include <AsyncTCP.h>
|
||||||
|
|
||||||
#define WIFI_getChipId() (uint32_t)ESP.getEfuseMac()
|
#define WIFI_getChipId() (uint32_t)ESP.getEfuseMac()
|
||||||
#define WM_WIFIOPEN WIFI_AUTH_OPEN
|
#define WM_WIFIOPEN WIFI_AUTH_OPEN
|
||||||
|
|
||||||
@@ -141,13 +142,46 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
// Include utility functions
|
// Include utility functions
|
||||||
#include "WiFiManagerUtils.h"
|
#include "WiFiManagerUtils.h"
|
||||||
|
#include "WiFiManagerPortalUI.h"
|
||||||
|
|
||||||
|
// A station profile is intentionally fixed-size. ESP Wi-Fi accepts one station
|
||||||
|
// configuration at a time, so multi-network behaviour belongs to the
|
||||||
|
// application/controller rather than the SDK's saved station configuration.
|
||||||
|
constexpr uint8_t WM_STATION_PROFILE_COUNT = 2;
|
||||||
|
constexpr uint8_t WM_NO_STATION_PROFILE = 0xFF;
|
||||||
|
|
||||||
|
struct WiFiManagerStationProfile {
|
||||||
|
bool enabled = false;
|
||||||
|
bool hasPassword = false;
|
||||||
|
char ssid[33] = {};
|
||||||
|
char password[65] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WiFiManagerStationProfiles {
|
||||||
|
WiFiManagerStationProfile slots[WM_STATION_PROFILE_COUNT] = {};
|
||||||
|
uint8_t preferredSlot = 0;
|
||||||
|
uint8_t lastSuccessfulSlot = WM_NO_STATION_PROFILE;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional durable backing for multi-profile station credentials. The manager
|
||||||
|
* owns profile policy and never owns this store. A consumer that needs durable
|
||||||
|
* profiles supplies one; otherwise the profiles remain in RAM.
|
||||||
|
*/
|
||||||
|
class WiFiManagerStationProfileStore {
|
||||||
|
public:
|
||||||
|
virtual bool load(WiFiManagerStationProfiles& profiles) = 0;
|
||||||
|
virtual bool save(const WiFiManagerStationProfiles& candidate) = 0;
|
||||||
|
virtual bool clear() = 0;
|
||||||
|
virtual ~WiFiManagerStationProfileStore() = default;
|
||||||
|
};
|
||||||
|
|
||||||
// prep string concat vars
|
// prep string concat vars
|
||||||
#define WM_STRING2(x) #x
|
#define WM_STRING2(x) #x
|
||||||
#define WM_STRING(x) WM_STRING2(x)
|
#define WM_STRING(x) WM_STRING2(x)
|
||||||
|
|
||||||
// WiFiManager version
|
// WiFiManager version
|
||||||
const char WM_VERSION_STR[] PROGMEM = "v2.0.19";
|
const char WM_VERSION_STR[] PROGMEM = "v3.1.0";
|
||||||
|
|
||||||
// #include <esp_idf_version.h>
|
// #include <esp_idf_version.h>
|
||||||
#ifdef ESP_IDF_VERSION
|
#ifdef ESP_IDF_VERSION
|
||||||
@@ -155,7 +189,7 @@ const char WM_VERSION_STR[] PROGMEM = "v2.0.19";
|
|||||||
// #pragma message "ESP_IDF_VERSION_MINOR = " WM_STRING(ESP_IDF_VERSION_MINOR)
|
// #pragma message "ESP_IDF_VERSION_MINOR = " WM_STRING(ESP_IDF_VERSION_MINOR)
|
||||||
// #pragma message "ESP_IDF_VERSION_PATCH = " WM_STRING(ESP_IDF_VERSION_PATCH)
|
// #pragma message "ESP_IDF_VERSION_PATCH = " WM_STRING(ESP_IDF_VERSION_PATCH)
|
||||||
#define VER_IDF_STR WM_STRING(ESP_IDF_VERSION_MAJOR) "." WM_STRING(ESP_IDF_VERSION_MINOR) "." WM_STRING(ESP_IDF_VERSION_PATCH)
|
#define VER_IDF_STR WM_STRING(ESP_IDF_VERSION_MAJOR) "." WM_STRING(ESP_IDF_VERSION_MINOR) "." WM_STRING(ESP_IDF_VERSION_PATCH)
|
||||||
#else
|
#else
|
||||||
#define VER_IDF_STR "Unknown"
|
#define VER_IDF_STR "Unknown"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -184,7 +218,7 @@ const char WM_VERSION_STR[] PROGMEM = "v2.0.19";
|
|||||||
#define VER_ARDUINO_STR "Unknown"
|
#define VER_ARDUINO_STR "Unknown"
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#else
|
#else
|
||||||
#define VER_ARDUINO_STR "Unknown"
|
#define VER_ARDUINO_STR "Unknown"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -232,10 +266,11 @@ struct PortalHomeCard {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct PortalBrandState {
|
struct PortalBrandState {
|
||||||
String title = "WiFiManager";
|
WiFiManagerPortalText title;
|
||||||
String identityTextOverride;
|
WiFiManagerPortalText identityTextOverride;
|
||||||
String homeIntro;
|
WiFiManagerPortalText tagline;
|
||||||
String logoSvg;
|
WiFiManagerPortalAsset logo;
|
||||||
|
WiFiManagerPortalText logoAltText;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PortalPageState {
|
struct PortalPageState {
|
||||||
@@ -257,12 +292,6 @@ struct PortalLayoutState {
|
|||||||
bool paramsOnWifiPage = true;
|
bool paramsOnWifiPage = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PortalAssetState {
|
|
||||||
String appendedCss;
|
|
||||||
String overriddenCss;
|
|
||||||
String appendedJs;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PortalStructuredExtrasState {
|
struct PortalStructuredExtrasState {
|
||||||
std::vector<PortalInfoSection> infoSections;
|
std::vector<PortalInfoSection> infoSections;
|
||||||
std::vector<PortalHomeCard> homeCards;
|
std::vector<PortalHomeCard> homeCards;
|
||||||
@@ -327,6 +356,27 @@ class WiFiManager
|
|||||||
WM_CP_CONNECT_FAILED,
|
WM_CP_CONNECT_FAILED,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum wm_station_state_t : uint8_t {
|
||||||
|
WM_STATION_IDLE = 0,
|
||||||
|
WM_STATION_LOADING,
|
||||||
|
WM_STATION_ATTEMPTING,
|
||||||
|
WM_STATION_SWITCHING,
|
||||||
|
WM_STATION_CONNECTED,
|
||||||
|
WM_STATION_BACKOFF,
|
||||||
|
WM_STATION_PORTAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct wm_station_status_t {
|
||||||
|
wm_station_state_t state = WM_STATION_IDLE;
|
||||||
|
uint8_t activeSlot = WM_NO_STATION_PROFILE;
|
||||||
|
uint8_t attemptedSlot = WM_NO_STATION_PROFILE;
|
||||||
|
uint8_t configuredProfiles = 0;
|
||||||
|
uint8_t wifiStatus = WL_IDLE_STATUS;
|
||||||
|
bool lastConnectionWasCandidate = false;
|
||||||
|
bool storageSaveFailed = false;
|
||||||
|
String message = "Idle";
|
||||||
|
};
|
||||||
|
|
||||||
/** Optional notification hook; prefer getters for consumers. */
|
/** Optional notification hook; prefer getters for consumers. */
|
||||||
enum wm_event_t : uint8_t {
|
enum wm_event_t : uint8_t {
|
||||||
WM_EVENT_PORTAL_STARTED = 0,
|
WM_EVENT_PORTAL_STARTED = 0,
|
||||||
@@ -334,10 +384,16 @@ class WiFiManager
|
|||||||
WM_EVENT_PORTAL_CONNECT_QUEUED,
|
WM_EVENT_PORTAL_CONNECT_QUEUED,
|
||||||
WM_EVENT_PORTAL_CONNECT_START,
|
WM_EVENT_PORTAL_CONNECT_START,
|
||||||
WM_EVENT_PORTAL_CONNECT_SUCCESS,
|
WM_EVENT_PORTAL_CONNECT_SUCCESS,
|
||||||
WM_EVENT_PORTAL_CONNECT_FAILED
|
WM_EVENT_PORTAL_CONNECT_FAILED,
|
||||||
|
WM_EVENT_STATION_PROFILE_ATTEMPT,
|
||||||
|
WM_EVENT_STATION_PROFILE_CONNECTED,
|
||||||
|
WM_EVENT_STATION_PROFILE_FAILED,
|
||||||
|
WM_EVENT_STATION_LINK_LOST,
|
||||||
|
WM_EVENT_STATION_BACKOFF,
|
||||||
|
WM_EVENT_STATION_PROFILES_CLEARED
|
||||||
};
|
};
|
||||||
using WiFiManagerEventCallback = std::function<void(wm_event_t)>;
|
using WiFiManagerEventCallback = std::function<void(wm_event_t)>;
|
||||||
|
|
||||||
WiFiManager(Print& consolePort);
|
WiFiManager(Print& consolePort);
|
||||||
WiFiManager();
|
WiFiManager();
|
||||||
~WiFiManager();
|
~WiFiManager();
|
||||||
@@ -347,14 +403,28 @@ class WiFiManager
|
|||||||
boolean autoConnect();
|
boolean autoConnect();
|
||||||
boolean autoConnect(char const *apName, char const *apPassword = NULL);
|
boolean autoConnect(char const *apName, char const *apPassword = NULL);
|
||||||
|
|
||||||
|
// Fixed two-profile station mode. When a store is attached, WiFiManager
|
||||||
|
// owns profile selection and explicit connection attempts rather than the
|
||||||
|
// platform's one saved station configuration.
|
||||||
|
void setStationProfileStore(WiFiManagerStationProfileStore* store);
|
||||||
|
bool startStationConnection(char const *apName = NULL, char const *apPassword = NULL);
|
||||||
|
bool startStationCandidate(const WiFiManagerStationProfiles& candidate);
|
||||||
|
bool startStationCandidate(const WiFiManagerStationProfiles& candidate, char const *apName, char const *apPassword = NULL);
|
||||||
|
bool saveStationProfiles(const WiFiManagerStationProfiles& profiles);
|
||||||
|
void clearStationProfiles();
|
||||||
|
bool isStationProfileMode() const;
|
||||||
|
void setStationRecoveryInterval(unsigned long intervalMs);
|
||||||
|
const WiFiManagerStationProfiles& getStationProfiles() const;
|
||||||
|
const wm_station_status_t& getStationStatus() const;
|
||||||
|
|
||||||
//manually start the config portal, autoconnect does this automatically on connect failure
|
//manually start the config portal, autoconnect does this automatically on connect failure
|
||||||
void startConfigPortal(); // auto generates apname
|
void startConfigPortal(); // auto generates apname
|
||||||
void startConfigPortal(char const *apName, char const *apPassword = NULL);
|
void startConfigPortal(char const *apName, char const *apPassword = NULL);
|
||||||
|
|
||||||
//manually stop the config portal - immediately shuts down the portal
|
//manually stop the config portal - immediately shuts down the portal
|
||||||
void stopConfigPortal();
|
void stopConfigPortal();
|
||||||
|
|
||||||
//manually start the web portal, autoconnect does this automatically on connect failure
|
//manually start the web portal, autoconnect does this automatically on connect failure
|
||||||
void startWebPortal();
|
void startWebPortal();
|
||||||
|
|
||||||
//manually stop the web portal if started manually
|
//manually stop the web portal if started manually
|
||||||
@@ -438,13 +508,13 @@ class WiFiManager
|
|||||||
|
|
||||||
// sets number of retries for autoconnect, force retry after wait failure exit
|
// sets number of retries for autoconnect, force retry after wait failure exit
|
||||||
void setConnectRetries(uint8_t numRetries); // default 1
|
void setConnectRetries(uint8_t numRetries); // default 1
|
||||||
|
|
||||||
//sets timeout for which to attempt connecting on saves, useful if there are bugs in esp waitforconnectloop
|
//sets timeout for which to attempt connecting on saves, useful if there are bugs in esp waitforconnectloop
|
||||||
void setSaveConnectTimeout(unsigned long seconds);
|
void setSaveConnectTimeout(unsigned long seconds);
|
||||||
|
|
||||||
// lets you disable automatically connecting after save from webportal
|
// lets you disable automatically connecting after save from webportal
|
||||||
void setSaveConnect(bool connect = true);
|
void setSaveConnect(bool connect = true);
|
||||||
|
|
||||||
void setLogEnabled(boolean enabled);
|
void setLogEnabled(boolean enabled);
|
||||||
void setLogPrefix(String prefix);
|
void setLogPrefix(String prefix);
|
||||||
void setLogOutput(boolean enabled, WiFiManagerLogLevel maxLevel);
|
void setLogOutput(boolean enabled, WiFiManagerLogLevel maxLevel);
|
||||||
@@ -463,44 +533,44 @@ class WiFiManager
|
|||||||
|
|
||||||
//set min quality percentage to include in scan, defaults to 8% if not specified
|
//set min quality percentage to include in scan, defaults to 8% if not specified
|
||||||
void setMinimumSignalQuality(int quality = 8);
|
void setMinimumSignalQuality(int quality = 8);
|
||||||
|
|
||||||
//sets a custom ip /gateway /subnet configuration
|
//sets a custom ip /gateway /subnet configuration
|
||||||
void setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
|
void setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
|
||||||
|
|
||||||
//sets config for a static IP
|
//sets config for a static IP
|
||||||
void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
|
void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
|
||||||
|
|
||||||
//sets config for a static IP with DNS
|
//sets config for a static IP with DNS
|
||||||
void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn, IPAddress dns);
|
void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn, IPAddress dns);
|
||||||
|
|
||||||
//if this is set, it will exit after config, even if connection is unsuccessful.
|
//if this is set, it will exit after config, even if connection is unsuccessful.
|
||||||
void setBreakAfterConfig(boolean shouldBreak);
|
void setBreakAfterConfig(boolean shouldBreak);
|
||||||
|
|
||||||
|
|
||||||
//if this is true, remove duplicated Access Points - defaut true
|
//if this is true, remove duplicated Access Points - defaut true
|
||||||
void setRemoveDuplicateAPs(boolean removeDuplicates);
|
void setRemoveDuplicateAPs(boolean removeDuplicates);
|
||||||
|
|
||||||
//setter for ESP wifi.persistent so we can remember it and restore user preference, as WIFi._persistent is protected
|
//setter for ESP wifi.persistent so we can remember it and restore user preference, as WIFi._persistent is protected
|
||||||
void setRestorePersistent(boolean persistent);
|
void setRestorePersistent(boolean persistent);
|
||||||
|
|
||||||
//if true, always show static net inputs, IP, subnet, gateway, else only show if set via setSTAStaticIPConfig
|
//if true, always show static net inputs, IP, subnet, gateway, else only show if set via setSTAStaticIPConfig
|
||||||
void setShowStaticFields(boolean alwaysShow);
|
void setShowStaticFields(boolean alwaysShow);
|
||||||
|
|
||||||
//if true, always show static dns, esle only show if set via setSTAStaticIPConfig
|
//if true, always show static dns, esle only show if set via setSTAStaticIPConfig
|
||||||
void setShowDnsFields(boolean alwaysShow);
|
void setShowDnsFields(boolean alwaysShow);
|
||||||
|
|
||||||
//if false, timeout captive portal even if a STA client connected to softAP (false), suggest disabling if captiveportal is open
|
//if false, timeout captive portal even if a STA client connected to softAP (false), suggest disabling if captiveportal is open
|
||||||
void setAPClientCheck(boolean enabled);
|
void setAPClientCheck(boolean enabled);
|
||||||
|
|
||||||
//if true, reset timeout when webclient connects (true), suggest disabling if captiveportal is open
|
//if true, reset timeout when webclient connects (true), suggest disabling if captiveportal is open
|
||||||
void setWebPortalClientCheck(boolean enabled);
|
void setWebPortalClientCheck(boolean enabled);
|
||||||
|
|
||||||
// if true, enable autoreconnecting
|
// if true, enable autoreconnecting
|
||||||
void setWiFiAutoReconnect(boolean enabled);
|
void setWiFiAutoReconnect(boolean enabled);
|
||||||
|
|
||||||
// if true, wifiscan will show percentage instead of quality icons, until we have better templating
|
// if true, wifiscan will show percentage instead of quality icons, until we have better templating
|
||||||
void setScanDispPerc(boolean enabled);
|
void setScanDispPerc(boolean enabled);
|
||||||
|
|
||||||
// if true (default) then start the config portal from autoConnect if connection failed
|
// if true (default) then start the config portal from autoConnect if connection failed
|
||||||
void setEnableConfigPortal(boolean enable);
|
void setEnableConfigPortal(boolean enable);
|
||||||
|
|
||||||
@@ -513,18 +583,17 @@ class WiFiManager
|
|||||||
|
|
||||||
// set ap channel
|
// set ap channel
|
||||||
void setWiFiAPChannel(int32_t channel);
|
void setWiFiAPChannel(int32_t channel);
|
||||||
|
|
||||||
// set ap hidden
|
// set ap hidden
|
||||||
void setWiFiAPHidden(bool hidden); // default false
|
void setWiFiAPHidden(bool hidden); // default false
|
||||||
|
|
||||||
// clean connect, always disconnect before connecting
|
// clean connect, always disconnect before connecting
|
||||||
void setCleanConnect(bool enable); // default false
|
void setCleanConnect(bool enable); // default false
|
||||||
|
|
||||||
// ---- Portal (all customization entry points use the portal* prefix) ----
|
// ---- Portal presentation ----
|
||||||
void portalSetBrandTitle(const String& title);
|
// Apply this before a portal is started. The portal response model is
|
||||||
void portalSetContextIdentityText(const String& identityText);
|
// immutable while active so async responses never observe partial UI state.
|
||||||
void portalSetBrandHomeIntro(const String& text);
|
bool setPortalConfig(const WiFiManagerPortalConfig& config);
|
||||||
void portalSetBrandLogoSvg(const String& svgMarkup);
|
|
||||||
|
|
||||||
void portalSetPageInfoVisible(bool visible);
|
void portalSetPageInfoVisible(bool visible);
|
||||||
void portalSetPageUpdateVisible(bool visible);
|
void portalSetPageUpdateVisible(bool visible);
|
||||||
@@ -557,16 +626,12 @@ class WiFiManager
|
|||||||
void portalAddHomeCard(const PortalHomeCard& card);
|
void portalAddHomeCard(const PortalHomeCard& card);
|
||||||
void portalClearHomeCards();
|
void portalClearHomeCards();
|
||||||
|
|
||||||
void portalAppendCss(const String& css);
|
|
||||||
void portalOverrideCss(const String& css);
|
|
||||||
void portalAppendJs(const String& js);
|
|
||||||
|
|
||||||
// get last connection result, including autoconnect and portal credential-save attempts
|
// get last connection result, including autoconnect and portal credential-save attempts
|
||||||
uint8_t getLastConxResult();
|
uint8_t getLastConxResult();
|
||||||
|
|
||||||
// get a status as string
|
// get a status as string
|
||||||
String getWLStatusString(uint8_t status);
|
String getWLStatusString(uint8_t status);
|
||||||
String getWLStatusString();
|
String getWLStatusString();
|
||||||
|
|
||||||
// get wifi mode as string
|
// get wifi mode as string
|
||||||
String getModeString(uint8_t mode);
|
String getModeString(uint8_t mode);
|
||||||
@@ -574,7 +639,7 @@ class WiFiManager
|
|||||||
// check if the module has a saved ap to connect to
|
// check if the module has a saved ap to connect to
|
||||||
bool getWiFiIsSaved();
|
bool getWiFiIsSaved();
|
||||||
|
|
||||||
// helper to get saved password, if persistent get stored, else get current if connected
|
// helper to get saved password, if persistent get stored, else get current if connected
|
||||||
String getWiFiPass(bool persistent = true);
|
String getWiFiPass(bool persistent = true);
|
||||||
|
|
||||||
// helper to get saved ssid, if persistent get stored, else get current if connected
|
// helper to get saved ssid, if persistent get stored, else get current if connected
|
||||||
@@ -588,17 +653,17 @@ class WiFiManager
|
|||||||
|
|
||||||
// helper for html
|
// helper for html
|
||||||
String htmlEntities(String str, bool whitespace = false);
|
String htmlEntities(String str, bool whitespace = false);
|
||||||
|
|
||||||
// set the country code for wifi settings, CN
|
// set the country code for wifi settings, CN
|
||||||
void setCountry(String cc);
|
void setCountry(String cc);
|
||||||
|
|
||||||
|
|
||||||
// get default ap esp uses , esp_chipid etc
|
// get default ap esp uses , esp_chipid etc
|
||||||
String getDefaultAPName();
|
String getDefaultAPName();
|
||||||
|
|
||||||
// set the WiFi SSID prefix for default AP name, default platform-specific (ESP/ESP32/WM)
|
// set the WiFi SSID prefix for default AP name, default platform-specific (ESP/ESP32/WM)
|
||||||
void setWiFiSSIDPrefix(String prefix);
|
void setWiFiSSIDPrefix(String prefix);
|
||||||
|
|
||||||
// set port of webserver, 80
|
// set port of webserver, 80
|
||||||
void setHttpPort(uint16_t port);
|
void setHttpPort(uint16_t port);
|
||||||
|
|
||||||
@@ -615,7 +680,7 @@ class WiFiManager
|
|||||||
uint8_t getConfigPortalConnectStatus() const;
|
uint8_t getConfigPortalConnectStatus() const;
|
||||||
String getConfigPortalConnectMessage() const;
|
String getConfigPortalConnectMessage() const;
|
||||||
void setEventCallback(WiFiManagerEventCallback cb);
|
void setEventCallback(WiFiManagerEventCallback cb);
|
||||||
|
|
||||||
// check if web portal is active (true)
|
// check if web portal is active (true)
|
||||||
bool getWebPortalActive();
|
bool getWebPortalActive();
|
||||||
|
|
||||||
@@ -624,10 +689,10 @@ class WiFiManager
|
|||||||
|
|
||||||
// get hostname helper
|
// get hostname helper
|
||||||
String getWiFiHostname();
|
String getWiFiHostname();
|
||||||
|
|
||||||
// get server instance (for testing)
|
// get server instance (for testing)
|
||||||
AsyncWebServer* getServer();
|
AsyncWebServer* getServer();
|
||||||
|
|
||||||
// get DNS server instance (for testing)
|
// get DNS server instance (for testing)
|
||||||
DNSServer* getDNSServer();
|
DNSServer* getDNSServer();
|
||||||
|
|
||||||
@@ -636,7 +701,7 @@ class WiFiManager
|
|||||||
class WiFiManagerRequestArgs {
|
class WiFiManagerRequestArgs {
|
||||||
public:
|
public:
|
||||||
std::unordered_map<std::string, std::string> args;
|
std::unordered_map<std::string, std::string> args;
|
||||||
|
|
||||||
// Constructor - builds from AsyncWebServerRequest
|
// Constructor - builds from AsyncWebServerRequest
|
||||||
WiFiManagerRequestArgs(AsyncWebServerRequest* request) {
|
WiFiManagerRequestArgs(AsyncWebServerRequest* request) {
|
||||||
if (request) {
|
if (request) {
|
||||||
@@ -647,19 +712,19 @@ class WiFiManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default constructor for tests and manually assembled argument sets
|
// Default constructor for tests and manually assembled argument sets
|
||||||
WiFiManagerRequestArgs() {}
|
WiFiManagerRequestArgs() {}
|
||||||
|
|
||||||
// Check if argument exists
|
// Check if argument exists
|
||||||
bool hasArg(const char* name) const {
|
bool hasArg(const char* name) const {
|
||||||
return args.find(std::string(name)) != args.end();
|
return args.find(std::string(name)) != args.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasArg(const String& name) const {
|
bool hasArg(const String& name) const {
|
||||||
return hasArg(name.c_str());
|
return hasArg(name.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get argument value as String
|
// Get argument value as String
|
||||||
String getArg(const char* name, const String& defaultValue = "") const {
|
String getArg(const char* name, const String& defaultValue = "") const {
|
||||||
auto it = args.find(std::string(name));
|
auto it = args.find(std::string(name));
|
||||||
@@ -668,29 +733,29 @@ class WiFiManager
|
|||||||
}
|
}
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getArg(const String& name, const String& defaultValue = "") const {
|
String getArg(const String& name, const String& defaultValue = "") const {
|
||||||
return getArg(name.c_str(), defaultValue);
|
return getArg(name.c_str(), defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type conversion helpers
|
// Type conversion helpers
|
||||||
int getArgAsInt(const char* name, int defaultValue = 0) const {
|
int getArgAsInt(const char* name, int defaultValue = 0) const {
|
||||||
String value = getArg(name);
|
String value = getArg(name);
|
||||||
return value.length() > 0 ? value.toInt() : defaultValue;
|
return value.length() > 0 ? value.toInt() : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
float getArgAsFloat(const char* name, float defaultValue = 0.0f) const {
|
float getArgAsFloat(const char* name, float defaultValue = 0.0f) const {
|
||||||
String value = getArg(name);
|
String value = getArg(name);
|
||||||
return value.length() > 0 ? value.toFloat() : defaultValue;
|
return value.length() > 0 ? value.toFloat() : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getArgAsBool(const char* name, bool defaultValue = false) const {
|
bool getArgAsBool(const char* name, bool defaultValue = false) const {
|
||||||
String value = getArg(name);
|
String value = getArg(name);
|
||||||
if (value.length() == 0) return defaultValue;
|
if (value.length() == 0) return defaultValue;
|
||||||
return value == "1" || value.equalsIgnoreCase("true") ||
|
return value == "1" || value.equalsIgnoreCase("true") ||
|
||||||
value.equalsIgnoreCase("on") || value.equalsIgnoreCase("yes");
|
value.equalsIgnoreCase("on") || value.equalsIgnoreCase("yes");
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t count() const {
|
size_t count() const {
|
||||||
return args.size();
|
return args.size();
|
||||||
}
|
}
|
||||||
@@ -723,7 +788,7 @@ class WiFiManager
|
|||||||
WiFiScanRuntimeState _scan;
|
WiFiScanRuntimeState _scan;
|
||||||
std::vector<WiFiScanNetwork> _scanResultsCache;
|
std::vector<WiFiScanNetwork> _scanResultsCache;
|
||||||
bool _scanLifecycleBlocked = false;
|
bool _scanLifecycleBlocked = false;
|
||||||
|
|
||||||
// async reboot/abort scheduling
|
// async reboot/abort scheduling
|
||||||
bool _rebootScheduled = false; // flag for scheduled reboot
|
bool _rebootScheduled = false; // flag for scheduled reboot
|
||||||
unsigned long _rebootTime = 0; // ms when reboot should occur
|
unsigned long _rebootTime = 0; // ms when reboot should occur
|
||||||
@@ -742,13 +807,30 @@ class WiFiManager
|
|||||||
String _defaultssid = ""; // preload ssid
|
String _defaultssid = ""; // preload ssid
|
||||||
String _defaultpass = ""; // preload pass
|
String _defaultpass = ""; // preload pass
|
||||||
|
|
||||||
|
WiFiManagerStationProfileStore* _stationProfileStore = nullptr;
|
||||||
|
WiFiManagerStationProfiles _stationProfiles;
|
||||||
|
WiFiManagerStationProfiles _stationCandidate;
|
||||||
|
wm_station_status_t _stationStatus;
|
||||||
|
bool _stationProfilesLoaded = false;
|
||||||
|
bool _stationCandidateActive = false;
|
||||||
|
bool _stationCandidateFromPortal = false;
|
||||||
|
bool _stationEverConnected = false;
|
||||||
|
uint8_t _stationAttemptMask = 0;
|
||||||
|
uint8_t _stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
unsigned long _stationNextAttemptAt = 0;
|
||||||
|
unsigned long _stationAttemptStartedAt = 0;
|
||||||
|
unsigned long _stationBackoffStartedAt = 0;
|
||||||
|
unsigned long _stationRecoveryInterval = 5000UL;
|
||||||
|
String _stationPortalApName = "";
|
||||||
|
String _stationPortalApPassword = "";
|
||||||
|
|
||||||
// options flags
|
// options flags
|
||||||
unsigned long _configPortalTimeout = 0; // ms close config portal loop if set (depending on _cp/webClientCheck options)
|
unsigned long _configPortalTimeout = 0; // ms close config portal loop if set (depending on _cp/webClientCheck options)
|
||||||
unsigned long _connectTimeout = 0; // ms stop trying to connect to ap if set
|
unsigned long _connectTimeout = 0; // ms stop trying to connect to ap if set
|
||||||
unsigned long _saveTimeout = 0; // ms stop trying to connect to ap on saves, in case bugs in esp waitforconnectresult
|
unsigned long _saveTimeout = 0; // ms stop trying to connect to ap on saves, in case bugs in esp waitforconnectresult
|
||||||
|
|
||||||
WiFiMode_t _usermode = WIFI_STA; // Default user mode
|
WiFiMode_t _usermode = WIFI_STA; // Default user mode
|
||||||
String _wifissidprefix =
|
String _wifissidprefix =
|
||||||
#ifdef ESP8266
|
#ifdef ESP8266
|
||||||
"ESP"
|
"ESP"
|
||||||
#elif defined(ESP32)
|
#elif defined(ESP32)
|
||||||
@@ -799,16 +881,21 @@ class WiFiManager
|
|||||||
boolean _disableConfigPortal = true; // FOR autoconnect - stop config portal if cp wifi save
|
boolean _disableConfigPortal = true; // FOR autoconnect - stop config portal if cp wifi save
|
||||||
String _hostname = ""; // hostname for esp8266 for dhcp, and or MDNS
|
String _hostname = ""; // hostname for esp8266 for dhcp, and or MDNS
|
||||||
|
|
||||||
// Grouped portal presentation / customization (see portal* setters; JSON in v2 bootstrap)
|
// Grouped portal presentation / customization (see portal APIs and JSON bootstrap)
|
||||||
PortalBrandState _portalBrand;
|
PortalBrandState _portalBrand;
|
||||||
|
WiFiManagerPortalTheme _portalTheme;
|
||||||
|
String _portalThemeStyle;
|
||||||
PortalPageState _portalPages;
|
PortalPageState _portalPages;
|
||||||
PortalActionState _portalActions;
|
PortalActionState _portalActions;
|
||||||
PortalLayoutState _portalLayout;
|
PortalLayoutState _portalLayout;
|
||||||
PortalAssetState _portalAssets;
|
|
||||||
PortalStructuredExtrasState _portalStructured;
|
PortalStructuredExtrasState _portalStructured;
|
||||||
|
|
||||||
|
bool canChangePortalPresentation() const;
|
||||||
|
bool isPortalThemeValid(const WiFiManagerPortalTheme& theme) const;
|
||||||
|
void rebuildPortalThemeStyle();
|
||||||
|
|
||||||
// internal options
|
// internal options
|
||||||
|
|
||||||
// wifiscan notes
|
// wifiscan notes
|
||||||
// currently disabled due to issues with caching, sometimes first scan is empty esp32 wifi not init yet race, or portals hit server nonstop flood
|
// currently disabled due to issues with caching, sometimes first scan is empty esp32 wifi not init yet race, or portals hit server nonstop flood
|
||||||
// The following are background wifi scanning optimizations
|
// The following are background wifi scanning optimizations
|
||||||
@@ -819,22 +906,22 @@ class WiFiManager
|
|||||||
// async enables asyncronous scans, so they do not block anything
|
// async enables asyncronous scans, so they do not block anything
|
||||||
// the refresh button bypasses cache
|
// the refresh button bypasses cache
|
||||||
// no aps found is problematic as scans are always going to want to run, leading to page load delays
|
// no aps found is problematic as scans are always going to want to run, leading to page load delays
|
||||||
//
|
//
|
||||||
// These settings really only make sense with _preloadwifiscan true
|
// These settings really only make sense with _preloadwifiscan true
|
||||||
// but not limited to, we could run continuous background scans on various page hits, or xhr hits
|
// but not limited to, we could run continuous background scans on various page hits, or xhr hits
|
||||||
// which would be better coupled with asyncscan
|
// which would be better coupled with asyncscan
|
||||||
// atm preload is only done on root hit and startcp
|
// atm preload is only done on root hit and startcp
|
||||||
//
|
//
|
||||||
// preload scanning causes AP to delay showing for users, but also caches and lets the cp load faster once its open
|
// 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
|
// my scan takes 7-10 seconds
|
||||||
public:
|
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
|
unsigned int _scancachetime = 30000; // ms cache time for preload scans
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
boolean _autoforcerescan = false; // automatically force rescan if scan networks is 0, ignoring cache
|
boolean _autoforcerescan = false; // automatically force rescan if scan networks is 0, ignoring cache
|
||||||
|
|
||||||
boolean _disableIpFields = false; // modify function of setShow_X_Fields(false), forces ip fields off instead of default show if set, eg. _staShowStaticFields=-1
|
boolean _disableIpFields = false; // modify function of setShow_X_Fields(false), forces ip fields off instead of default show if set, eg. _staShowStaticFields=-1
|
||||||
|
|
||||||
String _wificountry = ""; // country code, @todo define in strings lang
|
String _wificountry = ""; // country code, @todo define in strings lang
|
||||||
@@ -848,7 +935,7 @@ protected:
|
|||||||
void setupConfigPortal();
|
void setupConfigPortal();
|
||||||
bool shutdownConfigPortal();
|
bool shutdownConfigPortal();
|
||||||
bool setupHostname(bool restart);
|
bool setupHostname(bool restart);
|
||||||
|
|
||||||
#ifdef NO_EXTRA_4K_HEAP
|
#ifdef NO_EXTRA_4K_HEAP
|
||||||
boolean _tryWPS = false; // try WPS on save failure, unsupported
|
boolean _tryWPS = false; // try WPS on save failure, unsupported
|
||||||
void startWPS();
|
void startWPS();
|
||||||
@@ -869,6 +956,24 @@ protected:
|
|||||||
bool wifiConnectDefault();
|
bool wifiConnectDefault();
|
||||||
bool wifiConnectNew(String ssid, String pass,bool connect = true);
|
bool wifiConnectNew(String ssid, String pass,bool connect = true);
|
||||||
|
|
||||||
|
void processStationController();
|
||||||
|
bool beginStationProfile(uint8_t slot);
|
||||||
|
void queueStationProfile(uint8_t slot);
|
||||||
|
void beginStationCycle(bool preferLastSuccessful);
|
||||||
|
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;
|
||||||
|
uint8_t configuredStationProfileCount(const WiFiManagerStationProfiles& profiles) const;
|
||||||
|
uint8_t chooseStationProfile(const WiFiManagerStationProfiles& profiles, bool preferLastSuccessful) const;
|
||||||
|
const WiFiManagerStationProfiles& stationProfilesForAttempt() const;
|
||||||
|
WiFiManagerStationProfiles& stationProfilesForAttempt();
|
||||||
|
bool validateStationProfiles(const WiFiManagerStationProfiles& profiles) const;
|
||||||
|
unsigned long stationAttemptTimeout() const;
|
||||||
|
|
||||||
uint8_t waitForConnectResult();
|
uint8_t waitForConnectResult();
|
||||||
uint8_t waitForConnectResult(uint32_t timeout);
|
uint8_t waitForConnectResult(uint32_t timeout);
|
||||||
void updateConxResult(uint8_t status);
|
void updateConxResult(uint8_t status);
|
||||||
@@ -931,12 +1036,12 @@ protected:
|
|||||||
#define WM_DISCONWORKAROUND
|
#define WM_DISCONWORKAROUND
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#else
|
#else
|
||||||
#define WM_NOCOUNTRY
|
#define WM_NOCOUNTRY
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef WM_NOCOUNTRY
|
#ifdef WM_NOCOUNTRY
|
||||||
#warning "ESP32 set country unavailable"
|
#warning "ESP32 set country unavailable"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@@ -979,6 +1084,7 @@ protected:
|
|||||||
_cpConnectStationIp = stationIp;
|
_cpConnectStationIp = stationIp;
|
||||||
_cpConnectStatus = status;
|
_cpConnectStatus = status;
|
||||||
}
|
}
|
||||||
|
void wmTestCompleteProfilePortalConnectionSuccess();
|
||||||
void wmTestSetPortalConnectFailure(const String& message, uint8_t status = WL_CONNECT_FAILED) {
|
void wmTestSetPortalConnectFailure(const String& message, uint8_t status = WL_CONNECT_FAILED) {
|
||||||
_cpConnectState = wm_cp_connect_state_t::failed;
|
_cpConnectState = wm_cp_connect_state_t::failed;
|
||||||
_cpConnectMessage = message;
|
_cpConnectMessage = message;
|
||||||
@@ -1042,9 +1148,9 @@ protected:
|
|||||||
// if we decide to support this, these checks will need to be replaced with something client aware to check if client origin is ap or web
|
// if we decide to support this, these checks will need to be replaced with something client aware to check if client origin is ap or web
|
||||||
// These state checks are critical and used for internal function checks
|
// These state checks are critical and used for internal function checks
|
||||||
boolean webPortalActive = false;
|
boolean webPortalActive = false;
|
||||||
boolean storeSTAmode = true; // option store persistent STA mode in connectwifi
|
boolean storeSTAmode = true; // option store persistent STA mode in connectwifi
|
||||||
int timer = 0; // timer for debug throttle for numclients, and portal timeout messages
|
int timer = 0; // timer for debug throttle for numclients, and portal timeout messages
|
||||||
|
|
||||||
// WiFiManagerParameter
|
// WiFiManagerParameter
|
||||||
int _paramsCount = 0;
|
int _paramsCount = 0;
|
||||||
int _max_params;
|
int _max_params;
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ class WiFiManagerHandlers {
|
|||||||
void handleApiWifiMeta(AsyncWebServerRequest *request);
|
void handleApiWifiMeta(AsyncWebServerRequest *request);
|
||||||
void handleApiWifiSave(AsyncWebServerRequest *request);
|
void handleApiWifiSave(AsyncWebServerRequest *request);
|
||||||
void handleApiWifiConnectStatus(AsyncWebServerRequest *request);
|
void handleApiWifiConnectStatus(AsyncWebServerRequest *request);
|
||||||
|
void handleApiWifiConnectComplete(AsyncWebServerRequest *request);
|
||||||
|
void handleApiPortalTimeoutReset(AsyncWebServerRequest *request);
|
||||||
void handleApiParamsGet(AsyncWebServerRequest *request);
|
void handleApiParamsGet(AsyncWebServerRequest *request);
|
||||||
void handleApiParamsSave(AsyncWebServerRequest *request);
|
void handleApiParamsSave(AsyncWebServerRequest *request);
|
||||||
void handleApiInfo(AsyncWebServerRequest *request);
|
void handleApiInfo(AsyncWebServerRequest *request);
|
||||||
@@ -101,11 +103,11 @@ class WiFiManagerHandlers {
|
|||||||
void appendVisibleScanResultsJson(String& json, const std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
|
void appendVisibleScanResultsJson(String& json, const std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
|
||||||
|
|
||||||
void applyWifiAndParamsFromRequest(AsyncWebServerRequest *request);
|
void applyWifiAndParamsFromRequest(AsyncWebServerRequest *request);
|
||||||
|
bool buildStationProfilesFromRequest(AsyncWebServerRequest *request, WiFiManagerStationProfiles& profiles);
|
||||||
void buildPlainStatusSummary(String& out);
|
void buildPlainStatusSummary(String& out);
|
||||||
void appendPortalJsonStaticFields(String& json, bool& first);
|
void appendPortalJsonStaticFields(String& json, bool& first);
|
||||||
void appendPortalJsonCustomParams(String& json, bool& first);
|
void appendPortalJsonCustomParams(String& json, bool& first);
|
||||||
|
|
||||||
String composePortalStylesheet() const;
|
|
||||||
void appendPortalExtraInfoSectionsJson(String& json, bool& first);
|
void appendPortalExtraInfoSectionsJson(String& json, bool& first);
|
||||||
void appendPortalExtraHomeCardsJson(String& json, bool& first);
|
void appendPortalExtraHomeCardsJson(String& json, bool& first);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* WiFiManagerPortalUI.h
|
||||||
|
*
|
||||||
|
* Deliberate, portal-specific presentation configuration. This header does
|
||||||
|
* not depend on DeviceFramework: WiFiManager remains useful on its own.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef WIFI_MANAGER_PORTAL_UI_H
|
||||||
|
#define WIFI_MANAGER_PORTAL_UI_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
enum class WiFiManagerPortalStorage : uint8_t {
|
||||||
|
Ram,
|
||||||
|
Progmem,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Non-owning static text. The caller retains the data for the firmware lifetime. */
|
||||||
|
struct WiFiManagerPortalText {
|
||||||
|
const char* data = nullptr;
|
||||||
|
WiFiManagerPortalStorage storage = WiFiManagerPortalStorage::Ram;
|
||||||
|
|
||||||
|
static constexpr WiFiManagerPortalText ram(const char* value) {
|
||||||
|
return {value, WiFiManagerPortalStorage::Ram};
|
||||||
|
}
|
||||||
|
static constexpr WiFiManagerPortalText progmem(const char* value) {
|
||||||
|
return {value, WiFiManagerPortalStorage::Progmem};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() const { return data == nullptr || length() == 0; }
|
||||||
|
size_t length() const {
|
||||||
|
return data == nullptr ? 0 : (storage == WiFiManagerPortalStorage::Progmem ? strlen_P(data) : strlen(data));
|
||||||
|
}
|
||||||
|
char at(size_t index) const {
|
||||||
|
return storage == WiFiManagerPortalStorage::Progmem
|
||||||
|
? static_cast<char>(pgm_read_byte(data + index))
|
||||||
|
: data[index];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Optional inline SVG branding asset. It is not a general HTML extension point. */
|
||||||
|
struct WiFiManagerPortalAsset {
|
||||||
|
WiFiManagerPortalText svg;
|
||||||
|
|
||||||
|
static constexpr WiFiManagerPortalAsset svgFromRam(const char* value) {
|
||||||
|
return {WiFiManagerPortalText::ram(value)};
|
||||||
|
}
|
||||||
|
static constexpr WiFiManagerPortalAsset svgFromProgmem(const char* value) {
|
||||||
|
return {WiFiManagerPortalText::progmem(value)};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() const { return svg.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantic colour and shape values for WiFiManager's built-in portal.
|
||||||
|
*
|
||||||
|
* Leave a value empty to retain the built-in stylesheet value. Values are
|
||||||
|
* validated before they are emitted into the portal stylesheet; they are not
|
||||||
|
* a general CSS injection mechanism.
|
||||||
|
*/
|
||||||
|
struct WiFiManagerPortalTheme {
|
||||||
|
WiFiManagerPortalText pageBackground;
|
||||||
|
WiFiManagerPortalText surface;
|
||||||
|
WiFiManagerPortalText text;
|
||||||
|
WiFiManagerPortalText mutedText;
|
||||||
|
WiFiManagerPortalText border;
|
||||||
|
WiFiManagerPortalText accent;
|
||||||
|
WiFiManagerPortalText accentHover;
|
||||||
|
WiFiManagerPortalText accentText;
|
||||||
|
WiFiManagerPortalText danger;
|
||||||
|
WiFiManagerPortalText dangerHover;
|
||||||
|
WiFiManagerPortalText success;
|
||||||
|
uint8_t cornerRadiusPx = 0;
|
||||||
|
uint8_t smallCornerRadiusPx = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete setup-time presentation configuration for WiFiManager's portal.
|
||||||
|
*
|
||||||
|
* Text and SVG assets are non-owning static data in RAM or PROGMEM. Apply this
|
||||||
|
* before starting a portal; asynchronous portal responses use immutable state.
|
||||||
|
*/
|
||||||
|
struct WiFiManagerPortalConfig {
|
||||||
|
WiFiManagerPortalText title;
|
||||||
|
WiFiManagerPortalText identityText;
|
||||||
|
WiFiManagerPortalText tagline;
|
||||||
|
WiFiManagerPortalAsset logo;
|
||||||
|
WiFiManagerPortalText logoAltText;
|
||||||
|
WiFiManagerPortalTheme theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // WIFI_MANAGER_PORTAL_UI_H
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
* GET /api/wifi/meta
|
* GET /api/wifi/meta
|
||||||
* POST /api/wifi/save
|
* POST /api/wifi/save
|
||||||
* GET /api/wifi/connect-status
|
* GET /api/wifi/connect-status
|
||||||
|
* POST /api/wifi/connect-complete
|
||||||
|
* POST /api/portal/timeout-reset
|
||||||
* GET /api/params
|
* GET /api/params
|
||||||
* POST /api/params/save
|
* POST /api/params/save
|
||||||
* GET /api/info
|
* 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_meta[] PROGMEM = "/api/wifi/meta";
|
||||||
const char R_api_wifi_save[] PROGMEM = "/api/wifi/save";
|
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_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[] PROGMEM = "/api/params";
|
||||||
const char R_api_params_save[] PROGMEM = "/api/params/save";
|
const char R_api_params_save[] PROGMEM = "/api/params/save";
|
||||||
const char R_api_info[] PROGMEM = "/api/info";
|
const char R_api_info[] PROGMEM = "/api/info";
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
|||||||
"--wm-border:#e2e8f0;"
|
"--wm-border:#e2e8f0;"
|
||||||
"--wm-brand:#0ea5e9;"
|
"--wm-brand:#0ea5e9;"
|
||||||
"--wm-brand-hover:#0284c7;"
|
"--wm-brand-hover:#0284c7;"
|
||||||
|
"--wm-brand-text:#ffffff;"
|
||||||
"--wm-danger:#dc2626;"
|
"--wm-danger:#dc2626;"
|
||||||
"--wm-danger-hover:#b91c1c;"
|
"--wm-danger-hover:#b91c1c;"
|
||||||
"--wm-success:#16a34a;"
|
"--wm-success:#16a34a;"
|
||||||
@@ -73,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-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{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%}"
|
".wm-icon svg{display:block;width:100%;height:100%}"
|
||||||
/* Hero / page chrome */
|
/* Header / page chrome */
|
||||||
".wm-hero{margin-bottom:20px}"
|
".wm-site-header{display:flex;align-items:center;gap:10px;min-height:40px;margin:0 0 12px}"
|
||||||
".wm-hero-title{font-size:1.65rem;line-height:1.2;margin-bottom:.25em}"
|
".wm-site-logo{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:0 0 40px;line-height:0}"
|
||||||
".wm-hero-sub{font-size:1rem;color:var(--wm-muted);font-weight:500;margin:0}"
|
".wm-site-logo svg{display:block;width:100%;height:100%}"
|
||||||
".wm-brand-logo{margin-bottom:12px;line-height:0}"
|
".wm-site-brand-copy{min-width:0}"
|
||||||
".wm-brand-logo svg{display:block;max-width:160px;height:auto}"
|
".wm-site-brand-name{margin:0;font-size:.78rem;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--wm-text)}"
|
||||||
".wm-hero-intro{font-size:.95rem;color:var(--wm-muted);margin:.5em 0 0;line-height:1.45;max-width:42ch}"
|
".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{margin-bottom:16px}"
|
||||||
".wm-page-head h1{font-size:1.45rem}"
|
".wm-page-head h1{font-size:1.45rem}"
|
||||||
".wm-page-desc{font-size:.95rem;color:var(--wm-muted);margin:.35em 0 0}"
|
".wm-page-desc{font-size:.95rem;color:var(--wm-muted);margin:.35em 0 0}"
|
||||||
/* Cards */
|
/* Cards */
|
||||||
".wm-card{background:var(--wm-surface);border:1px solid var(--wm-border);border-radius:var(--wm-radius);"
|
".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}"
|
"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);"
|
".wm-card-title{font-size:1rem;margin:0 0 12px;padding-bottom:10px;border-bottom:1px solid var(--wm-border);"
|
||||||
"color:var(--wm-text)}"
|
"color:var(--wm-text)}"
|
||||||
".wm-card-subtitle{margin:-4px 0 12px;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
|
".wm-card-subtitle{margin:-4px 0 12px;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
|
||||||
@@ -119,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{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-checkbox-row label{display:inline;margin:0;font-weight:500;color:var(--wm-text)}"
|
||||||
".wm-form-actions{margin-top:16px}"
|
".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 */
|
/* Buttons */
|
||||||
".wm-btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 18px;"
|
".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;"
|
"font-size:1rem;font-weight:600;font-family:inherit;border-radius:var(--wm-radius-sm);border:0;cursor:pointer;"
|
||||||
@@ -128,16 +156,16 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
|||||||
"a.wm-btn{box-sizing:border-box;text-decoration:none}"
|
"a.wm-btn{box-sizing:border-box;text-decoration:none}"
|
||||||
".wm-btn .wm-icon-label{justify-content:center}"
|
".wm-btn .wm-icon-label{justify-content:center}"
|
||||||
".wm-btn--block{width:100%}"
|
".wm-btn--block{width:100%}"
|
||||||
".wm-btn--primary{background:var(--wm-brand);color:#fff}"
|
".wm-btn--primary{background:var(--wm-brand);color:var(--wm-brand-text)}"
|
||||||
".wm-btn--primary:hover{background:var(--wm-brand-hover)}"
|
".wm-btn--primary:hover{background:var(--wm-brand-hover)}"
|
||||||
".wm-btn--secondary{background:var(--wm-bg);color:var(--wm-text);border:1px solid var(--wm-border)}"
|
".wm-btn--secondary{background:var(--wm-bg);color:var(--wm-text);border:1px solid var(--wm-border)}"
|
||||||
".wm-btn--secondary:hover{border-color:var(--wm-brand);color:var(--wm-brand)}"
|
".wm-btn--secondary:hover{border-color:var(--wm-brand);color:var(--wm-brand)}"
|
||||||
".wm-btn--danger{background:var(--wm-danger);color:#fff}"
|
".wm-btn--danger{background:var(--wm-danger);color:var(--wm-brand-text)}"
|
||||||
".wm-btn--danger:hover{background:var(--wm-danger-hover)}"
|
".wm-btn--danger:hover{background:var(--wm-danger-hover)}"
|
||||||
"input[type=file]{width:100%;padding:10px;border:1px dashed var(--wm-border);border-radius:var(--wm-radius-sm);"
|
"input[type=file]{width:100%;padding:10px;border:1px dashed var(--wm-border);border-radius:var(--wm-radius-sm);"
|
||||||
"background:var(--wm-bg);font-size:.9rem}"
|
"background:var(--wm-bg);font-size:.9rem}"
|
||||||
/* Legacy: unclassed submit in forms */
|
/* Legacy: unclassed submit in forms */
|
||||||
"button[type=submit],.wm-form-actions .wm-btn--primary{background:var(--wm-brand);color:#fff}"
|
"button[type=submit],.wm-form-actions .wm-btn--primary{background:var(--wm-brand);color:var(--wm-brand-text)}"
|
||||||
"button[type=submit]:hover{background:var(--wm-brand-hover)}"
|
"button[type=submit]:hover{background:var(--wm-brand-hover)}"
|
||||||
/* Device actions / button stack */
|
/* Device actions / button stack */
|
||||||
".wm-actions-stack{display:flex;flex-direction:column;gap:10px;margin-top:4px}"
|
".wm-actions-stack{display:flex;flex-direction:column;gap:10px;margin-top:4px}"
|
||||||
@@ -179,8 +207,6 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
|||||||
".wm-kv dd:last-of-type{border-bottom:0}"
|
".wm-kv dd:last-of-type{border-bottom:0}"
|
||||||
/* Feedback areas */
|
/* Feedback areas */
|
||||||
"#wm-wifi-msg,#wm-param-msg{font-size:.9rem;margin-top:12px;min-height:1.2em;color:var(--wm-muted)}"
|
"#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
|
// invert legacy body
|
||||||
"body.invert{background-color:var(--wm-bg)}"
|
"body.invert{background-color:var(--wm-bg)}"
|
||||||
"body.invert .msg{border-color:var(--wm-border)}"
|
"body.invert .msg{border-color:var(--wm-border)}"
|
||||||
@@ -198,7 +224,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
|||||||
".wm-dialog-msg{margin:0 0 16px;line-height:1.45;color:var(--wm-text)}"
|
".wm-dialog-msg{margin:0 0 16px;line-height:1.45;color:var(--wm-text)}"
|
||||||
".wm-dialog-actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}"
|
".wm-dialog-actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}"
|
||||||
".wm-dialog-actions .wm-btn{min-width:108px}"
|
".wm-dialog-actions .wm-btn{min-width:108px}"
|
||||||
".wm-dialog-ok,.wm-btn.wm-dialog-ok{background:var(--wm-brand)!important;color:#fff!important}"
|
".wm-dialog-ok,.wm-btn.wm-dialog-ok{background:var(--wm-brand)!important;color:var(--wm-brand-text)!important}"
|
||||||
".wm-dialog-ok:hover{background:var(--wm-brand-hover)!important}"
|
".wm-dialog-ok:hover{background:var(--wm-brand-hover)!important}"
|
||||||
"body.invert .wm-toast.ok{background:rgba(22,163,74,.2)}"
|
"body.invert .wm-toast.ok{background:rgba(22,163,74,.2)}"
|
||||||
"body.invert .wm-toast.err{background:rgba(220,38,38,.2)}"
|
"body.invert .wm-toast.err{background:rgba(220,38,38,.2)}"
|
||||||
|
|||||||
@@ -66,8 +66,13 @@ function bindPortalChromeEvents(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindFormSubmitHandlers(){
|
function bindFormSubmitHandlers(){
|
||||||
var wf=$('wm-wifi-form'), pf=$('wm-param-form'), of=$('wm-ota-form');
|
var wf=document.querySelectorAll('.wm-wifi-data-form'), pf=$('wm-param-form'), of=$('wm-ota-form'), wa=document.querySelectorAll('[data-wm-wifi-action]'), i;
|
||||||
if(wf && window.portalWifiSave) wf.addEventListener('submit', window.portalWifiSave);
|
if(window.portalWifiSave){
|
||||||
|
for(i=0;i<wf.length;i++)wf[i].addEventListener('submit', window.portalWifiSave);
|
||||||
|
}
|
||||||
|
if(window.portalWifiAction){
|
||||||
|
for(i=0;i<wa.length;i++)wa[i].addEventListener('click', window.portalWifiAction);
|
||||||
|
}
|
||||||
if(pf && window.portalParamSave) pf.addEventListener('submit', window.portalParamSave);
|
if(pf && window.portalParamSave) pf.addEventListener('submit', window.portalParamSave);
|
||||||
if(of && window.portalOtaSubmit) of.addEventListener('submit', window.portalOtaSubmit);
|
if(of && window.portalOtaSubmit) of.addEventListener('submit', window.portalOtaSubmit);
|
||||||
}
|
}
|
||||||
@@ -115,7 +120,7 @@ function startPortalTimeoutCountdown(initialSeconds){
|
|||||||
function render(){
|
function render(){
|
||||||
if(!el)return;
|
if(!el)return;
|
||||||
el.className='wm-status'+(levelClass(remaining)?' '+levelClass(remaining):'');
|
el.className='wm-status'+(levelClass(remaining)?' '+levelClass(remaining):'');
|
||||||
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
|
el.textContent='Portal closes in '+formatDurationSeconds(remaining);
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
_wmPortalTimeoutTimer=setInterval(function(){
|
_wmPortalTimeoutTimer=setInterval(function(){
|
||||||
@@ -255,6 +260,28 @@ function rowsWithoutKeys(rows,blocked){
|
|||||||
return out;
|
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){
|
function navBar(active){
|
||||||
active=active||'home';
|
active=active||'home';
|
||||||
var pages=boot.pages||{};
|
var pages=boot.pages||{};
|
||||||
@@ -264,11 +291,11 @@ function navBar(active){
|
|||||||
var ps=pages.setup||{};
|
var ps=pages.setup||{};
|
||||||
var pi=pages.info||{};
|
var pi=pages.info||{};
|
||||||
var pu=pages.update||{};
|
var pu=pages.update||{};
|
||||||
var h="<nav class='wm-nav' aria-label='Configuration'>";
|
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','Home')+"</a>";
|
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','WiFi')+"</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','Setup')+"</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','Info')+"</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(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>";}
|
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>"+labelWithIcon('back','Back')+"</a>";}
|
||||||
h+="</nav>";
|
h+="</nav>";
|
||||||
@@ -306,29 +333,91 @@ function renderExtraHomeCards(cards){
|
|||||||
return out;
|
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(){
|
function viewHome(){
|
||||||
var brand=boot.brand||{};
|
var brand=boot.brand||{};
|
||||||
var ctx=boot.context||{};
|
var ctx=boot.context||{};
|
||||||
var t=esc(brand.title||'WiFiManager');
|
var title=esc(brand.title||'WiFiManager');
|
||||||
var ident=ctx.identityText?esc(ctx.identityText):'';
|
|
||||||
var summary=ctx.statusSummary||'';
|
var summary=ctx.statusSummary||'';
|
||||||
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
|
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
|
||||||
var html=navBar('home');
|
var html=navBar('home');
|
||||||
html+="<header class='wm-hero'>";
|
html+="<div class='wm-page-head'><h1>"+title+"</h1></div>";
|
||||||
if(brand.logoSvg){html+="<div class='wm-brand-logo'>"+brand.logoSvg+"</div>";}
|
html+="<div class='wm-card'>"+cardTitleHtml('status','Device status');
|
||||||
html+="<h1 class='wm-hero-title'>"+t+"</h1>";
|
if(summary){
|
||||||
if(ident){html+="<p class='wm-hero-sub'>"+ident+"</p>";}
|
html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";
|
||||||
if(brand.homeIntro){html+="<p class='wm-hero-intro'>"+esc(brand.homeIntro)+"</p>";}
|
}
|
||||||
html+="</header>";
|
if(timeoutSeconds>0){
|
||||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
html+="<div class='wm-portal-timeout-row'>";
|
||||||
if(summary){html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";}
|
html+="<p class='wm-status' id='wm-portal-timeout'></p>";
|
||||||
if(timeoutSeconds>0){html+="<p class='wm-status' id='wm-portal-timeout'></p>";}
|
html+="<button type='button' class='wm-icon-button'";
|
||||||
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+=" id='wm-reset-portal-timeout' aria-label='Reset portal timeout'";
|
||||||
html+="</div>";
|
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+=renderExtraHomeCards(boot.extraHomeCards||[]);
|
||||||
html+=deviceActionsHtml();
|
html+=deviceActionsHtml();
|
||||||
setView(html);
|
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 ---
|
// --- HTTP ---
|
||||||
@@ -360,27 +449,55 @@ function stopWifiScanPolling(){
|
|||||||
setScanRefreshEnabled(true);
|
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){
|
function startWifiScanPolling(box){
|
||||||
stopWifiScanPolling();
|
stopWifiScanPolling();
|
||||||
setScanRefreshEnabled(false);
|
setScanRefreshEnabled(false);
|
||||||
|
setWifiScanProgress(true,'Scanning Wi-Fi','Looking for nearby networks…');
|
||||||
var token=++_wmWifiScanPollToken;
|
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(){
|
function poll(){
|
||||||
api('/api/wifi/scan-status').then(function(res){
|
api('/api/wifi/scan-status').then(function(res){
|
||||||
if(token!==_wmWifiScanPollToken)return;
|
if(token!==_wmWifiScanPollToken)return;
|
||||||
var d={};
|
if(!res.ok)throw new Error('scan status unavailable');
|
||||||
try{d=JSON.parse(res.body);}catch(e){return;}
|
var data={};
|
||||||
|
try{data=JSON.parse(res.body);}catch(e){
|
||||||
|
if(retryAfterTemporaryRadioLoss())return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
box=getScanResultsBox()||box;
|
box=getScanResultsBox()||box;
|
||||||
if(box)box.innerHTML=renderScanList(d);
|
if(!data.scanning){
|
||||||
if(!d.scanning){
|
if(box)box.innerHTML=renderScanList(data);
|
||||||
|
setWifiScanProgress(false);
|
||||||
stopWifiScanPolling();
|
stopWifiScanPolling();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_wmWifiScanPollTimer=setTimeout(poll,800);
|
_wmWifiScanPollTimer=setTimeout(poll,800);
|
||||||
}).catch(function(){
|
}).catch(function(){
|
||||||
if(token!==_wmWifiScanPollToken)return;
|
if(token!==_wmWifiScanPollToken)return;
|
||||||
|
if(retryAfterTemporaryRadioLoss())return;
|
||||||
stopWifiScanPolling();
|
stopWifiScanPolling();
|
||||||
|
setWifiScanProgress(false);
|
||||||
box=getScanResultsBox()||box;
|
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();
|
poll();
|
||||||
@@ -434,21 +551,23 @@ function wifiRefresh(){
|
|||||||
var box=getScanResultsBox();
|
var box=getScanResultsBox();
|
||||||
if(!box)return;
|
if(!box)return;
|
||||||
setScanRefreshEnabled(false);
|
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){
|
api('/api/wifi/scan',{method:'POST'}).then(function(res){
|
||||||
box=getScanResultsBox()||box;
|
box=getScanResultsBox()||box;
|
||||||
if(!res.ok){
|
if(!res.ok){
|
||||||
setScanRefreshEnabled(true);
|
setScanRefreshEnabled(true);
|
||||||
if(box)box.innerHTML=renderScanError('Failed to start WiFi scan.');
|
setWifiScanProgress(false);
|
||||||
showToast('Failed to start WiFi scan.',true);
|
if(box)box.innerHTML=renderScanError('Failed to start Wi-Fi scan.');
|
||||||
|
showToast('Failed to start Wi-Fi scan.',true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startWifiScanPolling(box);
|
startWifiScanPolling(box);
|
||||||
}).catch(function(){
|
}).catch(function(){
|
||||||
stopWifiScanPolling();
|
stopWifiScanPolling();
|
||||||
|
setWifiScanProgress(false);
|
||||||
box=getScanResultsBox()||box;
|
box=getScanResultsBox()||box;
|
||||||
if(box)box.innerHTML=renderScanError('Failed to reach portal while starting scan.');
|
if(box)box.innerHTML=renderScanError('Failed to reach the portal while starting a scan.');
|
||||||
showToast('Failed to reach portal while starting scan.',true);
|
showToast('Failed to reach the portal while starting a scan.',true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +584,14 @@ function bindWifiViewEvents(){
|
|||||||
if(p)p.type=this.checked?'text':'password';
|
if(p)p.type=this.checked?'text':'password';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
var profileShowPass=document.querySelectorAll('.wm-showpass');
|
||||||
|
var profileIndex;
|
||||||
|
for(profileIndex=0;profileIndex<profileShowPass.length;profileIndex++){
|
||||||
|
profileShowPass[profileIndex].addEventListener('change',function(){
|
||||||
|
var target=$(this.getAttribute('data-target')||'');
|
||||||
|
if(target)target.type=this.checked?'text':'password';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var scanResults=$('wm-scan-results');
|
var scanResults=$('wm-scan-results');
|
||||||
if(scanResults){
|
if(scanResults){
|
||||||
@@ -473,8 +600,8 @@ function bindWifiViewEvents(){
|
|||||||
while(el && el !== scanResults){
|
while(el && el !== scanResults){
|
||||||
if(el.tagName==='A' && el.getAttribute('data-ssid')){
|
if(el.tagName==='A' && el.getAttribute('data-ssid')){
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var inp=$('wm-s');
|
var inp=$('wm-s0')||$('wm-s');
|
||||||
var pass=$('wm-p');
|
var pass=$('wm-p0')||$('wm-p');
|
||||||
if(inp)inp.value=el.getAttribute('data-ssid')||'';
|
if(inp)inp.value=el.getAttribute('data-ssid')||'';
|
||||||
if(pass){
|
if(pass){
|
||||||
if(pass.scrollIntoView)pass.scrollIntoView({behavior:'smooth',block:'center'});
|
if(pass.scrollIntoView)pass.scrollIntoView({behavior:'smooth',block:'center'});
|
||||||
@@ -491,48 +618,101 @@ function bindWifiViewEvents(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewWifi(){
|
function renderStationProfileFields(profiles){
|
||||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
|
var html='';
|
||||||
api('/api/wifi/meta').then(function(res){
|
var i;
|
||||||
try{var m=JSON.parse(res.body);}catch(e){m={};}
|
for(i=0;i<profiles.length;i++){
|
||||||
var html=navBar('wifi');
|
var p=profiles[i]||{};
|
||||||
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>";
|
var primary=i===0;
|
||||||
html+="<div class='wm-card'>"+cardTitleHtml('scan','Networks nearby');
|
var title=primary?'Primary WiFi':'Fallback WiFi (optional)';
|
||||||
html+="<div id='wm-scan-results'></div>";
|
var hint=primary
|
||||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
|
? 'This network is tried first for a new connection.'
|
||||||
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
|
: 'Tried when the primary network is unavailable.';
|
||||||
html+="<form id='wm-wifi-form'>";
|
html+="<form class='wm-card wm-station-profile wm-wifi-data-form' id='wm-wifi-profile-"+i+"'><h2>"+esc(title)+"</h2><p class='wm-help'>"+esc(hint)+"</p>";
|
||||||
html+=renderFieldList(m.wifiFields||[]);
|
html+="<label for='wm-s"+i+"'>SSID"+(primary?'':' (leave empty to disable)')+"</label>";
|
||||||
var wf=m.wifiFields||[];
|
html+="<input id='wm-s"+i+"' name='s"+i+"' type='text' autocomplete='username' maxlength='32' value='"+esc(p.ssid||'')+"'/>";
|
||||||
var hasPass=false;
|
html+="<label for='wm-p"+i+"'>Password</label>";
|
||||||
var i;
|
html+="<input id='wm-p"+i+"' class='wm-profile-password' name='p"+i+"' type='password' autocomplete='current-password' maxlength='64' placeholder='"+(p.passwordSet?'Configured — enter a new password to replace it':'Leave blank for an open network')+"'/>";
|
||||||
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
|
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass"+i+"' class='wm-showpass' data-target='wm-p"+i+"'/> <label for='wm-showpass"+i+"'>Show password</label></div>";
|
||||||
if(hasPass){
|
if(p.passwordSet){
|
||||||
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
|
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-clear"+i+"' name='clear"+i+"' value='1'/> <label for='wm-clear"+i+"'>Clear password / use an open network</label></div>";
|
||||||
}
|
}
|
||||||
html+=renderFieldList(m.staticFields||[]);
|
html+='</form>';
|
||||||
html+=renderFieldList(m.params||[]);
|
}
|
||||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('connect','Save and connect')+"</button></div></form>";
|
return html;
|
||||||
html+="<div id='wm-wifi-msg'></div></div>";
|
}
|
||||||
|
|
||||||
|
function viewWifi(){
|
||||||
|
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading Wi-Fi options…</p></div>");
|
||||||
|
api('/api/wifi/meta').then(function(res){
|
||||||
|
var meta={};
|
||||||
|
try{meta=JSON.parse(res.body);}catch(e){}
|
||||||
|
var html=navBar('wifi');
|
||||||
|
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(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(meta.staticFields||[]);
|
||||||
|
html+=renderFieldList(meta.params||[]);
|
||||||
|
html+="</form>";
|
||||||
|
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><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);
|
setView(html);
|
||||||
bindWifiViewEvents();
|
bindWifiViewEvents();
|
||||||
setScanRefreshEnabled(true);
|
setScanRefreshEnabled(true);
|
||||||
api('/api/wifi/scan-status').then(function(r2){
|
api('/api/wifi/scan-status').then(function(scanRes){
|
||||||
try{var d=JSON.parse(r2.body);}catch(e){d={};}
|
var scan={};
|
||||||
|
try{scan=JSON.parse(scanRes.body);}catch(e){}
|
||||||
var box=getScanResultsBox();
|
var box=getScanResultsBox();
|
||||||
if(shouldAutoStartWifiScan(d)){
|
if(shouldAutoStartWifiScan(scan)){
|
||||||
wifiRefresh();
|
wifiRefresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(box)box.innerHTML=renderScanList(d);
|
if(box)box.innerHTML=renderScanList(scan);
|
||||||
if(d&&d.scanning){startWifiScanPolling(box);}
|
if(scan&&scan.scanning)startWifiScanPolling(box);
|
||||||
}).catch(function(){
|
}).catch(function(){
|
||||||
var box=getScanResultsBox();
|
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);
|
setScanRefreshEnabled(true);
|
||||||
});
|
});
|
||||||
}).catch(function(){
|
}).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>");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,6 +720,33 @@ function viewWifi(){
|
|||||||
var _wmWifiConnectPollTimer=null;
|
var _wmWifiConnectPollTimer=null;
|
||||||
var _wmWifiConnectPollToken=0;
|
var _wmWifiConnectPollToken=0;
|
||||||
var _wmWifiConnectPending=false;
|
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(){
|
function stopWifiConnectPolling(){
|
||||||
_wmWifiConnectPollToken++;
|
_wmWifiConnectPollToken++;
|
||||||
@@ -549,56 +756,91 @@ 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(){
|
function pollWifiConnectStatus(){
|
||||||
stopWifiConnectPolling();
|
stopWifiConnectPolling();
|
||||||
var token=++_wmWifiConnectPollToken;
|
var token=++_wmWifiConnectPollToken;
|
||||||
function poll(){
|
function poll(){
|
||||||
api('/api/wifi/connect-status').then(function(res){
|
api('/api/wifi/connect-status').then(function(res){
|
||||||
if(token!==_wmWifiConnectPollToken)return;
|
if(token!==_wmWifiConnectPollToken)return;
|
||||||
var j={};
|
var data={};
|
||||||
var msg=$('wm-wifi-msg');
|
var msg=$('wm-wifi-msg');
|
||||||
try{j=JSON.parse(res.body);}catch(e){}
|
try{data=JSON.parse(res.body);}catch(e){}
|
||||||
if(msg&&j.message){
|
if(msg&&data.message)msg.innerHTML=esc(data.message);
|
||||||
msg.innerHTML=esc(j.message);
|
if(data.state==='success'){
|
||||||
}
|
|
||||||
if(j.state==='success'){
|
|
||||||
stopWifiConnectPolling();
|
stopWifiConnectPolling();
|
||||||
_wmWifiConnectPending=false;
|
_wmWifiConnectPending=false;
|
||||||
if(msg && j.stationIp){
|
if(!data.stationIp){
|
||||||
msg.innerHTML="Connected to WiFi. Redirecting to <a href='"+esc(j.redirectUrl||("http://"+j.stationIp+"/"))+"'>"+esc(j.stationIp)+"</a>…";
|
finishWifiSave();
|
||||||
|
if(msg)msg.innerHTML=esc(data.message||'Settings saved.');
|
||||||
|
showToast(data.message||'Settings saved.',false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
showToast(j.message||'WiFi connected',false);
|
var destination=data.redirectUrl||'';
|
||||||
if(j.redirectUrl){
|
setWifiSaveProgress(true,'Connected to Wi-Fi','Opening '+data.stationIp+'…');
|
||||||
setTimeout(function(){ window.location.href=j.redirectUrl; }, 1400);
|
if(msg){
|
||||||
}else{
|
msg.innerHTML="Connected. Opening <a href='"+esc(destination||('http://'+data.stationIp+'/'))+"'>";
|
||||||
location.hash='#/';
|
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();
|
stopWifiConnectPolling();
|
||||||
_wmWifiConnectPending=false;
|
_wmWifiConnectPending=false;
|
||||||
showToast(j.message||'WiFi connect failed',true);
|
finishWifiSave();
|
||||||
|
showToast(data.message||'Wi-Fi connection failed',true);
|
||||||
}else{
|
}else{
|
||||||
|
setWifiSaveProgress(true,'Connecting to Wi-Fi',data.message||'Waiting for the device to join the selected network…');
|
||||||
_wmWifiConnectPollTimer=setTimeout(poll,700);
|
_wmWifiConnectPollTimer=setTimeout(poll,700);
|
||||||
}
|
}
|
||||||
}).catch(function(){
|
}).catch(function(){
|
||||||
if(token!==_wmWifiConnectPollToken)return;
|
if(token!==_wmWifiConnectPollToken)return;
|
||||||
stopWifiConnectPolling();
|
stopWifiConnectPolling();
|
||||||
|
finishWifiSave();
|
||||||
var msg=$('wm-wifi-msg');
|
var msg=$('wm-wifi-msg');
|
||||||
if(msg){
|
if(msg)msg.innerHTML='Unable to check Wi-Fi connection status. Try again.';
|
||||||
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.';
|
|
||||||
}
|
|
||||||
_wmWifiConnectPending=false;
|
_wmWifiConnectPending=false;
|
||||||
|
showToast('Unable to check Wi-Fi connection status.',true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
poll();
|
poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.portalWifiSave=function(ev){
|
function collectWifiFormData(){
|
||||||
ev.preventDefault();
|
var fd=new FormData(), forms=document.querySelectorAll('.wm-wifi-data-form'), i;
|
||||||
var fd=new FormData(document.getElementById('wm-wifi-form'));
|
for(i=0;i<forms.length;i++){
|
||||||
|
new FormData(forms[i]).forEach(function(value,key){fd.append(key,value);});
|
||||||
|
}
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitWifi(action){
|
||||||
|
if(_wmWifiSaveInFlight)return false;
|
||||||
|
var fd=collectWifiFormData();
|
||||||
|
if(action)fd.append('stationAction',action);
|
||||||
var msg=$('wm-wifi-msg');
|
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...';
|
if(msg)msg.innerHTML='Submitting...';
|
||||||
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
|
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
|
||||||
var j={};
|
var j={};
|
||||||
@@ -608,10 +850,31 @@ window.portalWifiSave=function(ev){
|
|||||||
}
|
}
|
||||||
if(res.status===202){
|
if(res.status===202){
|
||||||
_wmWifiConnectPending=true;
|
_wmWifiConnectPending=true;
|
||||||
|
setWifiSaveProgress(true,'Connecting to WiFi',(j&&j.message)?j.message:'Waiting for the device to join the selected network...');
|
||||||
pollWifiConnectStatus();
|
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;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.portalWifiSave=function(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
return submitWifi(ev.submitter&&ev.submitter.value?ev.submitter.value:'connect');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.portalWifiAction=function(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
return submitWifi(ev.currentTarget.getAttribute('data-wm-wifi-action'));
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Info view ---
|
// --- Info view ---
|
||||||
@@ -620,7 +883,7 @@ function viewInfo(){
|
|||||||
api('/api/info').then(function(res){
|
api('/api/info').then(function(res){
|
||||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||||
var html=navBar('info');
|
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;
|
var st=d.status;
|
||||||
if(st && typeof st==='object' && !Array.isArray(st)){
|
if(st && typeof st==='object' && !Array.isArray(st)){
|
||||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
||||||
@@ -689,7 +952,7 @@ function viewSetup(){
|
|||||||
api('/api/params').then(function(res){
|
api('/api/params').then(function(res){
|
||||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||||
var html=navBar('setup');
|
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+="<div class='wm-card'><form id='wm-param-form'>";
|
||||||
html+=renderFieldList(d.params||[]);
|
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>";
|
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('save','Save parameters')+"</button></div>";
|
||||||
@@ -710,59 +973,100 @@ window.portalParamSave=function(ev){
|
|||||||
return false;
|
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(){
|
function viewUpdate(){
|
||||||
var pages=boot.pages||{};
|
var pages=boot.pages||{};
|
||||||
var pupd=pages.update||{};
|
var update=pages.update||{};
|
||||||
var html=navBar('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>";
|
html+="<div class='wm-page-head'><h1>Update firmware</h1>";
|
||||||
if(!pupd.visible){
|
html+="<p class='wm-page-desc'>Upload a firmware build for this device.</p></div>";
|
||||||
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this configuration.</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);
|
setView(html);
|
||||||
return;
|
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+="<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+="<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+="<div class='wm-form-actions'><button type='submit'";
|
||||||
html+="</form>";
|
html+=" class='wm-btn wm-btn--primary wm-btn--block'>";
|
||||||
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+=labelWithIcon('upload','Upload firmware')+"</button></div></form>";
|
||||||
html+="<pre id='wm-ota-log'></pre></div>";
|
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);
|
setView(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.portalOtaSubmit=function(ev){
|
window.portalOtaSubmit=function(ev){
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var fi=$('wm-ota-file');
|
var fileInput=$('wm-ota-file');
|
||||||
var logEl=$('wm-ota-log');
|
if(!fileInput||!fileInput.files||!fileInput.files[0]){
|
||||||
if(!fi||!fi.files||!fi.files[0]){
|
showToast('Choose a firmware file first.',true);
|
||||||
if(logEl)logEl.textContent='Choose a firmware file first.';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(logEl)logEl.textContent='Uploading…';
|
setOtaControlsDisabled(true);
|
||||||
|
setOtaProgress(true,'Uploading firmware','Preparing upload…');
|
||||||
var xhr=new XMLHttpRequest();
|
var xhr=new XMLHttpRequest();
|
||||||
xhr.upload.onprogress=function(e){
|
xhr.upload.onprogress=function(event){
|
||||||
if(!logEl||!e.lengthComputable)return;
|
if(!event.lengthComputable)return;
|
||||||
logEl.textContent='Uploading… '+Math.round(100*e.loaded/e.total)+'%';
|
var percent=100*event.loaded/event.total;
|
||||||
|
setOtaProgress(true,'Uploading firmware','Transferring firmware…',percent);
|
||||||
};
|
};
|
||||||
xhr.onreadystatechange=function(){
|
xhr.onreadystatechange=function(){
|
||||||
if(xhr.readyState!==4)return;
|
if(xhr.readyState!==4)return;
|
||||||
var t=xhr.responseText||'';
|
var text=xhr.responseText||'';
|
||||||
if(logEl)logEl.textContent=t;
|
var data={};
|
||||||
var httpOk=xhr.status>=200&&xhr.status<300;
|
try{data=JSON.parse(text);}catch(e){}
|
||||||
try{
|
var ok=xhr.status>=200&&xhr.status<300&&data.ok!==false;
|
||||||
var j=JSON.parse(t);
|
if(ok){
|
||||||
if(j&&j.message){
|
setOtaProgress(true,'Firmware updated','Restarting device…',100);
|
||||||
if(logEl)logEl.textContent=j.message;
|
showToast(data.message||'Firmware updated. Restarting device…',false);
|
||||||
showToast(j.message,j.ok===false);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
setOtaProgress(false);
|
||||||
}catch(e){}
|
setOtaControlsDisabled(false);
|
||||||
showToast(httpOk?'Upload finished.':('HTTP '+xhr.status),!httpOk);
|
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');
|
xhr.open('POST','/u');
|
||||||
var fd=new FormData();
|
var formData=new FormData();
|
||||||
fd.append('update',fi.files[0]);
|
formData.append('update',fileInput.files[0]);
|
||||||
xhr.send(fd);
|
xhr.send(formData);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -787,6 +1091,7 @@ function route(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bootPortal(){
|
function bootPortal(){
|
||||||
|
applyPortalFavicon();
|
||||||
route();
|
route();
|
||||||
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
|
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* Single-page portal shell: only HTML document served for GET /.
|
* Single-page portal shell: only HTML document served for GET /.
|
||||||
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
|
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
|
||||||
*
|
*
|
||||||
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%, %PORTAL_APPEND_JS%) are filled
|
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %PORTAL_THEME%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%) are filled
|
||||||
* per request in WiFiManagerHandlers::handleRoot; do not treat placeholders as a customization API.
|
* per request in WiFiManagerHandlers::handleRoot; do not treat placeholders as a customization API.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
|
|||||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||||
"<title>%PAGE_TITLE%</title>"
|
"<title>%PAGE_TITLE%</title>"
|
||||||
"%STYLES%"
|
"%STYLES%"
|
||||||
|
"%PORTAL_THEME%"
|
||||||
"</head>"
|
"</head>"
|
||||||
"<body class='portal'>"
|
"<body class='portal'>"
|
||||||
"<div id='wm-toast' class='wm-toast' aria-live='polite' role='status' style='display:none'></div>"
|
"<div id='wm-toast' class='wm-toast' aria-live='polite' role='status' style='display:none'></div>"
|
||||||
@@ -41,9 +42,6 @@ const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
|
|||||||
"<script>"
|
"<script>"
|
||||||
"%PORTAL_APP_JS%"
|
"%PORTAL_APP_JS%"
|
||||||
"</script>"
|
"</script>"
|
||||||
"<script>"
|
|
||||||
"%PORTAL_APPEND_JS%"
|
|
||||||
"</script>"
|
|
||||||
"</body>"
|
"</body>"
|
||||||
"</html>";
|
"</html>";
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ uint8_t WiFiManager::_lastconxresulttmp = WL_IDLE_STATUS;
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr size_t kMaxHostnameLength = 32;
|
constexpr size_t kMaxHostnameLength = 32;
|
||||||
constexpr uint8_t kSoftApStartMaxAttempts = 3;
|
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) {
|
bool isValidHostnameChar(char c) {
|
||||||
return isAlphaNumeric(c) || c == '-';
|
return isAlphaNumeric(c) || c == '-';
|
||||||
@@ -55,6 +60,25 @@ bool normalizeHostname(String& hostname) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char kDefaultPortalTitle[] PROGMEM = "WiFiManager";
|
||||||
|
|
||||||
|
bool isSafePortalText(const WiFiManagerPortalText& value) {
|
||||||
|
for (size_t i = 0; i < value.length(); ++i) {
|
||||||
|
const char c = value.at(i);
|
||||||
|
if (!(isAlphaNumeric(c) || c == '#' || c == '(' || c == ')' || c == ',' ||
|
||||||
|
c == '.' || c == '%' || c == ' ' || c == '-' || c == '/')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendPortalText(String& destination, const WiFiManagerPortalText& value) {
|
||||||
|
for (size_t i = 0; i < value.length(); ++i) {
|
||||||
|
destination += value.at(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,6 +172,7 @@ WiFiManager::WiFiManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WiFiManager::WiFiManagerInit(){
|
void WiFiManager::WiFiManagerInit(){
|
||||||
|
_portalBrand.title = WiFiManagerPortalText::progmem(kDefaultPortalTitle);
|
||||||
if (_logEnabled && (uint8_t)_runtimeMaxLevel >= (uint8_t)WiFiManagerLogLevel::Trace) {
|
if (_logEnabled && (uint8_t)_runtimeMaxLevel >= (uint8_t)WiFiManagerLogLevel::Trace) {
|
||||||
debugPlatformInfo();
|
debugPlatformInfo();
|
||||||
}
|
}
|
||||||
@@ -197,6 +222,496 @@ void WiFiManager::_end(){
|
|||||||
if(_userpersistent) WiFi.persistent(true); // reenable persistent, there is no getter we rely on _userpersistent
|
if(_userpersistent) WiFi.persistent(true); // reenable persistent, there is no getter we rely on _userpersistent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WiFiManager::setStationProfileStore(WiFiManagerStationProfileStore* store) {
|
||||||
|
_stationProfileStore = store;
|
||||||
|
_stationProfilesLoaded = false;
|
||||||
|
_stationCandidateActive = false;
|
||||||
|
_stationCandidateFromPortal = false;
|
||||||
|
_stationAttemptMask = 0;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
_stationStatus = wm_station_status_t();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::isStationProfileMode() const {
|
||||||
|
return _stationProfileStore != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::setStationRecoveryInterval(unsigned long intervalMs) {
|
||||||
|
if (intervalMs > 0) {
|
||||||
|
_stationRecoveryInterval = intervalMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const WiFiManagerStationProfiles& WiFiManager::getStationProfiles() const {
|
||||||
|
return _stationProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WiFiManager::wm_station_status_t& WiFiManager::getStationStatus() const {
|
||||||
|
return _stationStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::isStationProfileEnabled(const WiFiManagerStationProfiles& profiles, uint8_t slot) const {
|
||||||
|
return slot < WM_STATION_PROFILE_COUNT && profiles.slots[slot].enabled && profiles.slots[slot].ssid[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t WiFiManager::configuredStationProfileCount(const WiFiManagerStationProfiles& profiles) const {
|
||||||
|
uint8_t count = 0;
|
||||||
|
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
|
||||||
|
if (isStationProfileEnabled(profiles, slot)) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::validateStationProfiles(const WiFiManagerStationProfiles& profiles) const {
|
||||||
|
if (profiles.preferredSlot >= WM_STATION_PROFILE_COUNT || !isStationProfileEnabled(profiles, 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
|
||||||
|
const WiFiManagerStationProfile& profile = profiles.slots[slot];
|
||||||
|
if (!profile.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (memchr(profile.ssid, '\0', sizeof(profile.ssid)) == nullptr ||
|
||||||
|
memchr(profile.password, '\0', sizeof(profile.password)) == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles.lastSuccessfulSlot == WM_NO_STATION_PROFILE ||
|
||||||
|
isStationProfileEnabled(profiles, profiles.lastSuccessfulSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t WiFiManager::chooseStationProfile(const WiFiManagerStationProfiles& profiles,
|
||||||
|
bool preferLastSuccessful) const {
|
||||||
|
if (preferLastSuccessful && isStationProfileEnabled(profiles, profiles.lastSuccessfulSlot)) {
|
||||||
|
return profiles.lastSuccessfulSlot;
|
||||||
|
}
|
||||||
|
if (isStationProfileEnabled(profiles, profiles.preferredSlot)) {
|
||||||
|
return profiles.preferredSlot;
|
||||||
|
}
|
||||||
|
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
|
||||||
|
if (isStationProfileEnabled(profiles, slot)) {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return WM_NO_STATION_PROFILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WiFiManagerStationProfiles& WiFiManager::stationProfilesForAttempt() const {
|
||||||
|
return _stationCandidateActive ? _stationCandidate : _stationProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiManagerStationProfiles& WiFiManager::stationProfilesForAttempt() {
|
||||||
|
return _stationCandidateActive ? _stationCandidate : _stationProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long WiFiManager::stationAttemptTimeout() const {
|
||||||
|
// The synchronous legacy connection path allows an unlimited timeout. A
|
||||||
|
// profile controller must stay bounded so it can advance to the fallback.
|
||||||
|
return _connectTimeout > 0 ? _connectTimeout : 15000UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::hasUsableStationConnection() const {
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return WiFi.localIP() != IPAddress(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::startStationConnection(char const *apName, char const *apPassword) {
|
||||||
|
if (!isStationProfileMode()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_begin();
|
||||||
|
_stationPortalApName = apName ? apName : getDefaultAPName();
|
||||||
|
_stationPortalApPassword = apPassword ? apPassword : "";
|
||||||
|
_stationCandidateActive = false;
|
||||||
|
_stationCandidateFromPortal = false;
|
||||||
|
_stationAttemptMask = 0;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
_stationStatus = wm_station_status_t();
|
||||||
|
_stationStatus.state = WM_STATION_LOADING;
|
||||||
|
_stationStatus.message = F("Loading WiFi profiles");
|
||||||
|
|
||||||
|
WiFiManagerStationProfiles loaded;
|
||||||
|
if (_stationProfileStore->load(loaded)) {
|
||||||
|
_stationProfiles = loaded;
|
||||||
|
} else {
|
||||||
|
_stationProfiles = WiFiManagerStationProfiles();
|
||||||
|
}
|
||||||
|
_stationProfilesLoaded = true;
|
||||||
|
_stationStatus.configuredProfiles = configuredStationProfileCount(_stationProfiles);
|
||||||
|
|
||||||
|
if (!validateStationProfiles(_stationProfiles)) {
|
||||||
|
// In station-profile mode the caller store is the single source of
|
||||||
|
// truth. ESP32 can reconnect from credentials retained in its own NVS
|
||||||
|
// before the portal is started, which would otherwise bypass an empty or
|
||||||
|
// invalid external profile set. Clear only that SDK-owned state here;
|
||||||
|
// the external store remains untouched and is what the portal will save.
|
||||||
|
WiFi_Disconnect();
|
||||||
|
WiFi_eraseConfig();
|
||||||
|
_stationStatus.message = _stationStatus.configuredProfiles == 0
|
||||||
|
? F("No WiFi profiles configured")
|
||||||
|
: F("Stored WiFi profiles are invalid");
|
||||||
|
enterStationPortal();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginStationCycle(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::startStationCandidate(const WiFiManagerStationProfiles& candidate) {
|
||||||
|
if (!validateStationProfiles(candidate)) {
|
||||||
|
_stationStatus.state = WM_STATION_IDLE;
|
||||||
|
_stationStatus.message = F("WiFi profile candidate is invalid");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_stationProfilesLoaded && _stationProfileStore) {
|
||||||
|
WiFiManagerStationProfiles loaded;
|
||||||
|
_stationProfiles = _stationProfileStore->load(loaded)
|
||||||
|
? loaded
|
||||||
|
: WiFiManagerStationProfiles();
|
||||||
|
_stationProfilesLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_begin();
|
||||||
|
_stationCandidate = candidate;
|
||||||
|
_stationCandidateActive = true;
|
||||||
|
_stationCandidateFromPortal = configPortalActive;
|
||||||
|
_stationAttemptMask = 0;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
_stationStatus = wm_station_status_t();
|
||||||
|
_stationStatus.state = WM_STATION_LOADING;
|
||||||
|
_stationStatus.configuredProfiles = configuredStationProfileCount(candidate);
|
||||||
|
_stationStatus.message = F("WiFi profile candidate queued");
|
||||||
|
|
||||||
|
if (_stationCandidateFromPortal) {
|
||||||
|
_cpConnectStatus = WL_IDLE_STATUS;
|
||||||
|
_cpConnectStationIp = "";
|
||||||
|
_cpConnectState = wm_cp_connect_state_t::queued;
|
||||||
|
_cpConnectMessage = F("WiFi profile candidate queued");
|
||||||
|
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_QUEUED);
|
||||||
|
}
|
||||||
|
|
||||||
|
beginStationCycle(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::startStationCandidate(const WiFiManagerStationProfiles& candidate,
|
||||||
|
char const *apName, char const *apPassword) {
|
||||||
|
_stationPortalApName = apName ? apName : getDefaultAPName();
|
||||||
|
_stationPortalApPassword = apPassword ? apPassword : "";
|
||||||
|
return startStationCandidate(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::saveStationProfiles(const WiFiManagerStationProfiles& profiles) {
|
||||||
|
if (!isStationProfileMode() || !validateStationProfiles(profiles)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!_stationProfileStore->save(profiles)) {
|
||||||
|
_stationStatus.storageSaveFailed = true;
|
||||||
|
_stationStatus.message = F("WiFi profiles could not be saved");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stationProfiles = profiles;
|
||||||
|
_stationProfilesLoaded = true;
|
||||||
|
_stationStatus.configuredProfiles = configuredStationProfileCount(_stationProfiles);
|
||||||
|
_stationStatus.storageSaveFailed = false;
|
||||||
|
_stationStatus.message = F("WiFi profiles saved");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::clearStationProfiles() {
|
||||||
|
if (_stationProfileStore && !_stationProfileStore->clear()) {
|
||||||
|
_stationStatus.storageSaveFailed = true;
|
||||||
|
_stationStatus.message = F("WiFi profiles could not be cleared");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFi_Disconnect();
|
||||||
|
WiFi_eraseConfig();
|
||||||
|
_stationProfiles = WiFiManagerStationProfiles();
|
||||||
|
_stationCandidate = WiFiManagerStationProfiles();
|
||||||
|
_stationCandidateActive = false;
|
||||||
|
_stationCandidateFromPortal = false;
|
||||||
|
_stationProfilesLoaded = false;
|
||||||
|
_stationAttemptMask = 0;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
_stationEverConnected = false;
|
||||||
|
_stationStatus = wm_station_status_t();
|
||||||
|
_stationStatus.message = F("WiFi profiles cleared");
|
||||||
|
emitPortalEvent(WM_EVENT_STATION_PROFILES_CLEARED);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::beginStationCycle(bool preferLastSuccessful) {
|
||||||
|
_stationAttemptMask = 0;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
const uint8_t first = chooseStationProfile(stationProfilesForAttempt(), preferLastSuccessful);
|
||||||
|
if (first == WM_NO_STATION_PROFILE) {
|
||||||
|
enterStationPortal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
beginStationProfile(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WiFiManager::beginStationProfile(uint8_t slot) {
|
||||||
|
const WiFiManagerStationProfiles& profiles = stationProfilesForAttempt();
|
||||||
|
if (!isStationProfileEnabled(profiles, slot)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WiFiManagerStationProfile& profile = profiles.slots[slot];
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
_stationAttemptMask |= static_cast<uint8_t>(1U << slot);
|
||||||
|
_stationAttemptStartedAt = millis();
|
||||||
|
_stationStatus.state = WM_STATION_ATTEMPTING;
|
||||||
|
_stationStatus.attemptedSlot = slot;
|
||||||
|
_stationStatus.wifiStatus = WL_IDLE_STATUS;
|
||||||
|
_stationStatus.message = String(F("Connecting profile ")) + String(slot + 1);
|
||||||
|
emitPortalEvent(WM_EVENT_STATION_PROFILE_ATTEMPT);
|
||||||
|
|
||||||
|
WiFi.persistent(false);
|
||||||
|
WiFi.setAutoReconnect(false);
|
||||||
|
WiFi_enableSTA(true, false);
|
||||||
|
setSTAConfig();
|
||||||
|
if (_cleanConnect) {
|
||||||
|
WiFi_Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFi.begin(
|
||||||
|
profile.ssid,
|
||||||
|
profile.hasPassword ? profile.password : nullptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::queueStationProfile(uint8_t slot) {
|
||||||
|
_stationPendingSlot = slot;
|
||||||
|
_stationNextAttemptAt = millis() + kStationProfileSwitchDelayMs;
|
||||||
|
_stationStatus.state = WM_STATION_SWITCHING;
|
||||||
|
_stationStatus.message = String(F("Switching to profile ")) + String(slot + 1);
|
||||||
|
WiFi_Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, const String& message) {
|
||||||
|
if (!_stationCandidateFromPortal && !configPortalActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cpConnectStatus = status;
|
||||||
|
_cpConnectMessage = message;
|
||||||
|
_cpConnectStationIp = success ? WiFi.localIP().toString() : "";
|
||||||
|
_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();
|
||||||
|
}
|
||||||
|
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_SUCCESS);
|
||||||
|
if (_disableConfigPortal) {
|
||||||
|
shutdownConfigPortal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateConxResult(status);
|
||||||
|
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
const bool wasPortalCandidate = _stationCandidateFromPortal;
|
||||||
|
bool persisted = true;
|
||||||
|
|
||||||
|
if (wasCandidate) {
|
||||||
|
const WiFiManagerStationProfiles previous = _stationProfiles;
|
||||||
|
_stationCandidate.lastSuccessfulSlot = slot;
|
||||||
|
if (_stationProfileStore) {
|
||||||
|
persisted = _stationProfileStore->save(_stationCandidate);
|
||||||
|
}
|
||||||
|
if (persisted) {
|
||||||
|
_stationProfiles = _stationCandidate;
|
||||||
|
_stationProfilesLoaded = true;
|
||||||
|
} else {
|
||||||
|
_stationProfiles = previous;
|
||||||
|
}
|
||||||
|
_stationCandidateActive = false;
|
||||||
|
_stationCandidateFromPortal = false;
|
||||||
|
} else {
|
||||||
|
const bool changed = _stationProfiles.lastSuccessfulSlot != slot;
|
||||||
|
_stationProfiles.lastSuccessfulSlot = slot;
|
||||||
|
if (changed && _stationProfileStore) {
|
||||||
|
persisted = _stationProfileStore->save(_stationProfiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_stationEverConnected = true;
|
||||||
|
_stationStatus.state = WM_STATION_CONNECTED;
|
||||||
|
_stationStatus.activeSlot = slot;
|
||||||
|
_stationStatus.wifiStatus = WL_CONNECTED;
|
||||||
|
_stationStatus.lastConnectionWasCandidate = wasCandidate;
|
||||||
|
_stationStatus.storageSaveFailed = !persisted;
|
||||||
|
_stationStatus.message = persisted ? F("WiFi connected") : F("WiFi connected but profiles could not be saved");
|
||||||
|
emitPortalEvent(WM_EVENT_STATION_PROFILE_CONNECTED);
|
||||||
|
|
||||||
|
if (wasPortalCandidate) {
|
||||||
|
completePortalStationAttempt(persisted, persisted ? WL_CONNECTED : WL_CONNECT_FAILED,
|
||||||
|
_stationStatus.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::enterStationPortal() {
|
||||||
|
_stationStatus.state = WM_STATION_PORTAL;
|
||||||
|
if (_stationStatus.message.length() == 0) {
|
||||||
|
_stationStatus.message = F("WiFi configuration required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configPortalActive || !_enableConfigPortal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String apName = _stationPortalApName.length() ? _stationPortalApName : getDefaultAPName();
|
||||||
|
startConfigPortal(apName.c_str(), _stationPortalApPassword.length() ? _stationPortalApPassword.c_str() : NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::handleStationAttemptFailure(uint8_t status, const String& message) {
|
||||||
|
_stationStatus.wifiStatus = status;
|
||||||
|
_stationStatus.message = message;
|
||||||
|
const WiFiManagerStationProfiles& profiles = stationProfilesForAttempt();
|
||||||
|
|
||||||
|
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
|
||||||
|
const uint8_t bit = static_cast<uint8_t>(1U << slot);
|
||||||
|
if (isStationProfileEnabled(profiles, slot) && !(_stationAttemptMask & bit)) {
|
||||||
|
queueStationProfile(slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool wasCandidate = _stationCandidateActive;
|
||||||
|
const bool wasPortalCandidate = _stationCandidateFromPortal;
|
||||||
|
if (wasCandidate) {
|
||||||
|
_stationCandidateActive = false;
|
||||||
|
_stationCandidateFromPortal = false;
|
||||||
|
_stationAttemptMask = 0;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
if (wasPortalCandidate) {
|
||||||
|
_stationStatus.state = WM_STATION_PORTAL;
|
||||||
|
completePortalStationAttempt(false, status, message);
|
||||||
|
emitPortalEvent(WM_EVENT_STATION_PROFILE_FAILED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateStationProfiles(_stationProfiles)) {
|
||||||
|
_stationStatus.configuredProfiles = configuredStationProfileCount(_stationProfiles);
|
||||||
|
beginStationCycle(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitPortalEvent(WM_EVENT_STATION_PROFILE_FAILED);
|
||||||
|
if (_stationEverConnected) {
|
||||||
|
_stationStatus.state = WM_STATION_BACKOFF;
|
||||||
|
_stationBackoffStartedAt = millis();
|
||||||
|
_stationStatus.message = F("WiFi profiles unavailable; retrying shortly");
|
||||||
|
emitPortalEvent(WM_EVENT_STATION_BACKOFF);
|
||||||
|
} else {
|
||||||
|
enterStationPortal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiManager::processStationController() {
|
||||||
|
if (!isStationProfileMode() && !_stationCandidateActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_stationStatus.state) {
|
||||||
|
case WM_STATION_ATTEMPTING: {
|
||||||
|
const uint8_t status = WiFi.status();
|
||||||
|
_stationStatus.wifiStatus = status;
|
||||||
|
if (hasUsableStationConnection()) {
|
||||||
|
handleStationConnectionSuccess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((millis() - _stationAttemptStartedAt) >= stationAttemptTimeout()) {
|
||||||
|
handleStationAttemptFailure(WL_CONNECT_FAILED, F("WiFi connection timed out"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case WM_STATION_SWITCHING:
|
||||||
|
if (static_cast<long>(millis() - _stationNextAttemptAt) >= 0) {
|
||||||
|
const uint8_t nextSlot = _stationPendingSlot;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
if (nextSlot == WM_NO_STATION_PROFILE) {
|
||||||
|
handleStationAttemptFailure(WL_CONNECT_FAILED, F("WiFi profile switch lost its target"));
|
||||||
|
} else {
|
||||||
|
beginStationProfile(nextSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case WM_STATION_CONNECTED:
|
||||||
|
if (!hasUsableStationConnection()) {
|
||||||
|
emitPortalEvent(WM_EVENT_STATION_LINK_LOST);
|
||||||
|
_stationAttemptMask = 0;
|
||||||
|
_stationPendingSlot = WM_NO_STATION_PROFILE;
|
||||||
|
_stationNextAttemptAt = 0;
|
||||||
|
const uint8_t active = isStationProfileEnabled(_stationProfiles, _stationStatus.activeSlot)
|
||||||
|
? _stationStatus.activeSlot
|
||||||
|
: chooseStationProfile(_stationProfiles, true);
|
||||||
|
if (active == WM_NO_STATION_PROFILE) {
|
||||||
|
enterStationPortal();
|
||||||
|
} else {
|
||||||
|
beginStationProfile(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case WM_STATION_BACKOFF:
|
||||||
|
if ((millis() - _stationBackoffStartedAt) >= _stationRecoveryInterval) {
|
||||||
|
beginStationCycle(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
boolean WiFiManager::autoConnect() {
|
boolean WiFiManager::autoConnect() {
|
||||||
String ssid = getDefaultAPName();
|
String ssid = getDefaultAPName();
|
||||||
return autoConnect(ssid.c_str(), NULL);
|
return autoConnect(ssid.c_str(), NULL);
|
||||||
@@ -388,16 +903,6 @@ bool WiFiManager::startAP(){
|
|||||||
log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("StartAP with SSID: "),_apName);
|
log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("StartAP with SSID: "),_apName);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ESP8266
|
|
||||||
if(!WiFi.enableAP(true)) {
|
|
||||||
#ifndef WM_NO_LOG
|
|
||||||
log(WiFiManagerLogLevel::Error, kWiFiMgrLogSubsystem,F("[ERROR] enableAP failed!"));
|
|
||||||
#endif
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
delay(500); // workaround delay
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// setup optional soft AP static ip config
|
// setup optional soft AP static ip config
|
||||||
if (_ap_static_ip) {
|
if (_ap_static_ip) {
|
||||||
#ifndef WM_NO_LOG
|
#ifndef WM_NO_LOG
|
||||||
@@ -656,7 +1161,10 @@ boolean WiFiManager::process(){
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
processScan();
|
processScan();
|
||||||
processPortalConnect();
|
processStationController();
|
||||||
|
if (!_stationCandidateFromPortal) {
|
||||||
|
processPortalConnect();
|
||||||
|
}
|
||||||
|
|
||||||
if(webPortalActive || configPortalActive){
|
if(webPortalActive || configPortalActive){
|
||||||
// if timed out or abort, break
|
// if timed out or abort, break
|
||||||
@@ -704,7 +1212,6 @@ void WiFiManager::queuePortalConnect(const String& ssid, const String& pass) {
|
|||||||
|
|
||||||
void WiFiManager::processPortalConnect() {
|
void WiFiManager::processPortalConnect() {
|
||||||
const unsigned long now = millis();
|
const unsigned long now = millis();
|
||||||
const unsigned long successHandoffDelayMs = 2500UL;
|
|
||||||
auto finalizePortalConnectSuccess = [&]() {
|
auto finalizePortalConnectSuccess = [&]() {
|
||||||
_cpConnectState = wm_cp_connect_state_t::success;
|
_cpConnectState = wm_cp_connect_state_t::success;
|
||||||
if (_savewificallback != NULL) {
|
if (_savewificallback != NULL) {
|
||||||
@@ -806,14 +1313,15 @@ void WiFiManager::processPortalConnect() {
|
|||||||
? String(F("WiFi connected. Redirecting to ")) + _cpConnectStationIp
|
? String(F("WiFi connected. Redirecting to ")) + _cpConnectStationIp
|
||||||
: String(F("WiFi connected"));
|
: String(F("WiFi connected"));
|
||||||
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
|
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
|
||||||
_cpConnectCloseAt = now + successHandoffDelayMs;
|
_cpConnectCloseAt = now + kPortalSuccessHandoffFallbackMs;
|
||||||
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
|
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
|
||||||
} else {
|
} else {
|
||||||
finalizePortalConnectSuccess();
|
finalizePortalConnectSuccess();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL || status == WL_CONNECTION_LOST) {
|
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL ||
|
||||||
|
status == WL_CONNECTION_LOST) {
|
||||||
failPortalConnect(status, getWLStatusString(status));
|
failPortalConnect(status, getWLStatusString(status));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2095,20 +2603,78 @@ void WiFiManager::setWiFiAPHidden(bool hidden){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void WiFiManager::portalSetBrandTitle(const String& title) {
|
bool WiFiManager::canChangePortalPresentation() const {
|
||||||
_portalBrand.title = title;
|
return !configPortalActive && !webPortalActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiManager::portalSetContextIdentityText(const String& identityText) {
|
bool WiFiManager::isPortalThemeValid(const WiFiManagerPortalTheme& theme) const {
|
||||||
_portalBrand.identityTextOverride = identityText;
|
return isSafePortalText(theme.pageBackground) && isSafePortalText(theme.surface) &&
|
||||||
|
isSafePortalText(theme.text) && isSafePortalText(theme.mutedText) &&
|
||||||
|
isSafePortalText(theme.border) && isSafePortalText(theme.accent) &&
|
||||||
|
isSafePortalText(theme.accentHover) && isSafePortalText(theme.accentText) &&
|
||||||
|
isSafePortalText(theme.danger) && isSafePortalText(theme.dangerHover) &&
|
||||||
|
isSafePortalText(theme.success) && theme.cornerRadiusPx <= 64 &&
|
||||||
|
theme.smallCornerRadiusPx <= 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiManager::portalSetBrandHomeIntro(const String& text) {
|
void WiFiManager::rebuildPortalThemeStyle() {
|
||||||
_portalBrand.homeIntro = text;
|
_portalThemeStyle = "";
|
||||||
|
const bool hasColours = !_portalTheme.pageBackground.empty() || !_portalTheme.surface.empty() ||
|
||||||
|
!_portalTheme.text.empty() || !_portalTheme.mutedText.empty() || !_portalTheme.border.empty() ||
|
||||||
|
!_portalTheme.accent.empty() || !_portalTheme.accentHover.empty() ||
|
||||||
|
!_portalTheme.accentText.empty() || !_portalTheme.danger.empty() ||
|
||||||
|
!_portalTheme.dangerHover.empty() || !_portalTheme.success.empty();
|
||||||
|
if (!hasColours && _portalTheme.cornerRadiusPx == 0 && _portalTheme.smallCornerRadiusPx == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto appendToken = [this](const char* name, const WiFiManagerPortalText& value) {
|
||||||
|
if (value.empty()) return;
|
||||||
|
_portalThemeStyle += name;
|
||||||
|
_portalThemeStyle += ':';
|
||||||
|
appendPortalText(_portalThemeStyle, value);
|
||||||
|
_portalThemeStyle += ';';
|
||||||
|
};
|
||||||
|
|
||||||
|
_portalThemeStyle = F("<style id='wm-portal-theme'>:root{");
|
||||||
|
appendToken("--wm-bg", _portalTheme.pageBackground);
|
||||||
|
appendToken("--wm-surface", _portalTheme.surface);
|
||||||
|
appendToken("--wm-text", _portalTheme.text);
|
||||||
|
appendToken("--wm-muted", _portalTheme.mutedText);
|
||||||
|
appendToken("--wm-border", _portalTheme.border);
|
||||||
|
appendToken("--wm-brand", _portalTheme.accent);
|
||||||
|
appendToken("--wm-brand-hover", _portalTheme.accentHover);
|
||||||
|
appendToken("--wm-brand-text", _portalTheme.accentText);
|
||||||
|
appendToken("--wm-danger", _portalTheme.danger);
|
||||||
|
appendToken("--wm-danger-hover", _portalTheme.dangerHover);
|
||||||
|
appendToken("--wm-success", _portalTheme.success);
|
||||||
|
if (_portalTheme.cornerRadiusPx > 0) {
|
||||||
|
_portalThemeStyle += F("--wm-radius:");
|
||||||
|
_portalThemeStyle += String(_portalTheme.cornerRadiusPx);
|
||||||
|
_portalThemeStyle += F("px;");
|
||||||
|
}
|
||||||
|
if (_portalTheme.smallCornerRadiusPx > 0) {
|
||||||
|
_portalThemeStyle += F("--wm-radius-sm:");
|
||||||
|
_portalThemeStyle += String(_portalTheme.smallCornerRadiusPx);
|
||||||
|
_portalThemeStyle += F("px;");
|
||||||
|
}
|
||||||
|
_portalThemeStyle += F("}</style>");
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiManager::portalSetBrandLogoSvg(const String& svgMarkup) {
|
bool WiFiManager::setPortalConfig(const WiFiManagerPortalConfig& config) {
|
||||||
_portalBrand.logoSvg = svgMarkup;
|
if (!canChangePortalPresentation() || !isPortalThemeValid(config.theme)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_portalBrand.title = config.title.empty()
|
||||||
|
? WiFiManagerPortalText::progmem(kDefaultPortalTitle)
|
||||||
|
: config.title;
|
||||||
|
_portalBrand.identityTextOverride = config.identityText;
|
||||||
|
_portalBrand.tagline = config.tagline;
|
||||||
|
_portalBrand.logo = config.logo;
|
||||||
|
_portalBrand.logoAltText = config.logoAltText;
|
||||||
|
_portalTheme = config.theme;
|
||||||
|
rebuildPortalThemeStyle();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiManager::portalSetPageInfoVisible(bool visible) {
|
void WiFiManager::portalSetPageInfoVisible(bool visible) {
|
||||||
@@ -2123,6 +2689,7 @@ void WiFiManager::portalSetPageSetupVisible(bool visible) {
|
|||||||
_portalPages.setupVisible = visible;
|
_portalPages.setupVisible = visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void WiFiManager::portalSetActionEraseVisible(bool visible) {
|
void WiFiManager::portalSetActionEraseVisible(bool visible) {
|
||||||
_portalActions.eraseVisible = visible;
|
_portalActions.eraseVisible = visible;
|
||||||
}
|
}
|
||||||
@@ -2227,17 +2794,6 @@ void WiFiManager::portalClearHomeCards() {
|
|||||||
_portalStructured.homeCards.clear();
|
_portalStructured.homeCards.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiManager::portalAppendCss(const String& css) {
|
|
||||||
_portalAssets.appendedCss += css;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WiFiManager::portalOverrideCss(const String& css) {
|
|
||||||
_portalAssets.overriddenCss = css;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WiFiManager::portalAppendJs(const String& js) {
|
|
||||||
_portalAssets.appendedJs += js;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the config portal is running
|
* check if the config portal is running
|
||||||
|
|||||||
@@ -37,8 +37,25 @@ static void jsonAppendEscaped(String& out, const String& s) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void jsonAppendEscaped(String& out, const WiFiManagerPortalText& text) {
|
||||||
|
for (size_t i = 0; i < text.length(); ++i) {
|
||||||
|
const char c = text.at(i);
|
||||||
|
if (c == '"') {
|
||||||
|
out += F("\\\"");
|
||||||
|
} else if (c == '\\') {
|
||||||
|
out += F("\\\\");
|
||||||
|
} else if (c == '\n') {
|
||||||
|
out += F("\\n");
|
||||||
|
} else if (c != '\r') {
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
const char kEmptyPortalPlaceholder[] PROGMEM = "";
|
||||||
|
|
||||||
inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES) {
|
inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES) {
|
||||||
if (extraBytes == 0) return;
|
if (extraBytes == 0) return;
|
||||||
const size_t targetLen = page.length() + extraBytes;
|
const size_t targetLen = page.length() + extraBytes;
|
||||||
@@ -60,24 +77,26 @@ void configureDynamicStringDescriptor(DynamicDataDescriptor& descriptor, String&
|
|||||||
descriptor.userData = &value;
|
descriptor.userData = &value;
|
||||||
}
|
}
|
||||||
|
|
||||||
String readProgmemString(const char* p) {
|
const char* portalTextGetter(void* userData) {
|
||||||
return String(FPSTR(p));
|
const auto* text = static_cast<const WiFiManagerPortalText*>(userData);
|
||||||
|
return text && text->storage == WiFiManagerPortalStorage::Ram && text->data
|
||||||
|
? text->data
|
||||||
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
String stripStyleWrapper(const String& styleBlock) {
|
size_t portalTextLengthGetter(const char* /*data*/, void* userData) {
|
||||||
String css = styleBlock;
|
const auto* text = static_cast<const WiFiManagerPortalText*>(userData);
|
||||||
const String openTag = F("<style>");
|
return text ? text->length() : 0;
|
||||||
const String closeTag = F("</style>");
|
|
||||||
|
|
||||||
if (css.startsWith(openTag)) {
|
|
||||||
css.remove(0, openTag.length());
|
|
||||||
}
|
|
||||||
if (css.endsWith(closeTag)) {
|
|
||||||
css.remove(css.length() - closeTag.length());
|
|
||||||
}
|
|
||||||
return css;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void configurePortalTextDescriptor(DynamicDataDescriptor& descriptor,
|
||||||
|
const WiFiManagerPortalText& text) {
|
||||||
|
descriptor.getter = &portalTextGetter;
|
||||||
|
descriptor.getLength = &portalTextLengthGetter;
|
||||||
|
descriptor.userData = const_cast<WiFiManagerPortalText*>(&text);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const char* portalHomeCardKindJson(PortalHomeCardKind k) {
|
const char* portalHomeCardKindJson(PortalHomeCardKind k) {
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case PortalHomeCardKind::Text:
|
case PortalHomeCardKind::Text:
|
||||||
@@ -110,31 +129,27 @@ AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shell contract (must match templates/RootShell.h — customize UI via WiFiManager portal* APIs + bootstrap JSON, not registries):
|
// Shell contract (must match templates/RootShell.h):
|
||||||
// %PAGE_TITLE% -> document title (from WiFiManager title state)
|
// %PAGE_TITLE% -> document title (from WiFiManager title state)
|
||||||
// %STYLES% -> portal CSS (embedded default + portalAppendCss / portalOverrideCss)
|
// %STYLES% -> immutable built-in portal stylesheet in PROGMEM
|
||||||
|
// %PORTAL_THEME% -> optional validated semantic-token override
|
||||||
// %BOOTSTRAP_JSON% -> initial SPA runtime payload
|
// %BOOTSTRAP_JSON% -> initial SPA runtime payload
|
||||||
// %PORTAL_APP_JS% -> embedded SPA source
|
// %PORTAL_APP_JS% -> embedded SPA source
|
||||||
// %PORTAL_APPEND_JS% -> consumer hook script (portalAppendJs)
|
|
||||||
|
|
||||||
struct PortalShellRenderBundle {
|
struct PortalShellRenderBundle {
|
||||||
String bootstrapJson;
|
String bootstrapJson;
|
||||||
String pageTitleStatic;
|
WiFiManagerPortalText pageTitle;
|
||||||
String stylesStatic;
|
|
||||||
String appendJsStatic;
|
|
||||||
PlaceholderRegistry registry;
|
PlaceholderRegistry registry;
|
||||||
TemplateContext context;
|
TemplateContext context;
|
||||||
DynamicDataDescriptor bootstrapDescriptor;
|
DynamicDataDescriptor bootstrapDescriptor;
|
||||||
DynamicDataDescriptor pageTitleDescriptor;
|
DynamicDataDescriptor pageTitleDescriptor;
|
||||||
DynamicDataDescriptor stylesDescriptor;
|
DynamicDataDescriptor themeDescriptor;
|
||||||
DynamicDataDescriptor appendJsDescriptor;
|
|
||||||
|
|
||||||
PortalShellRenderBundle()
|
PortalShellRenderBundle()
|
||||||
: registry(WM_TEMPLATE_REGISTRY_CAPACITY),
|
: registry(WM_TEMPLATE_REGISTRY_CAPACITY),
|
||||||
bootstrapDescriptor{},
|
bootstrapDescriptor{},
|
||||||
pageTitleDescriptor{},
|
pageTitleDescriptor{},
|
||||||
stylesDescriptor{},
|
themeDescriptor{} {}
|
||||||
appendJsDescriptor{} {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom HTML attribute strings (e.g. from DeviceFramework generateCustomHTML) may include
|
// Custom HTML attribute strings (e.g. from DeviceFramework generateCustomHTML) may include
|
||||||
@@ -150,24 +165,6 @@ bool portalCustomAttrsIndicatePassword(const String& customAttrs) {
|
|||||||
|
|
||||||
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
|
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
|
||||||
|
|
||||||
String WiFiManagerHandlers::composePortalStylesheet() const {
|
|
||||||
String css;
|
|
||||||
if (_wm != nullptr && _wm->_portalAssets.overriddenCss.length() > 0) {
|
|
||||||
css = _wm->_portalAssets.overriddenCss;
|
|
||||||
} else {
|
|
||||||
css = stripStyleWrapper(readProgmemString(CSS_STYLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_wm != nullptr && _wm->_portalAssets.appendedCss.length() > 0) {
|
|
||||||
css += _wm->_portalAssets.appendedCss;
|
|
||||||
}
|
|
||||||
|
|
||||||
String out = F("<style>");
|
|
||||||
out += css;
|
|
||||||
out += F("</style>");
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WiFiManagerHandlers::appendPortalExtraInfoSectionsJson(String& json, bool& first) {
|
void WiFiManagerHandlers::appendPortalExtraInfoSectionsJson(String& json, bool& first) {
|
||||||
if (_wm == nullptr) {
|
if (_wm == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -199,7 +196,6 @@ void WiFiManagerHandlers::appendPortalExtraInfoSectionsJson(String& json, bool&
|
|||||||
json += F("]}");
|
json += F("]}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiManagerHandlers::appendPortalExtraHomeCardsJson(String& json, bool& first) {
|
void WiFiManagerHandlers::appendPortalExtraHomeCardsJson(String& json, bool& first) {
|
||||||
if (_wm == nullptr) {
|
if (_wm == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -686,25 +682,24 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
|
|||||||
// - WiFiManagerHandlers owns request-time shell assembly; WiFiManagerServer owns HTTP lifecycle only.
|
// - WiFiManagerHandlers owns request-time shell assembly; WiFiManagerServer owns HTTP lifecycle only.
|
||||||
// - Build one request-scoped PortalShellRenderBundle.
|
// - Build one request-scoped PortalShellRenderBundle.
|
||||||
// - Populate one request-scoped PlaceholderRegistry with shell defaults + request payloads.
|
// - Populate one request-scoped PlaceholderRegistry with shell defaults + request payloads.
|
||||||
// - Render WM_ROOT_SHELL_TEMPLATE.
|
// - Render WM_ROOT_SHELL_TEMPLATE with static CSS plus an optional small theme block.
|
||||||
auto bundle = std::make_shared<PortalShellRenderBundle>();
|
auto bundle = std::make_shared<PortalShellRenderBundle>();
|
||||||
const bool hasCustomStyles = _wm != nullptr &&
|
bundle->pageTitle = _wm ? _wm->_portalBrand.title : WiFiManagerPortalText{};
|
||||||
(_wm->_portalAssets.overriddenCss.length() > 0 || _wm->_portalAssets.appendedCss.length() > 0);
|
|
||||||
bundle->pageTitleStatic = _wm ? _wm->_portalBrand.title : String();
|
|
||||||
bundle->bootstrapJson = buildPortalBootstrapJson();
|
bundle->bootstrapJson = buildPortalBootstrapJson();
|
||||||
bundle->appendJsStatic = _wm ? _wm->_portalAssets.appendedJs : String();
|
bundle->registry.registerProgmemData("%STYLES%", CSS_STYLE);
|
||||||
bundle->registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
|
bundle->registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
|
||||||
if (hasCustomStyles) {
|
if (_wm) {
|
||||||
bundle->stylesStatic = composePortalStylesheet();
|
configureDynamicStringDescriptor(bundle->themeDescriptor, _wm->_portalThemeStyle);
|
||||||
configureDynamicStringDescriptor(bundle->stylesDescriptor, bundle->stylesStatic);
|
bundle->registry.registerDynamicData("%PORTAL_THEME%", &bundle->themeDescriptor);
|
||||||
bundle->registry.registerDynamicData("%STYLES%", &bundle->stylesDescriptor);
|
|
||||||
} else {
|
} else {
|
||||||
bundle->registry.registerProgmemData("%STYLES%", CSS_STYLE);
|
bundle->registry.registerProgmemData("%PORTAL_THEME%", kEmptyPortalPlaceholder);
|
||||||
|
}
|
||||||
|
if (bundle->pageTitle.storage == WiFiManagerPortalStorage::Progmem && bundle->pageTitle.data) {
|
||||||
|
bundle->registry.registerProgmemData("%PAGE_TITLE%", bundle->pageTitle.data);
|
||||||
|
} else {
|
||||||
|
configurePortalTextDescriptor(bundle->pageTitleDescriptor, bundle->pageTitle);
|
||||||
|
bundle->registry.registerDynamicData("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
|
||||||
}
|
}
|
||||||
configureDynamicStringDescriptor(bundle->appendJsDescriptor, bundle->appendJsStatic);
|
|
||||||
bundle->registry.registerDynamicData("%PORTAL_APPEND_JS%", &bundle->appendJsDescriptor);
|
|
||||||
configureDynamicStringDescriptor(bundle->pageTitleDescriptor, bundle->pageTitleStatic);
|
|
||||||
bundle->registry.registerDynamicData("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
|
|
||||||
configureDynamicStringDescriptor(bundle->bootstrapDescriptor, bundle->bootstrapJson);
|
configureDynamicStringDescriptor(bundle->bootstrapDescriptor, bundle->bootstrapJson);
|
||||||
bundle->registry.registerDynamicData("%BOOTSTRAP_JSON%", &bundle->bootstrapDescriptor);
|
bundle->registry.registerDynamicData("%BOOTSTRAP_JSON%", &bundle->bootstrapDescriptor);
|
||||||
|
|
||||||
@@ -783,6 +778,62 @@ void WiFiManagerHandlers::applyWifiAndParamsFromRequest(AsyncWebServerRequest *r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WiFiManagerHandlers::buildStationProfilesFromRequest(
|
||||||
|
AsyncWebServerRequest *request, WiFiManagerStationProfiles& profiles) {
|
||||||
|
profiles = _wm->_stationProfiles;
|
||||||
|
if (!_wm->validateStationProfiles(profiles)) {
|
||||||
|
profiles = WiFiManagerStationProfiles();
|
||||||
|
}
|
||||||
|
profiles.preferredSlot = 0;
|
||||||
|
|
||||||
|
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
|
||||||
|
WiFiManagerStationProfile& profile = profiles.slots[slot];
|
||||||
|
const String ssidName = String(F("s")) + String(slot);
|
||||||
|
const String passwordName = String(F("p")) + String(slot);
|
||||||
|
const String clearName = String(F("clear")) + String(slot);
|
||||||
|
|
||||||
|
if (request->hasParam(ssidName.c_str(), true)) {
|
||||||
|
const String ssid = request->getParam(ssidName.c_str(), true)->value();
|
||||||
|
if (ssid.length() >= sizeof(profile.ssid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memset(profile.ssid, 0, sizeof(profile.ssid));
|
||||||
|
memcpy(profile.ssid, ssid.c_str(), ssid.length());
|
||||||
|
profile.enabled = ssid.length() > 0;
|
||||||
|
if (!profile.enabled) {
|
||||||
|
profile.hasPassword = false;
|
||||||
|
memset(profile.password, 0, sizeof(profile.password));
|
||||||
|
if (profiles.lastSuccessfulSlot == slot) {
|
||||||
|
profiles.lastSuccessfulSlot = WM_NO_STATION_PROFILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request->hasParam(clearName.c_str(), true)) {
|
||||||
|
profile.hasPassword = false;
|
||||||
|
memset(profile.password, 0, sizeof(profile.password));
|
||||||
|
} else if (request->hasParam(passwordName.c_str(), true)) {
|
||||||
|
const String password = request->getParam(passwordName.c_str(), true)->value();
|
||||||
|
if (password.length() >= sizeof(profile.password)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// A blank password means "unchanged". Explicit clear is used for an
|
||||||
|
// open network so a browser never erases a stored secret by accident.
|
||||||
|
if (password.length() > 0) {
|
||||||
|
memset(profile.password, 0, sizeof(profile.password));
|
||||||
|
memcpy(profile.password, password.c_str(), password.length());
|
||||||
|
profile.hasPassword = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profiles.lastSuccessfulSlot != WM_NO_STATION_PROFILE &&
|
||||||
|
!_wm->isStationProfileEnabled(profiles, profiles.lastSuccessfulSlot)) {
|
||||||
|
profiles.lastSuccessfulSlot = WM_NO_STATION_PROFILE;
|
||||||
|
}
|
||||||
|
return _wm->validateStationProfiles(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
void WiFiManagerHandlers::doParamSave(WiFiManager::WiFiManagerRequestArgs requestArgs){
|
void WiFiManagerHandlers::doParamSave(WiFiManager::WiFiManagerRequestArgs requestArgs){
|
||||||
if ( _wm->_presaveparamscallback != NULL) {
|
if ( _wm->_presaveparamscallback != NULL) {
|
||||||
_wm->_presaveparamscallback();
|
_wm->_presaveparamscallback();
|
||||||
@@ -1013,14 +1064,16 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
|
|||||||
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
|
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
|
||||||
String json;
|
String json;
|
||||||
reservePage(json, 1200);
|
reservePage(json, 1200);
|
||||||
json += F("{\"contractVersion\":2");
|
json += F("{\"contractVersion\":3");
|
||||||
json += F(",\"brand\":{");
|
json += F(",\"brand\":{");
|
||||||
json += F("\"title\":\"");
|
json += F("\"title\":\"");
|
||||||
jsonAppendEscaped(json, _wm->_portalBrand.title);
|
jsonAppendEscaped(json, _wm->_portalBrand.title);
|
||||||
json += F("\",\"homeIntro\":\"");
|
json += F("\",\"tagline\":\"");
|
||||||
jsonAppendEscaped(json, _wm->_portalBrand.homeIntro);
|
jsonAppendEscaped(json, _wm->_portalBrand.tagline);
|
||||||
json += F("\",\"logoSvg\":\"");
|
json += F("\",\"logoSvg\":\"");
|
||||||
jsonAppendEscaped(json, _wm->_portalBrand.logoSvg);
|
jsonAppendEscaped(json, _wm->_portalBrand.logo.svg);
|
||||||
|
json += F("\",\"logoAltText\":\"");
|
||||||
|
jsonAppendEscaped(json, _wm->_portalBrand.logoAltText);
|
||||||
json += F("\"}");
|
json += F("\"}");
|
||||||
json += F(",\"context\":{");
|
json += F(",\"context\":{");
|
||||||
json += F("\"portalActive\":");
|
json += F("\"portalActive\":");
|
||||||
@@ -1038,15 +1091,12 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
|
|||||||
json += String(timeoutRemainingSeconds);
|
json += String(timeoutRemainingSeconds);
|
||||||
}
|
}
|
||||||
json += F(",\"identityText\":\"");
|
json += F(",\"identityText\":\"");
|
||||||
{
|
if (!_wm->_portalBrand.identityTextOverride.empty()) {
|
||||||
String idText;
|
jsonAppendEscaped(json, _wm->_portalBrand.identityTextOverride);
|
||||||
if (_wm->_portalBrand.identityTextOverride.length() > 0) {
|
} else {
|
||||||
idText = _wm->_portalBrand.identityTextOverride;
|
const String idText = _wm->configPortalActive
|
||||||
} else if (_wm->configPortalActive) {
|
? _wm->_apName
|
||||||
idText = _wm->_apName;
|
: _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
|
||||||
} else {
|
|
||||||
idText = _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
|
|
||||||
}
|
|
||||||
jsonAppendEscaped(json, idText);
|
jsonAppendEscaped(json, idText);
|
||||||
}
|
}
|
||||||
json += F("\",\"statusSummary\":\"");
|
json += F("\",\"statusSummary\":\"");
|
||||||
@@ -1193,6 +1243,48 @@ void WiFiManagerHandlers::handleApiWifiScan(AsyncWebServerRequest *request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String WiFiManagerHandlers::buildApiWifiMetaJson() {
|
String WiFiManagerHandlers::buildApiWifiMetaJson() {
|
||||||
|
if (_wm->isStationProfileMode()) {
|
||||||
|
const WiFiManagerStationProfiles& profiles = _wm->getStationProfiles();
|
||||||
|
const WiFiManager::wm_station_status_t& station = _wm->getStationStatus();
|
||||||
|
String json = F("{\"profiles\":[");
|
||||||
|
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
|
||||||
|
if (slot > 0) json += ',';
|
||||||
|
const WiFiManagerStationProfile& profile = profiles.slots[slot];
|
||||||
|
json += F("{\"slot\":\"");
|
||||||
|
json += slot == 0 ? F("primary") : F("fallback");
|
||||||
|
json += F("\",\"configured\":");
|
||||||
|
json += profile.enabled ? F("true") : F("false");
|
||||||
|
json += F(",\"ssid\":\"");
|
||||||
|
jsonAppendEscaped(json, profile.enabled ? String(profile.ssid) : String());
|
||||||
|
json += F("\",\"passwordSet\":");
|
||||||
|
json += profile.hasPassword ? F("true") : F("false");
|
||||||
|
json += F("}");
|
||||||
|
}
|
||||||
|
json += F("],\"activeSlot\":");
|
||||||
|
if (station.activeSlot == WM_NO_STATION_PROFILE) json += F("null");
|
||||||
|
else json += station.activeSlot == 0 ? F("\"primary\"") : F("\"fallback\"");
|
||||||
|
json += F(",\"state\":");
|
||||||
|
switch (station.state) {
|
||||||
|
case WiFiManager::WM_STATION_ATTEMPTING: json += F("\"connecting\""); break;
|
||||||
|
case WiFiManager::WM_STATION_CONNECTED: json += F("\"connected\""); break;
|
||||||
|
case WiFiManager::WM_STATION_BACKOFF: json += F("\"backoff\""); break;
|
||||||
|
case WiFiManager::WM_STATION_PORTAL: json += F("\"portal\""); break;
|
||||||
|
default: json += F("\"idle\""); break;
|
||||||
|
}
|
||||||
|
json += F(",\"wifiFields\":[],\"staticFields\":[");
|
||||||
|
bool first = true;
|
||||||
|
appendPortalJsonStaticFields(json, first);
|
||||||
|
json += F("],\"params\":[");
|
||||||
|
first = true;
|
||||||
|
if (_wm->_portalLayout.paramsOnWifiPage && _wm->getParametersCount() > 0) {
|
||||||
|
appendPortalJsonCustomParams(json, first);
|
||||||
|
}
|
||||||
|
json += F("],\"actions\":{\"canRefreshScan\":true,\"showBack\":");
|
||||||
|
json += _wm->_portalActions.backVisible ? F("true") : F("false");
|
||||||
|
json += F("}}");
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
String ssidPlaceholder = _wm->WiFi_SSID();
|
String ssidPlaceholder = _wm->WiFi_SSID();
|
||||||
String passwordPlaceholder = "";
|
String passwordPlaceholder = "";
|
||||||
switch (_wm->_portalPasswordPlaceholderMode) {
|
switch (_wm->_portalPasswordPlaceholderMode) {
|
||||||
@@ -1240,6 +1332,30 @@ void WiFiManagerHandlers::handleApiWifiSave(AsyncWebServerRequest *request) {
|
|||||||
#endif
|
#endif
|
||||||
handleRequest(request);
|
handleRequest(request);
|
||||||
applyWifiAndParamsFromRequest(request);
|
applyWifiAndParamsFromRequest(request);
|
||||||
|
if (_wm->isStationProfileMode()) {
|
||||||
|
WiFiManagerStationProfiles candidate;
|
||||||
|
if (!buildStationProfilesFromRequest(request, candidate)) {
|
||||||
|
sendApiJson(request, 400, F("{\"ok\":false,\"message\":\"Primary WiFi is required and SSID/password lengths must be valid\"}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool saveForLater = request->hasParam("stationAction", true) &&
|
||||||
|
request->getParam("stationAction", true)->value() == F("save");
|
||||||
|
if (saveForLater) {
|
||||||
|
if (!_wm->saveStationProfiles(candidate)) {
|
||||||
|
sendApiJson(request, 500, F("{\"ok\":false,\"message\":\"WiFi profiles could not be saved\"}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendApiJson(request, 200, F("{\"ok\":true,\"message\":\"WiFi profiles saved for later\"}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_wm->startStationCandidate(candidate)) {
|
||||||
|
sendApiJson(request, 400, F("{\"ok\":false,\"message\":\"WiFi profile candidate was rejected\"}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendApiJson(request, 202,
|
||||||
|
F("{\"ok\":true,\"message\":\"WiFi profiles accepted\",\"next\":{\"poll\":\"/api/wifi/connect-status\"}}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
_wm->queuePortalConnect(_wm->_ssid, _wm->_pass);
|
_wm->queuePortalConnect(_wm->_ssid, _wm->_pass);
|
||||||
sendApiJson(
|
sendApiJson(
|
||||||
request, 202,
|
request, 202,
|
||||||
@@ -1288,6 +1404,33 @@ void WiFiManagerHandlers::handleApiWifiConnectStatus(AsyncWebServerRequest *requ
|
|||||||
sendApiJson(request, 200, buildApiWifiConnectStatusJson());
|
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 WiFiManagerHandlers::buildApiParamsGetJson() {
|
||||||
String json = F("{\"params\":[");
|
String json = F("{\"params\":[");
|
||||||
bool first = true;
|
bool first = true;
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ void WiFiManagerServer::registerRoutes() {
|
|||||||
server->on(WM_G(R_api_wifi_connect_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
server->on(WM_G(R_api_wifi_connect_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||||
this->_handlers->handleApiWifiConnectStatus(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) {
|
server->on(WM_G(R_api_params), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||||
this->_handlers->handleApiParamsGet(request);
|
this->_handlers->handleApiParamsGet(request);
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "WiFiManager",
|
"name": "WiFiManager",
|
||||||
"version": "3.0.4",
|
"version": "3.2.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"wifi",
|
"wifi",
|
||||||
"wi-fi",
|
"wi-fi",
|
||||||
@@ -40,31 +40,30 @@
|
|||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
{
|
{
|
||||||
"owner": "ESP32Async",
|
"owner": "esp32async",
|
||||||
"name": "ESPAsyncWebServer",
|
"name": "ESPAsyncWebServer",
|
||||||
"version": "3.9.1"
|
"version": "3.9.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"owner": "ESP32Async",
|
"owner": "esp32async",
|
||||||
"name": "ESPAsyncTCP",
|
"name": "ESPAsyncTCP",
|
||||||
"version": "2.0.0",
|
"version": "^2.0.0",
|
||||||
"platforms": "espressif8266"
|
"platforms": "espressif8266"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"owner": "ESP32Async",
|
"owner": "esp32async",
|
||||||
"name": "AsyncTCP",
|
"name": "AsyncTCP",
|
||||||
"version": "3.4.9",
|
"version": "^3.4.9",
|
||||||
"platforms": "espressif32"
|
"platforms": "espressif32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "DeviceFrameworkTemplateEngine",
|
"name": "DeviceFrameworkTemplateEngine",
|
||||||
"version": "https://github.com/alexhopeoconnor/DFTE.git#v1.0.2"
|
"version": "https://github.com/alexhopeoconnor/DFTE.git#v1.2.0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
"includeDir": "lib/WiFiManager/include",
|
"includeDir": "lib/WiFiManager/include",
|
||||||
"srcDir": "lib/WiFiManager/src",
|
"srcDir": "lib/WiFiManager/src"
|
||||||
"libLDFMode": "deep+"
|
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"include": [
|
"include": [
|
||||||
@@ -72,6 +71,7 @@
|
|||||||
"lib/WiFiManager/src",
|
"lib/WiFiManager/src",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"CHANGELOG.md",
|
||||||
"library.json"
|
"library.json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-4
@@ -16,11 +16,11 @@ build_flags =
|
|||||||
-DUNIT_TEST
|
-DUNIT_TEST
|
||||||
lib_deps =
|
lib_deps =
|
||||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
ESP32Async/ESPAsyncWebServer@3.9.1
|
||||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2
|
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.2.0
|
||||||
ESP32Async/ESPAsyncTCP@2.0.0
|
ESP32Async/ESPAsyncTCP@2.0.0
|
||||||
|
|
||||||
[env:esp32]
|
[env:esp32]
|
||||||
platform = espressif32@6.13.0
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
framework = arduino
|
framework = arduino
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
@@ -30,12 +30,14 @@ build_unflags =
|
|||||||
-std=gnu++11
|
-std=gnu++11
|
||||||
build_flags =
|
build_flags =
|
||||||
-std=gnu++14
|
-std=gnu++14
|
||||||
|
-DSOC_WIFI_SUPPORTED=1
|
||||||
|
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||||
-DWM_LOG_LEVEL=5
|
-DWM_LOG_LEVEL=5
|
||||||
-DUNIT_TEST
|
-DUNIT_TEST
|
||||||
lib_deps =
|
lib_deps =
|
||||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
ESP32Async/ESPAsyncWebServer@3.9.1
|
||||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2
|
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.2.0
|
||||||
ESP32Async/AsyncTCP@3.4.9
|
ESP32Async/AsyncTCP@^3.4.9
|
||||||
|
|
||||||
; Optional: compile tests with DFTE logs bridged into WiFiManager::log (see README)
|
; Optional: compile tests with DFTE logs bridged into WiFiManager::log (see README)
|
||||||
[env:esp8266_dfte_log]
|
[env:esp8266_dfte_log]
|
||||||
|
|||||||
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."
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
link_pattern='\]\(([^ )]+)'
|
||||||
|
while IFS= read -r file; do
|
||||||
|
in_fence=false
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*\`\`\` ]]; then
|
||||||
|
[[ "$in_fence" == true ]] && in_fence=false || in_fence=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[[ "$in_fence" == true ]] && continue
|
||||||
|
remainder="$line"
|
||||||
|
while [[ "$remainder" =~ $link_pattern ]]; do
|
||||||
|
target="${BASH_REMATCH[1]}"
|
||||||
|
remainder="${remainder#*]($target)}"
|
||||||
|
case "$target" in
|
||||||
|
\#*|http://*|https://*|mailto:*|tel:*) continue ;;
|
||||||
|
esac
|
||||||
|
target="${target%%#*}"
|
||||||
|
[[ -z "$target" ]] && continue
|
||||||
|
if [[ "$target" == /* ]]; then
|
||||||
|
candidate="$root/${target#/}"
|
||||||
|
else
|
||||||
|
candidate="$(dirname "$file")/$target"
|
||||||
|
fi
|
||||||
|
if [[ ! -e "$candidate" ]]; then
|
||||||
|
printf 'Broken local Markdown link: %s -> %s\n' "${file#$root/}" "$target" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done < "$file"
|
||||||
|
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -type f -name '*.md' -print)
|
||||||
|
|
||||||
|
for required in README.md CHANGELOG.md docs/README.md docs/GETTING_STARTED.md docs/PORTAL_UI.md docs/PORTAL_API.md docs/TESTING.md docs/DEVELOPMENT.md; do
|
||||||
|
if [[ ! -f "$root/$required" ]]; then
|
||||||
|
printf 'Missing required documentation file: %s\n' "$required" >&2
|
||||||
|
failed=1
|
||||||
|
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
|
||||||
|
|
||||||
|
echo "WiFiManager documentation checks passed"
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 --platform esp8266|esp32" >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
[[ "${1:-}" == "--platform" ]] || usage
|
|
||||||
platform="${2:-}"
|
|
||||||
[[ "$platform" == "esp8266" || "$platform" == "esp32" ]] || usage
|
|
||||||
[[ $# -eq 2 ]] || usage
|
|
||||||
|
|
||||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
pio run -d "$root/test/compile-project" -e "$platform"
|
|
||||||
|
|
||||||
echo "WiFiManager consumer compile check passed for $platform"
|
|
||||||
@@ -19,6 +19,11 @@ if [[ "$manifest_version" != "$version" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
grep -q "^## $version$" "$root/CHANGELOG.md" || {
|
||||||
|
echo "CHANGELOG.md has no $version heading" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -f "$root/library.properties" ]]; then
|
if [[ -f "$root/library.properties" ]]; then
|
||||||
properties_version="$(sed -n 's/^version=//p' "$root/library.properties" | head -n 1)"
|
properties_version="$(sed -n 's/^version=//p' "$root/library.properties" | head -n 1)"
|
||||||
if [[ "$properties_version" != "$version" ]]; then
|
if [[ "$properties_version" != "$version" ]]; then
|
||||||
@@ -27,10 +32,30 @@ if [[ -f "$root/library.properties" ]]; then
|
|||||||
fi
|
fi
|
||||||
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)"
|
package_dir="$(mktemp -d)"
|
||||||
trap 'rm -rf "$package_dir"' EXIT
|
trap 'rm -rf "$package_dir"' EXIT
|
||||||
pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null
|
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
|
if [[ "${2:-}" == "--tag" ]]; then
|
||||||
git -C "$root" diff --quiet
|
git -C "$root" diff --quiet
|
||||||
|
|||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
tag="${1:-}"
|
||||||
|
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||||
|
echo "Usage: $0 vMAJOR.MINOR.PATCH" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
version="${tag#v}"
|
||||||
|
notes="$(mktemp)"
|
||||||
|
trap 'rm -f "$notes"' EXIT
|
||||||
|
awk -v heading="## ${version}" '
|
||||||
|
$0 == heading { found = 1; next }
|
||||||
|
found && /^## / { exit }
|
||||||
|
found { print }
|
||||||
|
END { if (!found) exit 1 }
|
||||||
|
' "$root/CHANGELOG.md" > "$notes" || {
|
||||||
|
echo "CHANGELOG.md has no ${version} section" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if [[ ! -s "$notes" ]]; then
|
||||||
|
echo "No release notes found for $tag in CHANGELOG.md" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s\n\n' "# WiFiManager $tag"
|
||||||
|
cat "$notes"
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 compile|examples --platform esp8266|esp32" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $# -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.
|
||||||
|
if [[ -d "$cached_library" || -e "${cached_library}.pio-link" ]]; then
|
||||||
|
pio pkg uninstall -d "$root/test/compile-project" -e "$platform" \
|
||||||
|
-l WiFiManager --no-save --skip-dependencies >/dev/null
|
||||||
|
fi
|
||||||
|
pio run -d "$root/test/compile-project" -e "$platform"
|
||||||
|
|
||||||
|
echo "WiFiManager consumer compile check passed for $platform"
|
||||||
@@ -3,10 +3,9 @@ default_envs = esp8266
|
|||||||
|
|
||||||
[common]
|
[common]
|
||||||
framework = arduino
|
framework = arduino
|
||||||
lib_ldf_mode = chain
|
lib_ldf_mode = deep+
|
||||||
lib_deps =
|
lib_deps =
|
||||||
WiFiManager=symlink://../..
|
WiFiManager=symlink://../..
|
||||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
|
||||||
|
|
||||||
[env:esp8266]
|
[env:esp8266]
|
||||||
extends = common
|
extends = common
|
||||||
@@ -18,16 +17,16 @@ build_flags =
|
|||||||
-DWM_LOG_LEVEL=3
|
-DWM_LOG_LEVEL=3
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${common.lib_deps}
|
${common.lib_deps}
|
||||||
ESP32Async/ESPAsyncTCP@2.0.0
|
|
||||||
|
|
||||||
[env:esp32]
|
[env:esp32]
|
||||||
extends = common
|
extends = common
|
||||||
platform = espressif32@6.13.0
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
build_unflags = -std=gnu++11
|
build_unflags = -std=gnu++11
|
||||||
build_flags =
|
build_flags =
|
||||||
-std=gnu++14
|
-std=gnu++14
|
||||||
|
-DSOC_WIFI_SUPPORTED=1
|
||||||
|
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||||
-DWM_LOG_LEVEL=3
|
-DWM_LOG_LEVEL=3
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${common.lib_deps}
|
${common.lib_deps}
|
||||||
ESP32Async/AsyncTCP@3.4.9
|
|
||||||
|
|||||||
@@ -1,9 +1,45 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <WiFiManager.h>
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char kTitle[] PROGMEM = "Set up Compile Fixture";
|
||||||
|
const char kIdentity[] PROGMEM = "WiFiManager";
|
||||||
|
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";
|
||||||
|
const char kSurface[] PROGMEM = "#ffffff";
|
||||||
|
const char kText[] PROGMEM = "#1c251e";
|
||||||
|
const char kMuted[] PROGMEM = "#607064";
|
||||||
|
const char kBorder[] PROGMEM = "#d6e0d7";
|
||||||
|
const char kAccent[] PROGMEM = "#347a45";
|
||||||
|
const char kAccentHover[] PROGMEM = "#245a32";
|
||||||
|
const char kAccentText[] PROGMEM = "#ffffff";
|
||||||
|
|
||||||
|
const WiFiManagerPortalConfig kPortalConfig = {
|
||||||
|
WiFiManagerPortalText::progmem(kTitle),
|
||||||
|
WiFiManagerPortalText::progmem(kIdentity),
|
||||||
|
WiFiManagerPortalText::progmem(kTagline),
|
||||||
|
WiFiManagerPortalAsset::svgFromProgmem(kLogo),
|
||||||
|
WiFiManagerPortalText::progmem(kLogoAlt),
|
||||||
|
{
|
||||||
|
WiFiManagerPortalText::progmem(kPage),
|
||||||
|
WiFiManagerPortalText::progmem(kSurface),
|
||||||
|
WiFiManagerPortalText::progmem(kText),
|
||||||
|
WiFiManagerPortalText::progmem(kMuted),
|
||||||
|
WiFiManagerPortalText::progmem(kBorder),
|
||||||
|
WiFiManagerPortalText::progmem(kAccent),
|
||||||
|
WiFiManagerPortalText::progmem(kAccentHover),
|
||||||
|
WiFiManagerPortalText::progmem(kAccentText),
|
||||||
|
{}, {}, {}, 10, 6,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
WiFiManager wifiManager;
|
WiFiManager wifiManager;
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
|
wifiManager.setPortalConfig(kPortalConfig);
|
||||||
wifiManager.setConfigPortalTimeout(1);
|
wifiManager.setConfigPortalTimeout(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ TestCase tests[] = {
|
|||||||
TEST_ENTRY(test_api_wifi_meta_json_shape),
|
TEST_ENTRY(test_api_wifi_meta_json_shape),
|
||||||
TEST_ENTRY(test_api_wifi_meta_password_field_type),
|
TEST_ENTRY(test_api_wifi_meta_password_field_type),
|
||||||
TEST_ENTRY(test_api_wifi_connect_status_success_redirect),
|
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_info_json_shape),
|
||||||
TEST_ENTRY(test_api_params_json_shape),
|
TEST_ENTRY(test_api_params_json_shape),
|
||||||
TEST_ENTRY(test_api_status_json_shape),
|
TEST_ENTRY(test_api_status_json_shape),
|
||||||
@@ -36,9 +37,11 @@ TestCase tests[] = {
|
|||||||
TEST_ENTRY(test_config_portal_already_active),
|
TEST_ENTRY(test_config_portal_already_active),
|
||||||
TEST_ENTRY(test_get_config_portal_ssid),
|
TEST_ENTRY(test_get_config_portal_ssid),
|
||||||
TEST_ENTRY(test_bootstrap_json_portal_feature_flags),
|
TEST_ENTRY(test_bootstrap_json_portal_feature_flags),
|
||||||
TEST_ENTRY(test_bootstrap_json_contract_v2),
|
TEST_ENTRY(test_portal_default_presentation),
|
||||||
|
TEST_ENTRY(test_bootstrap_json_contract_v3),
|
||||||
TEST_ENTRY(test_bootstrap_json_snapshot_consistency),
|
TEST_ENTRY(test_bootstrap_json_snapshot_consistency),
|
||||||
TEST_ENTRY(test_root_render_interleaved_context_isolation),
|
TEST_ENTRY(test_root_render_interleaved_context_isolation),
|
||||||
|
TEST_ENTRY(test_portal_presentation_configuration),
|
||||||
|
|
||||||
// Non-blocking tests
|
// Non-blocking tests
|
||||||
TEST_ENTRY(test_nonblocking_process),
|
TEST_ENTRY(test_nonblocking_process),
|
||||||
@@ -104,7 +107,7 @@ TestCase tests[] = {
|
|||||||
|
|
||||||
// Template rendering tests
|
// Template rendering tests
|
||||||
TEST_ENTRY(test_shell_template_renders_core_placeholders),
|
TEST_ENTRY(test_shell_template_renders_core_placeholders),
|
||||||
TEST_ENTRY(test_shell_template_renders_dynamic_styles_with_percent_values),
|
TEST_ENTRY(test_shell_template_renders_dynamic_theme_with_percent_values),
|
||||||
|
|
||||||
// State transition tests
|
// State transition tests
|
||||||
TEST_ENTRY(test_portal_to_connected_transition),
|
TEST_ENTRY(test_portal_to_connected_transition),
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ void test_config_portal_multiple_start_stop();
|
|||||||
void test_config_portal_already_active();
|
void test_config_portal_already_active();
|
||||||
void test_get_config_portal_ssid();
|
void test_get_config_portal_ssid();
|
||||||
void test_bootstrap_json_portal_feature_flags();
|
void test_bootstrap_json_portal_feature_flags();
|
||||||
void test_bootstrap_json_contract_v2();
|
void test_portal_default_presentation();
|
||||||
|
void test_bootstrap_json_contract_v3();
|
||||||
void test_bootstrap_json_snapshot_consistency();
|
void test_bootstrap_json_snapshot_consistency();
|
||||||
void test_root_render_interleaved_context_isolation();
|
void test_root_render_interleaved_context_isolation();
|
||||||
|
void test_portal_presentation_configuration();
|
||||||
|
|
||||||
// Non-blocking tests
|
// Non-blocking tests
|
||||||
void test_nonblocking_process();
|
void test_nonblocking_process();
|
||||||
@@ -108,13 +110,14 @@ void test_scan_generation_invalidated_on_reset();
|
|||||||
|
|
||||||
// Template rendering tests
|
// Template rendering tests
|
||||||
void test_shell_template_renders_core_placeholders();
|
void test_shell_template_renders_core_placeholders();
|
||||||
void test_shell_template_renders_dynamic_styles_with_percent_values();
|
void test_shell_template_renders_dynamic_theme_with_percent_values();
|
||||||
|
|
||||||
// API JSON + captive helper tests
|
// API JSON + captive helper tests
|
||||||
void test_captive_redirect_host_rule();
|
void test_captive_redirect_host_rule();
|
||||||
void test_api_wifi_meta_json_shape();
|
void test_api_wifi_meta_json_shape();
|
||||||
void test_api_wifi_meta_password_field_type();
|
void test_api_wifi_meta_password_field_type();
|
||||||
void test_api_wifi_connect_status_success_redirect();
|
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_info_json_shape();
|
||||||
void test_api_params_json_shape();
|
void test_api_params_json_shape();
|
||||||
void test_api_status_json_shape();
|
void test_api_status_json_shape();
|
||||||
|
|||||||
@@ -88,8 +88,10 @@ void test_ui_customization() {
|
|||||||
|
|
||||||
WiFiManager wm;
|
WiFiManager wm;
|
||||||
|
|
||||||
// Minimal customization retained: title
|
// Minimal customization uses the complete static portal configuration.
|
||||||
wm.portalSetBrandTitle("MyDevice");
|
WiFiManagerPortalConfig portal;
|
||||||
|
portal.title = WiFiManagerPortalText::ram("MyDevice");
|
||||||
|
TEST_ASSERT_TRUE(wm.setPortalConfig(portal));
|
||||||
|
|
||||||
// All setters executed without crash
|
// All setters executed without crash
|
||||||
TEST_ASSERT_TRUE_MESSAGE(true, "UI customization options executed without crash");
|
TEST_ASSERT_TRUE_MESSAGE(true, "UI customization options executed without crash");
|
||||||
|
|||||||
@@ -78,6 +78,24 @@ void test_api_wifi_connect_status_success_redirect() {
|
|||||||
Serial.println("[TEST] WiFi connect-status success redirect payload test completed successfully");
|
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() {
|
void test_api_info_json_shape() {
|
||||||
Serial.println("[TEST] Testing /api/info JSON shape...");
|
Serial.println("[TEST] Testing /api/info JSON shape...");
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ void test_configuration_setters() {
|
|||||||
wm.portalSetBehaviorCaptivePortalEnabled(true);
|
wm.portalSetBehaviorCaptivePortalEnabled(true);
|
||||||
wm.portalSetBehaviorCaptivePortalEnabled(false);
|
wm.portalSetBehaviorCaptivePortalEnabled(false);
|
||||||
|
|
||||||
// Test custom title path
|
// Test the typed portal presentation path.
|
||||||
wm.portalSetBrandTitle("TestTitle");
|
WiFiManagerPortalConfig portal;
|
||||||
|
portal.title = WiFiManagerPortalText::ram("TestTitle");
|
||||||
|
TEST_ASSERT_TRUE(wm.setPortalConfig(portal));
|
||||||
|
|
||||||
// Call setters multiple times with different values.
|
// Call setters multiple times with different values.
|
||||||
wm.setConfigPortalTimeout(60);
|
wm.setConfigPortalTimeout(60);
|
||||||
|
|||||||
@@ -76,16 +76,29 @@ void test_bootstrap_json_portal_feature_flags() {
|
|||||||
Serial.println("[TEST] Bootstrap portal feature flags test completed successfully");
|
Serial.println("[TEST] Bootstrap portal feature flags test completed successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_bootstrap_json_contract_v2() {
|
void test_portal_default_presentation() {
|
||||||
Serial.println("[TEST] Testing bootstrap JSON v2 contract...");
|
WiFiManager wm;
|
||||||
|
WiFiManagerHandlers handlers(&wm);
|
||||||
|
|
||||||
|
const String bootstrap = handlers.buildPortalBootstrapJson();
|
||||||
|
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"title\":\"WiFiManager\"")));
|
||||||
|
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_v3() {
|
||||||
|
Serial.println("[TEST] Testing bootstrap JSON v3 contract...");
|
||||||
|
|
||||||
WiFiManager wm;
|
WiFiManager wm;
|
||||||
WiFiManagerHandlers handlers(&wm);
|
WiFiManagerHandlers handlers(&wm);
|
||||||
|
|
||||||
wm.portalSetBrandTitle("Solar Battery Monitor Setup");
|
WiFiManagerPortalConfig portal;
|
||||||
wm.portalSetContextIdentityText("Solar Battery Monitor");
|
portal.title = WiFiManagerPortalText::ram("Solar Battery Monitor Setup");
|
||||||
wm.portalSetBrandHomeIntro("Connect this device to WiFi and finish setup.");
|
portal.identityText = WiFiManagerPortalText::ram("Solar Battery Monitor");
|
||||||
wm.portalSetBrandLogoSvg("<svg viewBox='0 0 24 24'></svg>");
|
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);
|
wm.portalSetPageInfoVisible(true);
|
||||||
wm.portalSetPageUpdateVisible(false);
|
wm.portalSetPageUpdateVisible(false);
|
||||||
wm.portalSetPageSetupVisible(true);
|
wm.portalSetPageSetupVisible(true);
|
||||||
@@ -102,10 +115,10 @@ void test_bootstrap_json_contract_v2() {
|
|||||||
wm.portalAddHomeCard(card);
|
wm.portalAddHomeCard(card);
|
||||||
|
|
||||||
String j = handlers.buildPortalBootstrapJson();
|
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("\"brand\":{")));
|
||||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"title\":\"Solar Battery Monitor Setup\"")));
|
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("\"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("\"context\":{")));
|
||||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"identityText\":\"Solar Battery Monitor\"")));
|
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"identityText\":\"Solar Battery Monitor\"")));
|
||||||
@@ -163,11 +176,11 @@ void test_root_render_interleaved_context_isolation() {
|
|||||||
|
|
||||||
registryA.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
registryA.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
||||||
registryA.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
registryA.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
||||||
registryA.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
|
registryA.registerProgmemData("%PORTAL_THEME%", kEmptyTemplateChunk);
|
||||||
|
|
||||||
registryB.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
registryB.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
||||||
registryB.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
registryB.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
||||||
registryB.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
|
registryB.registerProgmemData("%PORTAL_THEME%", kEmptyTemplateChunk);
|
||||||
|
|
||||||
String bootstrapA = F("{\"ctx\":\"A\"}");
|
String bootstrapA = F("{\"ctx\":\"A\"}");
|
||||||
String appJsA = F("// shell A");
|
String appJsA = F("// shell A");
|
||||||
@@ -217,3 +230,51 @@ void test_root_render_interleaved_context_isolation() {
|
|||||||
|
|
||||||
Serial.println("[TEST] Root render interleaved context isolation test completed successfully");
|
Serial.println("[TEST] Root render interleaved context isolation test completed successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_portal_presentation_configuration() {
|
||||||
|
Serial.println("[TEST] Testing portal presentation configuration...");
|
||||||
|
|
||||||
|
WiFiManager wm;
|
||||||
|
WiFiManagerHandlers handlers(&wm);
|
||||||
|
WiFiManagerPortalConfig config;
|
||||||
|
config.title = WiFiManagerPortalText::ram("Set up Temperature Monitor");
|
||||||
|
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("Example Devices logo");
|
||||||
|
config.theme.pageBackground = WiFiManagerPortalText::ram("#f4f7f3");
|
||||||
|
config.theme.surface = WiFiManagerPortalText::ram("#ffffff");
|
||||||
|
config.theme.text = WiFiManagerPortalText::ram("#1c251e");
|
||||||
|
config.theme.mutedText = WiFiManagerPortalText::ram("#607064");
|
||||||
|
config.theme.border = WiFiManagerPortalText::ram("#d6e0d7");
|
||||||
|
config.theme.accent = WiFiManagerPortalText::ram("#347a45");
|
||||||
|
config.theme.accentHover = WiFiManagerPortalText::ram("#245a32");
|
||||||
|
config.theme.accentText = WiFiManagerPortalText::ram("#ffffff");
|
||||||
|
config.theme.success = WiFiManagerPortalText::ram("#2f855a");
|
||||||
|
config.theme.danger = WiFiManagerPortalText::ram("#c53030");
|
||||||
|
config.theme.dangerHover = WiFiManagerPortalText::ram("#9b2c2c");
|
||||||
|
config.theme.cornerRadiusPx = 10;
|
||||||
|
config.theme.smallCornerRadiusPx = 6;
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE_MESSAGE(wm.setPortalConfig(config),
|
||||||
|
"A complete setup-time portal configuration should be accepted");
|
||||||
|
|
||||||
|
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\":\"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}");
|
||||||
|
TEST_ASSERT_FALSE_MESSAGE(wm.setPortalConfig(unsafeConfig),
|
||||||
|
"Semantic theme values must reject stylesheet injection characters");
|
||||||
|
|
||||||
|
wm.startWebPortal();
|
||||||
|
TEST_ASSERT_FALSE_MESSAGE(wm.setPortalConfig(config),
|
||||||
|
"Portal presentation must remain immutable while async routes are active");
|
||||||
|
wm.stopWebPortal();
|
||||||
|
TEST_ASSERT_TRUE_MESSAGE(wm.setPortalConfig(config),
|
||||||
|
"Presentation can be applied after the portal stops");
|
||||||
|
|
||||||
|
Serial.println("[TEST] Portal presentation configuration test completed successfully");
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const char kEmpty[] PROGMEM = "";
|
|||||||
const char kDocTitle[] PROGMEM = "Config ESP";
|
const char kDocTitle[] PROGMEM = "Config ESP";
|
||||||
const char kBootstrapJson[] PROGMEM = "{\"title\":\"Test\"}";
|
const char kBootstrapJson[] PROGMEM = "{\"title\":\"Test\"}";
|
||||||
const char kPortalAppJs[] PROGMEM = "console.log('portal');";
|
const char kPortalAppJs[] PROGMEM = "console.log('portal');";
|
||||||
const char kPortalAppendJs[] PROGMEM = "";
|
const char kPortalTheme[] PROGMEM = "";
|
||||||
|
|
||||||
const char* dynamicStringGetter(void* userData) {
|
const char* dynamicStringGetter(void* userData) {
|
||||||
const auto* value = static_cast<const String*>(userData);
|
const auto* value = static_cast<const String*>(userData);
|
||||||
@@ -55,7 +55,7 @@ void test_shell_template_renders_core_placeholders() {
|
|||||||
registry.registerProgmemData("%PAGE_TITLE%", kDocTitle);
|
registry.registerProgmemData("%PAGE_TITLE%", kDocTitle);
|
||||||
registry.registerProgmemData("%BOOTSTRAP_JSON%", kBootstrapJson);
|
registry.registerProgmemData("%BOOTSTRAP_JSON%", kBootstrapJson);
|
||||||
registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs);
|
registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs);
|
||||||
registry.registerProgmemData("%PORTAL_APPEND_JS%", kPortalAppendJs);
|
registry.registerProgmemData("%PORTAL_THEME%", kPortalTheme);
|
||||||
|
|
||||||
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
|
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
|
||||||
|
|
||||||
@@ -70,35 +70,32 @@ void test_shell_template_renders_core_placeholders() {
|
|||||||
Serial.println("[TEST] Portal shell template rendering test completed successfully");
|
Serial.println("[TEST] Portal shell template rendering test completed successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_shell_template_renders_dynamic_styles_with_percent_values() {
|
void test_shell_template_renders_dynamic_theme_with_percent_values() {
|
||||||
Serial.println("[TEST] Testing portal shell dynamic styles placeholder with percent values...");
|
Serial.println("[TEST] Testing portal shell dynamic theme placeholder with percent values...");
|
||||||
|
|
||||||
PlaceholderRegistry registry(8);
|
PlaceholderRegistry registry(8);
|
||||||
String styles = F("<style>body{width:100%;max-width:92%}</style>");
|
String theme = F("<style>:root{--wm-bg:rgb(20,50,30);--wm-width:100%}</style>");
|
||||||
String title = F("Config ESP");
|
String title = F("Config ESP");
|
||||||
String bootstrap = F("{\"title\":\"Test\"}");
|
String bootstrap = F("{\"title\":\"Test\"}");
|
||||||
String appendJs = F("");
|
DynamicDataDescriptor themeDescriptor{};
|
||||||
DynamicDataDescriptor stylesDescriptor{};
|
|
||||||
DynamicDataDescriptor titleDescriptor{};
|
DynamicDataDescriptor titleDescriptor{};
|
||||||
DynamicDataDescriptor bootstrapDescriptor{};
|
DynamicDataDescriptor bootstrapDescriptor{};
|
||||||
DynamicDataDescriptor appendJsDescriptor{};
|
|
||||||
|
|
||||||
configureDescriptor(stylesDescriptor, styles);
|
configureDescriptor(themeDescriptor, theme);
|
||||||
configureDescriptor(titleDescriptor, title);
|
configureDescriptor(titleDescriptor, title);
|
||||||
configureDescriptor(bootstrapDescriptor, bootstrap);
|
configureDescriptor(bootstrapDescriptor, bootstrap);
|
||||||
configureDescriptor(appendJsDescriptor, appendJs);
|
|
||||||
|
|
||||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%STYLES%", &stylesDescriptor));
|
TEST_ASSERT_TRUE(registry.registerProgmemData("%STYLES%", kEmpty));
|
||||||
|
TEST_ASSERT_TRUE(registry.registerDynamicData("%PORTAL_THEME%", &themeDescriptor));
|
||||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%PAGE_TITLE%", &titleDescriptor));
|
TEST_ASSERT_TRUE(registry.registerDynamicData("%PAGE_TITLE%", &titleDescriptor));
|
||||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptor));
|
TEST_ASSERT_TRUE(registry.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptor));
|
||||||
TEST_ASSERT_TRUE(registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs));
|
TEST_ASSERT_TRUE(registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs));
|
||||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%PORTAL_APPEND_JS%", &appendJsDescriptor));
|
|
||||||
|
|
||||||
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
|
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
|
||||||
|
|
||||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<style>body{width:100%;max-width:92%}</style>"));
|
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<style>:root{--wm-bg:rgb(20,50,30);--wm-width:100%}</style>"));
|
||||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<title>Config ESP</title>"));
|
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<title>Config ESP</title>"));
|
||||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}"));
|
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}"));
|
||||||
|
|
||||||
Serial.println("[TEST] Dynamic styles placeholder percent rendering test completed successfully");
|
Serial.println("[TEST] Dynamic theme placeholder percent rendering test completed successfully");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user