feat: implement the open fallback access point and captive portal

This commit is contained in:
2026-09-01 21:41:23 +03:00
parent fd978f323d
commit 50ed341474
13 changed files with 349 additions and 29 deletions
+9
View File
@@ -49,6 +49,10 @@ Failure (maximum 160 encoded bytes):
| -------------------------- | --------------: | --------------------------------------------- |
| `GET /api/info` | 128 B target | public device/slot state and names, 384 B |
| `GET /api/health` | 128 B target | diagnostics without secrets, 320 B; includes reset reason |
| `GET /api/network/status` | 128 B target | unauthenticated state/message; never includes SSID/password |
| `GET /api/network/scan` | 128 B target | unauthenticated bounded scan state and up to 12 SSIDs |
| `POST /api/network/validate` | 160 B body | unauthenticated `{ssid,password}` validation; password is never returned |
| `POST /api/network/delete` | 0 B body | unauthenticated deletion of the saved profile |
| `POST /api/session/join` | 192 B body | `{name,requestedRole}`; token and role, 192 B |
| `POST /api/session/resume` | 96 B body | `{token}`; role and state metadata, 192 B |
| `POST /api/session/leave` | 80 B body | `{token,gameId}`; releases only that session |
@@ -82,6 +86,11 @@ polling returns `SESSION_INVALIDATED` with the same bounded recovery metadata.
The recovery generation and `gameId` make delayed commands stale; retrying a
completed recovery is harmless and cannot mutate a newly registered session.
Network configuration is deliberately unauthenticated so it works from the
open fallback AP and the regular local address. Any reachable client can change
or delete the saved profile; the open AP and local HTTP do not provide password
confidentiality. Network responses and diagnostics never include the password.
## Role-safe state event
The HTTP state response and WebSocket `state` event use this single 768-byte
+13 -2
View File
@@ -13,8 +13,9 @@ does not replace the locked rules in [MVP.md](MVP.md), the HTTP contract in
`esp_littlefs` 1.20.4.
- USB serial device: normally `/dev/ttyACM0`; confirm with `pio device list`
before an upload.
- A local `include/wifi_config.h`, copied from `include/wifi_config.h.example`.
It is ignored by Git. Never commit the SSID or password.
- No firmware-embedded Wi-Fi credentials are required. On first boot, join the
open `Battleship-open` network and open `http://192.168.4.1/setup` if the
captive portal does not appear.
## Build and upload
@@ -49,6 +50,16 @@ The response includes free/minimum heap, largest free block, connection count,
rejected input count, Wi-Fi state, and reset reason. It must not contain Wi-Fi
credentials or session tokens.
## Network configuration and local-security limitation
The `/setup` screen and `/api/network/*` routes are intentionally unauthenticated
so a new device can be configured through either the fallback AP or its regular
local address. Any client that can reach the device can change or delete the
saved Wi-Fi configuration. The open AP and plain local HTTP do not protect a
submitted password from a nearby network observer. Configure the board only on
a network you trust, and do not use this feature for credentials that require
strong confidentiality.
## MVP acceptance checklist
Record the observed result, device address, browser/device model, and any
+34
View File
@@ -35,6 +35,40 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Health'
/api/network/status:
get:
summary: Read unauthenticated network configuration status without credentials
responses:
'200': { description: Network state and Russian status message. }
/api/network/scan:
get:
summary: Start or retrieve a bounded unauthenticated Wi-Fi scan
responses:
'200': { description: `scanning` or a bounded list of SSIDs; no passwords. }
'409': { $ref: '#/components/responses/Conflict' }
/api/network/validate:
post:
summary: Validate credentials and persist them only after association succeeds
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [ssid, password]
properties:
ssid: { type: string, minLength: 1, maxLength: 32 }
password: { type: string, maxLength: 63, writeOnly: true }
responses:
'200': { description: Validation started; poll network status for result. }
'400': { $ref: '#/components/responses/BadRequest' }
'409': { $ref: '#/components/responses/Conflict' }
/api/network/delete:
post:
summary: Delete the saved network profile and enter fallback mode
responses:
'200': { description: Profile deleted. }
'409': { $ref: '#/components/responses/Conflict' }
/api/session/join:
post:
summary: Create a session and request a role