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
+37
-27
@@ -5,9 +5,10 @@
|
||||
//! while waiting for solicited responses).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
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;
|
||||
|
||||
/// 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.
|
||||
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.
|
||||
///
|
||||
/// 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> {
|
||||
/// Create a new client wrapping the given transport.
|
||||
pub fn new(transport: T) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
rx_buffer: VecDeque::with_capacity(64),
|
||||
}
|
||||
Self { transport, rx_buffer: VecDeque::with_capacity(64) }
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
|
||||
for _ in 0..MAX_UNSOLICITED_BEFORE_TIMEOUT {
|
||||
let data = read_frame(&mut self.transport)?
|
||||
.ok_or_else(|| anyhow::anyhow!("timeout waiting for response"))?;
|
||||
let resp = Response::from_bytes(&data)
|
||||
.ok_or_else(|| {
|
||||
let hex: Vec<String> = data.iter().map(|b| format!("{b:02x}")).collect();
|
||||
anyhow::anyhow!("malformed response ({} bytes): [{}]", data.len(), hex.join(" "))
|
||||
})?;
|
||||
let data =
|
||||
read_frame(&mut self.transport)?.ok_or_else(|| anyhow::anyhow!("timeout waiting for response"))?;
|
||||
let resp = Response::from_bytes(&data).ok_or_else(|| {
|
||||
let hex: Vec<String> = data.iter().map(|b| format!("{b:02x}")).collect();
|
||||
anyhow::anyhow!("malformed response ({} bytes): [{}]", data.len(), hex.join(" "))
|
||||
})?;
|
||||
if resp.is_rx_packet() {
|
||||
self.buffer_rx(resp);
|
||||
continue;
|
||||
@@ -69,8 +69,8 @@ impl<T: Transport> Client<T> {
|
||||
let Some(data) = read_frame(&mut self.transport)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let resp = Response::from_bytes(&data)
|
||||
.ok_or_else(|| anyhow::anyhow!("malformed response ({} bytes)", data.len()))?;
|
||||
let resp =
|
||||
Response::from_bytes(&data).ok_or_else(|| anyhow::anyhow!("malformed response ({} bytes)", data.len()))?;
|
||||
if resp.is_rx_packet() {
|
||||
Ok(Some(resp))
|
||||
} else {
|
||||
@@ -87,8 +87,7 @@ impl<T: Transport> Client<T> {
|
||||
let mut packets: Vec<Response> = self.rx_buffer.drain(..).collect();
|
||||
|
||||
let old_timeout = self.transport.timeout();
|
||||
self.transport
|
||||
.set_timeout(std::time::Duration::from_millis(10))?;
|
||||
self.transport.set_timeout(std::time::Duration::from_millis(10))?;
|
||||
|
||||
let result: anyhow::Result<()> = (|| {
|
||||
loop {
|
||||
@@ -103,12 +102,27 @@ impl<T: Transport> Client<T> {
|
||||
}
|
||||
})();
|
||||
|
||||
// Always restore timeout, even if the loop errored
|
||||
self.transport.set_timeout(old_timeout)?;
|
||||
// Always restore timeout, even if the loop errored.
|
||||
// Preserve the original error if both fail.
|
||||
let restore_result = self.transport.set_timeout(old_timeout);
|
||||
result?;
|
||||
restore_result?;
|
||||
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.
|
||||
pub fn ping(&mut self) -> anyhow::Result<()> {
|
||||
match self.send(Command::Ping)? {
|
||||
@@ -145,15 +159,11 @@ impl<T: Transport> Client<T> {
|
||||
}
|
||||
|
||||
/// Transmit a LoRa packet.
|
||||
pub fn transmit(
|
||||
&mut self,
|
||||
payload: &[u8],
|
||||
config: Option<RadioConfig>,
|
||||
) -> anyhow::Result<()> {
|
||||
let cmd = Command::Transmit {
|
||||
config,
|
||||
payload: payload.to_vec(),
|
||||
};
|
||||
pub fn transmit(&mut self, payload: &[u8], config: Option<RadioConfig>) -> anyhow::Result<()> {
|
||||
if payload.len() > MAX_PAYLOAD {
|
||||
anyhow::bail!("payload too large ({} bytes, max {MAX_PAYLOAD})", payload.len());
|
||||
}
|
||||
let cmd = Command::Transmit { config, payload: payload.to_vec() };
|
||||
match self.send(cmd)? {
|
||||
Response::TxDone => Ok(()),
|
||||
Response::Error(ErrorCode::TxTimeout) => anyhow::bail!("transmit timed out"),
|
||||
|
||||
+9
-27
@@ -47,9 +47,7 @@ pub struct FrameReader {
|
||||
impl FrameReader {
|
||||
/// Create a new empty frame reader.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buf: Vec::with_capacity(512),
|
||||
}
|
||||
Self { buf: Vec::with_capacity(512) }
|
||||
}
|
||||
|
||||
/// 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]);
|
||||
}
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.kind(),
|
||||
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
|
||||
) =>
|
||||
{
|
||||
Err(e) if matches!(e.kind(), std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock) => {
|
||||
return Ok(None);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
decode_frame(&buf)
|
||||
.map(Some)
|
||||
.ok_or_else(|| anyhow::anyhow!("COBS decode error"))
|
||||
decode_frame(&buf).map(Some).ok_or_else(|| anyhow::anyhow!("COBS decode error"))
|
||||
}
|
||||
|
||||
// ── Tests ──────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -154,7 +146,7 @@ mod tests {
|
||||
// This is equivalent to an empty inter-frame gap and is correctly
|
||||
// skipped by both FrameReader and read_frame.
|
||||
let encoded = encode_frame(b"");
|
||||
assert_eq!(*encoded.last().unwrap(), 0x00);
|
||||
assert_eq!(encoded.last(), Some(&0x00));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -286,10 +278,7 @@ mod tests {
|
||||
// Exact Config response bytes: tag=1, freq=910525000, bw=62.5k, sf=7,
|
||||
// cr=5, sync=0x3444, tx=22dBm, preamble=16, cad=1
|
||||
// Note: preamble high byte is 0x00 — tests COBS with embedded zero.
|
||||
let data = vec![
|
||||
0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05,
|
||||
0x44, 0x34, 0x16, 0x10, 0x00, 0x01,
|
||||
];
|
||||
let data = vec![0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05, 0x44, 0x34, 0x16, 0x10, 0x00, 0x01];
|
||||
assert_eq!(data.len(), 14);
|
||||
let encoded = encode_frame(&data);
|
||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
||||
@@ -299,10 +288,7 @@ mod tests {
|
||||
#[test]
|
||||
fn config_response_cobs_roundtrip_cad_zero() {
|
||||
// Same but with cad=0 — two consecutive 0x00 bytes at end.
|
||||
let data = vec![
|
||||
0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05,
|
||||
0x44, 0x34, 0x16, 0x10, 0x00, 0x00,
|
||||
];
|
||||
let data = vec![0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05, 0x44, 0x34, 0x16, 0x10, 0x00, 0x00];
|
||||
assert_eq!(data.len(), 14);
|
||||
let encoded = encode_frame(&data);
|
||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
||||
@@ -316,15 +302,11 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
let (mut a, _b) = UnixStream::pair().unwrap();
|
||||
a.set_read_timeout(Some(Duration::from_millis(10)))
|
||||
.unwrap();
|
||||
a.set_read_timeout(Some(Duration::from_millis(10))).unwrap();
|
||||
|
||||
// No data written to _b, so read on `a` will time out with WouldBlock on Linux.
|
||||
let result = read_frame(&mut a);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"socket timeout should be Ok(None), got: {result:?}"
|
||||
);
|
||||
assert!(result.is_ok(), "socket timeout should be Ok(None), got: {result:?}");
|
||||
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.
|
||||
#[cfg(unix)]
|
||||
pub fn mux_connect(
|
||||
path: Option<&str>,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<Client<MuxTransport>> {
|
||||
pub fn mux_connect(path: Option<&str>, timeout: Duration) -> anyhow::Result<Client<MuxTransport>> {
|
||||
let path = match path {
|
||||
Some(p) => p.to_string(),
|
||||
None => find_mux_socket()
|
||||
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?,
|
||||
None => find_mux_socket().ok_or_else(|| anyhow::anyhow!("no mux socket found"))?,
|
||||
};
|
||||
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.
|
||||
pub fn mux_tcp_connect(
|
||||
host: &str,
|
||||
port: u16,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<Client<MuxTransport>> {
|
||||
pub fn mux_tcp_connect(host: &str, port: u16, timeout: Duration) -> anyhow::Result<Client<MuxTransport>> {
|
||||
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.
|
||||
@@ -84,15 +80,14 @@ pub fn mux_tcp_connect(
|
||||
/// 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.
|
||||
pub fn connect(
|
||||
port: Option<&str>,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<Client<AnyTransport>> {
|
||||
pub fn connect(port: Option<&str>, timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
|
||||
// If explicit port given, go direct
|
||||
if let Some(port) = port {
|
||||
debug!("opening serial port {port}");
|
||||
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
|
||||
@@ -100,7 +95,9 @@ pub fn connect(
|
||||
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
||||
{
|
||||
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
|
||||
@@ -109,7 +106,11 @@ pub fn connect(
|
||||
match MuxTransport::unix(&path, timeout) {
|
||||
Ok(transport) => {
|
||||
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(_) => {
|
||||
// Stale socket or connection refused — fall through
|
||||
@@ -119,11 +120,12 @@ pub fn connect(
|
||||
}
|
||||
|
||||
// Direct USB serial — auto-detect or wait
|
||||
let port_path = discovery::find_port()
|
||||
.unwrap_or_else(discovery::wait_for_device);
|
||||
let port_path = discovery::find_port().unwrap_or_else(discovery::wait_for_device);
|
||||
debug!("opening serial port {port_path}");
|
||||
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.
|
||||
@@ -144,23 +146,71 @@ pub fn connect_mux_auto(timeout: Duration) -> anyhow::Result<Client<AnyTransport
|
||||
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
||||
{
|
||||
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.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let path = find_mux_socket()
|
||||
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
|
||||
let path = find_mux_socket().ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
|
||||
let transport = MuxTransport::unix(&path, timeout)?;
|
||||
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))]
|
||||
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> {
|
||||
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
||||
let host = if h.is_empty() { "localhost" } else { h };
|
||||
|
||||
+6
-8
@@ -1,7 +1,7 @@
|
||||
//! DongLoRa host library — connect, configure, send/receive LoRa packets.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
@@ -9,7 +9,6 @@
|
||||
//! use donglora_client::*;
|
||||
//!
|
||||
//! let mut client = connect_default()?;
|
||||
//! client.ping()?;
|
||||
//! client.set_config(RadioConfig::default())?;
|
||||
//! client.start_rx()?;
|
||||
//!
|
||||
@@ -30,13 +29,12 @@ pub mod transport;
|
||||
|
||||
// Flat re-exports for convenience
|
||||
pub use client::Client;
|
||||
pub use codec::{decode_frame, encode_frame, read_frame, FrameReader};
|
||||
pub use connect::{connect, connect_default, connect_mux_auto, default_socket_path};
|
||||
pub use discovery::{find_port, wait_for_device, USB_PID, USB_VID};
|
||||
pub use codec::{FrameReader, decode_frame, encode_frame, read_frame};
|
||||
pub use connect::{connect, connect_default, connect_mux_auto, default_socket_path, try_connect};
|
||||
pub use discovery::{USB_PID, USB_VID, find_port, wait_for_device};
|
||||
pub use protocol::{
|
||||
Bandwidth, Command, ErrorCode, RadioConfig, Response, CMD_TAG_SET_CONFIG, CMD_TAG_START_RX,
|
||||
CMD_TAG_STOP_RX, ERROR_INVALID_CONFIG, MAX_PAYLOAD, PREAMBLE_DEFAULT, RADIO_CONFIG_SIZE,
|
||||
RESP_TAG_ERROR, RESP_TAG_OK, RESP_TAG_RX_PACKET, TX_POWER_MAX,
|
||||
Bandwidth, Command, ErrorCode, MAX_PAYLOAD, PREAMBLE_DEFAULT, RADIO_CONFIG_SIZE, RadioConfig, Response,
|
||||
TX_POWER_MAX,
|
||||
};
|
||||
pub use transport::{AnyTransport, MuxTransport, SerialTransport, Transport};
|
||||
|
||||
|
||||
+15
-49
@@ -1,7 +1,7 @@
|
||||
//! Wire protocol types and fixed-size little-endian serialization.
|
||||
//!
|
||||
//! 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.
|
||||
pub const MAX_PAYLOAD: usize = 256;
|
||||
@@ -156,10 +156,7 @@ pub enum Command {
|
||||
SetConfig(RadioConfig),
|
||||
StartRx,
|
||||
StopRx,
|
||||
Transmit {
|
||||
config: Option<RadioConfig>,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
Transmit { config: Option<RadioConfig>, payload: Vec<u8> },
|
||||
DisplayOn,
|
||||
DisplayOff,
|
||||
GetMac,
|
||||
@@ -241,10 +238,7 @@ impl Command {
|
||||
if rest.len() < data_start + len || len > MAX_PAYLOAD {
|
||||
return None;
|
||||
}
|
||||
Some(Self::Transmit {
|
||||
config,
|
||||
payload: rest[data_start..data_start + len].to_vec(),
|
||||
})
|
||||
Some(Self::Transmit { config, payload: rest[data_start..data_start + len].to_vec() })
|
||||
}
|
||||
6 => Some(Self::DisplayOn),
|
||||
7 => Some(Self::DisplayOff),
|
||||
@@ -283,6 +277,8 @@ impl ErrorCode {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ErrorCode {}
|
||||
|
||||
impl std::fmt::Display for ErrorCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -303,11 +299,7 @@ impl std::fmt::Display for ErrorCode {
|
||||
pub enum Response {
|
||||
Pong,
|
||||
Config(RadioConfig),
|
||||
RxPacket {
|
||||
rssi: i16,
|
||||
snr: i16,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
RxPacket { rssi: i16, snr: i16, payload: Vec<u8> },
|
||||
TxDone,
|
||||
Ok,
|
||||
Error(ErrorCode),
|
||||
@@ -378,11 +370,7 @@ impl Response {
|
||||
if rest.len() < 6 + len || len > MAX_PAYLOAD {
|
||||
return None;
|
||||
}
|
||||
Some(Self::RxPacket {
|
||||
rssi,
|
||||
snr,
|
||||
payload: rest[6..6 + len].to_vec(),
|
||||
})
|
||||
Some(Self::RxPacket { rssi, snr, payload: rest[6..6 + len].to_vec() })
|
||||
}
|
||||
3 => Some(Self::TxDone),
|
||||
4 => Some(Self::Ok),
|
||||
@@ -403,6 +391,7 @@ impl Response {
|
||||
// ── Tests ──────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -494,30 +483,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn command_transmit_no_config_roundtrip() {
|
||||
let cmd = Command::Transmit {
|
||||
config: None,
|
||||
payload: b"hello".to_vec(),
|
||||
};
|
||||
let cmd = Command::Transmit { config: None, payload: b"hello".to_vec() };
|
||||
let bytes = cmd.to_bytes();
|
||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_transmit_with_config_roundtrip() {
|
||||
let cmd = Command::Transmit {
|
||||
config: Some(make_config()),
|
||||
payload: b"test".to_vec(),
|
||||
};
|
||||
let cmd = Command::Transmit { config: Some(make_config()), payload: b"test".to_vec() };
|
||||
let bytes = cmd.to_bytes();
|
||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_transmit_empty_payload() {
|
||||
let cmd = Command::Transmit {
|
||||
config: None,
|
||||
payload: vec![],
|
||||
};
|
||||
let cmd = Command::Transmit { config: None, payload: vec![] };
|
||||
let bytes = cmd.to_bytes();
|
||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
||||
}
|
||||
@@ -572,22 +552,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn response_rx_packet_roundtrip() {
|
||||
let resp = Response::RxPacket {
|
||||
rssi: -80,
|
||||
snr: 10,
|
||||
payload: b"data".to_vec(),
|
||||
};
|
||||
let resp = Response::RxPacket { rssi: -80, snr: 10, payload: b"data".to_vec() };
|
||||
let bytes = resp.to_bytes();
|
||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_rx_packet_empty_payload() {
|
||||
let resp = Response::RxPacket {
|
||||
rssi: -120,
|
||||
snr: -5,
|
||||
payload: vec![],
|
||||
};
|
||||
let resp = Response::RxPacket { rssi: -120, snr: -5, payload: vec![] };
|
||||
let bytes = resp.to_bytes();
|
||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
||||
}
|
||||
@@ -619,10 +591,7 @@ mod tests {
|
||||
fn response_tags() {
|
||||
assert_eq!(Response::Pong.tag(), 0);
|
||||
assert_eq!(Response::Config(make_config()).tag(), 1);
|
||||
assert_eq!(
|
||||
Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.tag(),
|
||||
2
|
||||
);
|
||||
assert_eq!(Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.tag(), 2);
|
||||
assert_eq!(Response::TxDone.tag(), 3);
|
||||
assert_eq!(Response::Ok.tag(), 4);
|
||||
assert_eq!(Response::Error(ErrorCode::RadioBusy).tag(), 5);
|
||||
@@ -685,9 +654,6 @@ mod tests {
|
||||
let cmd = Command::SetConfig(cfg);
|
||||
let bytes = cmd.to_bytes();
|
||||
// Expected: 02 C0 CA 89 36 07 07 05 24 14 80 00 00 01
|
||||
assert_eq!(
|
||||
bytes,
|
||||
[0x02, 0xC0, 0xCA, 0x89, 0x36, 0x07, 0x07, 0x05, 0x24, 0x14, 0x80, 0x00, 0x00, 0x01]
|
||||
);
|
||||
assert_eq!(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 {
|
||||
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
||||
self.port
|
||||
.set_timeout(timeout)
|
||||
.map_err(|e| anyhow::anyhow!("failed to set serial timeout: {e}"))?;
|
||||
self.port.set_timeout(timeout).map_err(|e| anyhow::anyhow!("failed to set serial timeout: {e}"))?;
|
||||
self.timeout = timeout;
|
||||
Ok(())
|
||||
}
|
||||
@@ -99,10 +97,7 @@ impl MuxTransport {
|
||||
stream
|
||||
.set_write_timeout(Some(timeout))
|
||||
.map_err(|e| anyhow::anyhow!("failed to set socket write timeout: {e}"))?;
|
||||
Ok(Self {
|
||||
stream: MuxStream::Unix(stream),
|
||||
timeout,
|
||||
})
|
||||
Ok(Self { stream: MuxStream::Unix(stream), timeout })
|
||||
}
|
||||
|
||||
/// Connect to the mux daemon via TCP.
|
||||
@@ -117,16 +112,9 @@ impl MuxTransport {
|
||||
.ok_or_else(|| anyhow::anyhow!("no addresses found for {addr}"))?;
|
||||
let stream = std::net::TcpStream::connect_timeout(&sock_addr, timeout)
|
||||
.map_err(|e| anyhow::anyhow!("failed to connect to mux at {addr}: {e}"))?;
|
||||
stream
|
||||
.set_read_timeout(Some(timeout))
|
||||
.map_err(|e| anyhow::anyhow!("failed to set TCP read timeout: {e}"))?;
|
||||
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,
|
||||
})
|
||||
stream.set_read_timeout(Some(timeout)).map_err(|e| anyhow::anyhow!("failed to set TCP read timeout: {e}"))?;
|
||||
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;
|
||||
let result = match &self.stream {
|
||||
#[cfg(unix)]
|
||||
MuxStream::Unix(s) => s
|
||||
.set_read_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))),
|
||||
MuxStream::Unix(s) => s.set_read_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}"))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user