1use std::path::Path;
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::time::Duration;
25
26use donglora_protocol::Modulation;
27use tracing::{debug, info};
28
29use crate::discovery;
30use crate::dongle::{Dongle, TransportKind};
31use crate::errors::{ClientError, ClientResult};
32use crate::session::Session;
33#[cfg(unix)]
34use crate::transport::UnixSocketTransport;
35use crate::transport::{AnyTransport, SerialTransport, TcpTransport, Transport};
36
37static USED_MUX: AtomicBool = AtomicBool::new(false);
40
41const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
43
44#[derive(Debug, Clone, Default)]
57pub struct ConnectOptions {
58 port: Option<String>,
59 timeout: Option<Duration>,
60 config: Option<Modulation>,
61 auto_configure: bool,
62 keepalive: bool,
63}
64
65impl ConnectOptions {
66 #[must_use]
69 pub fn port(mut self, path: impl Into<String>) -> Self {
70 self.port = Some(path.into());
71 self
72 }
73
74 #[must_use]
77 pub fn timeout(mut self, timeout: Duration) -> Self {
78 self.timeout = Some(timeout);
79 self
80 }
81
82 #[must_use]
87 pub fn config(mut self, modulation: Modulation) -> Self {
88 self.config = Some(modulation);
89 self.auto_configure = true;
90 self
91 }
92
93 #[must_use]
96 pub fn auto_configure(mut self, enabled: bool) -> Self {
97 self.auto_configure = enabled;
98 self
99 }
100
101 #[must_use]
106 pub fn keepalive(mut self, enabled: bool) -> Self {
107 self.keepalive = enabled;
108 self
109 }
110}
111
112impl ConnectOptions {
115 #[must_use]
117 pub fn new() -> Self {
118 Self { keepalive: true, ..Self::default() }
119 }
120}
121
122#[must_use]
128pub fn default_socket_path() -> String {
129 if let Ok(env) = std::env::var("DONGLORA_MUX") {
130 return env;
131 }
132 if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
133 return format!("{xdg}/donglora/mux.sock");
134 }
135 "/tmp/donglora-mux.sock".to_string()
136}
137
138#[must_use]
141pub fn find_mux_socket() -> Option<String> {
142 if let Ok(env) = std::env::var("DONGLORA_MUX") {
143 if Path::new(&env).exists() {
144 return Some(env);
145 }
146 return None;
147 }
148 if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
149 let p = format!("{xdg}/donglora/mux.sock");
150 if Path::new(&p).exists() {
151 return Some(p);
152 }
153 }
154 let p = "/tmp/donglora-mux.sock";
155 if Path::new(p).exists() {
156 return Some(p.to_string());
157 }
158 None
159}
160
161pub async fn connect() -> ClientResult<Dongle> {
170 connect_with(ConnectOptions::new()).await
171}
172
173pub async fn connect_with(opts: ConnectOptions) -> ClientResult<Dongle> {
175 let timeout = opts.timeout.unwrap_or(DEFAULT_TIMEOUT);
176
177 if let Some(port) = opts.port.as_deref() {
179 debug!("opening serial port {port}");
180 let transport = SerialTransport::open(port)?;
181 return finalize(AnyTransport::Serial(transport), TransportKind::Serial(port.to_string()), &opts, timeout)
182 .await;
183 }
184
185 if USED_MUX.load(Ordering::Relaxed) {
188 return connect_mux_sticky(&opts, timeout).await;
189 }
190
191 if let Some((transport, endpoint)) = try_tcp_env(timeout).await {
193 USED_MUX.store(true, Ordering::Relaxed);
194 return finalize(AnyTransport::Tcp(transport), TransportKind::MuxTcp(endpoint), &opts, timeout).await;
195 }
196
197 #[cfg(unix)]
199 if let Some(path) = find_mux_socket() {
200 debug!("connecting to Unix mux at {path}");
201 let transport = UnixSocketTransport::connect(&path).await?;
202 USED_MUX.store(true, Ordering::Relaxed);
203 return finalize(AnyTransport::Unix(transport), TransportKind::MuxUnix(path), &opts, timeout).await;
204 }
205
206 let port = match discovery::find_port() {
208 Some(p) => p,
209 None => discovery::wait_for_device().await,
210 };
211 debug!("opening serial port {port}");
212 let transport = SerialTransport::open(&port)?;
213 finalize(AnyTransport::Serial(transport), TransportKind::Serial(port), &opts, timeout).await
214}
215
216pub async fn try_connect() -> ClientResult<Dongle> {
219 try_connect_with(ConnectOptions::new()).await
220}
221
222pub async fn try_connect_with(opts: ConnectOptions) -> ClientResult<Dongle> {
224 let timeout = opts.timeout.unwrap_or(DEFAULT_TIMEOUT);
225
226 if let Some(port) = opts.port.as_deref() {
227 let transport = SerialTransport::open(port)?;
228 return finalize(AnyTransport::Serial(transport), TransportKind::Serial(port.to_string()), &opts, timeout)
229 .await;
230 }
231
232 if USED_MUX.load(Ordering::Relaxed) {
233 let path =
234 find_mux_socket().ok_or_else(|| ClientError::Other("mux not available (waiting for restart)".into()))?;
235 #[cfg(unix)]
236 {
237 let transport = UnixSocketTransport::connect(&path).await?;
238 return finalize(AnyTransport::Unix(transport), TransportKind::MuxUnix(path), &opts, timeout).await;
239 }
240 #[cfg(not(unix))]
241 {
242 let _ = path;
243 return Err(ClientError::Other("Unix mux requires a unix target".into()));
244 }
245 }
246
247 if let Some((transport, endpoint)) = try_tcp_env(timeout).await {
248 USED_MUX.store(true, Ordering::Relaxed);
249 return finalize(AnyTransport::Tcp(transport), TransportKind::MuxTcp(endpoint), &opts, timeout).await;
250 }
251
252 #[cfg(unix)]
253 if let Some(path) = find_mux_socket() {
254 let transport = UnixSocketTransport::connect(&path).await?;
255 USED_MUX.store(true, Ordering::Relaxed);
256 return finalize(AnyTransport::Unix(transport), TransportKind::MuxUnix(path), &opts, timeout).await;
257 }
258
259 let port = discovery::find_port()
260 .ok_or_else(|| ClientError::Other("no DongLoRa device found (no mux, no USB device)".into()))?;
261 let transport = SerialTransport::open(&port)?;
262 finalize(AnyTransport::Serial(transport), TransportKind::Serial(port), &opts, timeout).await
263}
264
265pub async fn connect_mux_auto() -> ClientResult<Dongle> {
268 connect_mux_auto_with(ConnectOptions::new()).await
269}
270
271pub async fn connect_mux_auto_with(opts: ConnectOptions) -> ClientResult<Dongle> {
273 let timeout = opts.timeout.unwrap_or(DEFAULT_TIMEOUT);
274 if let Some((transport, endpoint)) = try_tcp_env(timeout).await {
275 USED_MUX.store(true, Ordering::Relaxed);
276 return finalize(AnyTransport::Tcp(transport), TransportKind::MuxTcp(endpoint), &opts, timeout).await;
277 }
278 #[cfg(unix)]
279 {
280 let path = find_mux_socket().ok_or_else(|| ClientError::Other("no mux socket found".into()))?;
281 let transport = UnixSocketTransport::connect(&path).await?;
282 USED_MUX.store(true, Ordering::Relaxed);
283 finalize(AnyTransport::Unix(transport), TransportKind::MuxUnix(path), &opts, timeout).await
284 }
285 #[cfg(not(unix))]
286 Err(ClientError::Other("mux-only mode requires Unix socket support or DONGLORA_MUX_TCP".into()))
287}
288
289#[cfg(unix)]
292pub async fn mux_unix_connect(path: &str) -> ClientResult<Dongle> {
293 let transport = UnixSocketTransport::connect(path).await?;
294 USED_MUX.store(true, Ordering::Relaxed);
295 finalize(
296 AnyTransport::Unix(transport),
297 TransportKind::MuxUnix(path.to_string()),
298 &ConnectOptions::new(),
299 DEFAULT_TIMEOUT,
300 )
301 .await
302}
303
304pub async fn mux_tcp_connect(host: &str, port: u16) -> ClientResult<Dongle> {
306 let transport = TcpTransport::connect(host, port, DEFAULT_TIMEOUT).await?;
307 USED_MUX.store(true, Ordering::Relaxed);
308 finalize(
309 AnyTransport::Tcp(transport),
310 TransportKind::MuxTcp(format!("{host}:{port}")),
311 &ConnectOptions::new(),
312 DEFAULT_TIMEOUT,
313 )
314 .await
315}
316
317async fn connect_mux_sticky(opts: &ConnectOptions, timeout: Duration) -> ClientResult<Dongle> {
320 if let Some((transport, endpoint)) = try_tcp_env(timeout).await {
321 return finalize(AnyTransport::Tcp(transport), TransportKind::MuxTcp(endpoint), opts, timeout).await;
322 }
323 #[cfg(unix)]
324 {
325 let path = default_socket_path();
326 let mut warned = false;
327 loop {
328 if Path::new(&path).exists() {
329 let transport = UnixSocketTransport::connect(&path).await?;
330 return finalize(AnyTransport::Unix(transport), TransportKind::MuxUnix(path), opts, timeout).await;
331 }
332 if !warned {
333 info!("waiting for mux at {path} ...");
334 warned = true;
335 }
336 tokio::time::sleep(Duration::from_millis(500)).await;
337 }
338 }
339 #[cfg(not(unix))]
340 Err(ClientError::Other("no mux endpoint available".into()))
341}
342
343async fn try_tcp_env(timeout: Duration) -> Option<(TcpTransport, String)> {
344 let tcp = std::env::var("DONGLORA_MUX_TCP").ok()?;
345 let (host, port) = parse_tcp_endpoint(&tcp)?;
346 match TcpTransport::connect(&host, port, timeout).await {
347 Ok(t) => {
348 debug!("connected to TCP mux at {host}:{port}");
349 Some((t, format!("{host}:{port}")))
350 }
351 Err(e) => {
352 debug!("DONGLORA_MUX_TCP connect failed: {e}");
353 None
354 }
355 }
356}
357
358fn parse_tcp_endpoint(addr: &str) -> Option<(String, u16)> {
359 if let Some((h, p)) = addr.rsplit_once(':') {
360 let host = if h.is_empty() { "localhost".to_string() } else { h.to_string() };
361 let port: u16 = p.parse().ok()?;
362 Some((host, port))
363 } else {
364 let port: u16 = addr.parse().ok()?;
365 Some(("localhost".to_string(), port))
366 }
367}
368
369async fn finalize<T: Transport>(
370 transport: T,
371 kind: TransportKind,
372 opts: &ConnectOptions,
373 timeout: Duration,
374) -> ClientResult<Dongle> {
375 let session = Session::spawn(transport);
376 session.ping(timeout).await?;
378 let info = session.get_info(timeout).await?;
379
380 let applied = if opts.auto_configure {
381 match opts.config {
382 Some(m) => {
383 let result = session.set_config(m, timeout).await?;
384 Some(result.current)
385 }
386 None => None,
387 }
388 } else {
389 None
390 };
391
392 Ok(Dongle::new(session, info, kind, applied, opts.keepalive))
393}