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}