# 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 ` 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:///` 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:///`. 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. ## As built Differences from the design above, found while building it: - The boot logo page is in its own sidebar group, HiBy R1, not Appearance. - The panel's cover comes from `/api/art?path=&max=256`, which can answer 202 while it prepares; the panel retries three times per track. - The heart sends `favourite` with the path and 1 or 0. - SonixLink's `bitrate` is in bits per second; the panel shows kbps. - Folders browses from an empty path: the player answers `path=/` with plain text, "outside the card". A folder plays as its first file with the folder as the list, since `play_all` refuses a folder. - The beacon helper is restarted after a fixed 5s, not a growing backoff. - New playlist adds a draft row with a hint; it does not switch tabs. - An open island panel closes when the player leaves, rather than showing a message; the drawer page shows the message.