1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
|
# 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.
## 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=<track>&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.
|