Skip to main content

donglora_client/
connect.rs

1//! Connection auto-detection and mux client helpers.
2//!
3//! The [`connect`] function tries mux connections first (TCP via env var, then
4//! Unix socket), falling back to direct USB serial. This matches the Python
5//! client's `connect()` behavior.
6
7use std::time::Duration;
8
9use tracing::debug;
10
11use crate::client::Client;
12use crate::discovery;
13use crate::transport::{AnyTransport, MuxTransport, SerialTransport};
14
15/// Default read timeout for connections.
16const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
17
18/// Resolve the mux socket path in priority order.
19///
20/// 1. `$DONGLORA_MUX` environment variable
21/// 2. `$XDG_RUNTIME_DIR/donglora/mux.sock`
22/// 3. `/tmp/donglora-mux.sock`
23pub fn default_socket_path() -> String {
24    if let Ok(env) = std::env::var("DONGLORA_MUX") {
25        return env;
26    }
27    if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
28        return format!("{xdg}/donglora/mux.sock");
29    }
30    "/tmp/donglora-mux.sock".to_string()
31}
32
33/// Find an existing mux socket path, or `None` if no socket file exists.
34fn find_mux_socket() -> Option<String> {
35    if let Ok(env) = std::env::var("DONGLORA_MUX") {
36        if std::path::Path::new(&env).exists() {
37            return Some(env);
38        }
39        return None;
40    }
41    if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
42        let p = format!("{xdg}/donglora/mux.sock");
43        if std::path::Path::new(&p).exists() {
44            return Some(p);
45        }
46    }
47    let p = "/tmp/donglora-mux.sock";
48    if std::path::Path::new(p).exists() {
49        return Some(p.to_string());
50    }
51    None
52}
53
54/// Connect to the mux daemon via Unix domain socket.
55#[cfg(unix)]
56pub fn mux_connect(path: Option<&str>, timeout: Duration) -> anyhow::Result<Client<MuxTransport>> {
57    let path = match path {
58        Some(p) => p.to_string(),
59        None => find_mux_socket().ok_or_else(|| anyhow::anyhow!("no mux socket found"))?,
60    };
61    let transport = MuxTransport::unix(&path, timeout)?;
62    let mut client = Client::new(transport);
63    client.validate()?;
64    Ok(client)
65}
66
67/// Connect to the mux daemon via TCP.
68pub fn mux_tcp_connect(host: &str, port: u16, timeout: Duration) -> anyhow::Result<Client<MuxTransport>> {
69    let transport = MuxTransport::tcp(host, port, timeout)?;
70    let mut client = Client::new(transport);
71    client.validate()?;
72    Ok(client)
73}
74
75/// Auto-detect and connect to a DongLoRa device.
76///
77/// Priority:
78/// 1. `DONGLORA_MUX_TCP` env var → TCP mux connection
79/// 2. Unix socket mux (if socket file exists)
80/// 3. Direct USB serial (auto-detect by VID:PID, blocks until device appears)
81///
82/// If `port` is `Some`, skips mux detection and connects directly to that serial port.
83pub fn connect(port: Option<&str>, timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
84    // If explicit port given, go direct
85    if let Some(port) = port {
86        debug!("opening serial port {port}");
87        let transport = SerialTransport::open(port, timeout)?;
88        let mut client = Client::new(AnyTransport::Serial(transport));
89        client.validate()?;
90        return Ok(client);
91    }
92
93    // Try TCP mux via environment variable
94    if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
95        && let Some(transport) = try_tcp_mux(&tcp, timeout)
96    {
97        debug!("connected to TCP mux at {tcp}");
98        let mut client = Client::new(AnyTransport::Mux(transport));
99        client.validate()?;
100        return Ok(client);
101    }
102
103    // Try Unix socket mux
104    #[cfg(unix)]
105    if let Some(path) = find_mux_socket() {
106        match MuxTransport::unix(&path, timeout) {
107            Ok(transport) => {
108                debug!("connected to mux socket at {path}");
109                let mut client = Client::new(AnyTransport::Mux(transport));
110                if client.validate().is_ok() {
111                    return Ok(client);
112                }
113                debug!("mux at {path} did not validate, falling back to USB");
114            }
115            Err(_) => {
116                // Stale socket or connection refused — fall through
117                debug!("mux socket at {path} not reachable, falling back to USB");
118            }
119        }
120    }
121
122    // Direct USB serial — auto-detect or wait
123    let port_path = discovery::find_port().unwrap_or_else(discovery::wait_for_device);
124    debug!("opening serial port {port_path}");
125    let transport = SerialTransport::open(&port_path, timeout)?;
126    let mut client = Client::new(AnyTransport::Serial(transport));
127    client.validate()?;
128    Ok(client)
129}
130
131/// Convenience: connect with default timeout.
132pub fn connect_default() -> anyhow::Result<Client<AnyTransport>> {
133    connect(None, DEFAULT_TIMEOUT)
134}
135
136/// Connect to a mux daemon only (TCP via env var, then Unix socket).
137///
138/// Unlike [`connect`], this **never** falls back to direct USB serial.
139/// Returns an error if no mux is reachable — the caller can retry with backoff.
140///
141/// This is the Rust equivalent of the Python client's "sticky mux" behavior:
142/// once you decide to use the mux, you stay on the mux.
143pub fn connect_mux_auto(timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
144    // Try TCP mux via environment variable.
145    if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
146        && let Some(transport) = try_tcp_mux(&tcp, timeout)
147    {
148        debug!("connected to TCP mux at {tcp}");
149        let mut client = Client::new(AnyTransport::Mux(transport));
150        client.validate()?;
151        return Ok(client);
152    }
153
154    // Try Unix socket mux.
155    #[cfg(unix)]
156    {
157        let path = find_mux_socket().ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
158        let transport = MuxTransport::unix(&path, timeout)?;
159        debug!("connected to mux socket at {path}");
160        let mut client = Client::new(AnyTransport::Mux(transport));
161        client.validate()?;
162        Ok(client)
163    }
164
165    #[cfg(not(unix))]
166    anyhow::bail!("mux-only mode requires Unix socket support or DONGLORA_MUX_TCP")
167}
168
169/// Like [`connect`] but returns an error instead of blocking when no USB
170/// device is present. Suitable for callers with their own retry logic.
171///
172/// Runs the same fallback chain as `connect(None, timeout)` — TCP mux, Unix
173/// socket mux, then direct USB serial — but uses a single non-blocking scan
174/// instead of polling indefinitely for a USB device.
175pub fn try_connect(timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
176    // Try TCP mux via environment variable
177    if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
178        && let Some(transport) = try_tcp_mux(&tcp, timeout)
179    {
180        debug!("connected to TCP mux at {tcp}");
181        let mut client = Client::new(AnyTransport::Mux(transport));
182        client.validate()?;
183        return Ok(client);
184    }
185
186    // Try Unix socket mux
187    #[cfg(unix)]
188    if let Some(path) = find_mux_socket() {
189        match MuxTransport::unix(&path, timeout) {
190            Ok(transport) => {
191                debug!("connected to mux socket at {path}");
192                let mut client = Client::new(AnyTransport::Mux(transport));
193                if client.validate().is_ok() {
194                    return Ok(client);
195                }
196                debug!("mux socket at {path} did not validate, falling back to USB");
197            }
198            Err(_) => {
199                debug!("mux socket at {path} not reachable, falling back to USB");
200            }
201        }
202    }
203
204    // Direct USB serial — single non-blocking scan
205    let port_path =
206        discovery::find_port().ok_or_else(|| anyhow::anyhow!("no DongLoRa device found (no mux, no USB device)"))?;
207    debug!("opening serial port {port_path}");
208    let transport = SerialTransport::open(&port_path, timeout)?;
209    let mut client = Client::new(AnyTransport::Serial(transport));
210    client.validate()?;
211    Ok(client)
212}
213
214fn try_tcp_mux(addr: &str, timeout: Duration) -> Option<MuxTransport> {
215    let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
216        let host = if h.is_empty() { "localhost" } else { h };
217        let port: u16 = p.parse().ok()?;
218        (host.to_string(), port)
219    } else {
220        let port: u16 = addr.parse().ok()?;
221        ("localhost".to_string(), port)
222    };
223    MuxTransport::tcp(&host, port, timeout).ok()
224}