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 — if a mux socket exists, commit to it. Never fall through
104    // to USB serial when a mux is (or was) running. This prevents a client from
105    // stealing the serial port while the mux is temporarily reconnecting.
106    #[cfg(unix)]
107    if let Some(path) = find_mux_socket() {
108        debug!("mux socket found at {path} — connecting via mux only");
109        let transport = MuxTransport::unix(&path, timeout)?;
110        let mut client = Client::new(AnyTransport::Mux(transport));
111        client.validate()?;
112        return Ok(client);
113    }
114
115    // Direct USB serial — only reached when no mux socket exists at all
116    let port_path = discovery::find_port().unwrap_or_else(discovery::wait_for_device);
117    debug!("opening serial port {port_path}");
118    let transport = SerialTransport::open(&port_path, timeout)?;
119    let mut client = Client::new(AnyTransport::Serial(transport));
120    client.validate()?;
121    Ok(client)
122}
123
124/// Convenience: connect with default timeout.
125pub fn connect_default() -> anyhow::Result<Client<AnyTransport>> {
126    connect(None, DEFAULT_TIMEOUT)
127}
128
129/// Connect to a mux daemon only (TCP via env var, then Unix socket).
130///
131/// Unlike [`connect`], this **never** falls back to direct USB serial.
132/// Returns an error if no mux is reachable — the caller can retry with backoff.
133///
134/// This is the Rust equivalent of the Python client's "sticky mux" behavior:
135/// once you decide to use the mux, you stay on the mux.
136pub fn connect_mux_auto(timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
137    // Try TCP mux via environment variable.
138    if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
139        && let Some(transport) = try_tcp_mux(&tcp, timeout)
140    {
141        debug!("connected to TCP mux at {tcp}");
142        let mut client = Client::new(AnyTransport::Mux(transport));
143        client.validate()?;
144        return Ok(client);
145    }
146
147    // Try Unix socket mux.
148    #[cfg(unix)]
149    {
150        let path = find_mux_socket().ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
151        let transport = MuxTransport::unix(&path, timeout)?;
152        debug!("connected to mux socket at {path}");
153        let mut client = Client::new(AnyTransport::Mux(transport));
154        client.validate()?;
155        Ok(client)
156    }
157
158    #[cfg(not(unix))]
159    anyhow::bail!("mux-only mode requires Unix socket support or DONGLORA_MUX_TCP")
160}
161
162/// Like [`connect`] but returns an error instead of blocking when no USB
163/// device is present. Suitable for callers with their own retry logic.
164///
165/// Runs the same fallback chain as `connect(None, timeout)` — TCP mux, Unix
166/// socket mux, then direct USB serial — but uses a single non-blocking scan
167/// instead of polling indefinitely for a USB device.
168pub fn try_connect(timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
169    // Try TCP mux via environment variable
170    if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
171        && let Some(transport) = try_tcp_mux(&tcp, timeout)
172    {
173        debug!("connected to TCP mux at {tcp}");
174        let mut client = Client::new(AnyTransport::Mux(transport));
175        client.validate()?;
176        return Ok(client);
177    }
178
179    // Try Unix socket mux — commit to it if socket exists (same rationale as connect())
180    #[cfg(unix)]
181    if let Some(path) = find_mux_socket() {
182        debug!("mux socket found at {path} — connecting via mux only");
183        let transport = MuxTransport::unix(&path, timeout)?;
184        let mut client = Client::new(AnyTransport::Mux(transport));
185        client.validate()?;
186        return Ok(client);
187    }
188
189    // Direct USB serial — single non-blocking scan (only when no mux socket exists)
190    let port_path =
191        discovery::find_port().ok_or_else(|| anyhow::anyhow!("no DongLoRa device found (no mux, no USB device)"))?;
192    debug!("opening serial port {port_path}");
193    let transport = SerialTransport::open(&port_path, timeout)?;
194    let mut client = Client::new(AnyTransport::Serial(transport));
195    client.validate()?;
196    Ok(client)
197}
198
199fn try_tcp_mux(addr: &str, timeout: Duration) -> Option<MuxTransport> {
200    let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
201        let host = if h.is_empty() { "localhost" } else { h };
202        let port: u16 = p.parse().ok()?;
203        (host.to_string(), port)
204    } else {
205        let port: u16 = addr.parse().ok()?;
206        ("localhost".to_string(), port)
207    };
208    MuxTransport::tcp(&host, port, timeout).ok()
209}