Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ build-esp32c61 = "build --release --target riscv32imac-unknown-none-elf -p ssh-s
build-esp32c6 = "build --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-devkitc"
build-esp32c6-generic = "build --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-generic"
build-esp32s2 = "build --profile esp32s2 --target xtensa-esp32s2-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32-s2-saola -Z build-std=core,alloc"
build-esp32s3-touch-lcd-43 = "build --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can -Z build-std=core,alloc"
build-esp32s3-touch-lcd-43 = "build --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can,i2c -Z build-std=core,alloc"
# Bench variant: CAN in no-acknowledge mode (exactly one frame per write with no ACKing node attached)
build-esp32s3-touch-lcd-43-noack = "build --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can-no-ack -Z build-std=core,alloc"
build-esp32s3-touch-lcd-43-noack = "build --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can-no-ack,i2c -Z build-std=core,alloc"

run-esp32c5 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c5-devkitc"
run-esp32c61 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c61-devkitc"
run-esp32c6 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-devkitc"
run-esp32c6-generic = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-generic"
run-esp32s2 = "run --profile esp32s2 --target xtensa-esp32s2-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32-s2-saola"
run-esp32s3-touch-lcd-43 = "run --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can -Z build-std=core,alloc"
run-esp32s3-touch-lcd-43-noack = "run --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can-no-ack -Z build-std=core,alloc"
run-esp32s3-touch-lcd-43 = "run --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can,i2c -Z build-std=core,alloc"
run-esp32s3-touch-lcd-43-noack = "run --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-waveshare-esp32-s3-touch-lcd-43,can-no-ack,i2c -Z build-std=core,alloc"

# IC-only targets (no BSP entry yet) build the library only — the binary hits
# the `compile_error!("No board feature selected.")` guard until a board module
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
{ soc: "esp32", board: "esp32", target: "xtensa-esp32-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--lib" },
{ soc: "esp32s2", board: "board-esp32-s2-saola", target: "xtensa-esp32s2-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--bin ssh-stamp-esp32" },
{ soc: "esp32s3", board: "esp32s3", target: "xtensa-esp32s3-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--lib" },
{ soc: "esp32s3", board: "board-waveshare-esp32-s3-touch-lcd-43,can", target: "xtensa-esp32s3-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--bin ssh-stamp-esp32" },
{ soc: "esp32s3", board: "board-waveshare-esp32-s3-touch-lcd-43,can,i2c", target: "xtensa-esp32s3-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--bin ssh-stamp-esp32" },
]
steps:
- name: Cache
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,6 @@ sftp-ota = []

# Enables CAN bus tunnelling over SSH via the "can" subsystem
can = ["dep:embedded-can"]

# Enables I2C master tunnelling over SSH via the "i2c" subsystem
i2c = []
18 changes: 4 additions & 14 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ use heapless::String;
use log::{debug, error, info, warn};
use ssh_key::HashAlg;
use ssh_stamp_hal::{BandMode, WifiApConfigStatic};
#[cfg(feature = "can")]
use sunset::ChanHandle;
use sunset::SignKey;
use sunset_async::SunsetMutex;

use crate::config::SSHStampConfig;
use crate::handle::{self, SessionType};
use crate::handle::{self, SessionQueues, SessionType};
use crate::platform::PlatformServices;
use crate::serial::BufferedSerial;
use crate::serve;
Expand Down Expand Up @@ -135,17 +133,9 @@ where
let ssh_server = serve::ssh_wait_for_initialisation(&mut inbuf, &mut outbuf);

let chan_pipe = Channel::<NoopRawMutex, SessionType, 1>::new();
#[cfg(feature = "can")]
let can_queue = Channel::<NoopRawMutex, ChanHandle, 1>::new();
#[cfg(feature = "can")]
let connection =
serve::connection_loop(&ssh_server, &chan_pipe, config, platform, &can_queue);
#[cfg(not(feature = "can"))]
let connection = serve::connection_loop(&ssh_server, &chan_pipe, config, platform);
#[cfg(feature = "can")]
let bridge = handle::ssh_client(uart, &ssh_server, &chan_pipe, platform, &can_queue);
#[cfg(not(feature = "can"))]
let bridge = handle::ssh_client(uart, &ssh_server, &chan_pipe, platform);
let queues = SessionQueues::new();
let connection = serve::connection_loop(&ssh_server, &chan_pipe, config, platform, &queues);
let bridge = handle::ssh_client(uart, &ssh_server, &chan_pipe, platform, &queues);

let (mut rsock, mut wsock) = tcp_socket.split();
let server = ssh_server.run(&mut rsock, &mut wsock);
Expand Down
153 changes: 115 additions & 38 deletions src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ use crate::serial::{BufferedSerial, serial_bridge};

#[cfg(feature = "can")]
use crate::can::can_bridge;
#[cfg(feature = "i2c")]
use crate::i2c::i2c_bridge;

#[cfg(feature = "can")]
#[cfg(all(
any(feature = "can", feature = "i2c"),
not(all(feature = "can", feature = "i2c"))
))]
use embassy_futures::select::{Either, select};
#[cfg(all(feature = "can", feature = "i2c"))]
use embassy_futures::select::{Either3, select3};
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::channel::Channel;

Expand Down Expand Up @@ -155,22 +162,50 @@ pub enum SessionType {
Sftp(ChanHandle),
}

/// Hand-off channels from the connection loop to the per-subsystem bridge
/// futures in [`ssh_client`]. One slot per optional bridge subsystem;
/// empty when no bridge feature is enabled.
pub struct SessionQueues {
#[cfg(feature = "can")]
pub can: Channel<NoopRawMutex, ChanHandle, 1>,
#[cfg(feature = "i2c")]
pub i2c: Channel<NoopRawMutex, ChanHandle, 1>,
}

impl SessionQueues {
#[must_use]
pub fn new() -> Self {
SessionQueues {
#[cfg(feature = "can")]
can: Channel::new(),
#[cfg(feature = "i2c")]
i2c: Channel::new(),
}
}
}

impl Default for SessionQueues {
fn default() -> Self {
Self::new()
}
}

pub struct EventContext<'a> {
pub session: &'a mut Option<ChanHandle>,
pub auth_checked: &'a mut bool,
pub config_changed: &'a mut bool,
pub needs_reset: &'a mut bool,
/// Hands accepted `can` subsystem channels to the CAN bridge, which
/// runs concurrently with the shell (UART) session.
#[cfg(feature = "can")]
pub can_queue: &'a Channel<NoopRawMutex, ChanHandle, 1>,
/// Set once a CAN session is dispatched on this connection. SFTP (OTA)
/// needs the connection's full bandwidth, so it is refused afterwards.
#[cfg(all(feature = "sftp-ota", feature = "can"))]
pub can_dispatched: &'a mut bool,
/// Hands accepted bridge subsystem channels (CAN, I2C) to their
/// bridges, which run concurrently with the shell (UART) session.
pub queues: &'a SessionQueues,
/// Set once a bridge session is dispatched on this connection. SFTP
/// (OTA) needs the connection's full bandwidth, so it is refused
/// afterwards.
#[cfg(all(feature = "sftp-ota", any(feature = "can", feature = "i2c")))]
pub bridge_dispatched: &'a mut bool,
}

/// Handles SSH session subsystem requests (e.g., SFTP, CAN).
/// Handles SSH session subsystem requests (e.g., SFTP, CAN, I2C).
///
/// # Errors
///
Expand All @@ -190,13 +225,13 @@ pub fn session_subsystem(
#[cfg(feature = "sftp-ota")]
{
// SFTP (OTA) is exclusive: it needs the connection's full
// bandwidth, so refuse it once a CAN session is active.
#[cfg(feature = "can")]
let can_active = *ctx.can_dispatched;
#[cfg(not(feature = "can"))]
let can_active = false;
if can_active {
warn!("SFTP subsystem refused: a CAN session is active on this connection");
// bandwidth, so refuse it once a bridge session is active.
#[cfg(any(feature = "can", feature = "i2c"))]
let bridge_active = *ctx.bridge_dispatched;
#[cfg(not(any(feature = "can", feature = "i2c")))]
let bridge_active = false;
if bridge_active {
warn!("SFTP subsystem refused: a bridge session is active on this connection");
a.fail()?;
} else if let Some(ch) = ctx.session.take() {
debug_assert_eq!(ch.num(), a.channel());
Expand Down Expand Up @@ -224,12 +259,12 @@ pub fn session_subsystem(
// auth_checked is deliberately left untouched so the same
// (already authenticated) connection can still request a
// shell session and bridge UART concurrently with CAN.
if let Err(e) = ctx.can_queue.try_send(ch) {
if let Err(e) = ctx.queues.can.try_send(ch) {
log::error!("Could not send the CAN channel: {e:?}");
}
#[cfg(feature = "sftp-ota")]
{
*ctx.can_dispatched = true;
*ctx.bridge_dispatched = true;
}
} else {
a.fail()?;
Expand All @@ -239,6 +274,29 @@ pub fn session_subsystem(
warn!("CAN subsystem requested but not supported in this build");
a.fail()?;
}
} else if a.command()?.to_lowercase().as_str() == "i2c" {
#[cfg(feature = "i2c")]
if let Some(ch) = ctx.session.take() {
debug_assert_eq!(ch.num(), a.channel());
a.succeed()?;
debug!("We got I2C subsystem");
// As with CAN, auth_checked is left untouched so shell and
// other bridge sessions can still be requested afterwards.
if let Err(e) = ctx.queues.i2c.try_send(ch) {
log::error!("Could not send the I2C channel: {e:?}");
}
#[cfg(feature = "sftp-ota")]
{
*ctx.bridge_dispatched = true;
}
} else {
a.fail()?;
}
#[cfg(not(feature = "i2c"))]
{
warn!("I2C subsystem requested but not supported in this build");
a.fail()?;
}
} else {
a.fail()?;
}
Expand Down Expand Up @@ -749,10 +807,10 @@ pub fn defunct() -> Result<(), sunset::Error> {
/// Handles an SSH client connection, bridging UART and SSH.
///
#[cfg_attr(
feature = "can",
doc = "A `can` subsystem channel is bridged concurrently with the shell",
doc = "(UART) session on the same connection. The whole connection is",
doc = "torn down when either bridge finishes.",
any(feature = "can", feature = "i2c"),
doc = "Bridge subsystem channels (CAN, I2C) are bridged concurrently",
doc = "with the shell (UART) session on the same connection. The whole",
doc = "connection is torn down when any bridge finishes.",
doc = ""
)]
/// # Errors
Expand All @@ -762,11 +820,12 @@ pub async fn ssh_client<'a, 'b, U, P>(
ssh_server: &'b SSHServer<'a>,
chan_pipe: &'b Channel<NoopRawMutex, SessionType, 1>,
#[cfg_attr(
not(any(feature = "sftp-ota", feature = "can")),
not(any(feature = "sftp-ota", feature = "can", feature = "i2c")),
allow(unused_variables)
)]
platform: &'b P,
#[cfg(feature = "can")] can_queue: &'b Channel<NoopRawMutex, ChanHandle, 1>,
#[cfg_attr(not(any(feature = "can", feature = "i2c")), allow(unused_variables))]
queues: &'b SessionQueues,
) -> Result<(), sunset::Error>
where
U: BufferedSerial,
Expand Down Expand Up @@ -796,20 +855,38 @@ where
};

#[cfg(feature = "can")]
let result = {
let can_session = async {
let ch = can_queue.receive().await;
info!("Handling CAN session");
let chan_io: ChanInOut<'_> = ssh_server.stdio(ch).await?;
let (stdin, stdout) = chan_io.split();
info!("Starting CAN bridge");
can_bridge(stdin, stdout, platform.can()).await
};
match select(session, can_session).await {
Either::First(r) | Either::Second(r) => r,
}
let can_session = async {
let ch = queues.can.receive().await;
info!("Handling CAN session");
let chan_io: ChanInOut<'_> = ssh_server.stdio(ch).await?;
let (stdin, stdout) = chan_io.split();
info!("Starting CAN bridge");
can_bridge(stdin, stdout, platform.can()).await
};

#[cfg(feature = "i2c")]
let i2c_session = async {
let ch = queues.i2c.receive().await;
info!("Handling I2C session");
let chan_io: ChanInOut<'_> = ssh_server.stdio(ch).await?;
let (stdin, stdout) = chan_io.split();
info!("Starting I2C bridge");
i2c_bridge(stdin, stdout, platform.i2c()).await
};

#[cfg(all(feature = "can", feature = "i2c"))]
let result = match select3(session, can_session, i2c_session).await {
Either3::First(r) | Either3::Second(r) | Either3::Third(r) => r,
};
#[cfg(all(feature = "can", not(feature = "i2c")))]
let result = match select(session, can_session).await {
Either::First(r) | Either::Second(r) => r,
};
#[cfg(all(feature = "i2c", not(feature = "can")))]
let result = match select(session, i2c_session).await {
Either::First(r) | Either::Second(r) => r,
};
#[cfg(not(feature = "can"))]
#[cfg(not(any(feature = "can", feature = "i2c")))]
let result = session.await;
result
}
Expand Down
Loading
Loading