From 903729ccbe1e41050de7dd4cb2691c05d6fddad2 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 7 Apr 2026 16:13:43 -0600 Subject: [PATCH] feat: upgrade to donglora-client 0.2, fix org URLs, publish-ready - Upgrade donglora-client from 0.1 (monorepo path dep) to 0.2 (crates.io) - Import wire-level constants from donglora_client::protocol::* - Fix repository URL to github.com/donglora/mux-rs - Add homepage, rust-version, exclude to Cargo.toml - Fix README (remove monorepo paths, add install instructions) - Allow clippy unwrap in test module --- CHANGELOG.md | 13 ++++++++ Cargo.lock | 6 ++-- Cargo.toml | 9 ++++-- README.md | 16 ++++++---- src/daemon.rs | 77 ++++++++++++------------------------------------ src/intercept.rs | 46 ++++++++--------------------- src/main.rs | 4 +-- src/session.rs | 6 +--- 8 files changed, 67 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07452c9..1fb18e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.2.0 — 2026-04-07 + +### Changed + +- Upgraded to `donglora-client` 0.2 (published crate, no more monorepo path dep). +- Wire-level protocol constants now imported from `donglora_client::protocol::*`. + +### Fixed + +- Repository URL now points to `github.com/donglora/mux-rs`. +- README links updated (removed stale monorepo paths). +- Added `rust-version`, `homepage`, and `exclude` to Cargo.toml. + ## 0.1.1 — 2026-04-06 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index ec5d4d5..68933b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,7 +161,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "donglora-client" -version = "0.1.0" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1936935d31ab56a8cd70ceecfd6d447f2628a73a123e1cfe37ee4987eab0fa70" dependencies = [ "anyhow", "serialport", @@ -171,7 +173,7 @@ dependencies = [ [[package]] name = "donglora-mux" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 2591586..7dc5260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,19 @@ [package] name = "donglora-mux" -version = "0.1.1" +version = "0.2.0" edition = "2024" +rust-version = "1.85" authors = ["Stephen Waits "] license = "MIT" description = "DongLoRa USB Multiplexer — share one dongle with multiple applications" -repository = "https://github.com/swaits/donglora" +repository = "https://github.com/donglora/mux-rs" +homepage = "https://github.com/donglora/mux-rs" keywords = ["lora", "radio", "usb", "multiplexer"] categories = ["hardware-support"] +exclude = [".jj/", "deny.toml", "justfile", "mise.toml", "rustfmt.toml"] [dependencies] -donglora-client = { version = "0.1.0", path = "../../clients/rust" } +donglora-client = "0.2" anyhow = "1" clap = { version = "4", features = ["derive"] } tokio = { version = "1", features = ["full"] } diff --git a/README.md b/README.md index 013aade..9b17032 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ USB multiplexer daemon — lets multiple applications share one DongLoRa dongle simultaneously. +## Install + +```sh +cargo install donglora-mux +``` + ## What It Does - Owns the USB serial connection exclusively @@ -18,12 +24,12 @@ dongle simultaneously. ## Running ```sh -just run # start the mux daemon -just verbose # start with verbose logging -just run -- --tcp 5741 --port /dev/ttyACM0 # with options +donglora-mux # start the mux daemon +donglora-mux --verbose # start with verbose logging +donglora-mux --tcp 5741 --port /dev/ttyACM0 # with options ``` ## Depends On -- [clients/rust](../../clients/rust/) — the `donglora-client` library - (protocol types, COBS framing, device discovery) +- [`donglora-client`](https://crates.io/crates/donglora-client) — protocol + types, COBS framing, device discovery diff --git a/src/daemon.rs b/src/daemon.rs index 92001e6..7f61473 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -9,13 +9,13 @@ use std::path::Path; use std::sync::Arc; use std::time::Duration; -use donglora_client::{ - encode_frame, FrameReader, CMD_TAG_SET_CONFIG, CMD_TAG_START_RX, CMD_TAG_STOP_RX, - RADIO_CONFIG_SIZE, RESP_TAG_OK, RESP_TAG_RX_PACKET, +use donglora_client::protocol::{ + CMD_TAG_SET_CONFIG, CMD_TAG_START_RX, CMD_TAG_STOP_RX, RADIO_CONFIG_SIZE, RESP_TAG_OK, RESP_TAG_RX_PACKET, }; +use donglora_client::{FrameReader, encode_frame}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, UnixListener}; -use tokio::sync::{mpsc, oneshot, Mutex}; +use tokio::sync::{Mutex, mpsc, oneshot}; /// A queued command: (client_id, raw_cmd_bytes). type QueuedCmd = (u64, Vec); @@ -63,28 +63,20 @@ impl MuxDaemon { tcp_addr: Option<(String, u16)>, shutdown: CancellationToken, ) -> Self { - Self { - serial_port, - socket_path, - tcp_addr, - shutdown, - } + Self { serial_port, socket_path, tcp_addr, shutdown } } /// Run the mux daemon. Returns when shutdown is signalled. pub async fn run(&self) -> anyhow::Result<()> { - let state: SharedState = Arc::new(Mutex::new(MuxInner { - sessions: HashMap::new(), - mux_state: MuxState::new(), - })); + let state: SharedState = + Arc::new(Mutex::new(MuxInner { sessions: HashMap::new(), mux_state: MuxState::new() })); // Command channel: client handlers → dongle writer let (cmd_tx, cmd_rx) = mpsc::channel::<(u64, Vec)>(64); let cmd_rx = Arc::new(Mutex::new(cmd_rx)); // Pending response slot: dongle writer installs, dongle reader resolves - let pending_response: Arc>>>> = - Arc::new(Mutex::new(None)); + let pending_response: Arc>>>> = Arc::new(Mutex::new(None)); // Prepare socket directory let sock_path = Path::new(&self.socket_path); @@ -134,24 +126,13 @@ impl MuxDaemon { }; // Spawn client accept tasks - tokio::spawn(accept_unix_clients( - unix_listener, - state.clone(), - cmd_tx.clone(), - self.shutdown.clone(), - )); + tokio::spawn(accept_unix_clients(unix_listener, state.clone(), cmd_tx.clone(), self.shutdown.clone())); if let Some(listener) = tcp_listener { - tokio::spawn(accept_tcp_clients( - listener, - state.clone(), - cmd_tx.clone(), - self.shutdown.clone(), - )); + tokio::spawn(accept_tcp_clients(listener, state.clone(), cmd_tx.clone(), self.shutdown.clone())); } // Connect/reconnect loop - self.reconnect_loop(state, cmd_tx, cmd_rx, pending_response) - .await; + self.reconnect_loop(state, cmd_tx, cmd_rx, pending_response).await; // Cleanup info!("shutting down..."); @@ -245,8 +226,8 @@ impl MuxDaemon { async fn open_and_ping(port: &str) -> anyhow::Result { let builder = tokio_serial::new(port, 115_200).timeout(Duration::from_secs(2)); - let mut serial = tokio_serial::SerialStream::open(&builder) - .map_err(|e| anyhow::anyhow!("failed to open {port}: {e}"))?; + let mut serial = + tokio_serial::SerialStream::open(&builder).map_err(|e| anyhow::anyhow!("failed to open {port}: {e}"))?; let ping_frame = encode_frame(&[0]); AsyncWriteExt::write_all(&mut serial, &ping_frame) @@ -360,9 +341,7 @@ async fn dispatch_frame( } } -async fn resolve_pending_with_error( - pending_response: &Arc>>>>, -) { +async fn resolve_pending_with_error(pending_response: &Arc>>>>) { let mut slot = pending_response.lock().await; if let Some(sender) = slot.take() { let _ = sender.send(vec![5, 1]); // Error(RadioBusy) @@ -397,12 +376,7 @@ async fn dongle_writer( let intercepted = { let mut inner = state.lock().await; let locked = inner.mux_state.locked_config; - intercept::maybe_intercept( - &raw_cmd, - client_id, - &mut inner.sessions, - &locked, - ) + intercept::maybe_intercept(&raw_cmd, client_id, &mut inner.sessions, &locked) }; if let Some(synthetic) = intercepted { let inner = state.lock().await; @@ -607,27 +581,16 @@ async fn client_writer( () = shutdown.cancelled() => return, }; - if AsyncWriteExt::write_all(&mut write_half, &frame) - .await - .is_err() - { + if AsyncWriteExt::write_all(&mut write_half, &frame).await.is_err() { return; } } } -async fn remove_client( - client_id: u64, - state: &SharedState, - label: &str, - cmd_tx: &mpsc::Sender<(u64, Vec)>, -) { +async fn remove_client(client_id: u64, state: &SharedState, label: &str, cmd_tx: &mpsc::Sender<(u64, Vec)>) { let mut inner = state.lock().await; - let was_interested = inner - .sessions - .get(&client_id) - .is_some_and(|s| s.rx_interested); + let was_interested = inner.sessions.get(&client_id).is_some_and(|s| s.rx_interested); inner.sessions.remove(&client_id); info!("{label} disconnected ({} remain)", inner.sessions.len()); @@ -637,9 +600,7 @@ async fn remove_client( info!("last RX-interested client gone — sending StopRx"); // Drop the lock before sending to avoid deadlock drop(inner); - let _ = cmd_tx - .send((SYNTHETIC_CLIENT_ID, vec![CMD_TAG_STOP_RX])) - .await; + let _ = cmd_tx.send((SYNTHETIC_CLIENT_ID, vec![CMD_TAG_STOP_RX])).await; return; } diff --git a/src/intercept.rs b/src/intercept.rs index 8e62643..07efdf0 100644 --- a/src/intercept.rs +++ b/src/intercept.rs @@ -6,9 +6,9 @@ use std::collections::HashMap; -use donglora_client::{ - CMD_TAG_SET_CONFIG, CMD_TAG_START_RX, CMD_TAG_STOP_RX, ERROR_INVALID_CONFIG, RADIO_CONFIG_SIZE, - RESP_TAG_ERROR, RESP_TAG_OK, +use donglora_client::protocol::{ + CMD_TAG_SET_CONFIG, CMD_TAG_START_RX, CMD_TAG_STOP_RX, ERROR_INVALID_CONFIG, RADIO_CONFIG_SIZE, RESP_TAG_ERROR, + RESP_TAG_OK, }; use tracing::{debug, warn}; @@ -22,9 +22,7 @@ pub struct MuxState { impl MuxState { pub fn new() -> Self { - Self { - locked_config: None, - } + Self { locked_config: None } } } @@ -67,27 +65,18 @@ fn intercept_set_config( // Same config — reply Ok without hitting the dongle if config_bytes == locked.as_slice() { - let label = sessions - .get(&client_id) - .map(ClientSession::label) - .unwrap_or_else(|| format!("id-{client_id}")); + let label = sessions.get(&client_id).map(ClientSession::label).unwrap_or_else(|| format!("id-{client_id}")); debug!("{label}: SetConfig matches locked config — Ok"); return Some(vec![RESP_TAG_OK]); } // Different config — reject - let label = sessions - .get(&client_id) - .map(ClientSession::label) - .unwrap_or_else(|| format!("id-{client_id}")); + let label = sessions.get(&client_id).map(ClientSession::label).unwrap_or_else(|| format!("id-{client_id}")); warn!("{label}: SetConfig rejected (conflicts with locked config)"); Some(vec![RESP_TAG_ERROR, ERROR_INVALID_CONFIG]) } -fn intercept_start_rx( - client_id: u64, - sessions: &mut HashMap, -) -> Option> { +fn intercept_start_rx(client_id: u64, sessions: &mut HashMap) -> Option> { let client = sessions.get(&client_id)?; // Already interested — no-op @@ -107,10 +96,7 @@ fn intercept_start_rx( None } -fn intercept_stop_rx( - client_id: u64, - sessions: &mut HashMap, -) -> Option> { +fn intercept_stop_rx(client_id: u64, sessions: &mut HashMap) -> Option> { let client = sessions.get(&client_id)?; // Wasn't interested — no-op @@ -140,6 +126,7 @@ pub fn rx_interest_count(sessions: &HashMap) -> usize { // ── Tests ────────────────────────────────────────────────────────── #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; @@ -201,10 +188,7 @@ mod tests { let id = first_id(&sessions); let mut cmd = vec![CMD_TAG_SET_CONFIG]; cmd.extend_from_slice(&[99, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); - assert_eq!( - maybe_intercept(&cmd, id, &mut sessions, &locked), - Some(vec![RESP_TAG_ERROR, ERROR_INVALID_CONFIG]) - ); + assert_eq!(maybe_intercept(&cmd, id, &mut sessions, &locked), Some(vec![RESP_TAG_ERROR, ERROR_INVALID_CONFIG])); } // ── StartRx ──────────────────────────────────────────────────── @@ -223,10 +207,7 @@ mod tests { let locked = None; let id = first_id(&sessions); sessions.get_mut(&id).unwrap().rx_interested = true; - assert_eq!( - maybe_intercept(&[CMD_TAG_START_RX], id, &mut sessions, &locked), - Some(vec![RESP_TAG_OK]) - ); + assert_eq!(maybe_intercept(&[CMD_TAG_START_RX], id, &mut sessions, &locked), Some(vec![RESP_TAG_OK])); } #[test] @@ -248,10 +229,7 @@ mod tests { let mut sessions = make_sessions(1); let locked = None; let id = first_id(&sessions); - assert_eq!( - maybe_intercept(&[CMD_TAG_STOP_RX], id, &mut sessions, &locked), - Some(vec![RESP_TAG_OK]) - ); + assert_eq!(maybe_intercept(&[CMD_TAG_STOP_RX], id, &mut sessions, &locked), Some(vec![RESP_TAG_OK])); } #[test] diff --git a/src/main.rs b/src/main.rs index dfcf0e5..414445b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,9 +61,7 @@ async fn main() -> anyhow::Result<()> { }; // Resolve socket path - let socket_path = args - .socket - .unwrap_or_else(donglora_client::default_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); diff --git a/src/session.rs b/src/session.rs index 55a1ce5..369277f 100644 --- a/src/session.rs +++ b/src/session.rs @@ -30,11 +30,7 @@ impl ClientSession { pub fn new() -> (Self, mpsc::Receiver>) { let id = NEXT_CLIENT_ID.fetch_add(1, Ordering::Relaxed); let (tx, rx) = mpsc::channel(SEND_QUEUE_CAP); - let session = Self { - id, - rx_interested: false, - tx, - }; + let session = Self { id, rx_interested: false, tx }; (session, rx) }