mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
feat: prepare WiFiManager 3.1.0
This commit is contained in:
@@ -2,12 +2,24 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- device-framework
|
||||||
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: ./scripts/check-docs.sh
|
||||||
|
|
||||||
compile-tests:
|
compile-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
@@ -20,4 +32,4 @@ 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 }}
|
||||||
|
|||||||
@@ -17,7 +17,14 @@ 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 compile --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 }}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
## 3.0.6
|
## 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.
|
- 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.
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
# WiFiManager
|
# WiFiManager
|
||||||
|
|
||||||
This repository is a **breaking fork** of upstream [`tzapu/WiFiManager`](https://github.com/tzapu/WiFiManager).
|
WiFiManager is the maintained ESP8266/ESP32 configuration-portal fork used by DeviceFramework. It is a deliberate breaking fork of upstream [`tzapu/WiFiManager`](https://github.com/tzapu/WiFiManager), with a streamed single-page portal and typed JSON APIs rather than upstream’s legacy page/template model.
|
||||||
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.
|
## Why use it
|
||||||
|
|
||||||
**This repository:** [alexhopeoconnor/WiFiManager](https://github.com/alexhopeoconnor/WiFiManager)
|
- **Single-shell portal:** one responsive SPA for WiFi, parameters, information, actions, and firmware update flow.
|
||||||
**Upstream:** [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager)
|
- **Data-first APIs:** portal state and actions are exposed under `/api/...`, not scraped from HTML.
|
||||||
|
- **Controlled customisation:** supported `portal*` APIs customise branding, parameters, cards, layout, and small style/JS enhancements without exposing portal internals.
|
||||||
|
- **ESP8266 and ESP32:** clean PlatformIO consumers resolve the right asynchronous TCP transport automatically.
|
||||||
|
|
||||||
## Basic provisioning
|
## Try it
|
||||||
|
|
||||||
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,247 +19,45 @@ WiFiManager wifi;
|
|||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
wifi.setConfigPortalTimeout(180); // seconds; 0 leaves it open
|
wifi.setConfigPortalTimeout(180);
|
||||||
|
|
||||||
if (!wifi.autoConnect("Device Setup", "change-me")) {
|
if (!wifi.autoConnect("Device Setup", "change-me")) {
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {}
|
||||||
// Normal application work after WiFi is connected.
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The AP password must meet the Wi-Fi password requirements. For applications
|
For a DeviceFramework device, configure the framework’s shared device password instead. DeviceFramework applies it consistently to the provisioning AP, OTA, HTTP Basic authentication, and WebSerial.
|
||||||
built on DeviceFramework, configure `setConfigDevicePassword(...)` instead:
|
|
||||||
DeviceFramework passes the same validated password to WiFiManager, Arduino OTA,
|
|
||||||
HTTP Basic authentication, and WebSerial.
|
|
||||||
|
|
||||||
|
## Explore the portal
|
||||||
|
|
||||||
## Breaking Changes
|
| Goal | Guide |
|
||||||
|
| --- | --- |
|
||||||
|
| Brand the portal, add parameters, cards, or small UI enhancements | [Portal customisation](docs/PORTAL_CUSTOMIZATION.md) |
|
||||||
|
| Configure primary/fallback station profiles | [Station profiles](docs/STATION_PROFILES.md) |
|
||||||
|
| Understand the JSON APIs and station-connect handoff | [Portal API](docs/PORTAL_API.md) |
|
||||||
|
| Build a clean consumer or work on this fork | [Testing](docs/TESTING.md) · [Development](docs/DEVELOPMENT.md) |
|
||||||
|
|
||||||
This fork intentionally modernizes and restructures the configuration portal.
|
## Install
|
||||||
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
|
|
||||||
WiFiManager wm;
|
|
||||||
|
|
||||||
wm.portalSetBrandTitle("Solar Battery Monitor Setup");
|
|
||||||
wm.portalSetContextIdentityText("Solar Battery Monitor");
|
|
||||||
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);
|
|
||||||
wm.portalSetPageUpdateVisible(false);
|
|
||||||
wm.portalSetActionEraseVisible(false);
|
|
||||||
wm.portalSetActionRestartVisible(true);
|
|
||||||
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
|
|
||||||
|
|
||||||
wm.portalSetBehaviorCaptivePortalEnabled(true);
|
|
||||||
wm.portalSetBehaviorConnectOnSave(true);
|
|
||||||
wm.portalSetBehaviorExitAllowed(true);
|
|
||||||
|
|
||||||
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Masked);
|
|
||||||
wm.portalSetFieldStaticIpVisibility(PortalFieldVisibility::Auto);
|
|
||||||
wm.portalSetFieldStaticDnsVisibility(PortalFieldVisibility::Auto);
|
|
||||||
|
|
||||||
WiFiManagerParameter mqttHost(
|
|
||||||
"mqtt_host",
|
|
||||||
"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:
|
|
||||||
|
|
||||||
- `stationIp` and `redirectUrl` are present only after a successful station join.
|
|
||||||
- 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
|
|
||||||
|
|
||||||
This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**.
|
|
||||||
|
|
||||||
The published `library.json` resolves DFTE from the maintained, pinned
|
|
||||||
`v1.0.2` Git tag. No sibling checkout is required to consume WiFiManager.
|
|
||||||
When developing both libraries together, use a local `symlink://` or
|
|
||||||
`file://` dependency in your own ignored PlatformIO override.
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Use the release tag. The package manifest resolves DFTE, the asynchronous web
|
|
||||||
server, and the platform-specific TCP library automatically:
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[common]
|
[common]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.0.6
|
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.1.0
|
||||||
|
|
||||||
[env:esp8266]
|
|
||||||
extends = common
|
|
||||||
platform = espressif8266
|
|
||||||
board = d1_mini
|
|
||||||
framework = arduino
|
|
||||||
|
|
||||||
[env:esp32]
|
|
||||||
extends = common
|
|
||||||
platform = espressif32
|
|
||||||
board = esp32dev
|
|
||||||
framework = arduino
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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. This repository is supported through PlatformIO. Use a local `symlink://` or `file://` dependency only while actively changing this fork.
|
||||||
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
|
## Development and releases
|
||||||
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
|
```bash
|
||||||
|
./scripts/test.sh compile --platform esp8266
|
||||||
|
./scripts/test.sh compile --platform esp32
|
||||||
|
./scripts/check-docs.sh
|
||||||
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
|
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
|
||||||
```
|
```
|
||||||
|
|
||||||
Push the branch and tag. GitHub Actions validates the PlatformIO package again
|
Tagging repeats the board-free compile checks, validates the package, and creates a GitHub Release from the corresponding changelog section. It does not publish to the PlatformIO Registry or deploy firmware.
|
||||||
and creates the GitHub Release from that tag.
|
|
||||||
|
|
||||||
## AI Assistance Notice
|
See the [documentation index](docs/README.md), [release history](CHANGELOG.md), and [licence](LICENSE).
|
||||||
|
|
||||||
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,21 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Before a release, update `library.json`, `CHANGELOG.md`, and the relevant public documentation, then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./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,41 @@
|
|||||||
|
# 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);
|
||||||
|
if (!wifi.autoConnect("Example Setup", "change-me")) {
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Normal work begins after autoConnect succeeds.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`autoConnect()` first tries stored station credentials. If that cannot connect, it starts the AP and portal with the supplied name and password. Use a Wi-Fi-valid AP password.
|
||||||
|
|
||||||
|
## PlatformIO dependency
|
||||||
|
|
||||||
|
```ini
|
||||||
|
lib_deps =
|
||||||
|
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The package manifest resolves DFTE, ESPAsyncWebServer, and the correct TCP transport for ESP8266 or ESP32. A consuming project should not duplicate those dependencies unless it is deliberately testing an unreleased stack change.
|
||||||
|
|
||||||
|
## DeviceFramework applications
|
||||||
|
|
||||||
|
DeviceFramework creates and configures WiFiManager for its normal lifecycle. Use DeviceFramework’s persistent shared device-password API instead of separately configuring an AP, OTA, HTTP, and WebSerial password.
|
||||||
|
|
||||||
|
Next: [portal customisation](PORTAL_CUSTOMIZATION.md) or [portal API](PORTAL_API.md).
|
||||||
|
|
||||||
|
Back to [documentation](README.md) · [project overview](../README.md).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
On success, WiFiManager keeps the portal alive briefly so the client can read the final status and navigate before the AP is shut down. Captive-portal helpers, DHCP timing, browser behaviour, and network isolation can still prevent an automatic redirect, so clients must handle a visible address as well.
|
||||||
|
|
||||||
|
## 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,65 @@
|
|||||||
|
# Portal customisation
|
||||||
|
|
||||||
|
The stable extension surface is the `portal*` API and JSON API contract. Templates, undocumented DOM IDs, and SPA routing internals are not consumer extension points.
|
||||||
|
|
||||||
|
## Supported changes
|
||||||
|
|
||||||
|
- `portalSetBrand*` and `portalSetContextIdentityText()` for the visible identity.
|
||||||
|
- `portalSetPage*`, `portalSetAction*`, `portalSetLayout*`, and `portalSetBehavior*` for built-in capability and layout choices.
|
||||||
|
- `portalAddParameter()`, including raw HTML blocks inside parameter-rendering surfaces.
|
||||||
|
- `portalAddInfoSection()` and `portalAddHomeCard()` for structured content.
|
||||||
|
- `portalAppendCss()`, `portalOverrideCss()`, and `portalAppendJs()` for small presentation enhancements.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
WiFiManager wm;
|
||||||
|
|
||||||
|
wm.portalSetBrandTitle("Solar Battery Monitor Setup");
|
||||||
|
wm.portalSetContextIdentityText("Solar Battery Monitor");
|
||||||
|
wm.portalSetBrandHomeIntro(
|
||||||
|
"Connect the monitor to WiFi, then review its battery settings."
|
||||||
|
);
|
||||||
|
wm.portalSetPageUpdateVisible(false);
|
||||||
|
wm.portalSetActionEraseVisible(false);
|
||||||
|
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
|
||||||
|
|
||||||
|
WiFiManagerParameter mqttHost(
|
||||||
|
"mqtt_host", "MQTT host", "broker.local", 64,
|
||||||
|
"placeholder='broker.local'"
|
||||||
|
);
|
||||||
|
wm.portalAddParameter(&mqttHost);
|
||||||
|
|
||||||
|
PortalHomeCard summary;
|
||||||
|
summary.id = "battery";
|
||||||
|
summary.title = "Battery";
|
||||||
|
summary.kind = PortalHomeCardKind::KeyValue;
|
||||||
|
summary.items.push_back({"voltage", "Voltage", "13.2V"});
|
||||||
|
wm.portalAddHomeCard(summary);
|
||||||
|
|
||||||
|
wm.portalAppendJs(
|
||||||
|
"document.addEventListener('wm:ready', function(event) {"
|
||||||
|
" console.log('Portal ready', event.detail.boot);"
|
||||||
|
"});"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enhancement events
|
||||||
|
|
||||||
|
Appended JavaScript may enhance the existing SPA through:
|
||||||
|
|
||||||
|
- `wm:ready`, with `event.detail.boot`;
|
||||||
|
- `wm:view-changed`, with `event.detail.route`.
|
||||||
|
|
||||||
|
It should not replace routing or built-in action flow.
|
||||||
|
|
||||||
|
## Intentional boundary
|
||||||
|
|
||||||
|
The following are fork-level changes, not supported consumer customisation:
|
||||||
|
|
||||||
|
- arbitrary home, information, navigation, or shell HTML injection;
|
||||||
|
- replacing built-in SPA routes or action flows;
|
||||||
|
- depending on undocumented DOM IDs or private templates;
|
||||||
|
- adding a new backend-to-frontend workflow without an API contract.
|
||||||
|
|
||||||
|
For data and endpoint behaviour, see [Portal API](PORTAL_API.md).
|
||||||
|
|
||||||
|
Back to [documentation](README.md) · [project overview](../README.md).
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# WiFiManager documentation
|
||||||
|
|
||||||
|
This maintained fork intentionally has a narrower, explicit customisation boundary than upstream WiFiManager.
|
||||||
|
|
||||||
|
| I want to… | Read |
|
||||||
|
| --- | --- |
|
||||||
|
| Start a basic portal or install a released dependency | [Getting started](GETTING_STARTED.md) |
|
||||||
|
| Brand the SPA, add parameters/cards, or use the supported hooks | [Portal customisation](PORTAL_CUSTOMIZATION.md) |
|
||||||
|
| Configure primary/fallback station profiles or their portal workflow | [Station profiles](STATION_PROFILES.md) |
|
||||||
|
| Consume JSON endpoints or station-connect status | [Portal API](PORTAL_API.md) |
|
||||||
|
| Build a clean consumer | [Testing](TESTING.md) |
|
||||||
|
| Work on this fork or make a release | [Development and releases](DEVELOPMENT.md) |
|
||||||
|
|
||||||
|
## Documentation rules
|
||||||
|
|
||||||
|
- Portal customisation owns the supported `portal*` surface.
|
||||||
|
- Portal API owns HTTP response contracts.
|
||||||
|
- The root README is a short onboarding page, not an API reference.
|
||||||
|
|
||||||
|
Back to the [project overview](../README.md).
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
DeviceFramework enables this controller and supplies its CRC-protected storage. A direct WiFiManager consumer supplies its own durable store, or can use the controller only for the current process.
|
||||||
|
|
||||||
|
## 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 and [DeviceFramework configuration](https://github.com/alexhopeoconnor/DeviceFramework/blob/main/docs/CONFIGURATION.md) for the local-profile JSON that supplies these slots.
|
||||||
|
|
||||||
|
Back to [documentation](README.md) · [project overview](../README.md).
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
```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 an application or DeviceFramework’s connected-device suite.
|
||||||
|
|
||||||
|
Back to [documentation](README.md) · [project overview](../README.md).
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -142,12 +143,44 @@
|
|||||||
// Include utility functions
|
// Include utility functions
|
||||||
#include "WiFiManagerUtils.h"
|
#include "WiFiManagerUtils.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 +188,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 +217,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
|
||||||
|
|
||||||
@@ -327,6 +360,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 +388,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 +407,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 +512,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 +537,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,13 +587,13 @@ 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 (all customization entry points use the portal* prefix) ----
|
||||||
void portalSetBrandTitle(const String& title);
|
void portalSetBrandTitle(const String& title);
|
||||||
void portalSetContextIdentityText(const String& identityText);
|
void portalSetContextIdentityText(const String& identityText);
|
||||||
@@ -563,10 +637,10 @@ class WiFiManager
|
|||||||
|
|
||||||
// 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 +648,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 +662,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 +689,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 +698,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 +710,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 +721,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 +742,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 +797,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 +816,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)
|
||||||
@@ -808,7 +899,7 @@ class WiFiManager
|
|||||||
PortalStructuredExtrasState _portalStructured;
|
PortalStructuredExtrasState _portalStructured;
|
||||||
|
|
||||||
// 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 +910,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 = false; // preload wifiscan if true
|
||||||
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 +939,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 +960,23 @@ 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 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 +1039,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
|
||||||
|
|
||||||
|
|
||||||
@@ -1042,9 +1150,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;
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ 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);
|
||||||
|
|||||||
@@ -465,6 +465,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 +481,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,6 +499,30 @@ function bindWifiViewEvents(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderStationProfileFields(profiles){
|
||||||
|
var html='';
|
||||||
|
var i;
|
||||||
|
for(i=0;i<profiles.length;i++){
|
||||||
|
var p=profiles[i]||{};
|
||||||
|
var primary=i===0;
|
||||||
|
var title=primary?'Primary WiFi':'Fallback WiFi (optional)';
|
||||||
|
var hint=primary
|
||||||
|
? 'This network is tried first for a new connection.'
|
||||||
|
: 'Tried when the primary network is unavailable.';
|
||||||
|
html+="<div class='wm-card wm-station-profile'><h2>"+esc(title)+"</h2><p class='wm-help'>"+esc(hint)+"</p>";
|
||||||
|
html+="<label for='wm-s"+i+"'>SSID"+(primary?'':' (leave empty to disable)')+"</label>";
|
||||||
|
html+="<input id='wm-s"+i+"' name='s"+i+"' type='text' maxlength='32' value='"+esc(p.ssid||'')+"'/>";
|
||||||
|
html+="<label for='wm-p"+i+"'>Password</label>";
|
||||||
|
html+="<input id='wm-p"+i+"' class='wm-profile-password' name='p"+i+"' type='password' maxlength='64' placeholder='"+(p.passwordSet?'Configured — enter a new password to replace it':'Leave blank for an open network')+"'/>";
|
||||||
|
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(p.passwordSet){
|
||||||
|
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+='</div>';
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function viewWifi(){
|
function viewWifi(){
|
||||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
|
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
|
||||||
api('/api/wifi/meta').then(function(res){
|
api('/api/wifi/meta').then(function(res){
|
||||||
@@ -502,17 +534,25 @@ function viewWifi(){
|
|||||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
|
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
|
||||||
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
|
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
|
||||||
html+="<form id='wm-wifi-form'>";
|
html+="<form id='wm-wifi-form'>";
|
||||||
html+=renderFieldList(m.wifiFields||[]);
|
if(Array.isArray(m.profiles) && m.profiles.length){
|
||||||
var wf=m.wifiFields||[];
|
html+=renderStationProfileFields(m.profiles);
|
||||||
var hasPass=false;
|
}else{
|
||||||
var i;
|
html+=renderFieldList(m.wifiFields||[]);
|
||||||
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
|
var wf=m.wifiFields||[];
|
||||||
if(hasPass){
|
var hasPass=false;
|
||||||
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
|
var i;
|
||||||
|
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
|
||||||
|
if(hasPass){
|
||||||
|
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
html+=renderFieldList(m.staticFields||[]);
|
html+=renderFieldList(m.staticFields||[]);
|
||||||
html+=renderFieldList(m.params||[]);
|
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>";
|
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block' name='stationAction' value='connect'>"+labelWithIcon('connect','Save and connect')+"</button>";
|
||||||
|
if(Array.isArray(m.profiles) && m.profiles.length){
|
||||||
|
html+="<button type='submit' class='wm-btn wm-btn--secondary wm-btn--block' name='stationAction' value='save'>Save for later</button>";
|
||||||
|
}
|
||||||
|
html+="</div></form>";
|
||||||
html+="<div id='wm-wifi-msg'></div></div>";
|
html+="<div id='wm-wifi-msg'></div></div>";
|
||||||
setView(html);
|
setView(html);
|
||||||
bindWifiViewEvents();
|
bindWifiViewEvents();
|
||||||
@@ -598,6 +638,9 @@ function pollWifiConnectStatus(){
|
|||||||
window.portalWifiSave=function(ev){
|
window.portalWifiSave=function(ev){
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var fd=new FormData(document.getElementById('wm-wifi-form'));
|
var fd=new FormData(document.getElementById('wm-wifi-form'));
|
||||||
|
if(ev.submitter && ev.submitter.name){
|
||||||
|
fd.append(ev.submitter.name,ev.submitter.value||'');
|
||||||
|
}
|
||||||
var msg=$('wm-wifi-msg');
|
var msg=$('wm-wifi-msg');
|
||||||
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){
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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.
|
||||||
|
|
||||||
bool isValidHostnameChar(char c) {
|
bool isValidHostnameChar(char c) {
|
||||||
return isAlphaNumeric(c) || c == '-';
|
return isAlphaNumeric(c) || c == '-';
|
||||||
@@ -197,6 +198,465 @@ 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)) {
|
||||||
|
_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) {
|
||||||
|
if (_savewificallback != NULL) {
|
||||||
|
_savewificallback();
|
||||||
|
}
|
||||||
|
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_SUCCESS);
|
||||||
|
if (_disableConfigPortal) {
|
||||||
|
shutdownConfigPortal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateConxResult(status);
|
||||||
|
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -656,7 +1116,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
|
||||||
@@ -813,7 +1276,8 @@ void WiFiManager::processPortalConnect() {
|
|||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -783,6 +783,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();
|
||||||
@@ -1193,6 +1249,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 +1338,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,
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "WiFiManager",
|
"name": "WiFiManager",
|
||||||
"version": "3.0.6",
|
"version": "3.1.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"wifi",
|
"wifi",
|
||||||
"wi-fi",
|
"wi-fi",
|
||||||
@@ -71,8 +71,8 @@
|
|||||||
"lib/WiFiManager/src",
|
"lib/WiFiManager/src",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
"library.json",
|
"CHANGELOG.md",
|
||||||
"tools"
|
"library.json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"$schema": "https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/schema/library.json",
|
"$schema": "https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/schema/library.json",
|
||||||
|
|||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/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_CUSTOMIZATION.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
|
||||||
|
|
||||||
|
if [[ "$failed" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "WiFiManager documentation checks passed"
|
||||||
@@ -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,6 +32,7 @@ if [[ -f "$root/library.properties" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
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"
|
||||||
@@ -2,14 +2,13 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $0 --platform esp8266|esp32" >&2
|
echo "Usage: $0 compile --platform esp8266|esp32" >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
||||||
[[ "${1:-}" == "--platform" ]] || usage
|
[[ "${1:-}" == "compile" && "${2:-}" == "--platform" && $# -eq 3 ]] || usage
|
||||||
platform="${2:-}"
|
platform="${3:-}"
|
||||||
[[ "$platform" == "esp8266" || "$platform" == "esp32" ]] || usage
|
[[ "$platform" == "esp8266" || "$platform" == "esp32" ]] || usage
|
||||||
[[ $# -eq 2 ]] || usage
|
|
||||||
|
|
||||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
pio run -d "$root/test/compile-project" -e "$platform"
|
pio run -d "$root/test/compile-project" -e "$platform"
|
||||||
Reference in New Issue
Block a user