mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-16 07:51:36 +10:00
feat: add reproducible portal README media
This commit is contained in:
@@ -37,3 +37,4 @@ node_modules/
|
||||
# Local portal station handoff credentials and optional direct test artifacts
|
||||
/test/portal-station.env
|
||||
/tests/portal-contract/artifacts/
|
||||
/artifacts/readme-media/
|
||||
|
||||
@@ -4,10 +4,12 @@ WiFiManager gives ESP8266 and ESP32 firmware a polished, self-hosted Wi-Fi setup
|
||||
|
||||
## See it on real hardware
|
||||
|
||||
| Brand the built-in portal | Combine Wi-Fi and application setup |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
| Give each product its own title, identity, icon, and color theme without copying portal HTML. | Show live nearby networks and collect application values, such as an MQTT broker, in the same setup flow. |
|
||||

|
||||
|
||||
WiFiManager provides a self-hosted setup portal for Wi-Fi and application
|
||||
settings without copying portal HTML into each firmware. See the detailed
|
||||
[portal UI guide](docs/PORTAL_UI.md) for supported branding and content APIs.
|
||||
|
||||
## Start with a working portal
|
||||
|
||||
|
||||
@@ -4,6 +4,16 @@ WiFiManagerPortalConfig is the supported presentation API for the built-in provi
|
||||
|
||||
Apply presentation before autoConnect(), startConfigPortal(), or startWebPortal(). Configure portal policy during boot as well so each portal session begins consistently. Portal text and SVG assets are non-owning, so their RAM or PROGMEM data must have static firmware lifetime. WiFiManager locks presentation while a portal is active so asynchronous responses cannot observe partial configuration; setPortalConfig() returns false if it cannot accept the configuration.
|
||||
|
||||
## Portal views
|
||||
|
||||
These ESP32 captures use the same real-board portal contract described in
|
||||
[Testing](TESTING.md). The nearby networks shown are the networks visible to
|
||||
the capture device when the portal scans.
|
||||
|
||||
| Overview | Wi-Fi and application settings |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## Standalone branded portal
|
||||
|
||||
~~~cpp
|
||||
|
||||
@@ -102,4 +102,39 @@ cp test/portal-station.env.example test/portal-station.env
|
||||
./tools/portal-hardware run ... --station-env test/portal-station.env
|
||||
```
|
||||
|
||||
## Refresh README media
|
||||
|
||||
README media is an explicit ESP32-only capture, not part of normal testing or
|
||||
CI. It uses the same real-board portal contract above, but records a short
|
||||
browser tour and stores all candidate files under the ignored
|
||||
`artifacts/readme-media/` directory by default:
|
||||
|
||||
```bash
|
||||
./tools/portal-hardware run \
|
||||
--platform esp32 \
|
||||
--port /dev/serial/by-id/usb-... \
|
||||
--client-interface USB_WIFI_ADAPTER \
|
||||
--capture-readme-media
|
||||
```
|
||||
|
||||
Review the printed artifact directory. To keep a run somewhere more convenient,
|
||||
pass `--output DIRECTORY`. After review, promote only the approved PNG/GIF
|
||||
files into tracked documentation assets:
|
||||
|
||||
```bash
|
||||
./tools/promote-readme-media \
|
||||
--from artifacts/readme-media/TIMESTAMP-esp32 \
|
||||
--replace
|
||||
./scripts/check-docs.sh
|
||||
```
|
||||
|
||||
The Docker renderer validates the GIF duration. The promotion tool requires the
|
||||
successful ESP32 media manifest, checks file types and size limits, and never
|
||||
copies raw video, browser reports, traces, or arbitrary artifact files. The
|
||||
renderer preserves the real recording but deliberately presents it at 1.25×
|
||||
duration and 6 fps so the
|
||||
README tour is readable; it does not change normal browser-contract timing.
|
||||
ESP8266 remains covered by the normal hardware and browser contract but does
|
||||
not produce duplicate README media.
|
||||
|
||||
Back to [documentation](README.md) · [project overview](../README.md).
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -337,7 +337,16 @@ class WiFiManager
|
||||
unsigned long startedAt = 0;
|
||||
unsigned long finishedAt = 0;
|
||||
unsigned long timeoutMs = 15000;
|
||||
// ESP32's high-level scan-complete notification can arrive before the
|
||||
// radio has fully released its previous scan. A second scan started in
|
||||
// that short window is reported only as WIFI_SCAN_FAILED by Arduino.
|
||||
// Keep the established short ESP8266 cadence, but give ESP32 its
|
||||
// documented radio hand-off time before retrying a user refresh.
|
||||
#ifdef ESP32
|
||||
unsigned long minRestartIntervalMs = 5000;
|
||||
#else
|
||||
unsigned long minRestartIntervalMs = 2000;
|
||||
#endif
|
||||
uint32_t generation = 0;
|
||||
uint32_t runningGeneration = 0;
|
||||
uint32_t completionGeneration = 0;
|
||||
@@ -1061,6 +1070,7 @@ protected:
|
||||
void wmTestForceScanState(wm_scan_state_t state) { _scan.state = state; }
|
||||
void wmTestSetScanStartedAt(unsigned long startedAt) { _scan.startedAt = startedAt; }
|
||||
void wmTestSetScanTimeoutMs(unsigned long timeoutMs) { _scan.timeoutMs = timeoutMs; }
|
||||
unsigned long wmTestGetScanRestartIntervalMs() const { return _scan.minRestartIntervalMs; }
|
||||
void wmTestInjectScanResults(const std::vector<WiFiScanNetwork>& results) {
|
||||
_scanResultsCache = results;
|
||||
_numNetworks = static_cast<int>(results.size());
|
||||
|
||||
@@ -42,6 +42,35 @@ for required in README.md CHANGELOG.md docs/README.md docs/GETTING_STARTED.md do
|
||||
fi
|
||||
done
|
||||
|
||||
check_readme_media() {
|
||||
local asset="$1"
|
||||
local expected_type="$2"
|
||||
local max_bytes="$3"
|
||||
local path="$root/docs/assets/readme/$asset"
|
||||
if [[ ! -s "$path" ]]; then
|
||||
printf 'Missing README media asset: %s\n' "docs/assets/readme/$asset" >&2
|
||||
failed=1
|
||||
return
|
||||
fi
|
||||
if [[ "$(file --brief --mime-type "$path")" != "$expected_type" ]]; then
|
||||
printf 'Unexpected README media type: %s\n' "docs/assets/readme/$asset" >&2
|
||||
failed=1
|
||||
fi
|
||||
if (( $(wc -c < "$path") > max_bytes )); then
|
||||
printf 'README media exceeds its size limit: %s\n' "docs/assets/readme/$asset" >&2
|
||||
failed=1
|
||||
fi
|
||||
}
|
||||
|
||||
check_readme_media portal-tour.gif image/gif $((2 * 1024 * 1024))
|
||||
check_readme_media portal-overview.png image/png $((1024 * 1024))
|
||||
check_readme_media portal-wifi-settings.png image/png $((1024 * 1024))
|
||||
|
||||
if [[ -n "$(git -C "$root" ls-files -- 'artifacts/readme-media/**')" ]]; then
|
||||
printf 'Ignored README media artifacts must not be tracked.\n' >&2
|
||||
failed=1
|
||||
fi
|
||||
|
||||
while IFS= read -r example; do
|
||||
for required in README.md platformio.ini; do
|
||||
if [[ ! -f "$example/$required" ]]; then
|
||||
|
||||
@@ -20,8 +20,13 @@ void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
|
||||
// The fixture intentionally has no station credentials. A finite window
|
||||
// exercises timeout reset without leaving a board in a permanent portal.
|
||||
// This fixture must be independent of whichever sketch was previously
|
||||
// flashed to the board. Clear saved station credentials before starting
|
||||
// the portal so browser artifacts always show the unconfigured flow.
|
||||
wifi.resetSettings();
|
||||
|
||||
// A finite window exercises timeout reset without leaving a board in a
|
||||
// permanent portal.
|
||||
wifi.setConfigPortalTimeout(15 * 60);
|
||||
wifi.setAPStaticIPConfig(
|
||||
IPAddress(192, 168, 4, 1),
|
||||
|
||||
@@ -105,6 +105,7 @@ TestCase tests[] = {
|
||||
TEST_ENTRY(test_scan_cancels_when_connect_pending),
|
||||
TEST_ENTRY(test_scan_cancels_when_lifecycle_blocked),
|
||||
TEST_ENTRY(test_scan_generation_invalidated_on_reset),
|
||||
TEST_ENTRY(test_scan_restart_interval_is_platform_appropriate),
|
||||
|
||||
TEST_ENTRY(test_real_async_scan_completes),
|
||||
// Template rendering tests
|
||||
|
||||
@@ -107,6 +107,7 @@ void test_scan_completion_wait();
|
||||
void test_scan_cancels_when_connect_pending();
|
||||
void test_scan_cancels_when_lifecycle_blocked();
|
||||
void test_scan_generation_invalidated_on_reset();
|
||||
void test_scan_restart_interval_is_platform_appropriate();
|
||||
void test_real_async_scan_completes();
|
||||
|
||||
// Template rendering tests
|
||||
|
||||
@@ -150,3 +150,24 @@ void test_scan_generation_invalidated_on_reset() {
|
||||
Serial.println("[TEST] Scan generation invalidation on reset test completed successfully");
|
||||
}
|
||||
|
||||
void test_scan_restart_interval_is_platform_appropriate() {
|
||||
Serial.println("[TEST] Testing platform scan restart interval...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
#ifdef ESP32
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(
|
||||
5000, wm.wmTestGetScanRestartIntervalMs(),
|
||||
"ESP32 must wait for the radio to settle after a completed scan");
|
||||
#else
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(
|
||||
2000, wm.wmTestGetScanRestartIntervalMs(),
|
||||
"ESP8266 keeps the established responsive scan restart interval");
|
||||
#endif
|
||||
#else
|
||||
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
|
||||
#endif
|
||||
|
||||
Serial.println("[TEST] Platform scan restart interval test completed successfully");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG PLAYWRIGHT_VERSION=1.63.0
|
||||
FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble
|
||||
FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble AS portal-contract
|
||||
|
||||
ARG PLAYWRIGHT_VERSION
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
@@ -8,4 +8,12 @@ COPY tests/portal-contract/package.json tests/portal-contract/package-lock.json
|
||||
RUN npm ci --ignore-scripts && \
|
||||
[ "$(node -p "require('@playwright/test/package.json').version")" = "$PLAYWRIGHT_VERSION" ]
|
||||
COPY tests/portal-contract/ ./
|
||||
CMD ["npm", "test"]
|
||||
RUN chmod 755 /work/run-portal-contract.sh /work/render-readme-media.sh
|
||||
CMD ["/work/run-portal-contract.sh"]
|
||||
|
||||
FROM portal-contract AS media
|
||||
USER root
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ffmpeg && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
USER pwuser
|
||||
|
||||
@@ -3,6 +3,7 @@ services:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: tests/portal-contract/Dockerfile
|
||||
target: ${PORTAL_CONTRACT_DOCKER_TARGET:-portal-contract}
|
||||
args:
|
||||
PLAYWRIGHT_VERSION: "1.63.0"
|
||||
network_mode: host
|
||||
@@ -13,6 +14,7 @@ services:
|
||||
HOME: /tmp
|
||||
PORTAL_URL: http://192.168.4.1
|
||||
PORTAL_BROWSER_MODE: ${PORTAL_BROWSER_MODE:-auto}
|
||||
PORTAL_CAPTURE_README_MEDIA: ${PORTAL_CAPTURE_README_MEDIA:-0}
|
||||
ARTIFACT_DIR: /artifacts
|
||||
volumes:
|
||||
- ${PORTAL_ARTIFACT_DIR:?portal artifact directory is required}:/artifacts
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
artifact_dir="${ARTIFACT_DIR:?ARTIFACT_DIR is required}"
|
||||
media_dir="$artifact_dir/readme-media"
|
||||
source_video="$media_dir/raw/portal-tour.webm"
|
||||
target_gif="$media_dir/portal-tour.gif"
|
||||
|
||||
[[ -s "$source_video" ]] || {
|
||||
echo "README media video was not recorded: $source_video" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ffmpeg -hide_banner -loglevel error -y -i "$source_video" \
|
||||
-filter_complex '[0:v]setpts=1.25*PTS,fps=6,scale=720:-2:flags=lanczos,split[a][b];[a]palettegen=max_colors=128[p];[b][p]paletteuse' \
|
||||
-loop 0 "$target_gif"
|
||||
|
||||
[[ -s "$target_gif" ]] || {
|
||||
echo "README media GIF was not rendered: $target_gif" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
duration="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$target_gif")"
|
||||
awk -v duration="$duration" 'BEGIN { exit !(duration >= 4 && duration <= 30) }' || {
|
||||
echo "README media GIF duration is outside the 4–30 second review range: $duration" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
max_bytes=$((2 * 1024 * 1024))
|
||||
(( $(wc -c < "$target_gif") <= max_bytes )) || {
|
||||
echo "README media GIF exceeds its 2 MiB documentation budget: $target_gif" >&2
|
||||
exit 1
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
/work/node_modules/.bin/playwright test --config /work/playwright.config.js
|
||||
|
||||
if [[ "${PORTAL_CAPTURE_README_MEDIA:-0}" == "1" ]]; then
|
||||
/work/render-readme-media.sh
|
||||
fi
|
||||
@@ -0,0 +1,68 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
function mediaPath(...parts) {
|
||||
const root = process.env.ARTIFACT_DIR || '/artifacts';
|
||||
const target = path.join(root, 'readme-media', ...parts);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
||||
return target;
|
||||
}
|
||||
|
||||
async function waitForCompletedScan(request) {
|
||||
await expect.poll(async () => {
|
||||
try {
|
||||
const response = await request.get('/api/wifi/scan-status');
|
||||
if (!response.ok()) return false;
|
||||
const result = JSON.parse(await response.text());
|
||||
return result.state === 'complete' && result.results_valid && result.count > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, { timeout: 45_000, intervals: [500, 800, 1_000] }).toBe(true);
|
||||
}
|
||||
|
||||
test.describe('WiFiManager README media', () => {
|
||||
test.skip(process.env.PORTAL_CAPTURE_README_MEDIA !== '1', 'README capture was not requested.');
|
||||
|
||||
test('records an approved ESP32 portal tour', async ({ browser, request }) => {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 720, height: 900 },
|
||||
recordVideo: {
|
||||
dir: mediaPath('raw'),
|
||||
size: { width: 720, height: 900 },
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') errors.push(message.text());
|
||||
});
|
||||
|
||||
const video = page.video();
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('#wm-reset-portal-timeout')).toBeVisible();
|
||||
// These pauses exist only in the README recording. The ordinary contract
|
||||
// remains timing-focused; this tour needs readable stable states.
|
||||
await page.waitForTimeout(1200);
|
||||
await page.screenshot({ path: mediaPath('portal-overview.png'), fullPage: true });
|
||||
|
||||
await page.locator('a[href="#/wifi"]').click();
|
||||
await expect(page.locator('#wm-refresh-scan')).toBeVisible();
|
||||
await waitForCompletedScan(request);
|
||||
await expect(page.locator('#wm-scan-results .wm-scan-row').first()).toBeVisible();
|
||||
await page.locator('#wm-f-installation_label').fill('Workshop sensor');
|
||||
await page.waitForTimeout(1200);
|
||||
await page.screenshot({ path: mediaPath('portal-wifi-settings.png'), fullPage: true });
|
||||
|
||||
await page.locator('#wm-refresh-scan').click();
|
||||
await expect(page.locator('#wm-wifi-scan-overlay')).toBeVisible();
|
||||
await page.waitForTimeout(1200);
|
||||
await context.close();
|
||||
|
||||
const source = await video.path();
|
||||
fs.renameSync(source, mediaPath('raw', 'portal-tour.webm'));
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
+69
-2
@@ -13,7 +13,8 @@ Usage:
|
||||
--client-interface IFACE [--take-over-client-adapter] [--output DIRECTORY]
|
||||
./tools/portal-hardware run --platform esp8266|esp32 --port /dev/serial/by-id/... \
|
||||
--client-interface IFACE [--take-over-client-adapter] [--keep] \
|
||||
[--browser auto|skip] [--station-env PATH] [--output DIRECTORY]
|
||||
[--browser auto|skip] [--station-env PATH] [--output DIRECTORY] \
|
||||
[--capture-readme-media]
|
||||
./tools/portal-hardware down
|
||||
|
||||
Only the named client interface may be disconnected or reconfigured. The tool
|
||||
@@ -35,6 +36,7 @@ keep="no"
|
||||
browser="auto"
|
||||
station_env=""
|
||||
output_dir=""
|
||||
capture_readme_media="no"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--platform) [[ $# -ge 2 ]] || usage; platform="$2"; shift 2 ;;
|
||||
@@ -45,6 +47,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--browser) [[ $# -ge 2 ]] || usage; browser="$2"; shift 2 ;;
|
||||
--station-env) [[ $# -ge 2 ]] || usage; station_env="$2"; shift 2 ;;
|
||||
--output) [[ $# -ge 2 ]] || usage; output_dir="$2"; shift 2 ;;
|
||||
--capture-readme-media) capture_readme_media="yes"; shift ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
@@ -54,12 +57,17 @@ require_common() {
|
||||
wm_require nmcli
|
||||
wm_require pio
|
||||
wm_require docker
|
||||
wm_require curl
|
||||
docker compose version >/dev/null
|
||||
}
|
||||
|
||||
prepare_output_dir() {
|
||||
if [[ -z "$output_dir" ]]; then
|
||||
output_dir="$(wm_portal_state_root)/runs/$(date -u +%Y%m%dT%H%M%SZ)-$platform"
|
||||
if [[ "$capture_readme_media" == "yes" ]]; then
|
||||
output_dir="$root/artifacts/readme-media/$(date -u +%Y%m%dT%H%M%SZ)-$platform"
|
||||
else
|
||||
output_dir="$(wm_portal_state_root)/runs/$(date -u +%Y%m%dT%H%M%SZ)-$platform"
|
||||
fi
|
||||
fi
|
||||
install -d -m 700 "$output_dir"
|
||||
output_dir="$(cd "$output_dir" && pwd)"
|
||||
@@ -77,6 +85,51 @@ validate_run_arguments() {
|
||||
echo "Station environment file is not readable: $station_env" >&2
|
||||
exit 1
|
||||
}
|
||||
if [[ "$capture_readme_media" == "yes" ]]; then
|
||||
[[ "$command_name" == "run" && "$platform" == "esp32" ]] || {
|
||||
echo "--capture-readme-media is supported only by run --platform esp32" >&2
|
||||
exit 2
|
||||
}
|
||||
[[ "$browser" == "auto" ]] || {
|
||||
echo "--capture-readme-media requires --browser auto" >&2
|
||||
exit 2
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
write_readme_media_manifest() {
|
||||
local media_dir="$output_dir/readme-media"
|
||||
local required
|
||||
for required in portal-tour.gif portal-overview.png portal-wifi-settings.png; do
|
||||
[[ -s "$media_dir/$required" ]] || {
|
||||
echo "README media output is incomplete: $media_dir/$required" >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
printf '{"schema":1,"project":"WiFiManager","kind":"readme-media","status":"passed","platform":"esp32","created_at":"%s"}\n' \
|
||||
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$media_dir/manifest.json"
|
||||
chmod 600 "$media_dir/manifest.json"
|
||||
}
|
||||
|
||||
wait_for_portal_ready() {
|
||||
# A portal SSID can be visible before its first background scan has
|
||||
# completed. Wait for that normal portal-start work before the browser
|
||||
# contract asks the device to perform a second, user-initiated refresh.
|
||||
local attempt response
|
||||
for attempt in $(seq 1 45); do
|
||||
response="$(curl --interface "$client_interface" --connect-timeout 3 --max-time 5 \
|
||||
--silent --show-error --fail http://192.168.4.1/api/wifi/scan-status 2>/dev/null || true)"
|
||||
if [[ "$response" == *'"state":"complete"'* && "$response" == *'"results_valid":true'* ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$response" == *'"state":"failed"'* || "$response" == *'"state":"timeout"'* ]]; then
|
||||
echo "Portal background scan did not become ready: $response" >&2
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo 'Portal did not complete its initial Wi-Fi scan within 45 seconds.' >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
start_portal_session() {
|
||||
@@ -97,6 +150,10 @@ start_portal_session() {
|
||||
wm_remove_connection "$WM_PORTAL_CONNECTION_UUID"
|
||||
return 1
|
||||
fi
|
||||
if ! wait_for_portal_ready; then
|
||||
wm_remove_connection "$WM_PORTAL_CONNECTION_UUID"
|
||||
return 1
|
||||
fi
|
||||
if ! wm_write_state "$client_interface" "$platform" "$WM_PORTAL_CONNECTION_UUID" "$WM_PORTAL_CONNECTION_NAME"; then
|
||||
wm_remove_connection "$WM_PORTAL_CONNECTION_UUID"
|
||||
return 1
|
||||
@@ -139,6 +196,13 @@ case "$command_name" in
|
||||
export LOCAL_UID="$(id -u)"
|
||||
export LOCAL_GID="$(id -g)"
|
||||
export PORTAL_BROWSER_MODE="$browser"
|
||||
if [[ "$capture_readme_media" == "yes" ]]; then
|
||||
export PORTAL_CAPTURE_README_MEDIA=1
|
||||
export PORTAL_CONTRACT_DOCKER_TARGET=media
|
||||
else
|
||||
export PORTAL_CAPTURE_README_MEDIA=0
|
||||
export PORTAL_CONTRACT_DOCKER_TARGET=portal-contract
|
||||
fi
|
||||
compose_files=(-f "$root/tests/portal-contract/compose.yaml")
|
||||
if [[ -n "$station_env" ]]; then
|
||||
export PORTAL_STATION_ENV_HOST="$(cd "$(dirname "$station_env")" && pwd)/$(basename "$station_env")"
|
||||
@@ -148,6 +212,9 @@ case "$command_name" in
|
||||
# this invocation always tests the checked-out files, not a stale image.
|
||||
docker compose "${compose_files[@]}" build portal-contract
|
||||
docker compose "${compose_files[@]}" run --rm portal-contract
|
||||
if [[ "$capture_readme_media" == "yes" ]]; then
|
||||
write_readme_media_manifest
|
||||
fi
|
||||
printf 'Portal contract passed. Artifacts: %s\n' "$output_dir"
|
||||
if [[ "$keep" == "yes" ]]; then
|
||||
printf 'Portal session remains connected; run ./tools/portal-hardware down when finished.\n'
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source_dir=""
|
||||
replace="no"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF' >&2
|
||||
Usage: ./tools/promote-readme-media --from ARTIFACT_DIRECTORY --replace
|
||||
|
||||
Copies approved WiFiManager README media from a successful ESP32 capture. The
|
||||
source directory is the run directory containing readme-media/manifest.json.
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from) [[ $# -ge 2 ]] || usage; source_dir="$2"; shift 2 ;;
|
||||
--replace) replace="yes"; shift ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$source_dir" && "$replace" == "yes" ]] || usage
|
||||
source_dir="$(cd "$source_dir" && pwd)"
|
||||
media_dir="$source_dir/readme-media"
|
||||
manifest="$media_dir/manifest.json"
|
||||
|
||||
[[ -r "$manifest" ]] || { echo "Missing README media manifest: $manifest" >&2; exit 1; }
|
||||
grep -Fq '"project":"WiFiManager"' "$manifest" &&
|
||||
grep -Fq '"kind":"readme-media"' "$manifest" &&
|
||||
grep -Fq '"status":"passed"' "$manifest" &&
|
||||
grep -Fq '"platform":"esp32"' "$manifest" || {
|
||||
echo "The media manifest is not a successful WiFiManager ESP32 capture." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
target_dir="$root/docs/assets/readme"
|
||||
validate_asset() {
|
||||
local source_file="$1" expected_type="$2" max_bytes="$3"
|
||||
[[ -s "$source_file" ]] || { echo "Missing media asset: $source_file" >&2; exit 1; }
|
||||
[[ "$(file --brief --mime-type "$source_file")" == "$expected_type" ]] || {
|
||||
echo "Unexpected media type for $source_file" >&2
|
||||
exit 1
|
||||
}
|
||||
(( $(wc -c < "$source_file") <= max_bytes )) || {
|
||||
echo "README media exceeds its size limit: $source_file" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Verify the complete candidate before changing any tracked README asset.
|
||||
validate_asset "$media_dir/portal-tour.gif" image/gif $((2 * 1024 * 1024))
|
||||
validate_asset "$media_dir/portal-overview.png" image/png $((1024 * 1024))
|
||||
validate_asset "$media_dir/portal-wifi-settings.png" image/png $((1024 * 1024))
|
||||
|
||||
install -d "$target_dir"
|
||||
for asset in portal-tour.gif portal-overview.png portal-wifi-settings.png; do
|
||||
source_file="$media_dir/$asset"
|
||||
cp -- "$source_file" "$target_dir/$asset"
|
||||
printf 'Promoted %s\n' "docs/assets/readme/$asset"
|
||||
done
|
||||
@@ -27,6 +27,8 @@ printf '%s\n' '#!/usr/bin/env bash' \
|
||||
'printf "docker %s\n" "$*" >>"$CALL_LOG"' \
|
||||
'if [[ "$1" == "compose" && "$2" == "version" ]]; then echo "Docker Compose"; exit 0; fi' \
|
||||
'exit 0' >"$stub_bin/docker"
|
||||
printf '%s\n' '#!/usr/bin/env bash' \
|
||||
'printf "{\"state\":\"complete\",\"results_valid\":true}"' >"$stub_bin/curl"
|
||||
chmod 755 "$stub_bin"/*
|
||||
export PATH="$stub_bin:$PATH"
|
||||
|
||||
@@ -44,6 +46,11 @@ if "$root/tools/portal-hardware" up --platform >/dev/null 2>&1; then
|
||||
echo 'missing option value did not reject the request' >&2
|
||||
exit 1
|
||||
fi
|
||||
if "$root/tools/portal-hardware" run --platform esp8266 --port /dev/null \
|
||||
--client-interface wlan-client --capture-readme-media >/dev/null 2>&1; then
|
||||
echo 'ESP8266 README media capture was accepted' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A failed association must delete the only connection it just created.
|
||||
source "$root/tools/lib/portal-hardware-session.sh"
|
||||
@@ -82,5 +89,9 @@ run_line="$(grep -n " run --rm portal-contract$" "$CALL_LOG" | tail -1 | cut -d:
|
||||
echo "portal contract was not rebuilt before execution" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fq 'wait_for_portal_ready' "$root/tools/portal-hardware"
|
||||
grep -Fq 'api/wifi/scan-status' "$root/tools/portal-hardware"
|
||||
grep -Fq 'README media GIF exceeds its 2 MiB documentation budget' \
|
||||
"$root/tests/portal-contract/render-readme-media.sh"
|
||||
|
||||
echo 'portal-hardware CLI safety checks passed'
|
||||
|
||||
Reference in New Issue
Block a user