···148148 Ok(())
149149}
150150151151+/// Read the current settings.toml without applying anything.
152152+pub fn read_settings() -> Result<NewGlobalSettings, Error> {
153153+ let home = std::env::var("HOME")?;
154154+ let path = format!("{}/.config/rockbox.org/settings.toml", home);
155155+ match std::fs::read_to_string(&path) {
156156+ Ok(content) => Ok(toml::from_str(&content)?),
157157+ Err(_) => Ok(NewGlobalSettings::default()),
158158+ }
159159+}
160160+161161+/// Persist `settings` to settings.toml as-is (all fields preserved, no C
162162+/// firmware involvement). Use this instead of `write_settings()` when you
163163+/// want to save audio-output-related fields that the C firmware doesn't know
164164+/// about.
165165+pub fn save_settings_to_file(settings: &NewGlobalSettings) -> Result<(), Error> {
166166+ let home = std::env::var("HOME")?;
167167+ let path = format!("{}/.config/rockbox.org/settings.toml", home);
168168+ let content = toml::to_string(settings)?;
169169+ std::fs::write(&path, content)?;
170170+ Ok(())
171171+}
172172+151173pub fn get_music_dir() -> Result<String, Error> {
152174 let home = std::env::var("HOME")?;
153175 let path = format!("{}/.config/rockbox.org/settings.toml", home);
+34
crates/slim/src/lib.rs
···1010use std::time::{Duration, SystemTime, UNIX_EPOCH};
11111212// ---------------------------------------------------------------------------
1313+// Connected-client registry — updated as squeezelite instances connect /
1414+// disconnect. Readable from outside the crate via `get_connected_clients()`.
1515+// ---------------------------------------------------------------------------
1616+1717+/// A squeezelite client currently connected to the Slim Protocol server.
1818+#[derive(Clone, Debug)]
1919+pub struct SlimClient {
2020+ /// MAC-address-based unique ID (lowercase hex, no colons).
2121+ pub id: String,
2222+ /// Friendly name from HELO capabilities ("Name=…" field), or IP as fallback.
2323+ pub name: String,
2424+ /// Peer IP address.
2525+ pub ip: String,
2626+}
2727+2828+static CLIENTS: Mutex<Vec<SlimClient>> = Mutex::new(Vec::new());
2929+3030+/// Snapshot of all currently connected squeezelite clients.
3131+pub fn get_connected_clients() -> Vec<SlimClient> {
3232+ CLIENTS.lock().unwrap().clone()
3333+}
3434+3535+pub(crate) fn add_client(client: SlimClient) {
3636+ let mut c = CLIENTS.lock().unwrap();
3737+ if !c.iter().any(|x| x.id == client.id) {
3838+ c.push(client);
3939+ }
4040+}
4141+4242+pub(crate) fn remove_client(id: &str) {
4343+ CLIENTS.lock().unwrap().retain(|c| c.id != id);
4444+}
4545+4646+// ---------------------------------------------------------------------------
1347// Broadcast buffer — one writer, N independent readers.
1448//
1549// Each chunk is stored with a monotonically-increasing sequence number.
+58-4
crates/slim/src/slimproto.rs
···3737 .unwrap_or_default();
3838 tracing::info!("slim: client connected from {peer}");
39394040- match read_client_packet(&mut stream) {
4141- Ok((opcode, _body)) if opcode == "HELO" => {
4242- tracing::info!("slim: HELO from {peer}");
4040+ let client_id = match read_client_packet(&mut stream) {
4141+ Ok((opcode, body)) if opcode == "HELO" => {
4242+ let id = parse_helo_mac_id(&body);
4343+ let peer_ip = stream
4444+ .peer_addr()
4545+ .map(|a| a.ip().to_string())
4646+ .unwrap_or_default();
4747+ let name = parse_helo_name(&body).unwrap_or_else(|| peer_ip.clone());
4848+ tracing::info!("slim: HELO from {peer} id={id} name={name:?}");
4949+ crate::add_client(crate::SlimClient {
5050+ id: id.clone(),
5151+ name,
5252+ ip: peer_ip,
5353+ });
5454+ id
4355 }
4456 Ok((opcode, _)) => {
4557 tracing::warn!("slim: expected HELO, got '{opcode}' from {peer}");
···4961 tracing::debug!("slim: read error from {peer}: {e}");
5062 return;
5163 }
5252- }
6464+ };
53655466 if let Err(e) = send_strm_start(&mut stream, http_port) {
5567 tracing::error!("slim: send STRM to {peer} failed: {e}");
···121133 }
122134 }
123135 }
136136+ crate::remove_client(&client_id);
124137}
125138126139// ---------------------------------------------------------------------------
···235248 data[offset + 3],
236249 ])
237250}
251251+252252+// ---------------------------------------------------------------------------
253253+// HELO body parsers
254254+//
255255+// HELO body layout:
256256+// [0] device_id
257257+// [1] revision
258258+// [2..8] mac address (6 bytes)
259259+// [8..24] uuid (16 bytes)
260260+// [24..26] wlan_channel_list (2 bytes)
261261+// [26..34] bytes_received (8 bytes)
262262+// [34..36] language (2 bytes)
263263+// [36..] capabilities (variable, comma-separated key=value pairs)
264264+// ---------------------------------------------------------------------------
265265+266266+fn parse_helo_mac_id(body: &[u8]) -> String {
267267+ if body.len() < 8 {
268268+ return "unknown".to_string();
269269+ }
270270+ let mac = &body[2..8];
271271+ format!(
272272+ "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
273273+ mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
274274+ )
275275+}
276276+277277+fn parse_helo_name(body: &[u8]) -> Option<String> {
278278+ const CAP_OFFSET: usize = 36;
279279+ if body.len() <= CAP_OFFSET {
280280+ return None;
281281+ }
282282+ let cap_str = std::str::from_utf8(&body[CAP_OFFSET..]).ok()?;
283283+ for part in cap_str.trim_end_matches('\0').split(',') {
284284+ if let Some(name) = part.strip_prefix("Name=") {
285285+ if !name.is_empty() {
286286+ return Some(name.to_string());
287287+ }
288288+ }
289289+ }
290290+ None
291291+}