Skip to main content

lsl_core/
config.rs

1//! API configuration, matching liblsl's `lsl_api.cfg`.
2//!
3//! Loads settings from (in priority order):
4//! 1. Environment variables (`LSL_IPV6`, `LSL_MULTICAST_PORT`, etc.)
5//! 2. `lsl_api.cfg` in current dir, home dir, or `/etc/lsl_api/lsl_api.cfg`
6//! 3. Built-in defaults
7
8use once_cell::sync::Lazy;
9use std::collections::HashMap;
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
11use std::path::Path;
12
13pub static CONFIG: Lazy<ApiConfig> = Lazy::new(ApiConfig::load);
14
15pub struct ApiConfig {
16    pub multicast_port: u16,
17    pub base_port: u16,
18    pub port_range: u16,
19    pub allow_random_ports: bool,
20    pub allow_ipv4: bool,
21    pub allow_ipv6: bool,
22    pub multicast_addresses: Vec<IpAddr>,
23    pub multicast_ttl: u32,
24    pub session_id: String,
25    pub use_protocol_version: i32,
26    pub smoothing_halftime: f32,
27    pub time_update_interval: f64,
28    pub time_probe_count: i32,
29    pub time_probe_interval: f64,
30    pub time_probe_max_rtt: f64,
31    pub time_update_minprobes: i32,
32}
33
34impl ApiConfig {
35    fn load() -> Self {
36        let file_cfg = load_config_file();
37
38        let get = |key: &str| -> Option<String> {
39            // Env var first (LSL_ prefix, uppercase)
40            std::env::var(format!("LSL_{}", key.to_uppercase()))
41                .ok()
42                .or_else(|| file_cfg.get(key).cloned())
43        };
44
45        let get_bool = |key: &str, default: bool| -> bool {
46            get(key)
47                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
48                .unwrap_or(default)
49        };
50        let get_u16 = |key: &str, default: u16| -> u16 {
51            get(key).and_then(|v| v.parse().ok()).unwrap_or(default)
52        };
53        let get_u32 = |key: &str, default: u32| -> u32 {
54            get(key).and_then(|v| v.parse().ok()).unwrap_or(default)
55        };
56        let get_f32 = |key: &str, default: f32| -> f32 {
57            get(key).and_then(|v| v.parse().ok()).unwrap_or(default)
58        };
59        let get_f64 = |key: &str, default: f64| -> f64 {
60            get(key).and_then(|v| v.parse().ok()).unwrap_or(default)
61        };
62
63        let allow_ipv6 = get_bool("ipv6", true);
64
65        // Parse multicast addresses from config or use defaults
66        let multicast_addresses = if let Some(addrs_str) = get("multicast_addresses") {
67            addrs_str
68                .split(&[',', ' '][..])
69                .filter_map(|s| s.trim().parse::<IpAddr>().ok())
70                .collect()
71        } else {
72            let mut addrs: Vec<IpAddr> = vec![
73                IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
74                IpAddr::V4(Ipv4Addr::BROADCAST),
75                IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
76                IpAddr::V4(Ipv4Addr::new(224, 0, 0, 183)),
77                IpAddr::V4(Ipv4Addr::new(239, 255, 172, 215)),
78            ];
79            if allow_ipv6 {
80                addrs.extend([
81                    IpAddr::V6(Ipv6Addr::LOCALHOST),
82                    IpAddr::V6(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1)),
83                    IpAddr::V6(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x113a)),
84                    IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 0x113a)),
85                ]);
86            }
87            addrs
88        };
89
90        Self {
91            multicast_port: get_u16("multicast_port", 16571),
92            base_port: get_u16("base_port", 16572),
93            port_range: get_u16("port_range", 32),
94            allow_random_ports: get_bool("allow_random_ports", true),
95            allow_ipv4: get_bool("ipv4", true),
96            allow_ipv6,
97            multicast_addresses,
98            multicast_ttl: get_u32("multicast_ttl", 24),
99            session_id: get("session_id").unwrap_or_else(|| "default".into()),
100            use_protocol_version: get("protocol_version")
101                .and_then(|v| v.parse().ok())
102                .unwrap_or(110),
103            smoothing_halftime: get_f32("smoothing_halftime", 90.0),
104            time_update_interval: get_f64("time_update_interval", 2.0),
105            time_probe_count: get("time_probe_count")
106                .and_then(|v| v.parse().ok())
107                .unwrap_or(8),
108            time_probe_interval: get_f64("time_probe_interval", 0.064),
109            time_probe_max_rtt: get_f64("time_probe_max_rtt", 0.128),
110            time_update_minprobes: get("time_update_minprobes")
111                .and_then(|v| v.parse().ok())
112                .unwrap_or(6),
113        }
114    }
115}
116
117/// Load key=value pairs from `lsl_api.cfg` (INI-like format).
118fn load_config_file() -> HashMap<String, String> {
119    let candidates = [
120        Some(std::path::PathBuf::from("lsl_api.cfg")),
121        dirs_path("lsl_api.cfg"),
122        Some(std::path::PathBuf::from("/etc/lsl_api/lsl_api.cfg")),
123    ];
124
125    for candidate in candidates.iter().flatten() {
126        if candidate.exists() {
127            if let Ok(contents) = std::fs::read_to_string(candidate) {
128                log::info!("Loaded LSL config from {}", candidate.display());
129                return parse_ini(&contents);
130            }
131        }
132    }
133
134    HashMap::new()
135}
136
137fn dirs_path(filename: &str) -> Option<std::path::PathBuf> {
138    std::env::var("HOME")
139        .ok()
140        .map(|h| Path::new(&h).join(".lsl").join(filename))
141}
142
143fn parse_ini(contents: &str) -> HashMap<String, String> {
144    let mut map = HashMap::new();
145    for line in contents.lines() {
146        let line = line.trim();
147        if line.is_empty()
148            || line.starts_with('#')
149            || line.starts_with(';')
150            || line.starts_with('[')
151        {
152            continue;
153        }
154        if let Some(eq) = line.find('=') {
155            let key = line[..eq].trim().to_lowercase().replace('-', "_");
156            let val = line[eq + 1..].trim().to_string();
157            map.insert(key, val);
158        }
159    }
160    map
161}