Skip to content

Commit 4444416

Browse files
committed
Experimental support for tunneled I2C over SSH
1 parent 96f2791 commit 4444416

16 files changed

Lines changed: 767 additions & 77 deletions

File tree

.cargo/config.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ build-esp32c61 = "build --release --target riscv32imac-unknown-none-elf -p ssh-s
1313
build-esp32c6 = "build --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-devkitc"
1414
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"
1515
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"
16-
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"
16+
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"
1717
# Bench variant: CAN in no-acknowledge mode (exactly one frame per write with no ACKing node attached)
18-
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"
18+
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"
1919

2020
run-esp32c5 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c5-devkitc"
2121
run-esp32c61 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c61-devkitc"
2222
run-esp32c6 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-devkitc"
2323
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"
2424
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"
25-
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"
26-
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"
25+
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"
26+
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"
2727

2828
# IC-only targets (no BSP entry yet) build the library only — the binary hits
2929
# the `compile_error!("No board feature selected.")` guard until a board module

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
{ soc: "esp32", board: "esp32", target: "xtensa-esp32-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--lib" },
3535
{ 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" },
3636
{ soc: "esp32s3", board: "esp32s3", target: "xtensa-esp32s3-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--lib" },
37-
{ 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" },
37+
{ 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" },
3838
]
3939
steps:
4040
- name: Cache

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,6 @@ sftp-ota = []
168168

169169
# Enables CAN bus tunnelling over SSH via the "can" subsystem
170170
can = ["dep:embedded-can"]
171+
172+
# Enables I2C master tunnelling over SSH via the "i2c" subsystem
173+
i2c = []

src/app.rs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ use heapless::String;
1919
use log::{debug, error, info, warn};
2020
use ssh_key::HashAlg;
2121
use ssh_stamp_hal::{BandMode, WifiApConfigStatic};
22-
#[cfg(feature = "can")]
23-
use sunset::ChanHandle;
2422
use sunset::SignKey;
2523
use sunset_async::SunsetMutex;
2624

2725
use crate::config::SSHStampConfig;
28-
use crate::handle::{self, SessionType};
26+
use crate::handle::{self, SessionQueues, SessionType};
2927
use crate::platform::PlatformServices;
3028
use crate::serial::BufferedSerial;
3129
use crate::serve;
@@ -135,17 +133,9 @@ where
135133
let ssh_server = serve::ssh_wait_for_initialisation(&mut inbuf, &mut outbuf);
136134

137135
let chan_pipe = Channel::<NoopRawMutex, SessionType, 1>::new();
138-
#[cfg(feature = "can")]
139-
let can_queue = Channel::<NoopRawMutex, ChanHandle, 1>::new();
140-
#[cfg(feature = "can")]
141-
let connection =
142-
serve::connection_loop(&ssh_server, &chan_pipe, config, platform, &can_queue);
143-
#[cfg(not(feature = "can"))]
144-
let connection = serve::connection_loop(&ssh_server, &chan_pipe, config, platform);
145-
#[cfg(feature = "can")]
146-
let bridge = handle::ssh_client(uart, &ssh_server, &chan_pipe, platform, &can_queue);
147-
#[cfg(not(feature = "can"))]
148-
let bridge = handle::ssh_client(uart, &ssh_server, &chan_pipe, platform);
136+
let queues = SessionQueues::new();
137+
let connection = serve::connection_loop(&ssh_server, &chan_pipe, config, platform, &queues);
138+
let bridge = handle::ssh_client(uart, &ssh_server, &chan_pipe, platform, &queues);
149139

150140
let (mut rsock, mut wsock) = tcp_socket.split();
151141
let server = ssh_server.run(&mut rsock, &mut wsock);

src/handle.rs

Lines changed: 115 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,16 @@ use crate::serial::{BufferedSerial, serial_bridge};
2222

2323
#[cfg(feature = "can")]
2424
use crate::can::can_bridge;
25+
#[cfg(feature = "i2c")]
26+
use crate::i2c::i2c_bridge;
2527

26-
#[cfg(feature = "can")]
28+
#[cfg(all(
29+
any(feature = "can", feature = "i2c"),
30+
not(all(feature = "can", feature = "i2c"))
31+
))]
2732
use embassy_futures::select::{Either, select};
33+
#[cfg(all(feature = "can", feature = "i2c"))]
34+
use embassy_futures::select::{Either3, select3};
2835
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
2936
use embassy_sync::channel::Channel;
3037

@@ -155,22 +162,50 @@ pub enum SessionType {
155162
Sftp(ChanHandle),
156163
}
157164

165+
/// Hand-off channels from the connection loop to the per-subsystem bridge
166+
/// futures in [`ssh_client`]. One slot per optional bridge subsystem;
167+
/// empty when no bridge feature is enabled.
168+
pub struct SessionQueues {
169+
#[cfg(feature = "can")]
170+
pub can: Channel<NoopRawMutex, ChanHandle, 1>,
171+
#[cfg(feature = "i2c")]
172+
pub i2c: Channel<NoopRawMutex, ChanHandle, 1>,
173+
}
174+
175+
impl SessionQueues {
176+
#[must_use]
177+
pub fn new() -> Self {
178+
SessionQueues {
179+
#[cfg(feature = "can")]
180+
can: Channel::new(),
181+
#[cfg(feature = "i2c")]
182+
i2c: Channel::new(),
183+
}
184+
}
185+
}
186+
187+
impl Default for SessionQueues {
188+
fn default() -> Self {
189+
Self::new()
190+
}
191+
}
192+
158193
pub struct EventContext<'a> {
159194
pub session: &'a mut Option<ChanHandle>,
160195
pub auth_checked: &'a mut bool,
161196
pub config_changed: &'a mut bool,
162197
pub needs_reset: &'a mut bool,
163-
/// Hands accepted `can` subsystem channels to the CAN bridge, which
164-
/// runs concurrently with the shell (UART) session.
165-
#[cfg(feature = "can")]
166-
pub can_queue: &'a Channel<NoopRawMutex, ChanHandle, 1>,
167-
/// Set once a CAN session is dispatched on this connection. SFTP (OTA)
168-
/// needs the connection's full bandwidth, so it is refused afterwards.
169-
#[cfg(all(feature = "sftp-ota", feature = "can"))]
170-
pub can_dispatched: &'a mut bool,
198+
/// Hands accepted bridge subsystem channels (CAN, I2C) to their
199+
/// bridges, which run concurrently with the shell (UART) session.
200+
pub queues: &'a SessionQueues,
201+
/// Set once a bridge session is dispatched on this connection. SFTP
202+
/// (OTA) needs the connection's full bandwidth, so it is refused
203+
/// afterwards.
204+
#[cfg(all(feature = "sftp-ota", any(feature = "can", feature = "i2c")))]
205+
pub bridge_dispatched: &'a mut bool,
171206
}
172207

173-
/// Handles SSH session subsystem requests (e.g., SFTP, CAN).
208+
/// Handles SSH session subsystem requests (e.g., SFTP, CAN, I2C).
174209
///
175210
/// # Errors
176211
///
@@ -190,13 +225,13 @@ pub fn session_subsystem(
190225
#[cfg(feature = "sftp-ota")]
191226
{
192227
// SFTP (OTA) is exclusive: it needs the connection's full
193-
// bandwidth, so refuse it once a CAN session is active.
194-
#[cfg(feature = "can")]
195-
let can_active = *ctx.can_dispatched;
196-
#[cfg(not(feature = "can"))]
197-
let can_active = false;
198-
if can_active {
199-
warn!("SFTP subsystem refused: a CAN session is active on this connection");
228+
// bandwidth, so refuse it once a bridge session is active.
229+
#[cfg(any(feature = "can", feature = "i2c"))]
230+
let bridge_active = *ctx.bridge_dispatched;
231+
#[cfg(not(any(feature = "can", feature = "i2c")))]
232+
let bridge_active = false;
233+
if bridge_active {
234+
warn!("SFTP subsystem refused: a bridge session is active on this connection");
200235
a.fail()?;
201236
} else if let Some(ch) = ctx.session.take() {
202237
debug_assert_eq!(ch.num(), a.channel());
@@ -224,12 +259,12 @@ pub fn session_subsystem(
224259
// auth_checked is deliberately left untouched so the same
225260
// (already authenticated) connection can still request a
226261
// shell session and bridge UART concurrently with CAN.
227-
if let Err(e) = ctx.can_queue.try_send(ch) {
262+
if let Err(e) = ctx.queues.can.try_send(ch) {
228263
log::error!("Could not send the CAN channel: {e:?}");
229264
}
230265
#[cfg(feature = "sftp-ota")]
231266
{
232-
*ctx.can_dispatched = true;
267+
*ctx.bridge_dispatched = true;
233268
}
234269
} else {
235270
a.fail()?;
@@ -239,6 +274,29 @@ pub fn session_subsystem(
239274
warn!("CAN subsystem requested but not supported in this build");
240275
a.fail()?;
241276
}
277+
} else if a.command()?.to_lowercase().as_str() == "i2c" {
278+
#[cfg(feature = "i2c")]
279+
if let Some(ch) = ctx.session.take() {
280+
debug_assert_eq!(ch.num(), a.channel());
281+
a.succeed()?;
282+
debug!("We got I2C subsystem");
283+
// As with CAN, auth_checked is left untouched so shell and
284+
// other bridge sessions can still be requested afterwards.
285+
if let Err(e) = ctx.queues.i2c.try_send(ch) {
286+
log::error!("Could not send the I2C channel: {e:?}");
287+
}
288+
#[cfg(feature = "sftp-ota")]
289+
{
290+
*ctx.bridge_dispatched = true;
291+
}
292+
} else {
293+
a.fail()?;
294+
}
295+
#[cfg(not(feature = "i2c"))]
296+
{
297+
warn!("I2C subsystem requested but not supported in this build");
298+
a.fail()?;
299+
}
242300
} else {
243301
a.fail()?;
244302
}
@@ -749,10 +807,10 @@ pub fn defunct() -> Result<(), sunset::Error> {
749807
/// Handles an SSH client connection, bridging UART and SSH.
750808
///
751809
#[cfg_attr(
752-
feature = "can",
753-
doc = "A `can` subsystem channel is bridged concurrently with the shell",
754-
doc = "(UART) session on the same connection. The whole connection is",
755-
doc = "torn down when either bridge finishes.",
810+
any(feature = "can", feature = "i2c"),
811+
doc = "Bridge subsystem channels (CAN, I2C) are bridged concurrently",
812+
doc = "with the shell (UART) session on the same connection. The whole",
813+
doc = "connection is torn down when any bridge finishes.",
756814
doc = ""
757815
)]
758816
/// # Errors
@@ -762,11 +820,12 @@ pub async fn ssh_client<'a, 'b, U, P>(
762820
ssh_server: &'b SSHServer<'a>,
763821
chan_pipe: &'b Channel<NoopRawMutex, SessionType, 1>,
764822
#[cfg_attr(
765-
not(any(feature = "sftp-ota", feature = "can")),
823+
not(any(feature = "sftp-ota", feature = "can", feature = "i2c")),
766824
allow(unused_variables)
767825
)]
768826
platform: &'b P,
769-
#[cfg(feature = "can")] can_queue: &'b Channel<NoopRawMutex, ChanHandle, 1>,
827+
#[cfg_attr(not(any(feature = "can", feature = "i2c")), allow(unused_variables))]
828+
queues: &'b SessionQueues,
770829
) -> Result<(), sunset::Error>
771830
where
772831
U: BufferedSerial,
@@ -796,20 +855,38 @@ where
796855
};
797856

798857
#[cfg(feature = "can")]
799-
let result = {
800-
let can_session = async {
801-
let ch = can_queue.receive().await;
802-
info!("Handling CAN session");
803-
let chan_io: ChanInOut<'_> = ssh_server.stdio(ch).await?;
804-
let (stdin, stdout) = chan_io.split();
805-
info!("Starting CAN bridge");
806-
can_bridge(stdin, stdout, platform.can()).await
807-
};
808-
match select(session, can_session).await {
809-
Either::First(r) | Either::Second(r) => r,
810-
}
858+
let can_session = async {
859+
let ch = queues.can.receive().await;
860+
info!("Handling CAN session");
861+
let chan_io: ChanInOut<'_> = ssh_server.stdio(ch).await?;
862+
let (stdin, stdout) = chan_io.split();
863+
info!("Starting CAN bridge");
864+
can_bridge(stdin, stdout, platform.can()).await
865+
};
866+
867+
#[cfg(feature = "i2c")]
868+
let i2c_session = async {
869+
let ch = queues.i2c.receive().await;
870+
info!("Handling I2C session");
871+
let chan_io: ChanInOut<'_> = ssh_server.stdio(ch).await?;
872+
let (stdin, stdout) = chan_io.split();
873+
info!("Starting I2C bridge");
874+
i2c_bridge(stdin, stdout, platform.i2c()).await
875+
};
876+
877+
#[cfg(all(feature = "can", feature = "i2c"))]
878+
let result = match select3(session, can_session, i2c_session).await {
879+
Either3::First(r) | Either3::Second(r) | Either3::Third(r) => r,
880+
};
881+
#[cfg(all(feature = "can", not(feature = "i2c")))]
882+
let result = match select(session, can_session).await {
883+
Either::First(r) | Either::Second(r) => r,
884+
};
885+
#[cfg(all(feature = "i2c", not(feature = "can")))]
886+
let result = match select(session, i2c_session).await {
887+
Either::First(r) | Either::Second(r) => r,
811888
};
812-
#[cfg(not(feature = "can"))]
889+
#[cfg(not(any(feature = "can", feature = "i2c")))]
813890
let result = session.await;
814891
result
815892
}

0 commit comments

Comments
 (0)