Skip to main content

donglora_client/
client.rs

1//! High-level DongLoRa client.
2//!
3//! The [`Client`] wraps a transport and provides ergonomic send/recv methods
4//! that handle the command/response discipline (buffering unsolicited RxPackets
5//! while waiting for solicited responses).
6
7use std::collections::VecDeque;
8use std::time::Duration;
9
10use crate::codec::{encode_frame, read_frame};
11use crate::protocol::{Command, ErrorCode, MAX_PAYLOAD, RadioConfig, Response};
12use crate::transport::Transport;
13
14/// Maximum number of buffered RxPackets before oldest are dropped.
15const RX_BUFFER_CAP: usize = 256;
16
17/// Maximum frames to read while waiting for a solicited response before giving up.
18const MAX_UNSOLICITED_BEFORE_TIMEOUT: usize = 50;
19
20/// Timeout for the ping-on-connect handshake.
21const HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(200);
22
23/// High-level DongLoRa client, generic over transport.
24///
25/// Works with any [`Transport`]: direct USB serial, Unix socket mux, or TCP mux.
26pub struct Client<T: Transport> {
27    transport: T,
28    rx_buffer: VecDeque<Response>,
29}
30
31impl<T: Transport> Client<T> {
32    /// Create a new client wrapping the given transport.
33    pub fn new(transport: T) -> Self {
34        Self { transport, rx_buffer: VecDeque::with_capacity(64) }
35    }
36
37    /// Send a command and wait for the solicited response.
38    ///
39    /// Any unsolicited `RxPacket` frames encountered while waiting are buffered
40    /// and retrievable via [`recv`](Self::recv) or [`drain_rx`](Self::drain_rx).
41    pub fn send(&mut self, cmd: Command) -> anyhow::Result<Response> {
42        let frame = encode_frame(&cmd.to_bytes());
43        std::io::Write::write_all(&mut self.transport, &frame)?;
44        std::io::Write::flush(&mut self.transport)?;
45
46        for _ in 0..MAX_UNSOLICITED_BEFORE_TIMEOUT {
47            let data =
48                read_frame(&mut self.transport)?.ok_or_else(|| anyhow::anyhow!("timeout waiting for response"))?;
49            let resp = Response::from_bytes(&data).ok_or_else(|| {
50                let hex: Vec<String> = data.iter().map(|b| format!("{b:02x}")).collect();
51                anyhow::anyhow!("malformed response ({} bytes): [{}]", data.len(), hex.join(" "))
52            })?;
53            if resp.is_rx_packet() {
54                self.buffer_rx(resp);
55                continue;
56            }
57            return Ok(resp);
58        }
59        anyhow::bail!("no solicited response after {MAX_UNSOLICITED_BEFORE_TIMEOUT} frames")
60    }
61
62    /// Return the next RxPacket from the buffer or the wire.
63    ///
64    /// Returns `Ok(None)` on timeout (no packet available).
65    pub fn recv(&mut self) -> anyhow::Result<Option<Response>> {
66        if let Some(pkt) = self.rx_buffer.pop_front() {
67            return Ok(Some(pkt));
68        }
69        let Some(data) = read_frame(&mut self.transport)? else {
70            return Ok(None);
71        };
72        let resp =
73            Response::from_bytes(&data).ok_or_else(|| anyhow::anyhow!("malformed response ({} bytes)", data.len()))?;
74        if resp.is_rx_packet() {
75            Ok(Some(resp))
76        } else {
77            tracing::warn!("recv: discarding unexpected unsolicited response: {resp:?}");
78            Ok(None)
79        }
80    }
81
82    /// Drain all buffered and pending RxPacket frames.
83    ///
84    /// Temporarily reduces the read timeout to quickly drain any frames still
85    /// in flight, then restores the original timeout.
86    pub fn drain_rx(&mut self) -> anyhow::Result<Vec<Response>> {
87        let mut packets: Vec<Response> = self.rx_buffer.drain(..).collect();
88
89        let old_timeout = self.transport.timeout();
90        self.transport.set_timeout(std::time::Duration::from_millis(10))?;
91
92        let result: anyhow::Result<()> = (|| {
93            loop {
94                let Some(data) = read_frame(&mut self.transport)? else {
95                    return Ok(());
96                };
97                if let Some(resp) = Response::from_bytes(&data)
98                    && resp.is_rx_packet()
99                {
100                    packets.push(resp);
101                }
102            }
103        })();
104
105        // Always restore timeout, even if the loop errored.
106        // Preserve the original error if both fail.
107        let restore_result = self.transport.set_timeout(old_timeout);
108        result?;
109        restore_result?;
110        Ok(packets)
111    }
112
113    /// Validate the connection by pinging the device.
114    ///
115    /// Uses a short timeout to fail fast on non-DongLoRa devices (e.g. USB-UART
116    /// bridges that happen to match known VID:PIDs). Called automatically by the
117    /// [`connect`](crate::connect::connect) family of functions.
118    pub fn validate(&mut self) -> anyhow::Result<()> {
119        let saved = self.transport.timeout();
120        self.transport.set_timeout(HANDSHAKE_TIMEOUT)?;
121        let result = self.ping();
122        self.transport.set_timeout(saved)?;
123        result.map_err(|_| anyhow::anyhow!("device did not respond to ping — not a DongLoRa device"))
124    }
125
126    /// Send a Ping and verify the Pong response.
127    pub fn ping(&mut self) -> anyhow::Result<()> {
128        match self.send(Command::Ping)? {
129            Response::Pong => Ok(()),
130            other => anyhow::bail!("unexpected response to Ping: {other:?}"),
131        }
132    }
133
134    /// Set the radio configuration.
135    pub fn set_config(&mut self, config: RadioConfig) -> anyhow::Result<()> {
136        match self.send(Command::SetConfig(config))? {
137            Response::Ok => Ok(()),
138            Response::Error(code) => anyhow::bail!("SetConfig failed: {code}"),
139            other => anyhow::bail!("unexpected response to SetConfig: {other:?}"),
140        }
141    }
142
143    /// Start receiving LoRa packets.
144    pub fn start_rx(&mut self) -> anyhow::Result<()> {
145        match self.send(Command::StartRx)? {
146            Response::Ok => Ok(()),
147            Response::Error(code) => anyhow::bail!("StartRx failed: {code}"),
148            other => anyhow::bail!("unexpected response to StartRx: {other:?}"),
149        }
150    }
151
152    /// Stop receiving LoRa packets.
153    pub fn stop_rx(&mut self) -> anyhow::Result<()> {
154        match self.send(Command::StopRx)? {
155            Response::Ok => Ok(()),
156            Response::Error(code) => anyhow::bail!("StopRx failed: {code}"),
157            other => anyhow::bail!("unexpected response to StopRx: {other:?}"),
158        }
159    }
160
161    /// Transmit a LoRa packet.
162    pub fn transmit(&mut self, payload: &[u8], config: Option<RadioConfig>) -> anyhow::Result<()> {
163        if payload.len() > MAX_PAYLOAD {
164            anyhow::bail!("payload too large ({} bytes, max {MAX_PAYLOAD})", payload.len());
165        }
166        let cmd = Command::Transmit { config, payload: payload.to_vec() };
167        match self.send(cmd)? {
168            Response::TxDone => Ok(()),
169            Response::Error(ErrorCode::TxTimeout) => anyhow::bail!("transmit timed out"),
170            Response::Error(code) => anyhow::bail!("Transmit failed: {code}"),
171            other => anyhow::bail!("unexpected response to Transmit: {other:?}"),
172        }
173    }
174
175    /// Get the board's MAC address.
176    pub fn get_mac(&mut self) -> anyhow::Result<[u8; 6]> {
177        match self.send(Command::GetMac)? {
178            Response::MacAddress(mac) => Ok(mac),
179            Response::Error(code) => anyhow::bail!("GetMac failed: {code}"),
180            other => anyhow::bail!("unexpected response to GetMac: {other:?}"),
181        }
182    }
183
184    /// Get the current radio configuration from the device.
185    pub fn get_config(&mut self) -> anyhow::Result<RadioConfig> {
186        match self.send(Command::GetConfig)? {
187            Response::Config(cfg) => Ok(cfg),
188            Response::Error(code) => anyhow::bail!("GetConfig failed: {code}"),
189            other => anyhow::bail!("unexpected response to GetConfig: {other:?}"),
190        }
191    }
192
193    /// Turn on the display (if present).
194    pub fn display_on(&mut self) -> anyhow::Result<()> {
195        match self.send(Command::DisplayOn)? {
196            Response::Ok => Ok(()),
197            Response::Error(code) => anyhow::bail!("DisplayOn failed: {code}"),
198            other => anyhow::bail!("unexpected response to DisplayOn: {other:?}"),
199        }
200    }
201
202    /// Turn off the display (if present).
203    pub fn display_off(&mut self) -> anyhow::Result<()> {
204        match self.send(Command::DisplayOff)? {
205            Response::Ok => Ok(()),
206            Response::Error(code) => anyhow::bail!("DisplayOff failed: {code}"),
207            other => anyhow::bail!("unexpected response to DisplayOff: {other:?}"),
208        }
209    }
210
211    /// Consume the client and return the inner transport.
212    pub fn into_inner(self) -> T {
213        self.transport
214    }
215
216    /// Get a reference to the inner transport.
217    pub fn transport(&self) -> &T {
218        &self.transport
219    }
220
221    /// Get a mutable reference to the inner transport.
222    pub fn transport_mut(&mut self) -> &mut T {
223        &mut self.transport
224    }
225
226    fn buffer_rx(&mut self, resp: Response) {
227        if self.rx_buffer.len() >= RX_BUFFER_CAP {
228            self.rx_buffer.pop_front(); // drop oldest
229        }
230        self.rx_buffer.push_back(resp);
231    }
232}