Skip to main content

lager/nets/
uart.rs

1//! UART nets: interactive serial streaming over the box's Socket.IO
2//! `/uart` namespace (feature `uart`).
3//!
4//! The box owns the serial port (exclusive open) and streams data as hex
5//! chunks over `uart_data` events; writes go up as `uart_write` events.
6//! Only one session per net or device is allowed — a second opener gets a
7//! clear "already in use" error.
8
9use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
10use std::time::{Duration, Instant};
11
12use rust_socketio::{ClientBuilder, Payload};
13use serde_json::{json, Value};
14
15use crate::error::{Error, Result};
16
17/// How long to wait for the box to confirm the serial port opened.
18const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
19
20#[derive(Debug)]
21enum UartEvent {
22    Connected { device_path: String, baudrate: u64 },
23    Data(Vec<u8>),
24    Status(String),
25    Error(String),
26    Stopped,
27}
28
29fn hex_encode(data: &[u8]) -> String {
30    let mut s = String::with_capacity(data.len() * 2);
31    for b in data {
32        s.push_str(&format!("{b:02x}"));
33    }
34    s
35}
36
37fn hex_decode(s: &str) -> Option<Vec<u8>> {
38    let s = s.trim();
39    if s.len() % 2 != 0 {
40        return None;
41    }
42    (0..s.len())
43        .step_by(2)
44        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
45        .collect()
46}
47
48/// Pull the first JSON object out of a Socket.IO payload.
49fn payload_json(payload: Payload) -> Option<Value> {
50    match payload {
51        Payload::Text(values) => values.into_iter().next(),
52        #[allow(deprecated)]
53        Payload::String(s) => serde_json::from_str(&s).ok(),
54        Payload::Binary(_) => None,
55    }
56}
57
58/// A live UART streaming session.
59///
60/// Created via [`crate::LagerBox::uart`]. Data received from the device is
61/// buffered internally; pull it with [`Uart::read`] or scan for expected
62/// output with [`Uart::wait_for`]. The session is stopped cleanly on
63/// [`Uart::stop`] or drop.
64pub struct Uart {
65    socket: Option<rust_socketio::client::Client>,
66    rx: Receiver<UartEvent>,
67    netname: String,
68    device_path: String,
69    baudrate: u64,
70    buf: Vec<u8>,
71    last_status: Option<String>,
72}
73
74impl Uart {
75    pub(crate) fn open(
76        base_url: &str,
77        netname: String,
78        bearer_token: Option<String>,
79    ) -> Result<Self> {
80        let (tx, rx) = std::sync::mpsc::channel::<UartEvent>();
81
82        let socket = {
83            let tx_connected: Sender<UartEvent> = tx.clone();
84            let tx_data = tx.clone();
85            let tx_status = tx.clone();
86            let tx_error = tx.clone();
87            let tx_stopped = tx;
88            let mut builder = ClientBuilder::new(base_url).namespace("/uart");
89            // Boxes behind an authenticating gateway need the bearer token
90            // on the Socket.IO handshake too (same reverse proxy).
91            if let Some(token) = &bearer_token {
92                builder = builder.opening_header("Authorization", format!("Bearer {token}"));
93            }
94            builder
95                .on("uart_connected", move |payload, _| {
96                    let info = payload_json(payload).unwrap_or(Value::Null);
97                    let device_path = info
98                        .get("device_path")
99                        .and_then(Value::as_str)
100                        .unwrap_or_default()
101                        .to_string();
102                    let baudrate = info.get("baudrate").and_then(Value::as_u64).unwrap_or(0);
103                    let _ = tx_connected.send(UartEvent::Connected {
104                        device_path,
105                        baudrate,
106                    });
107                })
108                .on("uart_data", move |payload, _| {
109                    if let Some(bytes) = payload_json(payload)
110                        .as_ref()
111                        .and_then(|v| v.get("data"))
112                        .and_then(Value::as_str)
113                        .and_then(hex_decode)
114                    {
115                        let _ = tx_data.send(UartEvent::Data(bytes));
116                    }
117                })
118                .on("uart_status", move |payload, _| {
119                    // reconnecting/reconnected notifications during device
120                    // re-enumeration; informational only.
121                    if let Some(status) = payload_json(payload)
122                        .as_ref()
123                        .and_then(|v| v.get("status"))
124                        .and_then(Value::as_str)
125                    {
126                        let _ = tx_status.send(UartEvent::Status(status.to_string()));
127                    }
128                })
129                .on("error", move |payload, _| {
130                    let message = payload_json(payload)
131                        .as_ref()
132                        .and_then(|v| v.get("message"))
133                        .and_then(Value::as_str)
134                        .unwrap_or("unknown UART error")
135                        .to_string();
136                    let _ = tx_error.send(UartEvent::Error(message));
137                })
138                .on("uart_stopped", move |_, _| {
139                    let _ = tx_stopped.send(UartEvent::Stopped);
140                })
141                .connect()
142                .map_err(|e| Error::Connection(format!("Socket.IO connect failed: {e}")))?
143        };
144
145        socket
146            .emit("start_uart", json!({ "netname": netname, "overrides": {} }))
147            .map_err(|e| Error::Stream(format!("could not start UART session: {e}")))?;
148
149        // Wait for the box to open the port (or refuse).
150        let deadline = Instant::now() + CONNECT_TIMEOUT;
151        let mut uart = Uart {
152            socket: Some(socket),
153            rx,
154            netname,
155            device_path: String::new(),
156            baudrate: 0,
157            buf: Vec::new(),
158            last_status: None,
159        };
160        loop {
161            let remaining = deadline.saturating_duration_since(Instant::now());
162            if remaining.is_zero() {
163                return Err(Error::Timeout(format!(
164                    "box did not confirm UART session for '{}' within {CONNECT_TIMEOUT:?}",
165                    uart.netname
166                )));
167            }
168            match uart.rx.recv_timeout(remaining) {
169                Ok(UartEvent::Connected {
170                    device_path,
171                    baudrate,
172                }) => {
173                    uart.device_path = device_path;
174                    uart.baudrate = baudrate;
175                    return Ok(uart);
176                }
177                // Data can beat the connected event; keep it.
178                Ok(UartEvent::Data(bytes)) => uart.buf.extend_from_slice(&bytes),
179                Ok(UartEvent::Error(msg)) => return Err(Error::Stream(msg)),
180                Ok(_) => {}
181                Err(RecvTimeoutError::Timeout) => {}
182                Err(RecvTimeoutError::Disconnected) => {
183                    return Err(Error::Stream("UART session closed unexpectedly".to_string()))
184                }
185            }
186        }
187    }
188
189    /// Name of the net this session streams.
190    pub fn netname(&self) -> &str {
191        &self.netname
192    }
193
194    /// Device path on the box (e.g. `/dev/ttyUSB0`).
195    pub fn device_path(&self) -> &str {
196        &self.device_path
197    }
198
199    /// Baud rate the port was opened at.
200    pub fn baudrate(&self) -> u64 {
201        self.baudrate
202    }
203
204    /// The most recent session status notification from the box, e.g.
205    /// `"reconnecting"` / `"reconnected"` while the adapter re-enumerates
206    /// (hub power-cycle, DUT reflash). Updated during reads.
207    pub fn last_status(&self) -> Option<&str> {
208        self.last_status.as_deref()
209    }
210
211    /// Write raw bytes to the device.
212    pub fn write(&self, data: &[u8]) -> Result<()> {
213        let socket = self
214            .socket
215            .as_ref()
216            .ok_or_else(|| Error::Stream("UART session already stopped".to_string()))?;
217        socket
218            .emit("uart_write", json!({ "data": hex_encode(data) }))
219            .map_err(|e| Error::Stream(format!("UART write failed: {e}")))
220    }
221
222    /// Write a string to the device.
223    pub fn write_str(&self, s: &str) -> Result<()> {
224        self.write(s.as_bytes())
225    }
226
227    /// Drain incoming events into the internal buffer. Returns an error if
228    /// the box reported a session error.
229    fn pump(&mut self, wait: Duration) -> Result<()> {
230        let mut wait = wait;
231        loop {
232            match self.rx.recv_timeout(wait) {
233                Ok(UartEvent::Data(bytes)) => {
234                    self.buf.extend_from_slice(&bytes);
235                    // Something arrived: keep draining whatever is already
236                    // queued without waiting again.
237                    wait = Duration::ZERO;
238                }
239                Ok(UartEvent::Error(msg)) => return Err(Error::Stream(msg)),
240                Ok(UartEvent::Status(status)) => self.last_status = Some(status),
241                Ok(_) => {}
242                Err(RecvTimeoutError::Timeout) => return Ok(()),
243                Err(RecvTimeoutError::Disconnected) => {
244                    return Err(Error::Stream("UART session closed unexpectedly".to_string()))
245                }
246            }
247        }
248    }
249
250    /// Return whatever bytes arrive within `timeout`. May return an empty
251    /// vector if the device was quiet.
252    pub fn read(&mut self, timeout: Duration) -> Result<Vec<u8>> {
253        self.pump(timeout)?;
254        Ok(std::mem::take(&mut self.buf))
255    }
256
257    /// Accumulate output until `needle` appears or `timeout` elapses.
258    ///
259    /// On success returns everything received up to and including `needle`;
260    /// bytes after the needle stay buffered for the next read.
261    pub fn wait_for(&mut self, needle: &[u8], timeout: Duration) -> Result<Vec<u8>> {
262        if needle.is_empty() {
263            return Ok(Vec::new());
264        }
265        let deadline = Instant::now() + timeout;
266        loop {
267            if let Some(pos) = self
268                .buf
269                .windows(needle.len())
270                .position(|w| w == needle)
271            {
272                let mut rest = self.buf.split_off(pos + needle.len());
273                std::mem::swap(&mut self.buf, &mut rest);
274                return Ok(rest);
275            }
276            let remaining = deadline.saturating_duration_since(Instant::now());
277            if remaining.is_zero() {
278                return Err(Error::Timeout(format!(
279                    "'{}' did not appear on UART '{}' within {timeout:?}",
280                    String::from_utf8_lossy(needle),
281                    self.netname
282                )));
283            }
284            self.pump(remaining)?;
285        }
286    }
287
288    /// Stop the session cleanly (tells the box to close the port).
289    pub fn stop(mut self) -> Result<()> {
290        self.shutdown();
291        Ok(())
292    }
293
294    fn shutdown(&mut self) {
295        if let Some(socket) = self.socket.take() {
296            let _ = socket.emit("stop_uart", json!({}));
297            let _ = socket.disconnect();
298        }
299    }
300}
301
302impl Drop for Uart {
303    fn drop(&mut self) {
304        self.shutdown();
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::{hex_decode, hex_encode};
311
312    #[test]
313    fn hex_roundtrip() {
314        let data = [0x00, 0x0a, 0xff, 0x42];
315        assert_eq!(hex_encode(&data), "000aff42");
316        assert_eq!(hex_decode(&hex_encode(&data)).unwrap(), data);
317        assert!(hex_decode("zz").is_none());
318        assert!(hex_decode("abc").is_none());
319    }
320}