BabyMobile Audio Manager (PWA)
A dependency-free, client-side web app that scans for the BabyMobile XIAO (nRF52840) over Bluetooth LE and manages the audio files on it. No backend, no build step, nothing leaves your device — all audio conversion happens in the browser with the Web Audio API.
Pairs with the firmware in ../baby_mobile_v2/baby_mobile_v2.ino.
What it does
- Scan & connect to the
BabyMobileBLE peripheral. - Convert any browser-decodable file (WAV/MP3/M4A/OGG/FLAC…) to a conformant mono PCM WAV — 8-bit / 8 kHz by default, with 8/16-bit and 8/16/32 kHz selectable. The full WAV (44-byte header + data) is streamed intact; the firmware self-configures bit depth and sample rate from the header.
- Upload to any of the 32 track slots, with a live progress bar driven by the device's status-notification byte count.
- Play / stop any slot.
- Remember what you uploaded per browser (localStorage), since the firmware exposes no track-listing command over BLE.
BLE protocol used
| UUID | Use | |
|---|---|---|
| Service | 12340001-… |
advertised |
| Cmd (write) | 12340002-… |
0x02 <slot> start upload · 0x03 finish · 0x04 <slot> play · 0x05 stop · 0x06 list · 0x07 <slot> delete · 0x08 <pct> set volume (persisted) · 0x09 get volume |
| Data (write-without-response) | 12340003-… |
raw WAV bytes, chunked to MTU−3 |
| Status (notify) | 12340004-… |
tagged by byte 0: 0x00… upload byte count (uint32 BE) · 0x80|idx, bits, kHz, secs list entry · 0xC0, pct volume reply · 0xD0, idx, ok delete reply · 0xFF, n list end |
Volume
- Global volume is a master gain on the device (Q8 fixed-point, applied to all
playback). It's persisted to LittleFS (
/volume) so it survives reboots. The app reads it on connect (0x09) and sets it via the Playback-volume slider (0x08). Lowering it reduces the PWM swing → less amp output → less supply current, which mitigates the regulator brown-out at high volume. - Per-file level is baked into the WAV in the browser before upload (the "This file's level" slider scales the samples). Effective loudness = global × per-file. Note: attenuating an 8-bit clip costs dynamic range, so use 16-bit if you plan to run a file much quieter than the rest.
Running it
Web Bluetooth requires a secure context: https:// or http://localhost.
# from this app/ directory — any static server works
python -m http.server 8000
# then open http://localhost:8000
Deploy the folder as-is to any static host (GitHub Pages, Netlify, Cloudflare Pages…) to install it as a PWA from a phone. It's a complete app shell with a service worker, so it also launches offline once cached.
Platform support
| Platform | BLE works? | How |
|---|---|---|
| Android | ✅ | Chrome or Edge; "Add to Home screen" installs the PWA |
| Windows / macOS / Linux | ✅ | Chrome or Edge |
| iPhone / iPad | ⚠️ via Bluefy | iOS Safari/WebKit has no Web Bluetooth |
iOS setup (no App Store)
Apple doesn't ship Web Bluetooth, so a plain Safari PWA can't reach BLE. Use the free Bluefy – Web BLE Browser: open this app's URL inside Bluefy and everything works. (Bluefy itself is the only App Store install needed — the app stays a web app.)
If you later want a true installable iOS app without Bluefy, wrap this same
folder in Capacitor with @capacitor-community/bluetooth-le
and reuse ble.js's protocol constants.
Notes & limits
- Flash size: the firmware's POC stores tracks in the nRF52840's internal LittleFS, which is small. 8-bit/8 kHz ≈ 8 KB per second of audio — keep clips short. If the device acknowledges fewer bytes than sent, the app warns you (flash likely full).
- Listing & delete are live: on connect (and after every upload/delete) the
app sends
0x06to read the device's real track table, and0x07deletes a slot on the device. Track titles aren't stored on the device, so they're remembered locally and shown next to the live entries; an uploaded slot with no local name just shows "Track N". - MTU: data is chunked to 180 bytes to stay within a modest negotiated ATT MTU (firmware char max is 240).
- Name shows as "BabyM" (truncated): the 128-bit service UUID fills the 31-byte
advertisement, leaving room for only 5 name characters. The browser can't read
the full GAP name (that service is blocklisted in Web Bluetooth). Fix it in the
firmware by moving the name to the scan response, which has its own 31 bytes:
replace
Bluefruit.Advertising.addName();withBluefruit.ScanResponse.addName();. - Reconnecting after a disconnect: a just-disconnected peripheral leaves the
link half-open briefly, so the first reconnect could fail until a page refresh.
connect()now retries (3×, 600 ms backoff) after an explicit disconnect, which recovers without refreshing. Scanning matches on the service UUID first, so a truncated advertised name no longer breaks the name filter.