Add donglora-client (clients/rust/) and donglora-mux (mux/rust/) as Rust implementations of the existing Python client library and mux daemon. Both are standalone Cargo projects with no-panic guarantees enforced by clippy deny lints. donglora-client provides protocol types, COBS framing, USB device discovery, serial/mux transports, and a high-level Client<T> with send/recv. donglora-mux is a fully async (tokio) multiplexer daemon with dongle hot-plug reconnect, per-client bounded send queues, SetConfig locking, and StartRx/StopRx reference counting. 52 unit tests across both crates. Updates README roadmap checkboxes and CONTRIBUTING docs to reflect the new implementations.
120 lines
3.6 KiB
Rust
120 lines
3.6 KiB
Rust
//! DongLoRa USB Multiplexer — share one dongle with multiple applications.
|
|
//!
|
|
//! Owns the USB serial connection and exposes a Unix domain socket (and
|
|
//! optional TCP) that speaks the same COBS-framed protocol. Clients connect
|
|
//! and get solicited responses routed, RxPackets broadcast, and StartRx/StopRx
|
|
//! reference-counted.
|
|
|
|
mod daemon;
|
|
mod intercept;
|
|
mod session;
|
|
|
|
use clap::Parser;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tracing::info;
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "donglora-mux", about = "DongLoRa USB Multiplexer")]
|
|
struct Args {
|
|
/// Serial port (auto-detect by USB VID:PID if omitted)
|
|
#[arg(short, long)]
|
|
port: Option<String>,
|
|
|
|
/// Unix socket path (default: XDG or /tmp/donglora-mux.sock)
|
|
#[arg(short, long)]
|
|
socket: Option<String>,
|
|
|
|
/// TCP listen address [HOST:]PORT (default: 0.0.0.0:5741, "none" to disable)
|
|
#[arg(short, long, default_value = "0.0.0.0:5741")]
|
|
tcp: String,
|
|
|
|
/// Enable debug logging
|
|
#[arg(short, long)]
|
|
verbose: bool,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
let args = Args::parse();
|
|
|
|
// Init tracing — daemon-style: wall-clock time, level, message
|
|
let filter = if args.verbose { "debug" } else { "info" };
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter)),
|
|
)
|
|
.with_target(false)
|
|
.init();
|
|
|
|
// Resolve serial port
|
|
let port = match args.port {
|
|
Some(p) => p,
|
|
None => {
|
|
tokio::task::spawn_blocking(|| {
|
|
donglora_client::find_port().unwrap_or_else(|| {
|
|
info!("waiting for DongLoRa device...");
|
|
donglora_client::wait_for_device()
|
|
})
|
|
})
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("device discovery task failed: {e}"))?
|
|
}
|
|
};
|
|
|
|
// Resolve socket path
|
|
let socket_path = args
|
|
.socket
|
|
.unwrap_or_else(donglora_client::default_socket_path);
|
|
|
|
// Parse TCP address
|
|
let tcp_addr = parse_tcp_addr(&args.tcp);
|
|
|
|
// Shutdown token
|
|
let shutdown = CancellationToken::new();
|
|
|
|
// Install signal handlers
|
|
let sig_shutdown = shutdown.clone();
|
|
tokio::spawn(async move {
|
|
if let Err(e) = signal_handler(sig_shutdown).await {
|
|
tracing::error!("signal handler error: {e}");
|
|
}
|
|
});
|
|
|
|
// Run the daemon
|
|
let daemon = daemon::MuxDaemon::new(port, socket_path, tcp_addr, shutdown);
|
|
daemon.run().await
|
|
}
|
|
|
|
fn parse_tcp_addr(tcp: &str) -> Option<(String, u16)> {
|
|
if tcp.eq_ignore_ascii_case("none") {
|
|
return None;
|
|
}
|
|
if let Some((host, port_str)) = tcp.rsplit_once(':')
|
|
&& let Ok(port) = port_str.parse()
|
|
{
|
|
return Some((host.to_string(), port));
|
|
}
|
|
if let Ok(port) = tcp.parse() {
|
|
return Some(("0.0.0.0".to_string(), port));
|
|
}
|
|
None
|
|
}
|
|
|
|
async fn signal_handler(shutdown: CancellationToken) -> anyhow::Result<()> {
|
|
let mut sigint =
|
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
|
|
.map_err(|e| anyhow::anyhow!("failed to register SIGINT handler: {e}"))?;
|
|
let mut sigterm =
|
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
|
.map_err(|e| anyhow::anyhow!("failed to register SIGTERM handler: {e}"))?;
|
|
|
|
tokio::select! {
|
|
_ = sigint.recv() => info!("received SIGINT"),
|
|
_ = sigterm.recv() => info!("received SIGTERM"),
|
|
}
|
|
|
|
shutdown.cancel();
|
|
Ok(())
|
|
}
|