Files
client-rs/src/discovery.rs
T
swaits 6937ad47ba feat!: Protocol v2 client rewrite with Dongle/Session model
The full 1.0 client surface, matching the protocol v2 wire definitions from donglora-protocol 1.0.0.

* Public API is now async. The blocking Client<T: Transport> is replaced by Dongle (from donglora_client::connect / connect_with / ConnectOptions).

* src/client.rs and src/codec.rs are removed in favor of src/dongle.rs (high-level surface), src/session.rs (per-client tag/slot state), src/retry.rs (RetryPolicy + tx_with_retry), src/errors.rs (thiserror-based ClientError / ClientResult). src/protocol.rs is replaced by direct donglora-protocol imports.

* Transport is now tokio-serial for USB and tokio::net::{UnixStream, TcpStream} for mux, replacing the blocking serial + shim model.

* Dongle::tx_with_retry(data, &RetryPolicy) implements PROTOCOL.md §6.10 randomized backoff for CHANNEL_BUSY / EBUSY, returning a TxOutcome with per-attempt trace.

* Background keepalive task pings every 500 ms on host idle, tracking the 1 s inactivity timeout from §3.4 with 2x margin. Disable via ConnectOptions::keepalive(false).

* Auto-recovery from ERR(ENOTCONFIGURED): the Dongle caches the last SET_CONFIG and silently re-applies on session drop, retrying once.

* Error taxonomy switches from anyhow to thiserror. Transport-level conditions (Timeout, TransportClosed, ReaderExited, BadFrame) are first-class.

* Examples rewritten for the async API: examples/ping.rs, examples/tx.rs, examples/rx.rs all target the MeshCore US preset (910.525 MHz / BW 62.5 / SF 7 / CR 4/5 / sync 0x1424 / 20 dBm). examples/common/mod.rs holds the shared helper.

* tests/vectors.rs gains protocol-level roundtrip coverage anchored to the published donglora-protocol 1.0.0 types.
2026-04-22 09:45:38 -06:00

78 lines
2.5 KiB
Rust

//! USB device discovery for DongLoRa dongles.
//!
//! Finds the serial port by matching USB VID:PID. Re-uses
//! `tokio_serial::available_ports` (which re-exports `mio-serial`'s
//! implementation) rather than pulling in `serialport` as a second
//! direct dep.
use std::time::Duration;
use tokio_serial::{SerialPortType, available_ports};
use tracing::info;
/// DongLoRa USB Vendor ID.
pub const USB_VID: u16 = 0x1209;
/// DongLoRa USB Product ID.
pub const USB_PID: u16 = 0x5741;
/// Known USB-UART bridge VID:PIDs found on some board revisions.
const BRIDGE_VID_PIDS: &[(u16, u16)] = &[
(0x10C4, 0xEA60), // CP2102 (Silicon Labs)
(0x1A86, 0x55D4), // CH9102
(0x1A86, 0x7523), // CH340
(0x0403, 0x6001), // FT232R (FTDI)
];
/// How long to wait for USB enumeration to settle after a device
/// appears but before we try to open the port. Empirically 300 ms is
/// enough on Linux and macOS to avoid the first-open race.
const USB_SETTLE: Duration = Duration::from_millis(300);
/// Poll interval for [`wait_for_device`].
const POLL_INTERVAL: Duration = Duration::from_millis(500);
/// Non-blocking one-shot scan. Returns the port path of the first
/// DongLoRa-like device present, or `None`.
///
/// Native VID:PID wins over known USB-UART bridges, so if both an
/// original and a bridge-chip board are plugged in, the native one is
/// selected.
pub fn find_port() -> Option<String> {
let ports = available_ports().ok()?;
if let Some(port) = ports.iter().find(|p| {
matches!(
&p.port_type,
SerialPortType::UsbPort(info) if info.vid == USB_VID && info.pid == USB_PID
)
}) {
return Some(port.port_name.clone());
}
ports
.into_iter()
.find(|p| {
matches!(
&p.port_type,
SerialPortType::UsbPort(info) if BRIDGE_VID_PIDS.contains(&(info.vid, info.pid))
)
})
.map(|p| p.port_name)
}
/// Await a DongLoRa device on USB. Polls [`find_port`] every 500 ms and
/// returns when one appears. Does not time out — the caller is expected
/// to wrap the future with `tokio::time::timeout` if that matters.
pub async fn wait_for_device() -> String {
info!("waiting for DongLoRa device...");
loop {
if let Some(port) = find_port() {
info!("found device at {port}");
tokio::time::sleep(USB_SETTLE).await;
return port;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}