Skip to main content

ssh_stamp/
serial.rs

1// SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
2// SPDX-FileCopyrightText: 2026 Angus Gratton <gus@projectgus.com>
3// SPDX-FileCopyrightText: 2026 Sergio Gasquez <sergio.gasquez@gmail.com>
4// SPDX-FileCopyrightText: 2026 Gabriel Ku Wei Bin <gabriel.ku@fsfe.org>
5// SPDX-FileCopyrightText: 2026 Anthony Tambasco <anthony.tambasco@fastmail.com>
6//
7// SPDX-License-Identifier: GPL-3.0-or-later
8
9use core::future::Future;
10
11use embassy_futures::select::select;
12use embedded_io_async::{Read, Write};
13use log::{debug, warn};
14
15/// Platform-agnostic buffered serial bridge.
16///
17/// The serial bridge is the inner loop that pumps bytes between the SSH
18/// channel and the target UART. Every platform provides a concrete type
19/// implementing this trait (ESP32: `ssh_stamp_esp32::BufferedUart`).
20///
21/// `read`/`write` take `&self` (not `&mut self`) because the bridge splits
22/// each direction into its own future and runs them concurrently via
23/// [`embassy_futures::select::select`]. Implementations back this with
24/// internal pipes / interrupt-filled buffers.
25pub trait BufferedSerial: Sync {
26    /// Read as many bytes as are available, up to `buf.len()`. Returns the
27    /// number of bytes read. Awaits until at least one byte is available.
28    fn read(&self, buf: &mut [u8]) -> impl Future<Output = usize>;
29
30    /// Queue bytes to be written. Completes once `buf` has been accepted
31    /// by the internal buffer (may still be in flight on the wire).
32    fn write(&self, buf: &[u8]) -> impl Future<Output = ()>;
33
34    /// Return how many received bytes were dropped since the last call
35    /// due to the internal buffer being full. Resets the counter.
36    fn check_dropped_bytes(&self) -> usize;
37}
38
39/// Forwards an incoming SSH connection to/from the local UART, until
40/// the connection drops.
41/// # Errors
42/// Returns an error if the SSH connection fails.
43pub async fn serial_bridge<U: BufferedSerial>(
44    chan_read: impl Read<Error = sunset::Error>,
45    chan_write: impl Write<Error = sunset::Error>,
46    uart: &U,
47) -> Result<(), sunset::Error> {
48    debug!("Starting serial <--> SSH bridge");
49    select(uart_to_ssh(uart, chan_write), ssh_to_uart(chan_read, uart)).await;
50    debug!("Stopping serial <--> SSH bridge");
51    Ok(())
52}
53
54async fn uart_to_ssh<U: BufferedSerial>(
55    uart_buf: &U,
56    mut chan_write: impl Write<Error = sunset::Error>,
57) -> Result<(), sunset::Error> {
58    let mut ssh_tx_buf = [0u8; 512];
59    loop {
60        let dropped = uart_buf.check_dropped_bytes();
61        if dropped > 0 {
62            warn!("UART RX dropped {dropped} bytes");
63        }
64        let n = uart_buf.read(&mut ssh_tx_buf).await;
65        chan_write.write_all(&ssh_tx_buf[..n]).await?;
66    }
67}
68
69async fn ssh_to_uart<U: BufferedSerial>(
70    mut chan_read: impl Read<Error = sunset::Error>,
71    uart_buf: &U,
72) -> Result<(), sunset::Error> {
73    let mut uart_tx_buf = [0u8; 64];
74    loop {
75        let n = chan_read.read(&mut uart_tx_buf).await?;
76        if n == 0 {
77            return Err(sunset::Error::ChannelEOF);
78        }
79        uart_buf.write(&uart_tx_buf[..n]).await;
80    }
81}