Web Portal¶
The Web Portal turns the CyberFidget into a WiFi access point with a captive portal, giving you a browser-based interface to manage music files, create playlists, and preview tracks — all from your phone. It can also join your home WiFi network for easy access at cyberfidget.local.
What is this?¶
Connect your phone to the "CyberFidget" WiFi network and a web portal opens automatically (captive portal). No app installs, no IP addresses to remember. From there you can:
- Upload MP3 files via drag-and-drop
- Browse your music library with real ID3 metadata (title, artist, album)
- Play tracks through your phone's speaker (web audio)
- Manage files — move, delete, create folders
- Build playlists in M3U format that persist on the SD card
- Connect to WiFi — join your home network for
cyberfidget.localaccess
How it works¶
Phone → Connects to "CyberFidget" WiFi AP
→ Captive portal auto-opens browser at 192.168.4.1
→ SPA loads (HTML/CSS/JS served from ESP32 flash)
→ API calls read/write files on the SD card
→ Audio served directly from SD over HTTP
Or, if connected to your home WiFi:
Phone → Same WiFi network as CyberFidget
→ Browse to http://cyberfidget.local
→ Same portal, no WiFi switching needed
The portal is a standalone app launched from the main menu under Tools → CyberFidget Portal. It stops Bluetooth (shared radio) and starts WiFi, so you can't play music through a BT speaker while the portal is running.
WiFi and Bluetooth share the ESP32 radio
The ESP32 can't run WiFi AP and BT A2DP simultaneously with enough bandwidth for audio streaming. The portal calls btStop() on entry and WiFi shuts down on exit. BT reconnects automatically when you return to the Music Player.
Architecture¶
App lifecycle¶
Menu → "CyberFidget Portal" → AppManager::switchToApp(APP_WEB_PORTAL)
1. MusicPlayerApp::end() — saves state, stops playback
2. WebPortalApp::begin() — btStop(), WiFi AP+STA, mDNS, DNSServer, AsyncWebServer
... user manages files via captive portal or cyberfidget.local ...
3. WebPortalApp::end() — server stop, mDNS stop, WiFi.mode(WIFI_OFF)
4. Back to menu
WiFi modes¶
The portal runs in AP+STA dual mode (WIFI_AP_STA):
- Access Point — "CyberFidget" network is always available. Any device can connect directly and access the portal at
192.168.4.1. - Station — If you've configured a WiFi network in Settings, the CyberFidget also joins your home WiFi. This makes the portal accessible at
cyberfidget.localor the device's LAN IP from any device on your network.
WiFi credentials are stored in NVS (non-volatile storage) and auto-connect on every portal launch. Use the Settings page to scan, connect, or forget networks.
mDNS: cyberfidget.local
When connected to your WiFi, the device registers cyberfidget.local via mDNS. This works on iOS, macOS, Linux, and Android 10+. If mDNS doesn't resolve on your device, the IP address is always shown on the OLED and in the portal status bar.
Captive portal¶
Uses DNSServer to redirect all DNS queries to 192.168.4.1. When a phone connects to the AP, its OS sends connectivity-check requests which hit our web server, triggering the "Sign in to WiFi" popup.
The onNotFound() handler redirects unknown URLs to / — this catches captive portal detection from iOS, Android, Windows, and macOS.
Android captive portal browser limitations
Android's "Sign in" mini-browser doesn't support file picker inputs. A banner detects this and prompts users to open 192.168.4.1 in their full browser (Chrome, etc.) where file upload works normally.
OLED display¶
While the portal is running, the 128x64 OLED shows:
┌────────────────────────────────┐
│ ===== CyberFidget Web ======== │
│ AP: 192.168.4.1 │
│ HomeNet 192.168.1.42 │
│ cyberfidget.local │
│ 42 files | 2 clients │
└────────────────────────────────┘
If not connected to a WiFi network, lines 3-4 show "WiFi: not connected" and the file count instead.
During uploads, the bottom line shows a progress bar.
SD card layout¶
Media files live under /media/ with arbitrary nesting:
/media/
├── track.mp3 ← flat files
├── Artist Name/
│ ├── track.mp3 ← artist folders
│ └── Album Name/
│ └── track.mp3 ← artist/album nesting
├── playlists/
│ ├── Chill.m3u ← M3U playlist files
│ └── Workout.m3u
└── My Folder/
└── track.mp3 ← user-defined folders
The Music Player's AudioSourceIdxSD and ID3Scanner both scan /media/ recursively — any .mp3 file at any depth is discovered.
Organize however you want
The scanner doesn't care about folder structure. Flat files, nested by artist/album, or any combination — it all works. Folders are just for your own organization.
API reference¶
All API routes are under the ESPAsyncWebServer running on port 80.
| Route | Method | Purpose |
|---|---|---|
/ |
GET | Portal page (SPA from PROGMEM) |
/media/* |
GET | Static file serving from SD (for audio playback) |
/api/files |
GET | Recursive JSON folder tree |
/api/tracks |
GET | Flat JSON array with ID3 metadata per track |
/api/upload?dir=/media/... |
POST | Multipart file upload |
/api/delete?path=/media/... |
POST | Delete file or folder |
/api/mkdir?path=/media/... |
POST | Create directory |
/api/move?from=...&to=... |
POST | Move/rename file |
/api/status |
GET | File count, SD space, connected clients |
/api/playlists |
GET | List all M3U playlists |
/api/playlist?name=... |
GET | Read playlist tracks |
/api/playlist?name=... |
POST | Save playlist (JSON body) |
/api/playlist/delete?name=... |
POST | Delete playlist |
/api/wifi/scan |
GET | Scan nearby WiFi networks |
/api/wifi/connect |
POST | Connect to WiFi (JSON: ssid, pass) |
/api/wifi/status |
GET | WiFi connection status, IP, mDNS |
/api/wifi/forget |
POST | Clear saved WiFi credentials |
Example: /api/tracks response¶
[
{
"path": "/media/Rock/song.mp3",
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name",
"size": 4521984
}
]
ID3 tags are read on-the-fly from each MP3 file (ID3v2 first, ID3v1 fallback). Title falls back to filename if no tags are present.
Example: /api/playlist save body¶
{
"tracks": [
"/media/Rock/song.mp3",
"/media/Pop/track.mp3"
]
}
Web UI features¶
Track table¶
The default view shows all tracks in a sortable, searchable table with columns for title, artist, album, and size. Data comes from /api/tracks with real ID3 metadata.
Each track has action buttons (visible on hover/tap): - Play — streams audio through the phone's browser - +PL — add to a playlist via dropdown - Del — delete with confirmation
Web audio player¶
Tracks are served directly from the SD card via serveStatic("/media/", SD, "/media/"). The browser's native <audio> element handles decoding — zero CPU cost on the ESP32.
The player bar shows: - Now-playing title and artist - Play/pause, previous, next controls - Visual progress bar (interactive scrubbing planned for future) - Elapsed and remaining time
Playing any track auto-builds a queue from all loaded tracks, so next/prev cycles through your library.
Playlists¶
M3U files stored in /media/playlists/. The web UI supports:
- Create/delete playlists
- Add tracks from the file browser
- Play entire playlists (sequential playback)
- Per-track play buttons within expanded playlists
- Missing file detection (dimmed with warning if referenced file no longer exists)
Now-playing highlight¶
The currently playing track is highlighted with a cyan accent bar: - In the track table when playing from the table or folder view - In the playlist when playing from a playlist
The highlight follows next/prev navigation and persists across sort/search/re-render.
Settings page¶
The Settings page provides WiFi network configuration:
- WiFi Status — shows current connection state, network name, IP address, and mDNS hostname
- Network Scanner — scan for nearby WiFi networks with signal strength bars
- Connect — select a network, enter password, connect. Credentials are saved to NVS for auto-reconnect.
- Forget — clear saved credentials and disconnect from the network
- AP Info — shows the always-available access point details
Build impact¶
| Before (Phase 2.75) | After (Phase 3.1) | Delta | |
|---|---|---|---|
| RAM | 16.3% (53 KB) | 22.7% (74 KB) | +21 KB |
| Flash | 52.3% (1.6 MB) | 74.6% (2.3 MB) | +700 KB |
The flash increase is primarily ESPAsyncWebServer + WiFi libraries + ESPmDNS + the portal HTML/CSS/JS. RAM increase is the WiFi AP+STA stack (freed when portal exits).
Files¶
| File | Lines | Purpose |
|---|---|---|
lib/WebPortalApp/WebPortalApp.h |
~80 | Class declaration (AP+STA, mDNS) |
lib/WebPortalApp/WebPortalApp.cpp |
~1000 | App lifecycle, WiFi STA, API routes, ID3 reader, OLED render |
lib/WebPortalApp/portal_page.h |
~950 | PROGMEM SPA (HTML + CSS + JS + Settings page) |
scripts/add_network_lib.py |
~35 | PlatformIO pre-build script for Network library |
Network library linkage¶
pioarduino's ESP32 Arduino 3.x core split the WiFi library into WiFi + Network. PlatformIO's library dependency finder discovers WiFi (via ESPAsyncWebServer) but misses Network. The add_network_lib.py script compiles and links the framework's Network library using env.BuildLibrary().
Common issues¶
| Symptom | Cause | Fix |
|---|---|---|
| "Sign in to WiFi" browser can't upload files | Android captive portal WebView has restricted file input | Open 192.168.4.1 in Chrome/Firefox instead |
| Portal page doesn't load | DNS redirect failed | Manually navigate to http://192.168.4.1 |
| Upload fails with 507 | SD card full | Delete files to free space |
| BT speaker won't reconnect after portal | WiFi didn't shut down cleanly | Restart the device |
idx.txt showing in file list |
Music index cache file | Filtered out in /api/files and /api/tracks |
| Track shows "-" for artist/album | No ID3 tags in the MP3 file | Re-tag the file with a tool like Mp3tag |
cyberfidget.local doesn't resolve |
mDNS not supported on device (older Android) | Use the IP address shown on the OLED or portal status bar |
| WiFi connection times out | Wrong password or network out of range | Re-enter password via Settings, move closer to router |
| Can't reach portal from home WiFi | Not connected to any WiFi network | Open Settings, scan, and connect to your WiFi first |