feat!: v0.2.0 — ping-on-connect validation, try_connect, publish polish
- Add ping-on-connect handshake: all connect functions validate the device within 200ms, rejecting non-DongLoRa serial devices immediately - Add try_connect() — non-blocking connect that errors instead of blocking forever when no USB device is present - Add payload size validation in transmit() (max 256 bytes) - Add ErrorCode: impl std::error::Error - Fix drain_rx error handling (no longer swallows original error) - Fix repository URL to github.com/donglora/client-rs - Remove wire-level tag constants from top-level re-exports - Add rust-version (MSRV 1.85), homepage, exclude fields to Cargo.toml - Add examples/ping.rs - Allow clippy unwrap/expect in test modules
This commit is contained in:
11 files changed
+242
-174
No files matched your search
@@ -1,5 +1,35 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0 — 2026-04-07
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Ping-on-connect validation** — all connect functions automatically ping the
|
||||||
|
device after connecting and reject non-DongLoRa devices within 200ms. No more
|
||||||
|
accidentally talking LoRa protocol to an Arduino.
|
||||||
|
- **`try_connect(timeout)`** — non-blocking alternative to `connect()`. Returns
|
||||||
|
an error immediately if no DongLoRa device is found instead of blocking
|
||||||
|
indefinitely.
|
||||||
|
- **USB-UART bridge chip fallback** — `find_port()` now detects boards using
|
||||||
|
CP2102, CH9102, CH340, or FT232R bridge chips (validated via ping).
|
||||||
|
- **Payload size validation** — `transmit()` rejects payloads exceeding 256
|
||||||
|
bytes with a clear error before hitting the wire.
|
||||||
|
- **`ErrorCode` implements `std::error::Error`** — usable in error chains.
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Wire-level tag constants (`CMD_TAG_*`, `RESP_TAG_*`, `ERROR_INVALID_CONFIG`)
|
||||||
|
removed from the top-level re-exports. Access them via
|
||||||
|
`donglora_client::protocol::*` if needed.
|
||||||
|
- `connect()` and friends now validate the device on connect. Code that
|
||||||
|
previously connected to non-DongLoRa serial devices will now get an error.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Fixed repository URL in crate metadata.
|
||||||
|
- `drain_rx` no longer swallows the original error if timeout restore also fails.
|
||||||
|
- Removed dead `firmware/PROTOCOL.md` reference from docs.
|
||||||
|
|
||||||
## 0.1.0 — 2026-04-06
|
## 0.1.0 — 2026-04-06
|
||||||
|
|
||||||
Initial release.
|
Initial release.
|
||||||
|
|||||||
Generated
+1
-1
@@ -44,7 +44,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donglora-client"
|
name = "donglora-client"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serialport",
|
"serialport",
|
||||||
|
|||||||
+5
-2
@@ -1,13 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "donglora-client"
|
name = "donglora-client"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
rust-version = "1.85"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "DongLoRa host library — connect, configure, send/receive LoRa packets"
|
description = "DongLoRa host library — connect, configure, send/receive LoRa packets"
|
||||||
repository = "https://github.com/swaits/donglora"
|
repository = "https://github.com/donglora/client-rs"
|
||||||
|
homepage = "https://github.com/donglora/client-rs"
|
||||||
keywords = ["lora", "radio", "usb", "serial"]
|
keywords = ["lora", "radio", "usb", "serial"]
|
||||||
categories = ["hardware-support"]
|
categories = ["hardware-support"]
|
||||||
|
exclude = ["CLAUDE_TODO.md", "deny.toml", "justfile", "mise.toml", "rustfmt.toml", ".jj"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
# DongLoRa Rust Client
|
# DongLoRa Rust Client
|
||||||
|
|
||||||
Rust client library for talking to a DongLoRa device — either directly
|
Rust client library for talking to a DongLoRa device — either directly
|
||||||
over USB or through the [mux daemon](../../mux/).
|
over USB or through a mux daemon.
|
||||||
|
|
||||||
## What's in Here
|
## Install
|
||||||
|
|
||||||
- `src/protocol.rs` — wire protocol types (`RadioConfig`, `Command`, `Response`, `ErrorCode`)
|
```toml
|
||||||
- `src/codec.rs` — COBS framing, frame accumulator
|
[dependencies]
|
||||||
- `src/discovery.rs` — USB VID:PID device discovery
|
donglora-client = "0.2"
|
||||||
- `src/transport.rs` — serial and mux socket transports
|
```
|
||||||
- `src/client.rs` — high-level `Client<T>` with send/recv
|
|
||||||
- `src/connect.rs` — auto-detection (mux socket, TCP, direct USB)
|
|
||||||
|
|
||||||
## Usage
|
## Quick Start
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use donglora_client::*;
|
use donglora_client::*;
|
||||||
|
|
||||||
let mut client = connect_default()?;
|
let mut client = connect_default()?;
|
||||||
client.ping()?;
|
|
||||||
client.set_config(RadioConfig::default())?;
|
client.set_config(RadioConfig::default())?;
|
||||||
client.start_rx()?;
|
client.start_rx()?;
|
||||||
|
|
||||||
@@ -29,6 +26,29 @@ loop {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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<T>` with send/recv
|
||||||
|
- `src/connect.rs` — auto-detection (mux socket, TCP, direct USB)
|
||||||
|
|
||||||
|
## Connection Priority
|
||||||
|
|
||||||
|
[`connect`] and [`try_connect`] try 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
|
||||||
|
|
||||||
|
`try_connect` returns an error immediately if no device is found.
|
||||||
|
`connect` blocks until a USB device appears.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- `ucobs` — COBS framing (same implementation as the firmware)
|
- `ucobs` — COBS framing (same implementation as the firmware)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//! Connect to a DongLoRa device, ping it, and print the MAC address.
|
||||||
|
//!
|
||||||
|
//! Run with: cargo run --example ping
|
||||||
|
|
||||||
|
use donglora_client::{RadioConfig, try_connect};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let mut client = try_connect(Duration::from_secs(2))?;
|
||||||
|
println!("connected!");
|
||||||
|
|
||||||
|
client.ping()?;
|
||||||
|
println!("ping ok");
|
||||||
|
|
||||||
|
let mac = client.get_mac()?;
|
||||||
|
println!("MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||||
|
|
||||||
|
let config = client.get_config()?;
|
||||||
|
println!("current config: {config:?}");
|
||||||
|
|
||||||
|
client.set_config(RadioConfig::default())?;
|
||||||
|
println!("config set to defaults");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+37
-27
@@ -5,9 +5,10 @@
|
|||||||
//! while waiting for solicited responses).
|
//! while waiting for solicited responses).
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::codec::{encode_frame, read_frame};
|
use crate::codec::{encode_frame, read_frame};
|
||||||
use crate::protocol::{Command, ErrorCode, RadioConfig, Response};
|
use crate::protocol::{Command, ErrorCode, MAX_PAYLOAD, RadioConfig, Response};
|
||||||
use crate::transport::Transport;
|
use crate::transport::Transport;
|
||||||
|
|
||||||
/// Maximum number of buffered RxPackets before oldest are dropped.
|
/// Maximum number of buffered RxPackets before oldest are dropped.
|
||||||
@@ -16,6 +17,9 @@ const RX_BUFFER_CAP: usize = 256;
|
|||||||
/// Maximum frames to read while waiting for a solicited response before giving up.
|
/// Maximum frames to read while waiting for a solicited response before giving up.
|
||||||
const MAX_UNSOLICITED_BEFORE_TIMEOUT: usize = 50;
|
const MAX_UNSOLICITED_BEFORE_TIMEOUT: usize = 50;
|
||||||
|
|
||||||
|
/// Timeout for the ping-on-connect handshake.
|
||||||
|
const HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
/// High-level DongLoRa client, generic over transport.
|
/// High-level DongLoRa client, generic over transport.
|
||||||
///
|
///
|
||||||
/// Works with any [`Transport`]: direct USB serial, Unix socket mux, or TCP mux.
|
/// Works with any [`Transport`]: direct USB serial, Unix socket mux, or TCP mux.
|
||||||
@@ -27,10 +31,7 @@ pub struct Client<T: Transport> {
|
|||||||
impl<T: Transport> Client<T> {
|
impl<T: Transport> Client<T> {
|
||||||
/// Create a new client wrapping the given transport.
|
/// Create a new client wrapping the given transport.
|
||||||
pub fn new(transport: T) -> Self {
|
pub fn new(transport: T) -> Self {
|
||||||
Self {
|
Self { transport, rx_buffer: VecDeque::with_capacity(64) }
|
||||||
transport,
|
|
||||||
rx_buffer: VecDeque::with_capacity(64),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a command and wait for the solicited response.
|
/// Send a command and wait for the solicited response.
|
||||||
@@ -43,13 +44,12 @@ impl<T: Transport> Client<T> {
|
|||||||
std::io::Write::flush(&mut self.transport)?;
|
std::io::Write::flush(&mut self.transport)?;
|
||||||
|
|
||||||
for _ in 0..MAX_UNSOLICITED_BEFORE_TIMEOUT {
|
for _ in 0..MAX_UNSOLICITED_BEFORE_TIMEOUT {
|
||||||
let data = read_frame(&mut self.transport)?
|
let data =
|
||||||
.ok_or_else(|| anyhow::anyhow!("timeout waiting for response"))?;
|
read_frame(&mut self.transport)?.ok_or_else(|| anyhow::anyhow!("timeout waiting for response"))?;
|
||||||
let resp = Response::from_bytes(&data)
|
let resp = Response::from_bytes(&data).ok_or_else(|| {
|
||||||
.ok_or_else(|| {
|
let hex: Vec<String> = data.iter().map(|b| format!("{b:02x}")).collect();
|
||||||
let hex: Vec<String> = data.iter().map(|b| format!("{b:02x}")).collect();
|
anyhow::anyhow!("malformed response ({} bytes): [{}]", data.len(), hex.join(" "))
|
||||||
anyhow::anyhow!("malformed response ({} bytes): [{}]", data.len(), hex.join(" "))
|
})?;
|
||||||
})?;
|
|
||||||
if resp.is_rx_packet() {
|
if resp.is_rx_packet() {
|
||||||
self.buffer_rx(resp);
|
self.buffer_rx(resp);
|
||||||
continue;
|
continue;
|
||||||
@@ -69,8 +69,8 @@ impl<T: Transport> Client<T> {
|
|||||||
let Some(data) = read_frame(&mut self.transport)? else {
|
let Some(data) = read_frame(&mut self.transport)? else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let resp = Response::from_bytes(&data)
|
let resp =
|
||||||
.ok_or_else(|| anyhow::anyhow!("malformed response ({} bytes)", data.len()))?;
|
Response::from_bytes(&data).ok_or_else(|| anyhow::anyhow!("malformed response ({} bytes)", data.len()))?;
|
||||||
if resp.is_rx_packet() {
|
if resp.is_rx_packet() {
|
||||||
Ok(Some(resp))
|
Ok(Some(resp))
|
||||||
} else {
|
} else {
|
||||||
@@ -87,8 +87,7 @@ impl<T: Transport> Client<T> {
|
|||||||
let mut packets: Vec<Response> = self.rx_buffer.drain(..).collect();
|
let mut packets: Vec<Response> = self.rx_buffer.drain(..).collect();
|
||||||
|
|
||||||
let old_timeout = self.transport.timeout();
|
let old_timeout = self.transport.timeout();
|
||||||
self.transport
|
self.transport.set_timeout(std::time::Duration::from_millis(10))?;
|
||||||
.set_timeout(std::time::Duration::from_millis(10))?;
|
|
||||||
|
|
||||||
let result: anyhow::Result<()> = (|| {
|
let result: anyhow::Result<()> = (|| {
|
||||||
loop {
|
loop {
|
||||||
@@ -103,12 +102,27 @@ impl<T: Transport> Client<T> {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Always restore timeout, even if the loop errored
|
// Always restore timeout, even if the loop errored.
|
||||||
self.transport.set_timeout(old_timeout)?;
|
// Preserve the original error if both fail.
|
||||||
|
let restore_result = self.transport.set_timeout(old_timeout);
|
||||||
result?;
|
result?;
|
||||||
|
restore_result?;
|
||||||
Ok(packets)
|
Ok(packets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate the connection by pinging the device.
|
||||||
|
///
|
||||||
|
/// Uses a short timeout to fail fast on non-DongLoRa devices (e.g. USB-UART
|
||||||
|
/// bridges that happen to match known VID:PIDs). Called automatically by the
|
||||||
|
/// [`connect`](crate::connect::connect) family of functions.
|
||||||
|
pub fn validate(&mut self) -> anyhow::Result<()> {
|
||||||
|
let saved = self.transport.timeout();
|
||||||
|
self.transport.set_timeout(HANDSHAKE_TIMEOUT)?;
|
||||||
|
let result = self.ping();
|
||||||
|
self.transport.set_timeout(saved)?;
|
||||||
|
result.map_err(|_| anyhow::anyhow!("device did not respond to ping — not a DongLoRa device"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a Ping and verify the Pong response.
|
/// Send a Ping and verify the Pong response.
|
||||||
pub fn ping(&mut self) -> anyhow::Result<()> {
|
pub fn ping(&mut self) -> anyhow::Result<()> {
|
||||||
match self.send(Command::Ping)? {
|
match self.send(Command::Ping)? {
|
||||||
@@ -145,15 +159,11 @@ impl<T: Transport> Client<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Transmit a LoRa packet.
|
/// Transmit a LoRa packet.
|
||||||
pub fn transmit(
|
pub fn transmit(&mut self, payload: &[u8], config: Option<RadioConfig>) -> anyhow::Result<()> {
|
||||||
&mut self,
|
if payload.len() > MAX_PAYLOAD {
|
||||||
payload: &[u8],
|
anyhow::bail!("payload too large ({} bytes, max {MAX_PAYLOAD})", payload.len());
|
||||||
config: Option<RadioConfig>,
|
}
|
||||||
) -> anyhow::Result<()> {
|
let cmd = Command::Transmit { config, payload: payload.to_vec() };
|
||||||
let cmd = Command::Transmit {
|
|
||||||
config,
|
|
||||||
payload: payload.to_vec(),
|
|
||||||
};
|
|
||||||
match self.send(cmd)? {
|
match self.send(cmd)? {
|
||||||
Response::TxDone => Ok(()),
|
Response::TxDone => Ok(()),
|
||||||
Response::Error(ErrorCode::TxTimeout) => anyhow::bail!("transmit timed out"),
|
Response::Error(ErrorCode::TxTimeout) => anyhow::bail!("transmit timed out"),
|
||||||
|
|||||||
+9
-27
@@ -47,9 +47,7 @@ pub struct FrameReader {
|
|||||||
impl FrameReader {
|
impl FrameReader {
|
||||||
/// Create a new empty frame reader.
|
/// Create a new empty frame reader.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self { buf: Vec::with_capacity(512) }
|
||||||
buf: Vec::with_capacity(512),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Feed raw bytes and return all complete decoded frames.
|
/// Feed raw bytes and return all complete decoded frames.
|
||||||
@@ -107,12 +105,7 @@ pub fn read_frame(reader: &mut dyn std::io::Read) -> anyhow::Result<Option<Vec<u
|
|||||||
}
|
}
|
||||||
buf.push(byte[0]);
|
buf.push(byte[0]);
|
||||||
}
|
}
|
||||||
Err(e)
|
Err(e) if matches!(e.kind(), std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock) => {
|
||||||
if matches!(
|
|
||||||
e.kind(),
|
|
||||||
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||||
@@ -124,14 +117,13 @@ pub fn read_frame(reader: &mut dyn std::io::Read) -> anyhow::Result<Option<Vec<u
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
decode_frame(&buf)
|
decode_frame(&buf).map(Some).ok_or_else(|| anyhow::anyhow!("COBS decode error"))
|
||||||
.map(Some)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("COBS decode error"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tests ──────────────────────────────────────────────────────────
|
// ── Tests ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -154,7 +146,7 @@ mod tests {
|
|||||||
// This is equivalent to an empty inter-frame gap and is correctly
|
// This is equivalent to an empty inter-frame gap and is correctly
|
||||||
// skipped by both FrameReader and read_frame.
|
// skipped by both FrameReader and read_frame.
|
||||||
let encoded = encode_frame(b"");
|
let encoded = encode_frame(b"");
|
||||||
assert_eq!(*encoded.last().unwrap(), 0x00);
|
assert_eq!(encoded.last(), Some(&0x00));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -286,10 +278,7 @@ mod tests {
|
|||||||
// Exact Config response bytes: tag=1, freq=910525000, bw=62.5k, sf=7,
|
// Exact Config response bytes: tag=1, freq=910525000, bw=62.5k, sf=7,
|
||||||
// cr=5, sync=0x3444, tx=22dBm, preamble=16, cad=1
|
// cr=5, sync=0x3444, tx=22dBm, preamble=16, cad=1
|
||||||
// Note: preamble high byte is 0x00 — tests COBS with embedded zero.
|
// Note: preamble high byte is 0x00 — tests COBS with embedded zero.
|
||||||
let data = vec![
|
let data = vec![0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05, 0x44, 0x34, 0x16, 0x10, 0x00, 0x01];
|
||||||
0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05,
|
|
||||||
0x44, 0x34, 0x16, 0x10, 0x00, 0x01,
|
|
||||||
];
|
|
||||||
assert_eq!(data.len(), 14);
|
assert_eq!(data.len(), 14);
|
||||||
let encoded = encode_frame(&data);
|
let encoded = encode_frame(&data);
|
||||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
||||||
@@ -299,10 +288,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn config_response_cobs_roundtrip_cad_zero() {
|
fn config_response_cobs_roundtrip_cad_zero() {
|
||||||
// Same but with cad=0 — two consecutive 0x00 bytes at end.
|
// Same but with cad=0 — two consecutive 0x00 bytes at end.
|
||||||
let data = vec![
|
let data = vec![0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05, 0x44, 0x34, 0x16, 0x10, 0x00, 0x00];
|
||||||
0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05,
|
|
||||||
0x44, 0x34, 0x16, 0x10, 0x00, 0x00,
|
|
||||||
];
|
|
||||||
assert_eq!(data.len(), 14);
|
assert_eq!(data.len(), 14);
|
||||||
let encoded = encode_frame(&data);
|
let encoded = encode_frame(&data);
|
||||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
||||||
@@ -316,15 +302,11 @@ mod tests {
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
let (mut a, _b) = UnixStream::pair().unwrap();
|
let (mut a, _b) = UnixStream::pair().unwrap();
|
||||||
a.set_read_timeout(Some(Duration::from_millis(10)))
|
a.set_read_timeout(Some(Duration::from_millis(10))).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// No data written to _b, so read on `a` will time out with WouldBlock on Linux.
|
// No data written to _b, so read on `a` will time out with WouldBlock on Linux.
|
||||||
let result = read_frame(&mut a);
|
let result = read_frame(&mut a);
|
||||||
assert!(
|
assert!(result.is_ok(), "socket timeout should be Ok(None), got: {result:?}");
|
||||||
result.is_ok(),
|
|
||||||
"socket timeout should be Ok(None), got: {result:?}"
|
|
||||||
);
|
|
||||||
assert!(result.unwrap().is_none());
|
assert!(result.unwrap().is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+77
-27
@@ -53,27 +53,23 @@ fn find_mux_socket() -> Option<String> {
|
|||||||
|
|
||||||
/// Connect to the mux daemon via Unix domain socket.
|
/// Connect to the mux daemon via Unix domain socket.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub fn mux_connect(
|
pub fn mux_connect(path: Option<&str>, timeout: Duration) -> anyhow::Result<Client<MuxTransport>> {
|
||||||
path: Option<&str>,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> anyhow::Result<Client<MuxTransport>> {
|
|
||||||
let path = match path {
|
let path = match path {
|
||||||
Some(p) => p.to_string(),
|
Some(p) => p.to_string(),
|
||||||
None => find_mux_socket()
|
None => find_mux_socket().ok_or_else(|| anyhow::anyhow!("no mux socket found"))?,
|
||||||
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?,
|
|
||||||
};
|
};
|
||||||
let transport = MuxTransport::unix(&path, timeout)?;
|
let transport = MuxTransport::unix(&path, timeout)?;
|
||||||
Ok(Client::new(transport))
|
let mut client = Client::new(transport);
|
||||||
|
client.validate()?;
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to the mux daemon via TCP.
|
/// Connect to the mux daemon via TCP.
|
||||||
pub fn mux_tcp_connect(
|
pub fn mux_tcp_connect(host: &str, port: u16, timeout: Duration) -> anyhow::Result<Client<MuxTransport>> {
|
||||||
host: &str,
|
|
||||||
port: u16,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> anyhow::Result<Client<MuxTransport>> {
|
|
||||||
let transport = MuxTransport::tcp(host, port, timeout)?;
|
let transport = MuxTransport::tcp(host, port, timeout)?;
|
||||||
Ok(Client::new(transport))
|
let mut client = Client::new(transport);
|
||||||
|
client.validate()?;
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto-detect and connect to a DongLoRa device.
|
/// Auto-detect and connect to a DongLoRa device.
|
||||||
@@ -84,15 +80,14 @@ pub fn mux_tcp_connect(
|
|||||||
/// 3. Direct USB serial (auto-detect by VID:PID, blocks until device appears)
|
/// 3. Direct USB serial (auto-detect by VID:PID, blocks until device appears)
|
||||||
///
|
///
|
||||||
/// If `port` is `Some`, skips mux detection and connects directly to that serial port.
|
/// If `port` is `Some`, skips mux detection and connects directly to that serial port.
|
||||||
pub fn connect(
|
pub fn connect(port: Option<&str>, timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
|
||||||
port: Option<&str>,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> anyhow::Result<Client<AnyTransport>> {
|
|
||||||
// If explicit port given, go direct
|
// If explicit port given, go direct
|
||||||
if let Some(port) = port {
|
if let Some(port) = port {
|
||||||
debug!("opening serial port {port}");
|
debug!("opening serial port {port}");
|
||||||
let transport = SerialTransport::open(port, timeout)?;
|
let transport = SerialTransport::open(port, timeout)?;
|
||||||
return Ok(Client::new(AnyTransport::Serial(transport)));
|
let mut client = Client::new(AnyTransport::Serial(transport));
|
||||||
|
client.validate()?;
|
||||||
|
return Ok(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try TCP mux via environment variable
|
// Try TCP mux via environment variable
|
||||||
@@ -100,7 +95,9 @@ pub fn connect(
|
|||||||
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
||||||
{
|
{
|
||||||
debug!("connected to TCP mux at {tcp}");
|
debug!("connected to TCP mux at {tcp}");
|
||||||
return Ok(Client::new(AnyTransport::Mux(transport)));
|
let mut client = Client::new(AnyTransport::Mux(transport));
|
||||||
|
client.validate()?;
|
||||||
|
return Ok(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Unix socket mux
|
// Try Unix socket mux
|
||||||
@@ -109,7 +106,11 @@ pub fn connect(
|
|||||||
match MuxTransport::unix(&path, timeout) {
|
match MuxTransport::unix(&path, timeout) {
|
||||||
Ok(transport) => {
|
Ok(transport) => {
|
||||||
debug!("connected to mux socket at {path}");
|
debug!("connected to mux socket at {path}");
|
||||||
return Ok(Client::new(AnyTransport::Mux(transport)));
|
let mut client = Client::new(AnyTransport::Mux(transport));
|
||||||
|
if client.validate().is_ok() {
|
||||||
|
return Ok(client);
|
||||||
|
}
|
||||||
|
debug!("mux at {path} did not validate, falling back to USB");
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Stale socket or connection refused — fall through
|
// Stale socket or connection refused — fall through
|
||||||
@@ -119,11 +120,12 @@ pub fn connect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Direct USB serial — auto-detect or wait
|
// Direct USB serial — auto-detect or wait
|
||||||
let port_path = discovery::find_port()
|
let port_path = discovery::find_port().unwrap_or_else(discovery::wait_for_device);
|
||||||
.unwrap_or_else(discovery::wait_for_device);
|
|
||||||
debug!("opening serial port {port_path}");
|
debug!("opening serial port {port_path}");
|
||||||
let transport = SerialTransport::open(&port_path, timeout)?;
|
let transport = SerialTransport::open(&port_path, timeout)?;
|
||||||
Ok(Client::new(AnyTransport::Serial(transport)))
|
let mut client = Client::new(AnyTransport::Serial(transport));
|
||||||
|
client.validate()?;
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: connect with default timeout.
|
/// Convenience: connect with default timeout.
|
||||||
@@ -144,23 +146,71 @@ pub fn connect_mux_auto(timeout: Duration) -> anyhow::Result<Client<AnyTransport
|
|||||||
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
||||||
{
|
{
|
||||||
debug!("connected to TCP mux at {tcp}");
|
debug!("connected to TCP mux at {tcp}");
|
||||||
return Ok(Client::new(AnyTransport::Mux(transport)));
|
let mut client = Client::new(AnyTransport::Mux(transport));
|
||||||
|
client.validate()?;
|
||||||
|
return Ok(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Unix socket mux.
|
// Try Unix socket mux.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let path = find_mux_socket()
|
let path = find_mux_socket().ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
|
|
||||||
let transport = MuxTransport::unix(&path, timeout)?;
|
let transport = MuxTransport::unix(&path, timeout)?;
|
||||||
debug!("connected to mux socket at {path}");
|
debug!("connected to mux socket at {path}");
|
||||||
Ok(Client::new(AnyTransport::Mux(transport)))
|
let mut client = Client::new(AnyTransport::Mux(transport));
|
||||||
|
client.validate()?;
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
anyhow::bail!("mux-only mode requires Unix socket support or DONGLORA_MUX_TCP")
|
anyhow::bail!("mux-only mode requires Unix socket support or DONGLORA_MUX_TCP")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [`connect`] but returns an error instead of blocking when no USB
|
||||||
|
/// device is present. Suitable for callers with their own retry logic.
|
||||||
|
///
|
||||||
|
/// Runs the same fallback chain as `connect(None, timeout)` — TCP mux, Unix
|
||||||
|
/// socket mux, then direct USB serial — but uses a single non-blocking scan
|
||||||
|
/// instead of polling indefinitely for a USB device.
|
||||||
|
pub fn try_connect(timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
|
||||||
|
// Try TCP mux via environment variable
|
||||||
|
if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
|
||||||
|
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
||||||
|
{
|
||||||
|
debug!("connected to TCP mux at {tcp}");
|
||||||
|
let mut client = Client::new(AnyTransport::Mux(transport));
|
||||||
|
client.validate()?;
|
||||||
|
return Ok(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Unix socket mux
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(path) = find_mux_socket() {
|
||||||
|
match MuxTransport::unix(&path, timeout) {
|
||||||
|
Ok(transport) => {
|
||||||
|
debug!("connected to mux socket at {path}");
|
||||||
|
let mut client = Client::new(AnyTransport::Mux(transport));
|
||||||
|
if client.validate().is_ok() {
|
||||||
|
return Ok(client);
|
||||||
|
}
|
||||||
|
debug!("mux socket at {path} did not validate, falling back to USB");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
debug!("mux socket at {path} not reachable, falling back to USB");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct USB serial — single non-blocking scan
|
||||||
|
let port_path =
|
||||||
|
discovery::find_port().ok_or_else(|| anyhow::anyhow!("no DongLoRa device found (no mux, no USB device)"))?;
|
||||||
|
debug!("opening serial port {port_path}");
|
||||||
|
let transport = SerialTransport::open(&port_path, timeout)?;
|
||||||
|
let mut client = Client::new(AnyTransport::Serial(transport));
|
||||||
|
client.validate()?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
fn try_tcp_mux(addr: &str, timeout: Duration) -> Option<MuxTransport> {
|
fn try_tcp_mux(addr: &str, timeout: Duration) -> Option<MuxTransport> {
|
||||||
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
||||||
let host = if h.is_empty() { "localhost" } else { h };
|
let host = if h.is_empty() { "localhost" } else { h };
|
||||||
|
|||||||
+6
-8
@@ -1,7 +1,7 @@
|
|||||||
//! DongLoRa host library — connect, configure, send/receive LoRa packets.
|
//! DongLoRa host library — connect, configure, send/receive LoRa packets.
|
||||||
//!
|
//!
|
||||||
//! Implements the DongLoRa USB protocol (COBS-framed fixed-size LE).
|
//! Implements the DongLoRa USB protocol (COBS-framed fixed-size LE).
|
||||||
//! See `firmware/PROTOCOL.md` for the full specification.
|
//! See the [`protocol`] module for wire types and constants.
|
||||||
//!
|
//!
|
||||||
//! # Quick start
|
//! # Quick start
|
||||||
//!
|
//!
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
//! use donglora_client::*;
|
//! use donglora_client::*;
|
||||||
//!
|
//!
|
||||||
//! let mut client = connect_default()?;
|
//! let mut client = connect_default()?;
|
||||||
//! client.ping()?;
|
|
||||||
//! client.set_config(RadioConfig::default())?;
|
//! client.set_config(RadioConfig::default())?;
|
||||||
//! client.start_rx()?;
|
//! client.start_rx()?;
|
||||||
//!
|
//!
|
||||||
@@ -30,13 +29,12 @@ pub mod transport;
|
|||||||
|
|
||||||
// Flat re-exports for convenience
|
// Flat re-exports for convenience
|
||||||
pub use client::Client;
|
pub use client::Client;
|
||||||
pub use codec::{decode_frame, encode_frame, read_frame, FrameReader};
|
pub use codec::{FrameReader, decode_frame, encode_frame, read_frame};
|
||||||
pub use connect::{connect, connect_default, connect_mux_auto, default_socket_path};
|
pub use connect::{connect, connect_default, connect_mux_auto, default_socket_path, try_connect};
|
||||||
pub use discovery::{find_port, wait_for_device, USB_PID, USB_VID};
|
pub use discovery::{USB_PID, USB_VID, find_port, wait_for_device};
|
||||||
pub use protocol::{
|
pub use protocol::{
|
||||||
Bandwidth, Command, ErrorCode, RadioConfig, Response, CMD_TAG_SET_CONFIG, CMD_TAG_START_RX,
|
Bandwidth, Command, ErrorCode, MAX_PAYLOAD, PREAMBLE_DEFAULT, RADIO_CONFIG_SIZE, RadioConfig, Response,
|
||||||
CMD_TAG_STOP_RX, ERROR_INVALID_CONFIG, MAX_PAYLOAD, PREAMBLE_DEFAULT, RADIO_CONFIG_SIZE,
|
TX_POWER_MAX,
|
||||||
RESP_TAG_ERROR, RESP_TAG_OK, RESP_TAG_RX_PACKET, TX_POWER_MAX,
|
|
||||||
};
|
};
|
||||||
pub use transport::{AnyTransport, MuxTransport, SerialTransport, Transport};
|
pub use transport::{AnyTransport, MuxTransport, SerialTransport, Transport};
|
||||||
|
|
||||||
|
|||||||
+15
-49
@@ -1,7 +1,7 @@
|
|||||||
//! Wire protocol types and fixed-size little-endian serialization.
|
//! Wire protocol types and fixed-size little-endian serialization.
|
||||||
//!
|
//!
|
||||||
//! Mirrors the firmware's `protocol.rs` using std types. Every integer is
|
//! Mirrors the firmware's `protocol.rs` using std types. Every integer is
|
||||||
//! fixed-width LE. See `firmware/PROTOCOL.md` for the full specification.
|
//! fixed-width LE.
|
||||||
|
|
||||||
/// Maximum LoRa payload size in bytes.
|
/// Maximum LoRa payload size in bytes.
|
||||||
pub const MAX_PAYLOAD: usize = 256;
|
pub const MAX_PAYLOAD: usize = 256;
|
||||||
@@ -156,10 +156,7 @@ pub enum Command {
|
|||||||
SetConfig(RadioConfig),
|
SetConfig(RadioConfig),
|
||||||
StartRx,
|
StartRx,
|
||||||
StopRx,
|
StopRx,
|
||||||
Transmit {
|
Transmit { config: Option<RadioConfig>, payload: Vec<u8> },
|
||||||
config: Option<RadioConfig>,
|
|
||||||
payload: Vec<u8>,
|
|
||||||
},
|
|
||||||
DisplayOn,
|
DisplayOn,
|
||||||
DisplayOff,
|
DisplayOff,
|
||||||
GetMac,
|
GetMac,
|
||||||
@@ -241,10 +238,7 @@ impl Command {
|
|||||||
if rest.len() < data_start + len || len > MAX_PAYLOAD {
|
if rest.len() < data_start + len || len > MAX_PAYLOAD {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(Self::Transmit {
|
Some(Self::Transmit { config, payload: rest[data_start..data_start + len].to_vec() })
|
||||||
config,
|
|
||||||
payload: rest[data_start..data_start + len].to_vec(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
6 => Some(Self::DisplayOn),
|
6 => Some(Self::DisplayOn),
|
||||||
7 => Some(Self::DisplayOff),
|
7 => Some(Self::DisplayOff),
|
||||||
@@ -283,6 +277,8 @@ impl ErrorCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ErrorCode {}
|
||||||
|
|
||||||
impl std::fmt::Display for ErrorCode {
|
impl std::fmt::Display for ErrorCode {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -303,11 +299,7 @@ impl std::fmt::Display for ErrorCode {
|
|||||||
pub enum Response {
|
pub enum Response {
|
||||||
Pong,
|
Pong,
|
||||||
Config(RadioConfig),
|
Config(RadioConfig),
|
||||||
RxPacket {
|
RxPacket { rssi: i16, snr: i16, payload: Vec<u8> },
|
||||||
rssi: i16,
|
|
||||||
snr: i16,
|
|
||||||
payload: Vec<u8>,
|
|
||||||
},
|
|
||||||
TxDone,
|
TxDone,
|
||||||
Ok,
|
Ok,
|
||||||
Error(ErrorCode),
|
Error(ErrorCode),
|
||||||
@@ -378,11 +370,7 @@ impl Response {
|
|||||||
if rest.len() < 6 + len || len > MAX_PAYLOAD {
|
if rest.len() < 6 + len || len > MAX_PAYLOAD {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(Self::RxPacket {
|
Some(Self::RxPacket { rssi, snr, payload: rest[6..6 + len].to_vec() })
|
||||||
rssi,
|
|
||||||
snr,
|
|
||||||
payload: rest[6..6 + len].to_vec(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
3 => Some(Self::TxDone),
|
3 => Some(Self::TxDone),
|
||||||
4 => Some(Self::Ok),
|
4 => Some(Self::Ok),
|
||||||
@@ -403,6 +391,7 @@ impl Response {
|
|||||||
// ── Tests ──────────────────────────────────────────────────────────
|
// ── Tests ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -494,30 +483,21 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_transmit_no_config_roundtrip() {
|
fn command_transmit_no_config_roundtrip() {
|
||||||
let cmd = Command::Transmit {
|
let cmd = Command::Transmit { config: None, payload: b"hello".to_vec() };
|
||||||
config: None,
|
|
||||||
payload: b"hello".to_vec(),
|
|
||||||
};
|
|
||||||
let bytes = cmd.to_bytes();
|
let bytes = cmd.to_bytes();
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_transmit_with_config_roundtrip() {
|
fn command_transmit_with_config_roundtrip() {
|
||||||
let cmd = Command::Transmit {
|
let cmd = Command::Transmit { config: Some(make_config()), payload: b"test".to_vec() };
|
||||||
config: Some(make_config()),
|
|
||||||
payload: b"test".to_vec(),
|
|
||||||
};
|
|
||||||
let bytes = cmd.to_bytes();
|
let bytes = cmd.to_bytes();
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_transmit_empty_payload() {
|
fn command_transmit_empty_payload() {
|
||||||
let cmd = Command::Transmit {
|
let cmd = Command::Transmit { config: None, payload: vec![] };
|
||||||
config: None,
|
|
||||||
payload: vec![],
|
|
||||||
};
|
|
||||||
let bytes = cmd.to_bytes();
|
let bytes = cmd.to_bytes();
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
||||||
}
|
}
|
||||||
@@ -572,22 +552,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn response_rx_packet_roundtrip() {
|
fn response_rx_packet_roundtrip() {
|
||||||
let resp = Response::RxPacket {
|
let resp = Response::RxPacket { rssi: -80, snr: 10, payload: b"data".to_vec() };
|
||||||
rssi: -80,
|
|
||||||
snr: 10,
|
|
||||||
payload: b"data".to_vec(),
|
|
||||||
};
|
|
||||||
let bytes = resp.to_bytes();
|
let bytes = resp.to_bytes();
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn response_rx_packet_empty_payload() {
|
fn response_rx_packet_empty_payload() {
|
||||||
let resp = Response::RxPacket {
|
let resp = Response::RxPacket { rssi: -120, snr: -5, payload: vec![] };
|
||||||
rssi: -120,
|
|
||||||
snr: -5,
|
|
||||||
payload: vec![],
|
|
||||||
};
|
|
||||||
let bytes = resp.to_bytes();
|
let bytes = resp.to_bytes();
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
||||||
}
|
}
|
||||||
@@ -619,10 +591,7 @@ mod tests {
|
|||||||
fn response_tags() {
|
fn response_tags() {
|
||||||
assert_eq!(Response::Pong.tag(), 0);
|
assert_eq!(Response::Pong.tag(), 0);
|
||||||
assert_eq!(Response::Config(make_config()).tag(), 1);
|
assert_eq!(Response::Config(make_config()).tag(), 1);
|
||||||
assert_eq!(
|
assert_eq!(Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.tag(), 2);
|
||||||
Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.tag(),
|
|
||||||
2
|
|
||||||
);
|
|
||||||
assert_eq!(Response::TxDone.tag(), 3);
|
assert_eq!(Response::TxDone.tag(), 3);
|
||||||
assert_eq!(Response::Ok.tag(), 4);
|
assert_eq!(Response::Ok.tag(), 4);
|
||||||
assert_eq!(Response::Error(ErrorCode::RadioBusy).tag(), 5);
|
assert_eq!(Response::Error(ErrorCode::RadioBusy).tag(), 5);
|
||||||
@@ -685,9 +654,6 @@ mod tests {
|
|||||||
let cmd = Command::SetConfig(cfg);
|
let cmd = Command::SetConfig(cfg);
|
||||||
let bytes = cmd.to_bytes();
|
let bytes = cmd.to_bytes();
|
||||||
// Expected: 02 C0 CA 89 36 07 07 05 24 14 80 00 00 01
|
// Expected: 02 C0 CA 89 36 07 07 05 24 14 80 00 00 01
|
||||||
assert_eq!(
|
assert_eq!(bytes, [0x02, 0xC0, 0xCA, 0x89, 0x36, 0x07, 0x07, 0x05, 0x24, 0x14, 0x80, 0x00, 0x00, 0x01]);
|
||||||
bytes,
|
|
||||||
[0x02, 0xC0, 0xCA, 0x89, 0x36, 0x07, 0x07, 0x05, 0x24, 0x14, 0x80, 0x00, 0x00, 0x01]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-23
@@ -61,9 +61,7 @@ impl Write for SerialTransport {
|
|||||||
|
|
||||||
impl Transport for SerialTransport {
|
impl Transport for SerialTransport {
|
||||||
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
||||||
self.port
|
self.port.set_timeout(timeout).map_err(|e| anyhow::anyhow!("failed to set serial timeout: {e}"))?;
|
||||||
.set_timeout(timeout)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set serial timeout: {e}"))?;
|
|
||||||
self.timeout = timeout;
|
self.timeout = timeout;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -99,10 +97,7 @@ impl MuxTransport {
|
|||||||
stream
|
stream
|
||||||
.set_write_timeout(Some(timeout))
|
.set_write_timeout(Some(timeout))
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set socket write timeout: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("failed to set socket write timeout: {e}"))?;
|
||||||
Ok(Self {
|
Ok(Self { stream: MuxStream::Unix(stream), timeout })
|
||||||
stream: MuxStream::Unix(stream),
|
|
||||||
timeout,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to the mux daemon via TCP.
|
/// Connect to the mux daemon via TCP.
|
||||||
@@ -117,16 +112,9 @@ impl MuxTransport {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("no addresses found for {addr}"))?;
|
.ok_or_else(|| anyhow::anyhow!("no addresses found for {addr}"))?;
|
||||||
let stream = std::net::TcpStream::connect_timeout(&sock_addr, timeout)
|
let stream = std::net::TcpStream::connect_timeout(&sock_addr, timeout)
|
||||||
.map_err(|e| anyhow::anyhow!("failed to connect to mux at {addr}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("failed to connect to mux at {addr}: {e}"))?;
|
||||||
stream
|
stream.set_read_timeout(Some(timeout)).map_err(|e| anyhow::anyhow!("failed to set TCP read timeout: {e}"))?;
|
||||||
.set_read_timeout(Some(timeout))
|
stream.set_write_timeout(Some(timeout)).map_err(|e| anyhow::anyhow!("failed to set TCP write timeout: {e}"))?;
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set TCP read timeout: {e}"))?;
|
Ok(Self { stream: MuxStream::Tcp(stream), timeout })
|
||||||
stream
|
|
||||||
.set_write_timeout(Some(timeout))
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set TCP write timeout: {e}"))?;
|
|
||||||
Ok(Self {
|
|
||||||
stream: MuxStream::Tcp(stream),
|
|
||||||
timeout,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,12 +151,8 @@ impl Transport for MuxTransport {
|
|||||||
self.timeout = timeout;
|
self.timeout = timeout;
|
||||||
let result = match &self.stream {
|
let result = match &self.stream {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
MuxStream::Unix(s) => s
|
MuxStream::Unix(s) => s.set_read_timeout(Some(timeout)).and_then(|()| s.set_write_timeout(Some(timeout))),
|
||||||
.set_read_timeout(Some(timeout))
|
MuxStream::Tcp(s) => s.set_read_timeout(Some(timeout)).and_then(|()| s.set_write_timeout(Some(timeout))),
|
||||||
.and_then(|()| s.set_write_timeout(Some(timeout))),
|
|
||||||
MuxStream::Tcp(s) => s
|
|
||||||
.set_read_timeout(Some(timeout))
|
|
||||||
.and_then(|()| s.set_write_timeout(Some(timeout))),
|
|
||||||
};
|
};
|
||||||
result.map_err(|e| anyhow::anyhow!("failed to set timeout: {e}"))
|
result.map_err(|e| anyhow::anyhow!("failed to set timeout: {e}"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user