aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/specs/2026-11-07-sonix-player-design.md194
1 files changed, 194 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-11-07-sonix-player-design.md b/docs/superpowers/specs/2026-11-07-sonix-player-design.md
new file mode 100644
index 0000000..5ab7794
--- /dev/null
+++ b/docs/superpowers/specs/2026-11-07-sonix-player-design.md
@@ -0,0 +1,194 @@
+# Sonix player
+
+A HiBy R1 running Sonix Player, reached over the LAN, gets three places in
+the desktop shell: a boot logo editor in the settings window, a drawer tile
+and page for its state and library, and an icon in the island's status pill
+that opens its playback controls.
+
+Everything goes through two servers the player runs:
+
+- **SonixLink**, HTTP on port 7800, no authentication. JSON state, commands,
+ covers, the library. Switched on from the player's control centre and
+ left on.
+- **Wi-Fi transfer**, HTTP on port 80, the card's file server. It runs only
+ while its page is open on the player: leaving the page by any route stops
+ it (Sonix `gui/wireless/wifitransfer.c`, on purpose: an unauthenticated
+ root file server). Used only for the boot logo upload.
+
+## Layout
+
+```
+desktop/
+ Sonix.qml singleton service: presence, state, commands, upload
+ island/SonixPanel.qml the controls panel; the status pill gains the R1 icon
+ modules/sonix/
+ Sonix.js pure functions, tested by test_sonix.js
+ test_sonix.js
+ sonix-beacon.py stdlib UDP listener for the player's beacon
+ test_sonix_beacon.py
+ SonixModule.qml drawer tile + page
+ SonixPage.qml tabs: Device, Playlists, Folders
+ BootlogoModule.qml settings window page, group Appearance
+ BootlogoTab.qml
+ fonts/ Bungee, Bungee Inline, Major Mono Display,
+ Michroma, Audiowide, and their OFL.txt
+ README.md
+```
+
+The fonts are SIL OFL 1.1, from google/fonts, bundled with their licence.
+
+## Sonix service
+
+One instance in the `desktop` process; the island, the drawer and the
+settings page all read it.
+
+**Presence.** SonixLink broadcasts `SONIXLINK1 <ip> <port> <name>` to UDP
+7801 every 2s. QML has no UDP socket, so `sonix-beacon.py` binds 7801 and
+prints each beacon as one line; the service reads it with a `Process` and a
+`SplitParser`. `present` is true while beacons arrive and false 6s after the
+last. `host`, `port` and `name` come from the beacon, so a DHCP change of
+address is followed without configuration. If the helper exits it is
+restarted with a backoff; while it is down `present` stays false.
+
+**State.** `/api/state` is polled every 1s while at least one view holds the
+service (`hold()` / `release()`, counted): the island panel and the drawer
+page. Nothing is polled otherwise. When the count reaches zero the service
+sends `/api/bye`, so the player stops counting a client. Fields used:
+`state` (0 stopped, 1 playing, 2 paused), `position` and `duration`
+(seconds), `title`, `artist`, `album`, `path`, `sample_rate`, `bits`,
+`bitrate`, `lossless`, `volume`, `mode`, `battery`, `charging`, `favourite`,
+`accent`, `scanning`, `scan_count`, `tracks`, `playlists_revision`.
+
+**Commands.** `command(do, params, body)` issues `/api/command?do=...` with
+`XMLHttpRequest`, 3s timeout. Track lists go in the POST body, one path per
+line. A failed request reports its error to the caller and never changes
+`present`: only the beacon does.
+
+**Upload.** `upload(localFile, cardDir)` runs `curl`: `POST /create` with
+`path` for the folder, then `POST /upload` with `path` and `files[]`, the
+same calls the transfer page makes. Card paths are under `/data/mnt/sd_0/`.
+
+## Island
+
+The status pill shows an R1 icon, left of the headphones icon, while
+`present`. It takes the accent while playing. A click opens `SonixPanel`,
+built like `HeadphonesPanel`:
+
+- the cover from `/api/cover`, reloaded when `path` changes
+- title, artist, album, and a **heart**: filled when `favourite`, a click
+ sends `favourites_add` or `favourites_remove` with the current path; the
+ heart flips at once and the next poll confirms or reverts it
+- the quality line, e.g. `FLAC · 24/96 · 2304 kbps`, and a lossless badge
+- a seek bar: position advances locally between polls, a click sends `seek`
+ in seconds
+- previous, play/pause, next
+- volume 0-100, slider and wheel (5 per notch), sent once it settles
+- the mode button: normal, repeat_all, repeat_one, shuffle, shuffle_repeat,
+ in that order
+- battery percentage and a charging mark
+
+The panel holds the service while open.
+
+## Drawer
+
+`SonixModule` has a tile while `present` (`tiled` bound to it). Label: the
+beacon's name. State line: `95% · playing`, or `idle`. Active (accent) while
+playing. The page holds the service while it shows, and has three tabs.
+
+**Device.** Name, `host:port`, battery and charging, the player's accent as a
+swatch, the track count; during a scan `scanning… N found`, otherwise a
+Rescan button (`scan`). One line of what is playing. Links: Open Wi-Fi
+transfer (`http://<host>/` in the browser) and Boot logo (opens the
+settings page).
+
+**Playlists.** `/api/playlists`: name and track count. A click opens the
+playlist (`/api/playlist?name=`), back arrow returns. In a playlist:
+play all (`play_all` with `playlist`), and per track play (`play_path`) and
+remove (`playlist_remove`). New playlist takes a name and sends the user to
+Folders for its first track: SonixLink creates a playlist by adding to a
+name that does not exist, and an empty one cannot be made, so until then the
+new name lives only in this page.
+
+**Folders.** `/api/browse?path=/`, folders first, breadcrumbs, back arrow up.
+A folder row has play (`play_all` with `folder`) and add; a file row has
+play (`play_path`) and add. Add opens a menu: the playlists, and New….
+
+Both lists reload when their tab opens and when `playlists_revision`
+changes, so edits made on the player appear.
+
+**Limits.** SonixLink has no rename, delete, or reorder for playlists. The
+player has the functions internally (`playlists_rename`,
+`playlists_delete`) but does not expose them, and the `.m3u` files on the
+card are backups, not the playlists. These are done on the player. Queue,
+history and a favourites list are not part of this.
+
+## Boot logo page
+
+In the settings window, group Appearance, keywords: boot, logo, sonix, hiby.
+Left, the canvas; right, the controls.
+
+**Canvas.** An exact 480x800 scene, shown scaled to fit, framed as the
+panel. The output is this scene, so what the player shows is what is seen.
+
+**Picture.** Choose picture opens a file dialog in `~/Pictures`, JPEG or
+PNG. It opens centred at minimum zoom, the crop the player itself would
+make. Drag pans, the wheel zooms from just covering 480x800 up to 4x. Pan is
+clamped so the picture always covers the frame. Reset recentres. A picture
+that fails to load shows its error and disables Save and Upload.
+
+**Text.**
+
+- text, default `$USER`
+- font: four buttons, each label in its own face: Bungee + Inline, Major
+ Mono Display, Michroma, Audiowide
+- position: a switch decides whether a drag on the canvas moves the picture
+ or the text; snaps for top, centre, bottom, centred horizontally
+- size: a slider
+- duotone: a fill colour and a second colour, each picked from swatches:
+ white, black, the six player accents, and the udt accent (`Theme.accent`,
+ live). Swap exchanges them. The second colour is an offset copy behind the
+ fill, with an offset slider and one of four diagonal directions; for
+ Bungee it is the Bungee Inline layer drawn over the fill, with no offset.
+
+Player accents: blue `#3584e4`, orange `#ff7800`, yellow `#e5a50a`, green
+`#2ec27e`, purple `#9141ac`, pink `#d56199`.
+
+Text, font, size, position, colours and offset are kept in `Prefs`. The
+picture and its pan and zoom are not.
+
+**Finalize.** In the page's header bar (`Page.actions`):
+
+- Output folder: a folder picker, kept in `Prefs`, default
+ `~/Pictures/sonix-bootlogo/`, created if missing.
+- Save: `grabToImage` writes `bootlogo-YYYYMMDD-HHMMSS.png`, 480x800, and
+ `bootlogo-YYYYMMDD-HHMMSS-mask.png`, the text alone on transparency. PNG,
+ which the player accepts for a boot logo. A folder that cannot be written
+ shows the error and nothing is uploaded.
+- Upload, enabled while `present`: saves, then checks `http://<host>/`. If
+ Wi-Fi transfer does not answer, the button reads `Waiting for Wi-Fi
+ transfer…` with the hint "Open Wi-Fi transfer on the player (Control
+ centre) and leave it on screen", and rechecks every 2s for 60s. Then
+ uploads into `Bootlogo/` on the card. Success: "Uploaded. Pick it on the
+ player: Settings > Appearance > Boot logo. You can leave Wi-Fi transfer
+ now." Failure: the HTTP status or curl's error; the local copy stays.
+
+No draft and no dirty mark: the page edits no file.
+
+## Errors
+
+- Player gone (no beacon for 6s): icon and tile hide; an open panel or page
+ says the player is not on the network and disables its controls.
+- A failed command shows one line in the panel or page; a 400 shows the
+ player's reason text.
+- Wi-Fi transfer off: handled by Upload's wait, above.
+
+## Testing
+
+- `node desktop/modules/sonix/test_sonix.js`, over `Sonix.js`: beacon line
+ parsing, the quality line, the mode cycle, the pan and zoom clamp, the
+ output file name, breadcrumbs from a path.
+- `/usr/bin/python3 desktop/modules/sonix/test_sonix_beacon.py`: sends one
+ datagram to the helper on a test port and checks the printed line.
+- API calls are checked against a live player. The canvas, the panel and the
+ page are checked by eye, by the user; the agent confirms loading from the
+ `qs` log.