From c43a78f8bbb553b994ab7c38f465f7a706eb68d9 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Wed, 22 Apr 2026 09:43:22 -0600 Subject: [PATCH] chore: release 1.0.0 --- CHANGELOG.md | 2 +- Cargo.lock | 6 ++- Cargo.toml | 6 +-- README.md | 105 +++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72920d2..d6553e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 1.0.0-alpha.1 — 2026-04-21 +## 1.0.0 — 2026-04-22 ### Breaking diff --git a/Cargo.lock b/Cargo.lock index 897cfc4..642a80b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "donglora-client" -version = "1.0.0-alpha.1" +version = "1.0.0" dependencies = [ "anyhow", "donglora-protocol", @@ -87,7 +87,9 @@ dependencies = [ [[package]] name = "donglora-protocol" -version = "0.1.0" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f2743ac7668845500e3686890917cdf5b0a09d17ec099bde2db4d3fae2a4a4" dependencies = [ "heapless", "ucobs", diff --git a/Cargo.toml b/Cargo.toml index 5b55708..7f089cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "donglora-client" -version = "1.0.0-alpha.1" +version = "1.0.0" edition = "2024" rust-version = "1.85" authors = ["Stephen Waits "] @@ -13,7 +13,7 @@ categories = ["hardware-support"] exclude = ["CLAUDE_TODO.md", "deny.toml", "justfile", "mise.toml", "rustfmt.toml", ".jj"] [dependencies] -donglora-protocol = { path = "../protocol", default-features = false, features = ["std"] } +donglora-protocol = { version = "1.0.0", default-features = false, features = ["std"] } thiserror = "2" tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "io-util", "net"] } tokio-serial = "5" @@ -27,7 +27,7 @@ anyhow = "1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Direct access to raw wire primitives for tests (frame encoder/decoder # helpers that are not part of the donglora-client public API). -donglora-protocol = { path = "../protocol", default-features = false, features = ["std"] } +donglora-protocol = { version = "1.0.0", default-features = false, features = ["std"] } heapless = "0.9" [[example]] diff --git a/README.md b/README.md index 196e83f..06537ca 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,104 @@ # DongLoRa Rust Client -Rust client library for talking to a DongLoRa device — either directly -over USB or through a mux daemon. +[![Crates.io](https://img.shields.io/crates/v/donglora-client.svg)](https://crates.io/crates/donglora-client) +[![Docs.rs](https://docs.rs/donglora-client/badge.svg)](https://docs.rs/donglora-client) + +Async Rust client library for talking to a DongLoRa device, either +directly over USB serial or through a mux daemon. Speaks DongLoRa +Protocol v2 via the [`donglora-protocol`][dp] crate. + +[dp]: https://crates.io/crates/donglora-protocol ## Install ```toml [dependencies] -donglora-client = "0.2" +donglora-client = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` ## Quick Start ```rust -use donglora_client::*; +use donglora_client::connect; +use donglora_protocol::{LoRaBandwidth, LoRaCodingRate, LoRaConfig, Modulation}; -let mut client = connect_default()?; -client.set_config(RadioConfig::default())?; -client.start_rx()?; +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let mut dongle = connect(None, std::time::Duration::from_secs(5)).await?; -loop { - if let Some(Response::RxPacket { rssi, snr, payload }) = client.recv()? { - println!("RX rssi={rssi} snr={snr} len={}", payload.len()); + dongle.set_config(&Modulation::LoRa(LoRaConfig { + freq_hz: 910_525_000, + bw: LoRaBandwidth::Khz62, + sf: 7, + cr: LoRaCodingRate::Cr4_5, + sync_word: 0x1424, + tx_power_dbm: 20, + ..Default::default() + })).await?; + + dongle.start_rx().await?; + while let Some(rx) = dongle.next_rx().await? { + println!("RX rssi={} snr={} len={}", rx.rssi_tenths_dbm, rx.snr_tenths_db, rx.data.len()); } + Ok(()) } ``` -All `connect` functions automatically validate the device by pinging it, -so you'll get a clear error immediately if the port isn't a real DongLoRa. +All `connect` functions automatically validate the device by pinging +it, so you get a clear error immediately if the port is not a real +DongLoRa. ## What's in Here -- `src/protocol.rs` — wire protocol types (`RadioConfig`, `Command`, `Response`, `ErrorCode`) -- `src/codec.rs` — COBS framing, frame accumulator -- `src/discovery.rs` — USB VID:PID device discovery -- `src/transport.rs` — serial and mux socket transports -- `src/client.rs` — high-level `Client` with send/recv -- `src/connect.rs` — auto-detection (mux socket, TCP, direct USB) +- `src/dongle.rs`: high-level `Dongle` API (connect, set_config, + transmit, start_rx, next_rx, tx_with_retry). +- `src/session.rs`: reader task, tag-allocation, per-command response + routing, async event fan-out. +- `src/retry.rs`: `RetryPolicy` + randomized backoff for + `CHANNEL_BUSY` / `EBUSY` per `PROTOCOL.md §6.10`. +- `src/connect.rs`: auto-detection pipeline (TCP mux env var, unix + socket mux, direct USB serial). +- `src/transport.rs`: tokio transports (`tokio-serial` for USB, + `tokio::net::{UnixStream, TcpStream}` for mux). +- `src/discovery.rs`: USB VID:PID discovery + ping validation. +- `src/errors.rs`: `ClientError` / `ClientResult` (thiserror-based + taxonomy mirroring the Python client's `DongloraError`). ## Connection Priority -[`connect`] and [`try_connect`] try these in order: +`connect(None, timeout)` tries these in order: -1. **TCP mux** — via `DONGLORA_MUX_TCP` env var -2. **Unix socket mux** — checks `$XDG_RUNTIME_DIR/donglora/mux.sock` or `/tmp/donglora-mux.sock` -3. **Direct USB serial** — auto-detects by VID:PID +1. **TCP mux**: via `DONGLORA_MUX_TCP` env var (host:port). +2. **Unix socket mux**: `$XDG_RUNTIME_DIR/donglora/mux.sock` or + `/tmp/donglora-mux.sock`. +3. **Direct USB serial**: auto-detects by VID:PID (CP210x, CH9102, + CH340, FT232R bridge chips too). -`try_connect` returns an error immediately if no device is found. -`connect` blocks until a USB device appears. +Once a mux path wins, subsequent reconnects commit to that path and +will not fall back to USB, preventing port-steal races during mux +restart. -## Dependencies +`try_connect(timeout)` returns an error immediately if no device is +found; `connect` blocks until one appears. -- `ucobs` — COBS framing (same implementation as the firmware) -- `serialport` — USB serial communication -- `anyhow` — error handling -- `tracing` — logging +## TX with Retry + +```rust +use donglora_client::retry::RetryPolicy; + +let policy = RetryPolicy::default(); // 3 attempts, 20-100 ms backoff, 500 ms cap +let outcome = dongle.tx_with_retry(payload, &policy).await?; +println!("sent in {} attempt(s)", outcome.attempts.len()); +``` + +## Keepalive + +A background task pings the device every 500 ms when the host has +been idle, tracking the 1 s inactivity window from `PROTOCOL.md §3.4` +with a 2x safety margin. Disable via `ConnectOptions::keepalive(false)` +if you want to manage liveness yourself. + +## License + +MIT, see [`LICENSE`](LICENSE).