# 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 `BabyMobile` BLE 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 ` start upload · `0x03` finish · `0x04 ` play · `0x05` stop · `0x06` list · `0x07 ` delete · `0x08 ` 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`. ```sh # 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](https://apps.apple.com/app/bluefy-web-ble-browser/id1492822055)**: 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](https://capacitorjs.com/) 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 `0x06` to read the device's real track table, and `0x07` deletes 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();` with `Bluefruit.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.