feat: add Rust client library and mux daemon

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.
This commit is contained in:
swaits committed 2026-04-03 17:21:51 -06:00
commit 2a0543d0b1
8 files changed
+2043

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
//! Per-client session state and bounded send queue.
//!
//! Each connected client gets a [`ClientSession`] that tracks its ID,
//! RX interest flag, and a bounded channel for outgoing COBS frames.
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::mpsc;
use tracing::warn;
/// Bounded send queue capacity per client.
const SEND_QUEUE_CAP: usize = 256;
/// Global client ID counter.
static NEXT_CLIENT_ID: AtomicU64 = AtomicU64::new(0);
/// Per-client state tracked by the mux daemon.
pub struct ClientSession {
/// Unique client identifier.
pub id: u64,
/// Whether this client has called StartRx.
pub rx_interested: bool,
/// Sender half of the bounded channel to the client's writer task.
tx: mpsc::Sender<Vec<u8>>,
}
impl ClientSession {
/// Create a new session. Returns the session and the receiver half
/// for the client's writer task to drain.
pub fn new() -> (Self, mpsc::Receiver<Vec<u8>>) {
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,
};
(session, rx)
}
/// Human-readable label for logging.
pub fn label(&self) -> String {
format!("client-{}", self.id)
}
/// Best-effort enqueue of a COBS frame for sending to this client.
///
/// If the channel is full (slow client), the frame is dropped with a warning.
/// This prevents backpressure from one slow client blocking the entire mux.
pub fn enqueue(&self, frame: Vec<u8>) {
if let Err(mpsc::error::TrySendError::Full(_)) = self.tx.try_send(frame) {
warn!("{}: send queue full, dropping frame", self.label());
}
// Closed channel (client disconnected) is silently ignored — the session
// will be cleaned up when the client handler task exits.
}
}