From 120fc38dbc5fbf1031ff19de2321fef6687d50f5 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 21 Mar 2026 04:56:06 +0100 Subject: [PATCH 01/14] Refactor logic from serve.rs connection_loop to handle.rs --- src/handle.rs | 408 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 10 +- src/serve.rs | 378 +++++----------------------------------------- 4 files changed, 453 insertions(+), 344 deletions(-) create mode 100644 src/handle.rs diff --git a/src/handle.rs b/src/handle.rs new file mode 100644 index 0000000..6639de6 --- /dev/null +++ b/src/handle.rs @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use log::{debug, info, warn}; + +use crate::config::SSHStampConfig; +use crate::espressif::buffered_uart::UART_SIGNAL; +use crate::serial::serial_bridge; +use crate::store; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::channel::Channel; +use esp_hal::system::software_reset; +use heapless::String; +use storage::flash; + +use core::fmt::Debug; +use core::option::Option::None; +use core::result::Result; + +use sunset::{ChanHandle, ServEvent}; +use sunset_async::{ChanInOut, SSHServer, SunsetMutex}; + +#[derive(Debug)] +pub enum SessionType { + Bridge(ChanHandle), + #[cfg(feature = "sftp-ota")] + Sftp(ChanHandle), +} + +pub struct EventContext<'a> { + pub session: &'a mut Option, + pub auth_checked: &'a mut bool, + pub config_changed: &'a mut bool, + pub needs_reset: &'a mut bool, +} + +pub fn session_subsystem( + ev: ServEvent<'_, '_>, + ctx: &mut EventContext<'_>, + _chan_pipe: &Channel, +) -> Result<(), sunset::Error> { + if let ServEvent::SessionSubsystem(a) = ev { + debug!("ServEvent::SessionSubsystem"); + + if !*ctx.auth_checked { + warn!("Unauthenticated SessionSubsystem rejected"); + a.fail()?; + } else if a.command()?.to_lowercase().as_str() == "sftp" { + if let Some(ch) = ctx.session.take() { + debug_assert!(ch.num() == a.channel()); + #[cfg(feature = "sftp-ota")] + { + a.succeed()?; + debug!("We got SFTP subsystem"); + match _chan_pipe.try_send(SessionType::Sftp(ch)) { + Ok(_) => *ctx.auth_checked = false, + Err(e) => log::error!("Could not send the channel: {:?}", e), + }; + } + #[cfg(not(feature = "sftp-ota"))] + { + warn!("SFTP subsystem requested but not supported in this build"); + a.fail()?; + } + } else { + a.fail()?; + } + } else { + a.fail()?; + } + } + Ok(()) +} + +pub async fn session_shell( + ev: ServEvent<'_, '_>, + ctx: &mut EventContext<'_>, + config: &SunsetMutex, + chan_pipe: &Channel, +) -> Result<(), sunset::Error> { + if let ServEvent::SessionShell(a) = ev { + debug!("ServEvent::SessionShell"); + + if !*ctx.auth_checked { + warn!("Unauthenticated SessionShell rejected"); + a.fail()?; + } else if let Some(ch) = ctx.session.take() { + if *ctx.config_changed { + *ctx.config_changed = false; + let config_guard = config.lock().await; + let Some(flash_storage_guard) = flash::get_flash_n_buffer() else { + panic!("Could not acquire flash storage lock"); + }; + let mut flash_storage = flash_storage_guard.lock().await; + let _result = store::save(&mut flash_storage, &config_guard).await; + drop(config_guard); + if *ctx.needs_reset { + info!("Configuration saved. Rebooting to apply WiFi changes..."); + software_reset(); + } + } + debug_assert!(ch.num() == a.channel()); + a.succeed()?; + debug!("We got shell"); + UART_SIGNAL.signal(1); + debug!("Connection loop: UART_SIGNAL sent"); + match chan_pipe.try_send(SessionType::Bridge(ch)) { + Ok(_) => *ctx.auth_checked = false, + Err(e) => log::error!("Could not send the channel: {:?}", e), + }; + } else { + a.fail()?; + } + } + Ok(()) +} + +pub async fn first_auth( + ev: ServEvent<'_, '_>, + config: &SunsetMutex, +) -> Result<(), sunset::Error> { + if let ServEvent::FirstAuth(mut a) = ev { + debug!("ServEvent::FirstAuth"); + let config_guard = config.lock().await; + + a.enable_password_auth(false)?; + + a.enable_pubkey_auth(true)?; + if config_guard.first_login { + a.allow()?; + } else { + debug!( + "FirstAuth received but not first-login, allowing pubkey auth but rejecting \ + additions of new public keys on already provisioned device" + ); + a.reject()?; + } + } + Ok(()) +} + +pub async fn hostkeys( + ev: ServEvent<'_, '_>, + config: &SunsetMutex, +) -> Result<(), sunset::Error> { + if let ServEvent::Hostkeys(h) = ev { + debug!("ServEvent::Hostkeys"); + let config_guard = config.lock().await; + h.hostkeys(&[&config_guard.hostkey])?; + } + Ok(()) +} + +pub fn password_auth(ev: ServEvent<'_, '_>) -> Result<(), sunset::Error> { + if let ServEvent::PasswordAuth(a) = ev { + warn!("Password auth is not supported, use public key auth instead."); + a.reject()?; + } + Ok(()) +} + +pub async fn pubkey_auth( + ev: ServEvent<'_, '_>, + ctx: &mut EventContext<'_>, + config: &SunsetMutex, +) -> Result<(), sunset::Error> { + if let ServEvent::PubkeyAuth(a) = ev { + debug!("ServEvent::PubkeyAuth"); + let config_guard = config.lock().await; + let client_pubkey = a.pubkey()?; + + match client_pubkey { + sunset::packets::PubKey::Ed25519(presented) => { + let matched = config_guard + .pubkeys + .iter() + .any(|slot| slot.as_ref().is_some_and(|stored| *stored == presented)); + + if matched { + a.allow()?; + *ctx.auth_checked = true; + } else { + debug!("No matching pubkey slot found"); + a.reject()?; + } + } + _ => { + a.reject()?; + } + } + } + Ok(()) +} + +pub fn open_session( + ev: ServEvent<'_, '_>, + ctx: &mut EventContext<'_>, +) -> Result<(), sunset::Error> { + if let ServEvent::OpenSession(a) = ev { + debug!("ServEvent::OpenSession"); + match ctx.session { + Some(_) => { + todo!("Can't have two sessions"); + } + None => { + *ctx.session = Some(a.accept()?); + } + } + } + Ok(()) +} + +pub async fn session_env( + ev: ServEvent<'_, '_>, + ctx: &mut EventContext<'_>, + config: &SunsetMutex, +) -> Result<(), sunset::Error> { + if let ServEvent::SessionEnv(a) = ev { + debug!("Got ENV request"); + debug!("ENV name: {}", a.name()?); + debug!("ENV value: {}", a.value()?); + + match a.name()? { + "LANG" => { + a.succeed()?; + } + "SSH_STAMP_PUBKEY" => { + let mut config_guard = config.lock().await; + + if !config_guard.first_login { + warn!("SSH_STAMP_PUBKEY env received but not first-login; rejecting"); + a.fail()?; + } else if config_guard.add_pubkey(a.value()?).is_ok() { + debug!("Added new pubkey from ENV"); + a.succeed()?; + config_guard.first_login = false; + *ctx.config_changed = true; + *ctx.auth_checked = true; + } else { + warn!("Failed to add new pubkey from ENV"); + a.fail()?; + } + } + "SSH_STAMP_WIFI_SSID" => { + let mut config_guard = config.lock().await; + if !(*ctx.auth_checked || config_guard.first_login) { + warn!("SSH_STAMP_WIFI_SSID env received but not authenticated; rejecting"); + a.fail()?; + } else { + let mut s = String::<32>::new(); + if s.push_str(a.value()?).is_ok() { + config_guard.wifi_ssid = s; + debug!("Set wifi SSID from ENV"); + a.succeed()?; + *ctx.config_changed = true; + *ctx.needs_reset = true; + } else { + warn!("SSH_STAMP_WIFI_SSID too long"); + a.fail()?; + } + } + } + "SSH_STAMP_WIFI_PSK" => { + let mut config_guard = config.lock().await; + if !(*ctx.auth_checked || config_guard.first_login) { + warn!("SSH_STAMP_WIFI_PSK env received but not authenticated; rejecting"); + a.fail()?; + } else { + let value = a.value()?; + if value.len() < 8 { + warn!("SSH_STAMP_WIFI_PSK too short (min 8 characters)"); + a.fail()?; + } else if value.len() > 63 { + warn!("SSH_STAMP_WIFI_PSK too long (max 63 characters)"); + a.fail()?; + } else { + let mut s = String::<63>::new(); + if s.push_str(value).is_ok() { + config_guard.wifi_pw = Some(s); + debug!("Set WIFI PSK from ENV"); + a.succeed()?; + *ctx.config_changed = true; + *ctx.needs_reset = true; + } else { + warn!("SSH_STAMP_WIFI_PSK push_str failed unexpectedly"); + a.fail()?; + } + } + } + } + "SSH_STAMP_WIFI_MAC_ADDRESS" => { + let mut config_guard = config.lock().await; + if !(*ctx.auth_checked || config_guard.first_login) { + warn!( + "SSH_STAMP_WIFI_MAC_ADDRESS env received but not authenticated; rejecting" + ); + a.fail()?; + } else { + let value = a.value()?; + if value.len() != 17 { + warn!("SSH_STAMP_WIFI_MAC_ADDRESS must be XX:XX:XX:XX:XX:XX format"); + a.fail()?; + } else { + let parts: heapless::Vec = value + .split(':') + .filter_map(|p| u8::from_str_radix(p, 16).ok()) + .collect(); + if parts.len() == 6 { + let mac: [u8; 6] = + [parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]]; + config_guard.mac = mac; + debug!("Set MAC address from ENV: {:02X?}", mac); + a.succeed()?; + *ctx.config_changed = true; + *ctx.needs_reset = true; + } else { + warn!("SSH_STAMP_WIFI_MAC_ADDRESS invalid format"); + a.fail()?; + } + } + } + } + "SSH_STAMP_WIFI_MAC_RANDOM" => { + let mut config_guard = config.lock().await; + if !(*ctx.auth_checked || config_guard.first_login) { + warn!( + "SSH_STAMP_WIFI_MAC_RANDOM env received but not authenticated; rejecting" + ); + a.fail()?; + } else { + config_guard.mac = [0xFF; 6]; + debug!("Set MAC address to random mode"); + a.succeed()?; + *ctx.config_changed = true; + *ctx.needs_reset = true; + } + } + _ => { + debug!("Ignoring unknown environment variable: {}", a.name()?); + a.succeed()?; + } + } + } + Ok(()) +} + +pub async fn session_pty( + ev: ServEvent<'_, '_>, + ctx: &mut EventContext<'_>, + config: &SunsetMutex, +) -> Result<(), sunset::Error> { + if let ServEvent::SessionPty(a) = ev { + let first_login = { config.lock().await.first_login }; + + if *ctx.auth_checked || first_login { + debug!("ServEvent::SessionPty: Session granted"); + a.succeed()?; + } else { + debug!("ServEvent::SessionPty: No auth not session"); + a.fail()?; + } + } + Ok(()) +} + +pub fn session_exec(ev: ServEvent<'_, '_>) -> Result<(), sunset::Error> { + if let ServEvent::SessionExec(a) = ev { + a.fail()?; + } + Ok(()) +} + +pub fn defunct() -> Result<(), sunset::Error> { + debug!("Expected caller to handle event"); + sunset::error::BadUsage.fail() +} + +pub async fn ssh_client<'a, 'b>( + uart_buff: &'a crate::espressif::buffered_uart::BufferedUart, + ssh_server: &'b SSHServer<'a>, + chan_pipe: &'b Channel, +) -> Result<(), sunset::Error> { + debug!("Preparing bridge"); + let session_type = chan_pipe.receive().await; + debug!("Checking bridge session type"); + match session_type { + SessionType::Bridge(ch) => { + info!("Handling bridge session"); + let stdio: ChanInOut<'_> = ssh_server.stdio(ch).await?; + let (stdin, stdout) = stdio.split(); + info!("Starting bridge"); + serial_bridge(stdin, stdout, uart_buff).await? + } + #[cfg(feature = "sftp-ota")] + SessionType::Sftp(ch) => { + debug!("Handling SFTP session"); + let stdio = ssh_server.stdio(ch).await?; + let ota_writer = storage::esp_ota::OtaWriter::new(); + ota::run_ota_server::(stdio, ota_writer).await? + } + }; + Ok(()) +} + +pub async fn bridge_disable() { + debug!("Bridge disabled: WIP"); +} diff --git a/src/lib.rs b/src/lib.rs index 90026f6..fe63a92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod config; pub mod errors; pub mod espressif; +pub mod handle; pub mod serial; pub mod serve; pub mod settings; diff --git a/src/main.rs b/src/main.rs index 0a16760..6291981 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use ssh_stamp::{ buffered_uart::{BufferedUart, UART_BUF, UartPins, uart_task}, net, rng, }, - serve, + handle, serve, settings::UART_BUFFER_SIZE, }; @@ -345,7 +345,7 @@ where pub ssh_server: &'b SSHServer<'a>, pub uart_buf: &'a BufferedUart, pub config: &'a SunsetMutex, - pub chan_pipe: &'b Channel, + pub chan_pipe: &'b Channel, pub connection_loop: CL, } @@ -353,7 +353,7 @@ async fn ssh_enabled<'a>(s: SocketEnabled<'a>) -> Result<(), sunset::Error> { debug!("HSM: ssh_enabled"); // loop { debug!("HSM: Starting channel pipe"); - let chan_pipe = Channel::::new(); + let chan_pipe = Channel::::new(); debug!("HSM: Started channel pipe. Calling connection_loop from ssh_enabled"); let connection = serve::connection_loop(&s.ssh_server, &chan_pipe, s.config); debug!("HSM: Started connection loop"); @@ -397,7 +397,7 @@ where debug!("HSM: client_connected"); debug!("HSM: Setting up serial bridge"); - let bridge = serve::handle_ssh_client(s.uart_buf, s.ssh_server, s.chan_pipe); + let bridge = handle::ssh_client(s.uart_buf, s.ssh_server, s.chan_pipe); let uart_enabled_struct = ClientConnected { ssh_server: s.ssh_server, @@ -412,7 +412,7 @@ where } } - serve::bridge_disable().await; + handle::bridge_disable().await; Ok(()) } diff --git a/src/serve.rs b/src/serve.rs index c1741d7..5ba7375 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -2,48 +2,32 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -use log::{debug, info, trace, warn}; +use log::{debug, trace}; use crate::config::SSHStampConfig; -use crate::espressif::buffered_uart::UART_SIGNAL; +use crate::handle::{ + EventContext, SessionType, defunct, first_auth, hostkeys, open_session, password_auth, + pubkey_auth, session_env, session_exec, session_pty, session_shell, session_subsystem, +}; use crate::settings::UART_BUFFER_SIZE; -use crate::store; -use esp_hal::system::software_reset; -use heapless::String; -use storage::flash; +use sunset::{ChanHandle, ServEvent}; +use sunset_async::SunsetMutex; -use core::fmt::Debug; -use core::option::Option::{self, None, Some}; +use core::option::Option::None; use core::result::Result; -// Embassy use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::channel::Channel; - -// Sunset -use sunset::{ChanHandle, ServEvent, error}; -use sunset_async::SunsetMutex; use sunset_async::{ProgressHolder, SSHServer}; -#[derive(Debug)] -pub enum SessionType { - Bridge(ChanHandle), - #[cfg(feature = "sftp-ota")] - Sftp(ChanHandle), -} - pub async fn connection_loop( serv: &SSHServer<'_>, chan_pipe: &Channel, config: &SunsetMutex, ) -> Result<(), sunset::Error> { let mut session: Option = None; - - debug!("Entering connection_loop and prog_loop is next..."); let mut config_changed: bool = false; let mut needs_reset: bool = false; - - // Will be set in `ev` PubkeyAuth is accepted and cleared once the channel is sent down chan_pipe let mut auth_checked = false; loop { @@ -51,297 +35,55 @@ pub async fn connection_loop( let ev = serv.progress(&mut ph).await?; trace!("{:?}", &ev); - match ev { - ServEvent::SessionSubsystem(a) => { - debug!("ServEvent::SessionSubsystem"); - if !auth_checked { - warn!("Unauthenticated SessionSubsystem rejected"); - a.fail()?; - // TODO: Provide a message back to the client and the close the session? - } else if a.command()?.to_lowercase().as_str() == "sftp" { - if let Some(ch) = session.take() { - debug_assert!(ch.num() == a.channel()); - #[cfg(feature = "sftp-ota")] - { - a.succeed()?; - debug!("We got SFTP subsystem"); - match chan_pipe.try_send(SessionType::Sftp(ch)) { - Ok(_) => auth_checked = false, - Err(e) => log::error!("Could not send the channel: {:?}", e), - }; - } - #[cfg(not(feature = "sftp-ota"))] - { - warn!("SFTP subsystem requested but not supported in this build"); - a.fail()?; - } - } else { - a.fail()?; - } - } - } - ServEvent::SessionShell(a) => { - debug!("ServEvent::SessionShell"); + let mut ctx = EventContext { + session: &mut session, + auth_checked: &mut auth_checked, + config_changed: &mut config_changed, + needs_reset: &mut needs_reset, + }; - if !auth_checked { - warn!("Unauthenticated SessionShell rejected"); - a.fail()?; - } else if let Some(ch) = session.take() { - // Save config after connection successful (SessionEnv completed) - if config_changed { - config_changed = false; - let config_guard = config.lock().await; - let Some(flash_storage_guard) = flash::get_flash_n_buffer() else { - panic!("Could not acquire flash storage lock"); - }; - let mut flash_storage = flash_storage_guard.lock().await; - let _result = store::save(&mut flash_storage, &config_guard).await; - drop(config_guard); - if needs_reset { - info!("Configuration saved. Rebooting to apply WiFi changes..."); - software_reset(); - } - } - debug_assert!(ch.num() == a.channel()); - a.succeed()?; - debug!("We got shell"); - UART_SIGNAL.signal(1); - debug!("Connection loop: UART_SIGNAL sent"); - match chan_pipe.try_send(SessionType::Bridge(ch)) { - Ok(_) => auth_checked = false, - Err(e) => log::error!("Could not send the channel: {:?}", e), - }; - } else { - a.fail()?; - } + match ev { + ServEvent::SessionSubsystem(_) => { + session_subsystem(ev, &mut ctx, chan_pipe)?; } - ServEvent::FirstAuth(mut a) => { - debug!("ServEvent::FirstAuth"); - let config_guard = config.lock().await; - - a.enable_password_auth(false)?; - - a.enable_pubkey_auth(true)?; - if config_guard.first_login { - a.allow()?; - } else { - debug!( - "FirstAuth received but not first-login, allowing pubkey auth but rejecting - additions of new public keys on already provisioned device" - ); - a.reject()?; - } + ServEvent::SessionShell(_) => { + session_shell(ev, &mut ctx, config, chan_pipe).await?; } - ServEvent::Hostkeys(h) => { - debug!("ServEvent::Hostkeys"); - let config_guard = config.lock().await; - // Just take it from config as private hostkey is generated on first boot. - h.hostkeys(&[&config_guard.hostkey])?; + ServEvent::FirstAuth(_) => { + first_auth(ev, config).await?; } - ServEvent::PasswordAuth(a) => { - warn!("Password auth is not supported, use public key auth instead."); - a.reject()?; + ServEvent::Hostkeys(_) => { + hostkeys(ev, config).await?; } - ServEvent::PubkeyAuth(a) => { - debug!("ServEvent::PubkeyAuth"); - let config_guard = config.lock().await; - let client_pubkey = a.pubkey()?; - - match client_pubkey { - sunset::packets::PubKey::Ed25519(presented) => { - let matched = config_guard - .pubkeys - .iter() - .any(|slot| slot.as_ref().is_some_and(|stored| *stored == presented)); - - if matched { - a.allow()?; - auth_checked = true; - } else { - debug!("No matching pubkey slot found"); - a.reject()?; - } - } - _ => { - // Only Ed25519 keys supported - a.reject()?; - } - } + ServEvent::PasswordAuth(_) => { + password_auth(ev)?; } - ServEvent::OpenSession(a) => { - debug!("ServEvent::OpenSession"); - match session { - Some(_) => { - todo!("Can't have two sessions"); - } - None => { - // Track the session - session = Some(a.accept()?); - } - } + ServEvent::PubkeyAuth(_) => { + pubkey_auth(ev, &mut ctx, config).await?; } - ServEvent::SessionEnv(a) => { - debug!("Got ENV request"); - debug!("ENV name: {}", a.name()?); - debug!("ENV value: {}", a.value()?); - - match a.name()? { - "LANG" => { - // Ignore, but succeed to avoid client-side warnings - // This env variable will always be sent by OpenSSH client. - a.succeed()?; - } - "SSH_STAMP_PUBKEY" => { - let mut config_guard = config.lock().await; - - if !config_guard.first_login { - warn!("SSH_STAMP_PUBKEY env received but not first-login; rejecting"); - a.fail()?; - } else if config_guard.add_pubkey(a.value()?).is_ok() { - debug!("Added new pubkey from ENV"); - a.succeed()?; - config_guard.first_login = false; - config_changed = true; - auth_checked = true; - } else { - warn!("Failed to add new pubkey from ENV"); - a.fail()?; - } - } - "SSH_STAMP_WIFI_SSID" => { - let mut config_guard = config.lock().await; - if !(auth_checked || config_guard.first_login) { - warn!( - "SSH_STAMP_WIFI_SSID env received but not authenticated; rejecting" - ); - a.fail()?; - } else { - let mut s = String::<32>::new(); - if s.push_str(a.value()?).is_ok() { - config_guard.wifi_ssid = s; - debug!("Set wifi SSID from ENV"); - a.succeed()?; - config_changed = true; - needs_reset = true; - } else { - warn!("SSH_STAMP_WIFI_SSID too long"); - a.fail()?; - } - } - } - "SSH_STAMP_WIFI_PSK" => { - let mut config_guard = config.lock().await; - if !(auth_checked || config_guard.first_login) { - warn!( - "SSH_STAMP_WIFI_PSK env received but not authenticated; rejecting" - ); - a.fail()?; - } else { - let value = a.value()?; - if value.len() < 8 { - warn!("SSH_STAMP_WIFI_PSK too short (min 8 characters)"); - a.fail()?; - } else if value.len() > 63 { - warn!("SSH_STAMP_WIFI_PSK too long (max 63 characters)"); - a.fail()?; - } else { - let mut s = String::<63>::new(); - if s.push_str(value).is_ok() { - config_guard.wifi_pw = Some(s); - debug!("Set WIFI PSK from ENV"); - a.succeed()?; - config_changed = true; - needs_reset = true; - } else { - warn!("SSH_STAMP_WIFI_PSK push_str failed unexpectedly"); - a.fail()?; - } - } - } - } - "SSH_STAMP_WIFI_MAC_ADDRESS" => { - let mut config_guard = config.lock().await; - if !(auth_checked || config_guard.first_login) { - warn!( - "SSH_STAMP_WIFI_MAC_ADDRESS env received but not authenticated; rejecting" - ); - a.fail()?; - } else { - let value = a.value()?; - if value.len() != 17 { - warn!( - "SSH_STAMP_WIFI_MAC_ADDRESS must be XX:XX:XX:XX:XX:XX format" - ); - a.fail()?; - } else { - let parts: heapless::Vec = value - .split(':') - .filter_map(|p| u8::from_str_radix(p, 16).ok()) - .collect(); - if parts.len() == 6 { - let mac: [u8; 6] = [ - parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], - ]; - config_guard.mac = mac; - debug!("Set MAC address from ENV: {:02X?}", mac); - a.succeed()?; - config_changed = true; - needs_reset = true; - } else { - warn!("SSH_STAMP_WIFI_MAC_ADDRESS invalid format"); - a.fail()?; - } - } - } - } - "SSH_STAMP_WIFI_MAC_RANDOM" => { - let mut config_guard = config.lock().await; - if !(auth_checked || config_guard.first_login) { - warn!( - "SSH_STAMP_WIFI_MAC_RANDOM env received but not authenticated; rejecting" - ); - a.fail()?; - } else { - config_guard.mac = [0xFF; 6]; - debug!("Set MAC address to random mode"); - a.succeed()?; - config_changed = true; - needs_reset = true; - } - } - _ => { - debug!("Ignoring unknown environment variable: {}", a.name()?); - a.succeed()?; - } - } + ServEvent::OpenSession(_) => { + open_session(ev, &mut ctx)?; } - ServEvent::SessionPty(a) => { - let first_login = { config.lock().await.first_login }; - - if auth_checked || first_login { - debug!("ServEvent::SessionPty: Session granted"); - a.succeed()?; - } else { - debug!("ServEvent::SessionPty: No auth not session"); - a.fail()?; - } + ServEvent::SessionEnv(_) => { + session_env(ev, &mut ctx, config).await?; + } + ServEvent::SessionPty(_) => { + session_pty(ev, &mut ctx, config).await?; } - ServEvent::SessionExec(a) => { - a.fail()?; + ServEvent::SessionExec(_) => { + session_exec(ev)?; } ServEvent::Defunct => { - debug!("Expected caller to handle event"); - error::BadUsage.fail()? + defunct()?; } ServEvent::PollAgain => {} } } } -pub async fn connection_disable() -> () { +pub async fn connection_disable() { debug!("Connection loop disabled: WIP"); - // TODO: Correctly disable/restart Conection loop and/or send messsage to user over SSH } pub async fn ssh_wait_for_initialisation<'server>( @@ -351,48 +93,6 @@ pub async fn ssh_wait_for_initialisation<'server>( SSHServer::new(inbuf, outbuf) } -pub async fn ssh_disable() -> () { +pub async fn ssh_disable() { debug!("SSH Server disabled: WIP"); - // TODO: Correctly disable/restart SSH Server and/or send messsage to user over SSH -} - -use crate::espressif::buffered_uart::BufferedUart; -use crate::serial::serial_bridge; -use sunset_async::ChanInOut; - -pub async fn handle_ssh_client<'a, 'b>( - uart_buff: &'a BufferedUart, - ssh_server: &'b SSHServer<'a>, - chan_pipe: &'b Channel, -) -> Result<(), sunset::Error> { - debug!("Preparing bridge"); - let session_type = chan_pipe.receive().await; - debug!("Checking bridge session type"); - match session_type { - SessionType::Bridge(ch) => { - info!("Handling bridge session"); - let stdio: ChanInOut<'_> = ssh_server.stdio(ch).await?; - let (stdin, stdout) = stdio.split(); - info!("Starting bridge"); - serial_bridge(stdin, stdout, uart_buff).await? - } - #[cfg(feature = "sftp-ota")] - SessionType::Sftp(ch) => { - { - debug!("Handling SFTP session"); - let stdio = ssh_server.stdio(ch).await?; - // TODO: Use a configuration flag to select the hardware specific OtaActions implementer - let ota_writer = storage::esp_ota::OtaWriter::new(); - ota::run_ota_server::(stdio, ota_writer).await? - } - } - }; - Ok(()) -} - -pub async fn bridge_disable() -> () { - // disable bridge - debug!("Bridge disabled: WIP"); - // TODO: Correctly disable/restart bridge and/or send message to user over SSH - // software_reset(); } From 4e0c442e0c6add59cea15e55f329b718fc2e3a90 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 14:59:40 +0100 Subject: [PATCH 02/14] Cargo.lock noise --- Cargo.lock | 57 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a44f860..ed1018b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1307,6 +1307,46 @@ dependencies = [ "wasi", ] +[[package]] +name = "hal" +version = "0.2.0" +dependencies = [ + "embassy-executor", + "embassy-sync 0.7.2", + "heapless 0.8.0", +] + +[[package]] +name = "hal-espressif" +version = "0.2.0" +dependencies = [ + "edge-dhcp", + "edge-nal", + "edge-nal-embassy", + "embassy-executor", + "embassy-futures", + "embassy-net", + "embassy-sync 0.7.2", + "embassy-time 0.5.0", + "embedded-storage", + "embedded-storage-async", + "esp-bootloader-esp-idf", + "esp-hal", + "esp-radio", + "esp-storage", + "getrandom", + "hal", + "heapless 0.8.0", + "hmac", + "log", + "once_cell", + "portable-atomic", + "sha2", + "smoltcp", + "static_cell", + "sunset-async", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1574,6 +1614,7 @@ name = "ota" version = "0.1.0" dependencies = [ "clap", + "hal", "log", "rustc-hash", "sha2", @@ -2007,6 +2048,7 @@ dependencies = [ "esp-rtos", "esp-storage", "getrandom", + "hal-espressif", "heapless 0.8.0", "hex", "hmac", @@ -2021,7 +2063,6 @@ dependencies = [ "snafu", "ssh-key", "static_cell", - "storage", "subtle", "sunset", "sunset-async", @@ -2044,20 +2085,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "storage" -version = "0.1.0" -dependencies = [ - "embedded-storage", - "esp-bootloader-esp-idf", - "esp-hal", - "esp-storage", - "log", - "once_cell", - "ota", - "sunset-async", -] - [[package]] name = "strsim" version = "0.11.1" From fc83cc5d131989ddd21e07c993faad0b31deb2c8 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:03:27 +0100 Subject: [PATCH 03/14] Ota traits, packer and storage renaming --- .cargo/config.toml | 6 +- .github/workflows/build.yml | 4 +- .github/workflows/tests.yml | 2 +- ota/Cargo.toml | 5 +- ota/README.md | 4 +- ota/src/bin/README.md | 8 +- ota/src/bin/{ota-packer.rs => packer.rs} | 2 +- ota/src/handler.rs | 2 +- ota/src/lib.rs | 12 +- ota/src/otatraits.rs | 53 ----- ota/src/sftpserver.rs | 2 +- ota/src/traits.rs | 29 +++ ota/test-hil-esp32c6-e2e.sh | 2 +- src/store.rs | 2 +- storage/src/esp_ota.rs | 260 ----------------------- 15 files changed, 55 insertions(+), 338 deletions(-) rename ota/src/bin/{ota-packer.rs => packer.rs} (99%) delete mode 100644 ota/src/otatraits.rs create mode 100644 ota/src/traits.rs delete mode 100644 storage/src/esp_ota.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 9faa942..085e543 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -32,9 +32,9 @@ run-esp32s3 = "run --release --target xtensa-esp32s3-none-elf --no-default-featu # Test alias test-ota = "test --package ota --target x86_64-unknown-linux-gnu" -# ota-packer aliases -build-ota-packer = "build --package ota --bin ota-packer --target x86_64-unknown-linux-gnu" -ota-packer = "run --package ota --bin ota-packer --target x86_64-unknown-linux-gnu" +# ota packer aliases +build-packer = "build --package ota --bin packer --target x86_64-unknown-linux-gnu" +packer = "run --package ota --bin packer --target x86_64-unknown-linux-gnu" [target.xtensa-esp32-none-elf] # ESP32 runner = "espflash flash --baud=921600 --monitor --chip esp32" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8d8654..2d68d57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: run: | cargo +${{ matrix.device.toolchain }} clippy --release --features ${{ matrix.device.soc }} --target riscv32imac-unknown-none-elf -- -D warnings cargo +${{ matrix.device.toolchain }} fmt -- --check - ota-packer: + packer: name: OTA Packer runs-on: ubuntu-latest strategy: @@ -68,4 +68,4 @@ jobs: target: riscv32imac-unknown-none-elf toolchain: stable - name: Build utility - run: cargo build-ota-packer + run: cargo build-packer diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e09f26b..8ea114c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: jobs: - ota-packer: + packer: name: OTA tests runs-on: ubuntu-latest strategy: diff --git a/ota/Cargo.toml b/ota/Cargo.toml index c83ea7a..721b123 100644 --- a/ota/Cargo.toml +++ b/ota/Cargo.toml @@ -8,6 +8,7 @@ default = [] std = [] [dependencies] +hal = { path = "../hal" } sunset.workspace = true sunset-async.workspace = true sunset-sftp.workspace = true @@ -21,5 +22,5 @@ rustc-hash.workspace = true clap = "4.5" [[bin]] -name = "ota-packer" -path = "src/bin/ota-packer.rs" +name = "packer" +path = "src/bin/packer.rs" diff --git a/ota/README.md b/ota/README.md index aa0be40..0df07b4 100644 --- a/ota/README.md +++ b/ota/README.md @@ -2,7 +2,7 @@ Over The Air updates (OTA) is a convenient way to upload the firmware of your target device. In many devices out there you will find that the process is done using a side channel rather rather than the core functionality of the application. -In SSH-Stamp by definition there is a SSH server running so why not using this secure channel to perform the OTA updates? It only took us implementing a basic SFTP subsystem that uses a SSH tunnel and a helper terminal application, ota-packer, to add some metadata to the SSH-Stamp binary to pack it into an `ota` file. +In SSH-Stamp by definition there is a SSH server running so why not using this secure channel to perform the OTA updates? It only took us implementing a basic SFTP subsystem that uses a SSH tunnel and a helper terminal application, packer, to add some metadata to the SSH-Stamp binary to pack it into an `ota` file. Using SFTP is good news since any user with access to a sftp client and the keys for the target ssh-stamp device can upload a packed binary to it. @@ -28,7 +28,7 @@ espflash save-image --chip=esp32c6 target/riscv32imac-unknown-none-elf/release/s #### 2. Pack the application for ota: ``` -cargo ota-packer -- ssh-stamp.bin +cargo packer -- ssh-stamp.bin ``` diff --git a/ota/src/bin/README.md b/ota/src/bin/README.md index 21d517e..2e7bd96 100644 --- a/ota/src/bin/README.md +++ b/ota/src/bin/README.md @@ -1,6 +1,6 @@ -# Purpose of ota-packer +# Purpose of packer -The content of this file is provided for illustrative purposes. For a complete understanding of what this utility does read `ota-packer.rs`. +The content of this file is provided for illustrative purposes. For a complete understanding of what this utility does read `packer.rs`. This binary is a helper cli application to pack binary files together with a header to allow for the sftp-ota procedure to validate the binary before applying the OTA. @@ -29,7 +29,7 @@ It takes one binary file and adds the following Type Length Value fields (TLV): For updated information on how to use this tool build and run the binary from the `ssh-stamp/ota` directory ```sh -ssh-stamp/ota$ cargo run --bin ota-packer -- --help +ssh-stamp/ota$ cargo run --bin packer -- --help ``` At the moment of redaction, this command outputs: @@ -37,7 +37,7 @@ At the moment of redaction, this command outputs: ```sh SSH-Stamp utility 0.1.0 to pack (unpack) OTA update files adding the required metadata. -Usage: ota-packer [OPTIONS] +Usage: packer [OPTIONS] Arguments: The file to process diff --git a/ota/src/bin/ota-packer.rs b/ota/src/bin/packer.rs similarity index 99% rename from ota/src/bin/ota-packer.rs rename to ota/src/bin/packer.rs index 97a5552..fc06ad6 100644 --- a/ota/src/bin/ota-packer.rs +++ b/ota/src/bin/packer.rs @@ -10,7 +10,7 @@ use std::{ const OTA_PACKER_VERSION: &str = env!("CARGO_PKG_VERSION"); fn main() { - let matches = Command::new("ota-packer") + let matches = Command::new("packer") .about(format!("SSH-Stamp utility {} to pack (unpack) OTA update files adding the required metadata.", OTA_PACKER_VERSION)) .arg(clap::arg!( "The file to process").required(true)) .arg( diff --git a/ota/src/handler.rs b/ota/src/handler.rs index 89a4941..e8de2d9 100644 --- a/ota/src/handler.rs +++ b/ota/src/handler.rs @@ -4,7 +4,7 @@ use sunset::sshwire::{SSHDecode, SSHSource, WireError}; -use crate::{OtaHeader, otatraits::OtaActions, tlv}; +use crate::{OtaHeader, tlv, traits::OtaActions}; use log::{debug, error, info, warn}; use sha2::{Digest, Sha256}; diff --git a/ota/src/lib.rs b/ota/src/lib.rs index fab90f7..778e6c5 100644 --- a/ota/src/lib.rs +++ b/ota/src/lib.rs @@ -14,22 +14,22 @@ pub use sftpserver::run_ota_server; /// It will be called from the sftpserver module to handle the OTA update process #[cfg(target_os = "none")] mod handler; -/// Defining the target hardware abstraction for OTA updates -/// -/// This module defines traits for platform specific implementations -pub mod otatraits; /// Module implementing the OTA SFTP server #[cfg(target_os = "none")] mod sftpserver; +/// Defining the target hardware abstraction for OTA updates +/// +/// This module defines traits for platform specific implementations +pub mod traits; /// Module defining TLV types and constants for OTA updates /// -/// Re-exporting this module for easier access from outside the crate: ota-packer +/// Re-exporting this module for easier access from outside the crate: packer pub mod tlv; /// OTA Header structure and deserialization logic /// -/// Re-exporting Header for easier access from outside the crate: ota-packer +/// Re-exporting Header for easier access from outside the crate: packer pub use tlv::OtaHeader; #[cfg(test)] diff --git a/ota/src/otatraits.rs b/ota/src/otatraits.rs deleted file mode 100644 index 98ca0fe..0000000 --- a/ota/src/otatraits.rs +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 -// -// SPDX-License-Identifier: GPL-3.0-or-later - -// use embedded_storage::Storage; -// use esp_bootloader_esp_idf; -// use esp_hal::system; -// use storage::flash; - -#[allow(unused_imports)] -use log::{debug, error, info, warn}; - -#[derive(Debug)] -pub enum StorageError { - ReadError, - WriteError, - EraseError, - InternalError, -} - -pub type StorageResult = Result; - -/// Any target hardware vendor supporting ota for ssh-stamp requires an implementation of this trait. -/// This facilitates the integration of the OTA across platforms -pub trait OtaActions { - /// This function tries to validate the current loaded OTA partition. - /// - /// A failure in this validation might means that the OTA will be rolled back in the next reboot - fn try_validating_current_ota_partition() - -> impl core::future::Future> + Send; - - /// Obtains the space available for writing OTAs - fn get_ota_partition_size() -> impl core::future::Future> + Send; - - /// Writes data in the given offset as part of an OTA transfer process - fn write_ota_data( - &self, - offset: u32, - data: &[u8], - ) -> impl core::future::Future> + Send; - - /// Completes the ota process - /// - /// Final checks if the platworm requires it - fn finalize_ota_update( - &mut self, - ) -> impl core::future::Future> + Send; - - /// Resets the target device to apply the OTA update - /// - /// This function should not return - fn reset_device(&self) -> !; -} diff --git a/ota/src/sftpserver.rs b/ota/src/sftpserver.rs index d1f0865..4feeb35 100644 --- a/ota/src/sftpserver.rs +++ b/ota/src/sftpserver.rs @@ -4,7 +4,7 @@ use core::hash::Hasher; -use crate::{handler::UpdateProcessor, otatraits::OtaActions}; +use crate::{handler::UpdateProcessor, traits::OtaActions}; use sunset::sshwire::{BinString, WireError}; use sunset_async::ChanInOut; diff --git a/ota/src/traits.rs b/ota/src/traits.rs new file mode 100644 index 0000000..56af541 --- /dev/null +++ b/ota/src/traits.rs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! OTA traits - re-exported from hal crate +//! +//! This module exists for backward compatibility. +//! The actual trait definition is in `hal::traits::OtaActions`. + +pub use hal::{HalError, OtaActions}; + +/// Storage error type for OTA operations +#[derive(Debug)] +pub enum StorageError { + ReadError, + WriteError, + EraseError, + InternalError, +} + +/// Storage result type alias +pub type StorageResult = core::result::Result; + +/// Convert HalError to StorageError +impl From for StorageError { + fn from(_: HalError) -> Self { + StorageError::InternalError + } +} diff --git a/ota/test-hil-esp32c6-e2e.sh b/ota/test-hil-esp32c6-e2e.sh index dc6895c..00e2e3e 100755 --- a/ota/test-hil-esp32c6-e2e.sh +++ b/ota/test-hil-esp32c6-e2e.sh @@ -69,7 +69,7 @@ pack_ota(){ echo "saving app binary to app.bin" espflash save-image --chip esp32c6 $SSH_STAMP_ELF $OUTPUT_DIR/app.bin echo "saving app binary to app.ota" - cargo ota-packer -- $OUTPUT_DIR/app.bin + cargo packer -- $OUTPUT_DIR/app.bin } clean_flash(){ diff --git a/src/store.rs b/src/store.rs index 91dc790..293c48a 100644 --- a/src/store.rs +++ b/src/store.rs @@ -11,7 +11,7 @@ use crate::errors::Error as SSHStampError; use sunset::error::Error as SunsetError; use crate::config::SSHStampConfig; -use storage::flash::FlashBuffer; +use hal_espressif::flash::FlashBuffer; use sunset::sshwire::{self, OwnOrBorrow}; use sunset_sshwire_derive::*; diff --git a/storage/src/esp_ota.rs b/storage/src/esp_ota.rs deleted file mode 100644 index 6e17459..0000000 --- a/storage/src/esp_ota.rs +++ /dev/null @@ -1,260 +0,0 @@ -#[allow(unused_imports)] -use log::{debug, error, info, warn}; - -use crate::flash; -use embedded_storage::Storage; -use esp_bootloader_esp_idf; -use esp_hal::system; -use ota::otatraits::{OtaActions, StorageError, StorageResult}; - -/// This structure is meant to wrap the media access for writing the OTA firmware -/// It will abstract the flash memory or other storage media so later we can implement it for different platforms -#[derive(Debug, Copy, Clone)] -pub struct OtaWriter {} - -impl OtaWriter { - /// Creates a new OtaWriter for the given target OTA slot. - /// - /// To obtain a target OTA slot use [get_next_app_slot] - pub fn new() -> Self { - OtaWriter {} - } -} - -impl Default for OtaWriter { - fn default() -> Self { - OtaWriter::new() - } -} - -impl OtaActions for OtaWriter { - // TODO: build bootloader with auto-rollback to avoid invalid images rendering the device unbootable - /// Validate the current OTA partition - /// - /// Mark the current OTA slot as VALID - this is only needed if the bootloader was built with auto-rollback support. - /// The default pre-compiled bootloader in espflash is NOT. - /// - async fn try_validating_current_ota_partition() -> StorageResult<()> { - // Taken from [esp-hal ota_update example](https://github.com/esp-rs/esp-hal/examples/src/bin/ota_update.rs) - let Some(fb) = flash::get_flash_n_buffer() else { - error!("Flash storage not initialized"); - return Err(StorageError::InternalError); - }; - let mut fb = fb.lock().await; - - let (storage, _buffer) = fb.split_ref_mut(); - - // OtaUpdater is very particular. It needs a mut ref of a buffer of the exact size of the partition table. - // This is why we create it here and did not reuse the buffer from fb. - let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; - - let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) - .map_err(|e| { - error!("Could not create OtaUpdater: {:?}", e); - StorageError::InternalError - })?; - let current = ota.selected_partition().map_err(|e| { - error!("Could not get selected partition: {:?}", e); - StorageError::InternalError - })?; - - debug!( - "current image state {:?} (only relevant if the bootloader was built with auto-rollback support)", - ota.current_ota_state() - ); - debug!("currently selected partition {:?}", current); - - if let Ok(state) = ota.current_ota_state() - && (state == esp_bootloader_esp_idf::ota::OtaImageState::New - || state == esp_bootloader_esp_idf::ota::OtaImageState::PendingVerify) - { - ota.set_current_ota_state(esp_bootloader_esp_idf::ota::OtaImageState::Valid) - .map_err(|e| { - error!("Could not set OTA image state to Valid: {:?}", e); - StorageError::WriteError - })?; - debug!("Changed state to VALID"); - } - - Ok(()) - } - - /// Gets the size of the target OTA partition. - async fn get_ota_partition_size() -> StorageResult { - let partition_size = - u32::try_from(next_ota_size().await?).map_err(|_| StorageError::InternalError)?; - Ok(partition_size) - } - - /// Writes data to the target OTA partition at the given offset. - async fn write_ota_data(&self, offset: u32, data: &[u8]) -> StorageResult<()> { - write_to_target(offset, data).await - } - - /// Finalizes the OTA update by setting the target slot as current. - async fn finalize_ota_update(&mut self) -> StorageResult<()> { - activate_next_ota_slot().await?; - Ok(()) - } - - fn reset_device(&self) -> ! { - system::software_reset() - } -} - -/// Gets the size of the next OTA partition. -async fn next_ota_size() -> StorageResult { - let Some(fb) = flash::get_flash_n_buffer() else { - error!("Flash storage not initialized"); - return Err(StorageError::InternalError); - }; - let mut fb = fb.lock().await; - - let (storage, _buffer) = fb.split_ref_mut(); - - // OtaUpdater is very particular. It needs a mut ref of a buffer of the exact size of the partition table. - // This is why we create it here and did not reuse the buffer from fb. - let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; - - let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) - .map_err(|e| { - error!("Could not create OtaUpdater: {:?}", e); - StorageError::InternalError - })?; - let (target_partition, _) = ota.next_partition().map_err(|e| { - error!("Could not get next partition: {:?}", e); - StorageError::InternalError - })?; - - Ok(target_partition.partition_size()) -} - -/// Finds the next app slot to write the OTA update to. -/// -/// We use an slot since it does not require lifetimes and is easier to handle. -// Tested with espflash md5 and espflash write-bin. Writing with SFTP seems to work fine. -async fn write_to_target(offset: u32, data: &[u8]) -> StorageResult<()> { - let Some(fb) = flash::get_flash_n_buffer() else { - error!("Flash storage not initialized"); - return Err(StorageError::InternalError); - }; - let mut fb = fb.lock().await; - - let (storage, _buffer) = fb.split_ref_mut(); - - // OtaUpdater is very particular. It needs a mut ref of a buffer of the exact size of the partition table. - // This is why we create it here and did not reuse the buffer from fb. - let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; - - let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) - .map_err(|e| { - error!("Could not create OtaUpdater: {:?}", e); - StorageError::InternalError - })?; - let (mut target_partition, part_type) = ota.next_partition().map_err(|e| { - error!("Could not get next partition: {:?}", e); - StorageError::InternalError - })?; - - debug!("Flashing image to {:?}", part_type); - - debug!( - "Writing data to target_partition at offset {}, with len {}", - offset, - data.len() - ); - target_partition.write(offset, data).map_err(|e| { - error!("Failed to write data to target_partition: {}", e); - StorageError::WriteError - })?; - - Ok(()) -} - -/// The provided target slot will be marked as current and the image will be set as New so after -/// the reboot it will boot from it and be validated if the bootloader requires it. -/// -/// We use a slot since it does not require lifetimes and is easier to handle. -async fn activate_next_ota_slot() -> StorageResult<()> { - let Some(fb) = flash::get_flash_n_buffer() else { - error!("Flash storage not initialized"); - return Err(StorageError::InternalError); - }; - let mut fb = fb.lock().await; - - let (storage, _buffer) = fb.split_ref_mut(); - - // OtaUpdater is very particular. It needs a mut ref of a buffer of the exact size of the partition table. - // This is why we create it here and did not reuse the buffer from fb. - let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; - - let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) - .map_err(|e| { - error!("Could not create OtaUpdater: {:?}", e); - StorageError::InternalError - })?; - - ota.activate_next_partition().map_err(|e| { - error!("Could not activate next partition: {:?}", e); - StorageError::WriteError - })?; - ota.set_current_ota_state(esp_bootloader_esp_idf::ota::OtaImageState::New) - .map_err(|e| { - error!("Could not set OTA image state to New: {:?}", e); - StorageError::WriteError - })?; - - Ok(()) -} - -// TODO: build bootloader with auto-rollback to avoid invalid images rendering the device unbootable - -/// Validate the current OTA partition -/// -/// Mark the current OTA slot as VALID - this is only needed if the bootloader was built with auto-rollback support. -/// The default pre-compiled bootloader in espflash is NOT. -/// -pub async fn try_validating_current_ota_partition() -> StorageResult<()> { - // Taken from [esp-hal ota_update example](https://github.com/esp-rs/esp-hal/examples/src/bin/ota_update.rs) - let Some(fb) = flash::get_flash_n_buffer() else { - error!("Flash storage not initialized"); - return Err(StorageError::InternalError); - }; - let mut fb = fb.lock().await; - - let (storage, _buffer) = fb.split_ref_mut(); - - // OtaUpdater is very particular. It needs a mut ref of a buffer of the exact size of the partition table. - // This is why we create it here and did not reuse the buffer from fb. - let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; - - let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) - .map_err(|e| { - error!("Could not create OtaUpdater: {:?}", e); - StorageError::InternalError - })?; - let current = ota.selected_partition().map_err(|e| { - error!("Could not get selected partition: {:?}", e); - StorageError::InternalError - })?; - - debug!( - "current image state {:?} (only relevant if the bootloader was built with auto-rollback support)", - ota.current_ota_state() - ); - debug!("currently selected partition {:?}", current); - - if let Ok(state) = ota.current_ota_state() - && (state == esp_bootloader_esp_idf::ota::OtaImageState::New - || state == esp_bootloader_esp_idf::ota::OtaImageState::PendingVerify) - { - ota.set_current_ota_state(esp_bootloader_esp_idf::ota::OtaImageState::Valid) - .map_err(|e| { - error!("Could not set OTA image state to Valid: {:?}", e); - StorageError::WriteError - })?; - debug!("Changed state to VALID"); - } - - Ok(()) -} From ea1a5a3ee2934aa076b8558fe4a26afa4d2125e7 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:09:06 +0100 Subject: [PATCH 04/14] HAL generic traits, those are the most generic traits for MCUs in general... draft, incomplete, but should work for our usecase for now. --- hal/Cargo.toml | 17 ++++++++ hal/src/config.rs | 60 ++++++++++++++++++++++++++ hal/src/error.rs | 69 ++++++++++++++++++++++++++++++ hal/src/lib.rs | 44 +++++++++++++++++++ hal/src/traits/executor.rs | 25 +++++++++++ hal/src/traits/flash.rs | 50 ++++++++++++++++++++++ hal/src/traits/hash.rs | 25 +++++++++++ hal/src/traits/mod.rs | 19 ++++++++ hal/src/traits/network/ethernet.rs | 13 ++++++ hal/src/traits/network/mod.rs | 9 ++++ hal/src/traits/network/wifi.rs | 16 +++++++ hal/src/traits/rng.rs | 22 ++++++++++ hal/src/traits/timer.rs | 19 ++++++++ hal/src/traits/uart.rs | 27 ++++++++++++ 14 files changed, 415 insertions(+) create mode 100644 hal/Cargo.toml create mode 100644 hal/src/config.rs create mode 100644 hal/src/error.rs create mode 100644 hal/src/lib.rs create mode 100644 hal/src/traits/executor.rs create mode 100644 hal/src/traits/flash.rs create mode 100644 hal/src/traits/hash.rs create mode 100644 hal/src/traits/mod.rs create mode 100644 hal/src/traits/network/ethernet.rs create mode 100644 hal/src/traits/network/mod.rs create mode 100644 hal/src/traits/network/wifi.rs create mode 100644 hal/src/traits/rng.rs create mode 100644 hal/src/traits/timer.rs create mode 100644 hal/src/traits/uart.rs diff --git a/hal/Cargo.toml b/hal/Cargo.toml new file mode 100644 index 0000000..f92f58e --- /dev/null +++ b/hal/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "hal" +version = "0.2.0" +edition = "2021" +authors = ["Roman Valls Guimera "] +description = "Hardware Abstraction Layer traits for ssh-stamp" +license = "GPL-3.0-or-later" +repository = "https://github.com/brainstorm/ssh-stamp" + +[dependencies] +embassy-sync = { workspace = true } +embassy-executor = { workspace = true } +heapless = { workspace = true } + +[features] +default = [] +sftp-ota = [] \ No newline at end of file diff --git a/hal/src/config.rs b/hal/src/config.rs new file mode 100644 index 0000000..33f8ba3 --- /dev/null +++ b/hal/src/config.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#[derive(Clone, Debug)] +pub struct HardwareConfig { + pub uart: UartConfig, + pub wifi: WifiApConfigStatic, +} + +#[derive(Clone, Debug)] +pub struct UartConfig { + pub tx_pin: u8, + pub rx_pin: u8, + pub cts_pin: Option, + pub rts_pin: Option, + pub baud_rate: u32, +} + +impl Default for UartConfig { + fn default() -> Self { + Self { + tx_pin: 0, + rx_pin: 0, + cts_pin: None, + rts_pin: None, + baud_rate: 115_200, + } + } +} + +#[derive(Clone, Debug)] +pub struct WifiApConfigStatic { + pub ssid: heapless::String<32>, + pub password: Option>, + pub channel: u8, + pub mac: [u8; 6], +} + +impl Default for WifiApConfigStatic { + fn default() -> Self { + Self { + ssid: heapless::String::new(), + password: None, + channel: 1, + mac: [0; 6], + } + } +} + +#[derive(Clone, Debug)] +pub struct EthernetConfig { + pub mac: [u8; 6], +} + +impl Default for EthernetConfig { + fn default() -> Self { + Self { mac: [0; 6] } + } +} diff --git a/hal/src/error.rs b/hal/src/error.rs new file mode 100644 index 0000000..1772a77 --- /dev/null +++ b/hal/src/error.rs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::fmt; + +#[derive(Debug)] +pub enum HalError { + Config, + Uart(UartError), + Wifi(WifiError), + Flash(FlashError), + Rng, + Hash(HashError), + Timer, + Executor, +} + +#[derive(Debug)] +pub enum UartError { + Config, + BufferOverflow, + Read, + Write, +} + +#[derive(Debug)] +pub enum WifiError { + Initialization, + SocketCreate, + SocketAccept, + SocketRead, + SocketWrite, + SocketClose, + Dhcpc, +} + +#[derive(Debug)] +pub enum FlashError { + Read, + Write, + Erase, + PartitionNotFound, + ValidationFailed, + ConfigLoad, + ConfigSave, + InternalError, +} + +#[derive(Debug)] +pub enum HashError { + Config, + Compute, +} + +impl fmt::Display for HalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HalError::Config => write!(f, "configuration error"), + HalError::Uart(e) => write!(f, "UART error: {:?}", e), + HalError::Wifi(e) => write!(f, "WiFi error: {:?}", e), + HalError::Flash(e) => write!(f, "Flash error: {:?}", e), + HalError::Rng => write!(f, "RNG error"), + HalError::Hash(e) => write!(f, "Hash error: {:?}", e), + HalError::Timer => write!(f, "Timer error"), + HalError::Executor => write!(f, "Executor error"), + } + } +} diff --git a/hal/src/lib.rs b/hal/src/lib.rs new file mode 100644 index 0000000..f49bd8d --- /dev/null +++ b/hal/src/lib.rs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#![no_std] + +pub mod config; +pub mod error; +pub mod traits; + +pub use config::*; +pub use error::{FlashError, HalError, HashError, UartError, WifiError}; +pub use traits::*; + +use core::future::Future; +use embassy_executor::Spawner; + +/// Platform abstraction bundling all HAL peripherals +/// +/// Each platform implementation (ESP32, Nordic nRF, etc.) implements this trait +/// to provide access to all hardware peripherals needed by the firmware. +pub trait HalPlatform { + type Uart: UartHal; + type Wifi: WifiHal; + type Rng: RngHal; + type Flash: FlashHal; + type Hash: HashHal; + type Timer: TimerHal; + type Executor: ExecutorHal; + + /// Initialize all peripherals with given configuration + fn init( + config: HardwareConfig, + spawner: Spawner, + ) -> impl Future> + where + Self: Sized; + + /// Perform hardware reset + fn reset() -> !; + + /// Get MAC address from hardware (eFuse or similar) + fn mac_address() -> [u8; 6]; +} diff --git a/hal/src/traits/executor.rs b/hal/src/traits/executor.rs new file mode 100644 index 0000000..e3de2c6 --- /dev/null +++ b/hal/src/traits/executor.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +use embassy_executor::Spawner; + +/// Async runtime/executor operations +/// +/// Provides access to the async runtime and interrupt management. +/// Implementations wrap platform-specific executors (e.g., esp-rtos for ESP32). +pub trait ExecutorHal { + /// Get the spawner for spawning async tasks + fn spawner(&self) -> &Spawner; + + /// Run the executor with the main future (blocking) + fn run>(&self, main_future: F) -> !; + + /// Set interrupt priority + fn set_interrupt_priority(&self, irq: usize, priority: u8); + + /// Get current core ID (for multi-core systems) + fn core_id(&self) -> u8; +} diff --git a/hal/src/traits/flash.rs b/hal/src/traits/flash.rs new file mode 100644 index 0000000..94ca7cd --- /dev/null +++ b/hal/src/traits/flash.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +use crate::HalError; + +/// Flash storage operations +pub trait FlashHal { + /// Read from flash at offset + fn read(&self, offset: u32, buf: &mut [u8]) -> impl Future>; + + /// Write to flash at offset (must be erased first) + fn write(&self, offset: u32, buf: &[u8]) -> impl Future>; + + /// Erase flash region + fn erase(&self, offset: u32, len: u32) -> impl Future>; + + /// Get flash storage size in bytes + fn size(&self) -> u32; +} + +/// OTA update operations +/// +/// Implementations handle platform-specific partition management +/// for Over-The-Air firmware updates. +pub trait OtaActions { + /// Validate the current OTA partition + /// + /// Mark the current OTA slot as VALID - this is only needed if the bootloader + /// was built with auto-rollback support. + fn try_validating_current_ota_partition() -> impl Future> + Send; + + /// Get size of OTA partition in bytes + fn get_ota_partition_size() -> impl Future> + Send; + + /// Write data to OTA partition at offset + fn write_ota_data( + &self, + offset: u32, + data: &[u8], + ) -> impl Future> + Send; + + /// Finalize OTA update and mark for boot + fn finalize_ota_update(&mut self) -> impl Future> + Send; + + /// Reset device to boot into new partition + fn reset_device(&self) -> !; +} diff --git a/hal/src/traits/hash.rs b/hal/src/traits/hash.rs new file mode 100644 index 0000000..89dede2 --- /dev/null +++ b/hal/src/traits/hash.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +use crate::HalError; + +/// Hash/HMAC operations +pub trait HashHal { + /// Compute HMAC-SHA256 + fn hmac_sha256( + &mut self, + key: &[u8], + message: &[u8], + output: &mut [u8; 32], + ) -> impl Future>; + + /// Compute SHA256 + fn sha256( + &mut self, + message: &[u8], + output: &mut [u8; 32], + ) -> impl Future>; +} diff --git a/hal/src/traits/mod.rs b/hal/src/traits/mod.rs new file mode 100644 index 0000000..1f0ac30 --- /dev/null +++ b/hal/src/traits/mod.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +mod executor; +mod flash; +mod hash; +mod network; +mod rng; +mod timer; +mod uart; + +pub use executor::ExecutorHal; +pub use flash::{FlashHal, OtaActions}; +pub use hash::HashHal; +pub use network::{EthernetHal, WifiHal}; +pub use rng::RngHal; +pub use timer::TimerHal; +pub use uart::UartHal; diff --git a/hal/src/traits/network/ethernet.rs b/hal/src/traits/network/ethernet.rs new file mode 100644 index 0000000..3e21068 --- /dev/null +++ b/hal/src/traits/network/ethernet.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +use crate::{EthernetConfig, HalError}; + +/// Ethernet hardware abstraction +pub trait EthernetHal { + /// Initialize Ethernet with given configuration + fn init(&mut self, config: EthernetConfig) -> impl Future>; +} diff --git a/hal/src/traits/network/mod.rs b/hal/src/traits/network/mod.rs new file mode 100644 index 0000000..2879403 --- /dev/null +++ b/hal/src/traits/network/mod.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +mod ethernet; +mod wifi; + +pub use ethernet::EthernetHal; +pub use wifi::WifiHal; diff --git a/hal/src/traits/network/wifi.rs b/hal/src/traits/network/wifi.rs new file mode 100644 index 0000000..a329d1c --- /dev/null +++ b/hal/src/traits/network/wifi.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +use crate::{HalError, WifiApConfigStatic}; + +/// WiFi hardware abstraction for access point mode +pub trait WifiHal { + /// Start WiFi access point with given configuration + fn start_ap( + &mut self, + config: WifiApConfigStatic, + ) -> impl Future>; +} diff --git a/hal/src/traits/rng.rs b/hal/src/traits/rng.rs new file mode 100644 index 0000000..d3b06ab --- /dev/null +++ b/hal/src/traits/rng.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +use crate::HalError; + +/// Random number generation +pub trait RngHal { + /// Fill buffer with random bytes + fn fill_bytes(&mut self, buf: &mut [u8]) -> impl Future>; + + /// Generate a random u32 + fn random_u32(&mut self) -> impl Future> { + async { + let mut buf = [0u8; 4]; + self.fill_bytes(&mut buf).await?; + Ok(u32::from_le_bytes(buf)) + } + } +} diff --git a/hal/src/traits/timer.rs b/hal/src/traits/timer.rs new file mode 100644 index 0000000..baa645f --- /dev/null +++ b/hal/src/traits/timer.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +/// Timer operations +pub trait TimerHal { + /// Get current time in microseconds since boot + fn now_micros(&self) -> u64; + + /// Get current time in milliseconds since boot + fn now_millis(&self) -> u64 { + self.now_micros() / 1000 + } + + /// Wait for specified duration in milliseconds + fn delay(&self, millis: u64) -> impl Future; +} diff --git a/hal/src/traits/uart.rs b/hal/src/traits/uart.rs new file mode 100644 index 0000000..741efd1 --- /dev/null +++ b/hal/src/traits/uart.rs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use core::future::Future; + +use crate::{HalError, UartConfig}; + +/// UART hardware abstraction +pub trait UartHal { + /// Read bytes into buffer, returns number of bytes read + fn read(&mut self, buf: &mut [u8]) -> impl Future>; + + /// Write bytes from buffer, returns number of bytes written + fn write(&mut self, buf: &[u8]) -> impl Future>; + + /// Check if data is available to read + fn can_read(&self) -> bool; + + /// Signal for async notification when data is available + fn signal( + &self, + ) -> &embassy_sync::signal::Signal; + + /// Reconfigure UART with new settings + fn reconfigure(&mut self, config: UartConfig) -> impl Future>; +} From f655a7ab988bd36ad7acf9a4fef5f1110ffe6ba2 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:09:38 +0100 Subject: [PATCH 05/14] Cleanup unused bits of code... --- src/espressif/hash.rs | 28 ---------------- src/espressif/rng.rs | 34 ------------------- storage/Cargo.toml | 30 ----------------- storage/src/lib.rs | 78 ------------------------------------------- 4 files changed, 170 deletions(-) delete mode 100644 src/espressif/hash.rs delete mode 100644 src/espressif/rng.rs delete mode 100644 storage/Cargo.toml delete mode 100644 storage/src/lib.rs diff --git a/src/espressif/hash.rs b/src/espressif/hash.rs deleted file mode 100644 index 8672bdc..0000000 --- a/src/espressif/hash.rs +++ /dev/null @@ -1,28 +0,0 @@ -use esp_hal::hmac::Hmac; -use hmac::Hmac; -use sha2::Sha256; - -pub trait EspressifHmac { - fn new_from_slice(key: &[u8]) -> Result - where - Self: Sized; - fn update(&mut self, data: &[u8]); - fn finalize(self) -> [u8; 32]; -} - -impl EspressifHmac for Hmac { - fn new_from_slice(key: &[u8]) -> Result { - Hmac::new_from_slice(key).map_err(|_| ()) - } - - fn update(&mut self, data: &[u8]) { - self.update(data); - } - - fn finalize(self) -> [u8; 32] { - let result = self.finalize(); - let mut arr = [0u8; 32]; - arr.copy_from_slice(&result.into_bytes()); - arr - } -} \ No newline at end of file diff --git a/src/espressif/rng.rs b/src/espressif/rng.rs deleted file mode 100644 index 00b8ce0..0000000 --- a/src/espressif/rng.rs +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 -// -// SPDX-License-Identifier: GPL-3.0-or-later - -use core::cell::RefCell; - -use embassy_sync::blocking_mutex::Mutex; -use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use esp_hal::rng::Rng; -use getrandom::register_custom_getrandom; -use static_cell::StaticCell; - -static RNG: StaticCell = StaticCell::new(); -static RNG_MUTEX: Mutex>> = - Mutex::new(RefCell::new(None)); - -pub fn register_custom_rng(rng: Rng) { - let rng = RNG.init(rng); - RNG_MUTEX.lock(|t| t.borrow_mut().replace(rng)); - register_custom_getrandom!(esp_getrandom_custom_func); -} - -// esp-hal specific variation of getrandom custom function as seen in: -// https://github.com/rust-random/getrandom/issues/340 -pub fn esp_getrandom_custom_func(buf: &mut [u8]) -> Result<(), getrandom::Error> { - RNG_MUTEX.lock(|t| { - let mut rng = t.borrow_mut(); - let rng = rng - .as_mut() - .expect("register_custom_rng should have set this"); - rng.read(buf); - }); - Ok(()) -} diff --git a/storage/Cargo.toml b/storage/Cargo.toml deleted file mode 100644 index 9fe417b..0000000 --- a/storage/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "storage" -version = "0.1.0" -edition = "2024" - -[dependencies] - -ota = { path = "../ota" } - -log.workspace = true -once_cell.workspace = true -sunset-async.workspace = true - -# Avoids issues with ota-packer building for x86_64-unknown-linux-gnu target -[target.'cfg(target_os = "none")'.dependencies] -esp-bootloader-esp-idf.workspace = true -embedded-storage.workspace = true -esp-storage.workspace = true -esp-hal.workspace = true - -[features] - -default = ["esp32c6"] - -esp32 = [] -esp32s2 = [] -esp32s3 = [] -esp32c2 = [] -esp32c3 = [] -esp32c6 = [] diff --git a/storage/src/lib.rs b/storage/src/lib.rs deleted file mode 100644 index c458d48..0000000 --- a/storage/src/lib.rs +++ /dev/null @@ -1,78 +0,0 @@ -#![no_std] -// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 -// -// SPDX-License-Identifier: GPL-3.0-or-later - -/// Module defining the ESP32-specific storage traits implementations for OTA updates -#[cfg(any( - feature = "esp32", - feature = "esp32s2", - feature = "esp32s3", - feature = "esp32c3", - feature = "esp32c6" -))] -pub mod esp_ota; - -// TODO: When the time comes, generalise the flash so it can be used with all supported targets -/// [[flash]] is a packet to provide safe access to the Flash storage used by SSH-Stamp -/// -/// It does so by storing the FlashStorage and a buffer for read/write operations in a single structure -/// protected by a SunsetMutex for safe concurrent access in async contexts. -pub mod flash { - use esp_hal::peripherals::FLASH; - use esp_storage::FlashStorage; - const FLASH_BUF_SIZE: usize = esp_storage::FlashStorage::SECTOR_SIZE as usize; - static FLASH_STORAGE: OnceCell> = OnceCell::new(); - - #[allow(unused_imports)] - use log::{debug, error, info, warn}; - use once_cell::sync::OnceCell; - use sunset_async::SunsetMutex; - - /// A structure that holds both the FlashStorage and a buffer for read/write operations - /// - /// The buffer is stored here to avoid allocating multiple buffers in different parts of the code. - /// It has a fixed size defined by FLASH_BUF_SIZE. - #[derive(Debug)] - pub struct FlashBuffer<'d> { - pub flash: FlashStorage<'d>, - pub buf: [u8; FLASH_BUF_SIZE], - } - - impl<'d> FlashBuffer<'d> { - pub fn new(flash: FlashStorage<'static>) -> Self { - Self { - flash, - buf: [0u8; FLASH_BUF_SIZE], - } - } - - /// For cases where it is necessary to use both flash and buffer mutably at the same time - pub fn split_ref_mut(&mut self) -> (&mut FlashStorage<'d>, &mut [u8]) { - (&mut self.flash, &mut self.buf) - } - } - - /// Shall be called at startup to avoid lazy initialization during runtime - /// - /// Calls to [`with_flash`] or [`get_flash`] will initialize the flash storage if not already done. - /// - /// Multiple calls to init() are safe and will have no effect after the first one. - pub fn init(flash: FLASH<'static>) { - let fl = FlashBuffer::new(FlashStorage::new(flash)); - - let Ok(()) = FLASH_STORAGE.set(SunsetMutex::new(fl)) else { - warn!("Flash storage already initialized"); - return; - }; - } - - /// Static accessor for the flash storage mutex. Warning: It will fail if not initialized. - /// - /// call [`init()`] at startup. - /// - /// It is expected that the user will drop the lock on the mutex after use... - pub fn get_flash_n_buffer() -> Option<&'static SunsetMutex>> { - FLASH_STORAGE.get() - } -} From 47ffefaf7d608726635fde14ab54c8b0a6b13c5d Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:11:42 +0100 Subject: [PATCH 06/14] Espressif-specific HAL bits... --- hal-espressif/Cargo.toml | 44 +++++ hal-espressif/src/config.rs | 126 ++++++++++++++ hal-espressif/src/executor.rs | 48 ++++++ hal-espressif/src/flash.rs | 228 +++++++++++++++++++++++++ hal-espressif/src/hash.rs | 40 +++++ hal-espressif/src/lib.rs | 124 ++++++++++++++ hal-espressif/src/network/mod.rs | 10 ++ hal-espressif/src/network/wifi.rs | 265 ++++++++++++++++++++++++++++++ hal-espressif/src/rng.rs | 70 ++++++++ hal-espressif/src/timer.rs | 23 +++ hal-espressif/src/uart.rs | 209 +++++++++++++++++++++++ 11 files changed, 1187 insertions(+) create mode 100644 hal-espressif/Cargo.toml create mode 100644 hal-espressif/src/config.rs create mode 100644 hal-espressif/src/executor.rs create mode 100644 hal-espressif/src/flash.rs create mode 100644 hal-espressif/src/hash.rs create mode 100644 hal-espressif/src/lib.rs create mode 100644 hal-espressif/src/network/mod.rs create mode 100644 hal-espressif/src/network/wifi.rs create mode 100644 hal-espressif/src/rng.rs create mode 100644 hal-espressif/src/timer.rs create mode 100644 hal-espressif/src/uart.rs diff --git a/hal-espressif/Cargo.toml b/hal-espressif/Cargo.toml new file mode 100644 index 0000000..33c3b05 --- /dev/null +++ b/hal-espressif/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "hal-espressif" +version = "0.2.0" +edition = "2021" +authors = ["Roman Valls Guimera "] +description = "ESP32 implementation of the hal traits for ssh-stamp" +license = "GPL-3.0-or-later" +repository = "https://github.com/brainstorm/ssh-stamp" + +[dependencies] +hal = { path = "../hal" } +embassy-sync = { workspace = true } +embassy-executor = { workspace = true } +embassy-time = { workspace = true } +embassy-futures = { workspace = true } +embassy-net = { workspace = true } +heapless = { workspace = true } +esp-hal = { workspace = true } +esp-storage = { version = "0.8.0" } +esp-bootloader-esp-idf = { version = "0.4.0" } +esp-radio = { version = "0.17.0", features = ["wifi", "log-04"] } +embedded-storage = { workspace = true } +embedded-storage-async = { workspace = true } +once_cell = { workspace = true } +sunset-async = { workspace = true } +sha2 = { workspace = true } +hmac = { workspace = true } +getrandom = { version = "0.2.10", features = ["custom"] } +log = { workspace = true } +static_cell = { workspace = true } +portable-atomic = { version = "1" } +edge-dhcp = { workspace = true } +edge-nal = { workspace = true } +edge-nal-embassy = { workspace = true } +smoltcp = { workspace = true } + +[features] +default = ["esp32c6"] +esp32 = ["esp-hal/esp32", "esp-storage/esp32", "esp-bootloader-esp-idf/esp32", "esp-radio/esp32"] +esp32c2 = ["esp-hal/esp32c2", "esp-storage/esp32c2", "esp-bootloader-esp-idf/esp32c2", "esp-radio/esp32c2"] +esp32c3 = ["esp-hal/esp32c3", "esp-storage/esp32c3", "esp-bootloader-esp-idf/esp32c3", "esp-radio/esp32c3"] +esp32c6 = ["esp-hal/esp32c6", "esp-storage/esp32c6", "esp-bootloader-esp-idf/esp32c6", "esp-radio/esp32c6"] +esp32s2 = ["esp-hal/esp32s2", "esp-storage/esp32s2", "esp-bootloader-esp-idf/esp32s2", "esp-radio/esp32s2"] +esp32s3 = ["esp-hal/esp32s3", "esp-storage/esp32s3", "esp-bootloader-esp-idf/esp32s3", "esp-radio/esp32s3"] \ No newline at end of file diff --git a/hal-espressif/src/config.rs b/hal-espressif/src/config.rs new file mode 100644 index 0000000..a2206d8 --- /dev/null +++ b/hal-espressif/src/config.rs @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use hal::{HardwareConfig, UartConfig, WifiApConfigStatic}; +use heapless::String; + +/// Default peripheral configuration for ESP32-C6 +#[cfg(feature = "esp32c6")] +pub fn default_config() -> HardwareConfig { + HardwareConfig { + uart: UartConfig { + tx_pin: 16, + rx_pin: 17, + cts_pin: Some(15), + rts_pin: Some(18), + baud_rate: 115_200, + }, + wifi: WifiApConfigStatic { + ssid: String::try_from("ssh-stamp").unwrap_or_default(), + password: None, + channel: 1, + mac: [0; 6], // Will be set from eFuse + }, + } +} + +/// Default peripheral configuration for ESP32-S3 +#[cfg(feature = "esp32s3")] +pub fn default_config() -> HardwareConfig { + HardwareConfig { + uart: UartConfig { + tx_pin: 43, + rx_pin: 44, + cts_pin: Some(45), + rts_pin: Some(46), + baud_rate: 115_200, + }, + wifi: WifiApConfigStatic { + ssid: String::try_from("ssh-stamp").unwrap_or_default(), + password: None, + channel: 1, + mac: [0; 6], + }, + } +} + +/// Default peripheral configuration for ESP32 +#[cfg(feature = "esp32")] +pub fn default_config() -> HardwareConfig { + HardwareConfig { + uart: UartConfig { + tx_pin: 4, + rx_pin: 5, + cts_pin: Some(6), + rts_pin: Some(7), + baud_rate: 115_200, + }, + wifi: WifiApConfigStatic { + ssid: String::try_from("ssh-stamp").unwrap_or_default(), + password: None, + channel: 1, + mac: [0; 6], + }, + } +} + +/// Default peripheral configuration for ESP32-S2 +#[cfg(feature = "esp32s2")] +pub fn default_config() -> HardwareConfig { + HardwareConfig { + uart: UartConfig { + tx_pin: 43, + rx_pin: 44, + cts_pin: None, + rts_pin: None, + baud_rate: 115_200, + }, + wifi: WifiApConfigStatic { + ssid: String::try_from("ssh-stamp").unwrap_or_default(), + password: None, + channel: 1, + mac: [0; 6], + }, + } +} + +/// Default peripheral configuration for ESP32-C3 +#[cfg(feature = "esp32c3")] +pub fn default_config() -> HardwareConfig { + HardwareConfig { + uart: UartConfig { + tx_pin: 2, + rx_pin: 3, + cts_pin: None, + rts_pin: None, + baud_rate: 115_200, + }, + wifi: WifiApConfigStatic { + ssid: String::try_from("ssh-stamp").unwrap_or_default(), + password: None, + channel: 1, + mac: [0; 6], + }, + } +} + +/// Default peripheral configuration for ESP32-C2 +#[cfg(feature = "esp32c2")] +pub fn default_config() -> HardwareConfig { + HardwareConfig { + uart: UartConfig { + tx_pin: 20, + rx_pin: 21, + cts_pin: None, + rts_pin: None, + baud_rate: 115_200, + }, + wifi: WifiApConfigStatic { + ssid: String::try_from("ssh-stamp").unwrap_or_default(), + password: None, + channel: 1, + mac: [0; 6], + }, + } +} diff --git a/hal-espressif/src/executor.rs b/hal-espressif/src/executor.rs new file mode 100644 index 0000000..9c77e54 --- /dev/null +++ b/hal-espressif/src/executor.rs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Executor implementation for ESP32 family +//! +//! Provides async runtime integration using Embassy executor. + +use core::future::Future; + +use embassy_executor::Spawner; +use hal::ExecutorHal; + +/// ESP32 executor wrapper +pub struct EspExecutor { + spawner: Spawner, +} + +impl EspExecutor { + /// Create a new executor wrapper + pub fn new(spawner: Spawner) -> Self { + Self { spawner } + } +} + +impl ExecutorHal for EspExecutor { + fn spawner(&self) -> &Spawner { + &self.spawner + } + + fn run>(&self, _main_future: F) -> ! { + loop {} + } + + fn set_interrupt_priority(&self, _irq: usize, _priority: u8) {} + + fn core_id(&self) -> u8 { + #[cfg(any(feature = "esp32", feature = "esp32s3"))] + { + 0 + } + + #[cfg(not(any(feature = "esp32", feature = "esp32s3")))] + { + 0 + } + } +} diff --git a/hal-espressif/src/flash.rs b/hal-espressif/src/flash.rs new file mode 100644 index 0000000..1c71255 --- /dev/null +++ b/hal-espressif/src/flash.rs @@ -0,0 +1,228 @@ +//! Flash storage and OTA implementation for ESP32 family +//! +//! Provides access to flash storage for configuration persistence and firmware updates. + +use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; +use esp_bootloader_esp_idf; +use esp_hal::peripherals::FLASH; +use esp_storage::FlashStorage; +use hal::{FlashError, FlashHal, HalError, OtaActions}; +use log::{debug, error}; +use once_cell::sync::OnceCell; +use sunset_async::SunsetMutex; + +const FLASH_BUF_SIZE: usize = FlashStorage::SECTOR_SIZE as usize; + +/// Flash storage singleton +static FLASH_STORAGE: OnceCell>> = OnceCell::new(); + +/// Flash buffer holding both storage and read/write buffer +#[derive(Debug)] +pub struct FlashBuffer<'d> { + pub flash: FlashStorage<'d>, + pub buf: [u8; FLASH_BUF_SIZE], +} + +impl<'d> FlashBuffer<'d> { + pub fn new(flash: FlashStorage<'static>) -> Self { + Self { + flash, + buf: [0u8; FLASH_BUF_SIZE], + } + } + + /// Get mutable references to both flash and buffer + pub fn split_ref_mut(&mut self) -> (&mut FlashStorage<'d>, &mut [u8]) { + (&mut self.flash, &mut self.buf) + } +} + +/// Initialize flash storage +pub fn init(flash: FLASH<'static>) { + let fl = FlashBuffer::new(FlashStorage::new(flash)); + + let Ok(()) = FLASH_STORAGE.set(SunsetMutex::new(fl)) else { + log::warn!("Flash storage already initialized"); + return; + }; +} + +/// Get flash storage and buffer +pub fn get_flash_n_buffer() -> Option<&'static SunsetMutex>> { + FLASH_STORAGE.get() +} + +/// ESP Flash implementation +pub struct EspFlash; + +impl FlashHal for EspFlash { + async fn read(&self, offset: u32, buf: &mut [u8]) -> Result<(), HalError> { + let Some(fb) = get_flash_n_buffer() else { + return Err(HalError::Flash(FlashError::Read)); + }; + let mut fb = fb.lock().await; + + fb.flash + .read(offset, buf) + .map_err(|_| HalError::Flash(FlashError::Read)) + } + + async fn write(&self, offset: u32, buf: &[u8]) -> Result<(), HalError> { + let Some(fb) = get_flash_n_buffer() else { + return Err(HalError::Flash(FlashError::Write)); + }; + let mut fb = fb.lock().await; + + NorFlash::write(&mut fb.flash, offset, buf).map_err(|_| HalError::Flash(FlashError::Write)) + } + + async fn erase(&self, offset: u32, len: u32) -> Result<(), HalError> { + let Some(fb) = get_flash_n_buffer() else { + return Err(HalError::Flash(FlashError::Erase)); + }; + let mut fb = fb.lock().await; + + NorFlash::erase(&mut fb.flash, offset, len).map_err(|_| HalError::Flash(FlashError::Erase)) + } + + fn size(&self) -> u32 { + // ESP32 flash size varies by chip, return a reasonable default + // Actual size can be read from efuse in production + 4 * 1024 * 1024 // 4MB default + } +} + +/// OTA writer for ESP32 +#[derive(Debug, Copy, Clone)] +pub struct EspOtaWriter {} + +impl EspOtaWriter { + pub fn new() -> Self { + EspOtaWriter {} + } + + async fn next_ota_size() -> Result { + let Some(fb) = get_flash_n_buffer() else { + error!("Flash storage not initialized"); + return Err(HalError::Flash(FlashError::InternalError)); + }; + let mut fb = fb.lock().await; + + let (storage, _buffer) = fb.split_ref_mut(); + let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; + + let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) + .map_err(|_| HalError::Flash(FlashError::InternalError))?; + let (target_partition, _) = ota + .next_partition() + .map_err(|_| HalError::Flash(FlashError::InternalError))?; + + Ok(target_partition.partition_size() as u32) + } + + async fn write_to_target(offset: u32, data: &[u8]) -> Result<(), HalError> { + let Some(fb) = get_flash_n_buffer() else { + error!("Flash storage not initialized"); + return Err(HalError::Flash(FlashError::InternalError)); + }; + let mut fb = fb.lock().await; + + let (storage, _buffer) = fb.split_ref_mut(); + let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; + + let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) + .map_err(|_| HalError::Flash(FlashError::InternalError))?; + let (mut target_partition, part_type) = ota + .next_partition() + .map_err(|_| HalError::Flash(FlashError::InternalError))?; + + debug!("Flashing image to {:?}", part_type); + debug!( + "Writing data to target_partition at offset {}, with len {}", + offset, + data.len() + ); + + NorFlash::write(&mut target_partition, offset, data) + .map_err(|_| HalError::Flash(FlashError::Write))?; + + Ok(()) + } + + async fn activate_next_ota_slot() -> Result<(), HalError> { + let Some(fb) = get_flash_n_buffer() else { + error!("Flash storage not initialized"); + return Err(HalError::Flash(FlashError::InternalError)); + }; + let mut fb = fb.lock().await; + + let (storage, _buffer) = fb.split_ref_mut(); + let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; + + let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) + .map_err(|_| HalError::Flash(FlashError::InternalError))?; + + ota.activate_next_partition() + .map_err(|_| HalError::Flash(FlashError::Write))?; + ota.set_current_ota_state(esp_bootloader_esp_idf::ota::OtaImageState::New) + .map_err(|_| HalError::Flash(FlashError::Write))?; + + Ok(()) + } +} + +impl Default for EspOtaWriter { + fn default() -> Self { + Self::new() + } +} + +impl OtaActions for EspOtaWriter { + async fn try_validating_current_ota_partition() -> Result<(), HalError> { + let Some(fb) = get_flash_n_buffer() else { + error!("Flash storage not initialized"); + return Err(HalError::Flash(FlashError::InternalError)); + }; + let mut fb = fb.lock().await; + + let (storage, _buffer) = fb.split_ref_mut(); + let mut buff_ota = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; + + let mut ota = esp_bootloader_esp_idf::ota_updater::OtaUpdater::new(storage, &mut buff_ota) + .map_err(|_| HalError::Flash(FlashError::InternalError))?; + let _current = ota + .selected_partition() + .map_err(|_| HalError::Flash(FlashError::InternalError))?; + + debug!("current image state {:?}", ota.current_ota_state()); + + let state_result = ota.current_ota_state(); + if let Ok(state) = state_result { + if state == esp_bootloader_esp_idf::ota::OtaImageState::New + || state == esp_bootloader_esp_idf::ota::OtaImageState::PendingVerify + { + ota.set_current_ota_state(esp_bootloader_esp_idf::ota::OtaImageState::Valid) + .map_err(|_| HalError::Flash(FlashError::Write))?; + debug!("Changed state to VALID"); + } + } + + Ok(()) + } + + async fn get_ota_partition_size() -> Result { + Self::next_ota_size().await + } + + async fn write_ota_data(&self, offset: u32, data: &[u8]) -> Result<(), HalError> { + Self::write_to_target(offset, data).await + } + + async fn finalize_ota_update(&mut self) -> Result<(), HalError> { + Self::activate_next_ota_slot().await + } + + fn reset_device(&self) -> ! { + esp_hal::system::software_reset() + } +} diff --git a/hal-espressif/src/hash.rs b/hal-espressif/src/hash.rs new file mode 100644 index 0000000..74fcc21 --- /dev/null +++ b/hal-espressif/src/hash.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! HMAC-SHA256 implementation for ESP32 family +//! +//! Uses ESP32's hardware-accelerated HMAC peripheral. + +use hal::{HashError, HashHal}; +use hmac::{Hmac, Mac}; +use sha2::{Digest, Sha256 as Sha256Impl}; + +/// ESP32 HMAC implementation +pub struct EspHmac; + +impl HashHal for EspHmac { + async fn hmac_sha256( + &mut self, + key: &[u8], + message: &[u8], + output: &mut [u8; 32], + ) -> Result<(), hal::HalError> { + // Use software HMAC implementation for now + // ESP32 hardware HMAC requires special key handling + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| hal::HalError::Hash(HashError::Config))?; + mac.update(message); + let result = mac.finalize(); + output.copy_from_slice(&result.into_bytes()); + Ok(()) + } + + async fn sha256(&mut self, message: &[u8], output: &mut [u8; 32]) -> Result<(), hal::HalError> { + let mut hasher = Sha256Impl::new(); + hasher.update(message); + let result = hasher.finalize(); + output.copy_from_slice(&result); + Ok(()) + } +} diff --git a/hal-espressif/src/lib.rs b/hal-espressif/src/lib.rs new file mode 100644 index 0000000..60b60df --- /dev/null +++ b/hal-espressif/src/lib.rs @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#![no_std] + +extern crate alloc; + +mod config; +mod executor; +pub mod flash; +mod hash; +mod network; +mod rng; +mod timer; +mod uart; + +pub use config::*; +pub use executor::EspExecutor; +pub use flash::{get_flash_n_buffer, init as flash_init, EspFlash, EspOtaWriter, FlashBuffer}; +pub use hash::EspHmac; +pub use network::{ + accept_requests, ap_stack_disable, dhcp_server, init_wifi_ap, net_up, tcp_socket_disable, + wifi_controller_disable, wifi_up, EspWifi, DEFAULT_SSID, WIFI_PASSWORD_CHARS, +}; +pub use rng::EspRng; +pub use timer::EspTimer; +pub use uart::{ + uart_buffer_wait_for_initialisation, uart_task, BufferedUart, EspUart, EspUartPins, UART_BUF, + UART_SIGNAL, +}; + +use embassy_executor::Spawner; +use hal::{HalError, HalPlatform, HardwareConfig}; + +impl HalPlatform for EspHalPlatform { + type Uart = EspUart; + type Wifi = EspWifi; + type Rng = EspRng; + type Flash = EspFlash; + type Hash = EspHmac; + type Timer = EspTimer; + type Executor = EspExecutor; + + async fn init(_config: HardwareConfig, spawner: Spawner) -> Result + where + Self: Sized, + { + // Initialization is done separately in main.rs with the actual peripherals + // This is a placeholder for future unified initialization + Ok(Self { + uart: EspUart::new(uart_buffer_wait_for_initialisation().await), + wifi: EspWifi::new(), + rng: EspRng::new(), + flash: EspFlash, + hash: EspHmac, + timer: EspTimer, + executor: EspExecutor::new(spawner), + }) + } + + fn reset() -> ! { + #[cfg(any( + feature = "esp32", + feature = "esp32c2", + feature = "esp32c3", + feature = "esp32c6", + feature = "esp32s2", + feature = "esp32s3" + ))] + { + esp_hal::system::software_reset() + } + + #[cfg(not(any( + feature = "esp32", + feature = "esp32c2", + feature = "esp32c3", + feature = "esp32c6", + feature = "esp32s2", + feature = "esp32s3" + )))] + { + loop {} + } + } + + fn mac_address() -> [u8; 6] { + #[cfg(any( + feature = "esp32", + feature = "esp32c2", + feature = "esp32c3", + feature = "esp32c6", + feature = "esp32s2", + feature = "esp32s3" + ))] + { + esp_hal::efuse::Efuse::mac_address() + } + + #[cfg(not(any( + feature = "esp32", + feature = "esp32c2", + feature = "esp32c3", + feature = "esp32c6", + feature = "esp32s2", + feature = "esp32s3" + )))] + { + [0; 6] + } + } +} + +/// ESP32 platform bundle +pub struct EspHalPlatform { + pub uart: EspUart, + pub wifi: EspWifi, + pub rng: EspRng, + pub flash: EspFlash, + pub hash: EspHmac, + pub timer: EspTimer, + pub executor: EspExecutor, +} diff --git a/hal-espressif/src/network/mod.rs b/hal-espressif/src/network/mod.rs new file mode 100644 index 0000000..4d28924 --- /dev/null +++ b/hal-espressif/src/network/mod.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +mod wifi; + +pub use wifi::{ + accept_requests, ap_stack_disable, dhcp_server, init_wifi_ap, net_up, tcp_socket_disable, + wifi_controller_disable, wifi_up, EspWifi, DEFAULT_SSID, WIFI_PASSWORD_CHARS, +}; diff --git a/hal-espressif/src/network/wifi.rs b/hal-espressif/src/network/wifi.rs new file mode 100644 index 0000000..5405303 --- /dev/null +++ b/hal-espressif/src/network/wifi.rs @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! WiFi implementation for ESP32 family +//! +//! Provides WiFi access point functionality for SSH-Stamp. + +use core::net::Ipv4Addr; +use core::net::SocketAddrV4; + +use edge_dhcp::io::{self, DEFAULT_SERVER_PORT}; +use edge_dhcp::server::{Server, ServerOptions}; +use edge_nal::UdpBind; +use edge_nal_embassy::{Udp, UdpBuffers}; +use embassy_executor::Spawner; +use embassy_net::tcp::TcpSocket; +use embassy_net::{IpListenEndpoint, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; +use embassy_time::{Duration, Timer}; +use esp_hal::efuse::Efuse; +use esp_hal::peripherals::WIFI; +use esp_hal::rng::Rng; +use esp_radio::wifi::{ + AccessPointConfig, AuthMethod, ModeConfig, WifiApState, WifiController, WifiEvent, +}; +use esp_radio::Controller; +use hal::{HalError, WifiApConfigStatic, WifiError, WifiHal}; +use heapless::String; +use log::{debug, error, info, warn}; + +extern crate alloc; +use alloc::boxed::Box; +use alloc::string::String as AllocString; +use alloc::string::ToString; + +/// WiFi password character set for generation +pub const WIFI_PASSWORD_CHARS: &[u8; 62] = + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + +/// Default WiFi SSID +pub const DEFAULT_SSID: &str = "ssh-stamp"; + +macro_rules! mk_static { + ($t:ty, $val:expr) => {{ + static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); + #[deny(unused_attributes)] + let x = STATIC_CELL.uninit().write($val); + x + }}; +} + +/// ESP32 WiFi implementation +pub struct EspWifi { + ap_config: Option, +} + +impl EspWifi { + pub fn new() -> Self { + Self { ap_config: None } + } +} + +impl Default for EspWifi { + fn default() -> Self { + Self::new() + } +} + +impl WifiHal for EspWifi { + async fn start_ap(&mut self, config: WifiApConfigStatic) -> Result<(), HalError> { + self.ap_config = Some(config); + Ok(()) + } +} + +/// WiFi task for Embassy executor +#[embassy_executor::task] +pub async fn wifi_up( + mut wifi_controller: WifiController<'static>, + ssid: &'static str, + password: &'static str, +) { + debug!("Device capabilities: {:?}", wifi_controller.capabilities()); + + loop { + let client_config = ModeConfig::AccessPoint( + AccessPointConfig::default() + .with_ssid(AllocString::from(ssid)) + .with_auth_method(AuthMethod::Wpa2Wpa3Personal) + .with_password(AllocString::from(password)), + ); + + if esp_radio::wifi::ap_state() == WifiApState::Started { + wifi_controller.wait_for_event(WifiEvent::ApStop).await; + Timer::after(Duration::from_millis(5000)).await; + } + if !matches!(wifi_controller.is_started(), Ok(true)) { + if let Err(e) = wifi_controller.set_config(&client_config) { + debug!("Failed to set wifi config: {:?}", e); + Timer::after(Duration::from_millis(1000)).await; + continue; + } + debug!("Starting wifi"); + if let Err(e) = wifi_controller.start_async().await { + debug!("Failed to start wifi: {:?}", e); + Timer::after(Duration::from_millis(1000)).await; + continue; + } + debug!("Wifi started!"); + } + Timer::after(Duration::from_millis(10)).await; + } +} + +/// Network task for Embassy executor +#[embassy_executor::task] +pub async fn net_up(mut runner: Runner<'static, esp_radio::wifi::WifiDevice<'static>>) { + debug!("Bringing up network stack..."); + runner.run().await +} + +/// DHCP server task for Embassy executor +#[embassy_executor::task] +pub async fn dhcp_server(stack: Stack<'static>, ip: Ipv4Addr) { + let mut buf = [0u8; 1500]; + let mut gw_buf = [Ipv4Addr::UNSPECIFIED]; + + let buffers = UdpBuffers::<3, 1024, 1024, 10>::new(); + let unbound_socket = Udp::new(stack, &buffers); + let mut bound_socket = match unbound_socket + .bind(core::net::SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::UNSPECIFIED, + DEFAULT_SERVER_PORT, + ))) + .await + { + Ok(socket) => socket, + Err(e) => { + warn!("Failed to bind DHCP server socket: {e:?}"); + return; + } + }; + + loop { + if let Err(e) = io::server::run( + &mut Server::<_, 64>::new_with_et(ip), + &ServerOptions::new(ip, Some(&mut gw_buf)), + &mut bound_socket, + &mut buf, + ) + .await + { + error!("DHCP server error: {e:?}"); + } + Timer::after(Duration::from_millis(500)).await; + } +} + +/// Accept incoming TCP connection +pub async fn accept_requests<'a>( + tcp_stack: Stack<'a>, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], +) -> TcpSocket<'a> { + let mut tcp_socket = TcpSocket::new(tcp_stack, rx_buffer, tx_buffer); + + debug!("Waiting for SSH client..."); + if let Err(_e) = tcp_socket + .accept(IpListenEndpoint { + addr: None, + port: 22, + }) + .await + { + // Continue trying to accept + } + debug!("Connected, port 22"); + + tcp_socket +} + +/// Initialize WiFi AP with given configuration +#[allow(dead_code)] +pub async fn init_wifi_ap( + spawner: Spawner, + controller: Controller<'static>, + wifi: WIFI<'static>, + rng: Rng, + ssid: String<32>, + password: String<63>, + mac: [u8; 6], + gw_ip: Ipv4Addr, +) -> Result, HalError> { + let wifi_init = &*mk_static!(Controller<'static>, controller); + let (mut wifi_controller, interfaces) = + esp_radio::wifi::new(wifi_init, wifi, Default::default()) + .map_err(|_| HalError::Wifi(WifiError::Initialization))?; + + // Set MAC address + Efuse::set_mac_address(mac).map_err(|_| HalError::Config)?; + + let ap_config = ModeConfig::AccessPoint( + AccessPointConfig::default() + .with_ssid(AllocString::from(ssid.as_str())) + .with_auth_method(AuthMethod::Wpa2Wpa3Personal) + .with_password(AllocString::from(password.as_str())), + ); + let _res = wifi_controller.set_config(&ap_config); + + let net_config = embassy_net::Config::ipv4_static(StaticConfigV4 { + address: Ipv4Cidr::new(gw_ip, 24), + gateway: Some(gw_ip), + dns_servers: Default::default(), + }); + + let seed = (rng.random() as u64) << 32 | rng.random() as u64; + + let (ap_stack, runner) = embassy_net::new( + interfaces.ap, + net_config, + mk_static!(StackResources<3>, StackResources::<3>::new()), + seed, + ); + + // Convert ssid and password to static - caller must ensure they live long enough + // For now we use leak to make them 'static + let ssid_static: &'static str = Box::leak(ssid.as_str().to_string().into_boxed_str()); + let password_static: &'static str = Box::leak(password.as_str().to_string().into_boxed_str()); + + spawner + .spawn(wifi_up(wifi_controller, ssid_static, password_static)) + .ok(); + spawner.spawn(net_up(runner)).ok(); + spawner.spawn(dhcp_server(ap_stack, gw_ip)).ok(); + + loop { + debug!("Checking if link is up"); + if ap_stack.is_link_up() { + break; + } + Timer::after(Duration::from_millis(500)).await; + } + + info!( + "Connect to the AP `ssh-stamp` as a DHCP client with IP: {}", + gw_ip + ); + + Ok(ap_stack) +} + +/// Disable AP stack +pub async fn ap_stack_disable() { + debug!("AP Stack disabled: WIP"); +} + +/// Disable TCP socket +pub async fn tcp_socket_disable() { + debug!("TCP socket disabled: WIP"); +} + +/// Disable WiFi controller +pub async fn wifi_controller_disable() { + debug!("Disabling wifi: WIP"); +} diff --git a/hal-espressif/src/rng.rs b/hal-espressif/src/rng.rs new file mode 100644 index 0000000..8851651 --- /dev/null +++ b/hal-espressif/src/rng.rs @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! RNG implementation for ESP32 family +//! +//! Provides hardware random number generation using ESP32's true RNG. + +use core::cell::RefCell; + +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use esp_hal::rng::Rng; +use getrandom::register_custom_getrandom; +use hal::{HalError, RngHal}; +use static_cell::StaticCell; + +static RNG: StaticCell = StaticCell::new(); +static RNG_MUTEX: Mutex>> = + Mutex::new(RefCell::new(None)); + +/// ESP32 RNG implementation +pub struct EspRng { + _inner: (), +} + +impl EspRng { + /// Create a new ESP RNG instance + /// + /// Note: The actual RNG must be registered via `register()` before use. + pub fn new() -> Self { + Self { _inner: () } + } + + /// Register the RNG for use with getrandom + pub fn register(rng: Rng) { + let rng_ref = RNG.init(rng); + RNG_MUTEX.lock(|t| t.borrow_mut().replace(rng_ref)); + register_custom_getrandom!(esp_getrandom_custom_func); + } +} + +impl Default for EspRng { + fn default() -> Self { + Self::new() + } +} + +impl RngHal for EspRng { + async fn fill_bytes(&mut self, buf: &mut [u8]) -> Result<(), HalError> { + RNG_MUTEX.lock(|t| { + let mut rng = t.borrow_mut(); + let rng = rng.as_mut().ok_or(HalError::Rng)?; + rng.read(buf); + Ok(()) + }) + } +} + +/// ESP32-specific getrandom implementation +pub fn esp_getrandom_custom_func(buf: &mut [u8]) -> Result<(), getrandom::Error> { + RNG_MUTEX.lock(|t| { + let mut rng = t.borrow_mut(); + let rng = rng + .as_mut() + .expect("register() should have been called first"); + rng.read(buf); + }); + Ok(()) +} diff --git a/hal-espressif/src/timer.rs b/hal-espressif/src/timer.rs new file mode 100644 index 0000000..4d2d141 --- /dev/null +++ b/hal-espressif/src/timer.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Timer implementation for ESP32 family +//! +//! Provides microsecond and millisecond timing using ESP32 hardware timers. + +use embassy_time::{Duration, Instant}; +use hal::TimerHal; + +/// ESP32 Timer implementation using Embassy time +pub struct EspTimer; + +impl TimerHal for EspTimer { + fn now_micros(&self) -> u64 { + Instant::now().as_micros() + } + + async fn delay(&self, millis: u64) { + embassy_time::Timer::after(Duration::from_millis(millis)).await; + } +} diff --git a/hal-espressif/src/uart.rs b/hal-espressif/src/uart.rs new file mode 100644 index 0000000..edb6eac --- /dev/null +++ b/hal-espressif/src/uart.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! UART implementation for ESP32 family +//! +//! Uses DMA-based buffered I/O for efficient serial communication. + +use embassy_sync::pipe::TryWriteError; +use embassy_sync::signal::Signal; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pipe::Pipe}; +use esp_hal::gpio::AnyPin; +use esp_hal::peripherals::UART1; +use esp_hal::uart::{Config, RxConfig, Uart}; +use esp_hal::Async; +use hal::{HalError, UartConfig, UartHal}; +use portable_atomic::{AtomicUsize, Ordering}; +use static_cell::StaticCell; + +const INWARD_BUF_SZ: usize = 512; +const OUTWARD_BUF_SZ: usize = 256; +const UART_BUF_SZ: usize = 64; + +/// Bidirectional pipe buffer for UART communications +pub struct BufferedUart { + outward: Pipe, + inward: Pipe, + dropped_rx_bytes: AtomicUsize, + signal: Signal, +} + +impl BufferedUart { + pub fn new() -> Self { + BufferedUart { + outward: Pipe::new(), + inward: Pipe::new(), + dropped_rx_bytes: AtomicUsize::from(0), + signal: Signal::new(), + } + } + + /// Transfer data between UART hardware and internal buffers. + /// + /// This should be awaited from an Embassy task run in an InterruptExecutor + /// for lower latency. + pub async fn run(&self, uart: Uart<'_, Async>) { + let (mut uart_rx, mut uart_tx) = uart.split(); + let mut uart_rx_buf = [0u8; UART_BUF_SZ]; + let mut uart_tx_buf = [0u8; UART_BUF_SZ]; + + loop { + use embassy_futures::select::select; + + let rd_from = async { + loop { + let Ok(n) = uart_rx.read_async(&mut uart_rx_buf).await else { + continue; + }; + + let mut rx_slice = &uart_rx_buf[..n]; + + while !rx_slice.is_empty() { + rx_slice = match self.inward.try_write(rx_slice) { + Ok(w) => &rx_slice[w..], + Err(TryWriteError::Full) => { + let mut drop_buf = [0u8; UART_BUF_SZ]; + let dropped = self + .inward + .try_read(&mut drop_buf[..rx_slice.len()]) + .unwrap_or_default(); + let _ = self.dropped_rx_bytes.fetch_update( + Ordering::Relaxed, + Ordering::Relaxed, + |d| Some(d.saturating_add(dropped)), + ); + rx_slice + } + }; + } + } + }; + + let rd_to = async { + loop { + let n = self.outward.read(&mut uart_tx_buf).await; + let _ = uart_tx.write_async(&uart_tx_buf[..n]).await; + } + }; + + select(rd_from, rd_to).await; + } + } + + pub async fn read(&self, buf: &mut [u8]) -> usize { + self.inward.read(buf).await + } + + pub async fn write(&self, buf: &[u8]) { + self.outward.write_all(buf).await; + } + + /// Return the number of dropped bytes since last check and reset counter + pub fn check_dropped_bytes(&self) -> usize { + self.dropped_rx_bytes.swap(0, Ordering::Relaxed) + } + + /// Signal that UART should start + pub fn signal(&self) -> &Signal { + &self.signal + } +} + +impl Default for BufferedUart { + fn default() -> Self { + Self::new() + } +} + +/// UART pins configuration +pub struct EspUartPins<'a> { + pub rx: AnyPin<'a>, + pub tx: AnyPin<'a>, +} + +/// ESP UART implementation +pub struct EspUart { + buffered: &'static BufferedUart, + configured: bool, +} + +impl EspUart { + /// Create a new ESP UART instance + pub fn new(buffered: &'static BufferedUart) -> Self { + Self { + buffered, + configured: false, + } + } + + /// Get the buffered UART for task spawning + pub fn buffered(&self) -> &'static BufferedUart { + self.buffered + } +} + +impl UartHal for EspUart { + async fn read(&mut self, buf: &mut [u8]) -> Result { + Ok(self.buffered.read(buf).await) + } + + async fn write(&mut self, buf: &[u8]) -> Result { + self.buffered.write(buf).await; + Ok(buf.len()) + } + + fn can_read(&self) -> bool { + // Check if there's data in the inward pipe + // This is a heuristic - actual implementation may need adjustment + self.buffered.check_dropped_bytes() > 0 || self.configured + } + + fn signal(&self) -> &Signal { + self.buffered.signal() + } + + async fn reconfigure(&mut self, _config: UartConfig) -> Result<(), HalError> { + // TODO: Implement runtime reconfiguration + // Currently pins are configured at compile time + self.configured = true; + Ok(()) + } +} + +/// Static storage for buffered UART +pub static UART_BUF: StaticCell = StaticCell::new(); + +/// Signal for UART task synchronization +pub static UART_SIGNAL: Signal = Signal::new(); + +/// Initialize and get the buffered UART +pub async fn uart_buffer_wait_for_initialisation() -> &'static BufferedUart { + UART_BUF.init_with(BufferedUart::new) +} + +/// UART task for Embassy executor +#[embassy_executor::task] +pub async fn uart_task( + uart_buf: &'static BufferedUart, + uart1: UART1<'static>, + pins: EspUartPins<'static>, +) { + // Wait until SSH shell is ready + UART_SIGNAL.wait().await; + + // Hardware UART setup + let uart_config = Config::default().with_rx( + RxConfig::default() + .with_fifo_full_threshold(16) + .with_timeout(1), + ); + + let Ok(uart) = Uart::new(uart1, uart_config) else { + return; + }; + let uart = uart.with_rx(pins.rx).with_tx(pins.tx).into_async(); + + // Run buffered TX/RX loop + uart_buf.run(uart).await; +} From 3fee44a72f0542d17537c067bd3dfc09d0b9b6ac Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:12:17 +0100 Subject: [PATCH 07/14] SSH-Stamp application-specific bits --- Cargo.toml | 130 +++++++++++++----------- src/espressif/buffered_uart.rs | 176 +++++---------------------------- src/espressif/mod.rs | 16 ++- src/espressif/net.rs | 141 ++++++++++---------------- src/handle.rs | 6 +- src/main.rs | 10 +- src/settings.rs | 2 +- 7 files changed, 171 insertions(+), 310 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46bf828..00314f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,22 @@ edition = "2024" license = "MIT OR Apache-2.0" [workspace] -members = ["storage", "ota"] +members = ["hal", "hal-espressif", "ota"] [workspace.dependencies] +# Async runtime +embassy-sync = "0.7" +embassy-executor = { version = "0.9.0" } +embassy-time = { version = "0.5.0" } +embassy-futures = "0.1.2" + +# Collections +heapless = "0.8" + +# ESP32 +esp-hal = { version = "1", features = ["unstable", "log-04"] } + +# SSH protocol sunset = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp-basic", default-features = false, features = [ "openssh-key", "embedded-io", @@ -27,23 +40,52 @@ sunset-async = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp sunset-sshwire-derive = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp-basic", default-features = false } sunset-sftp = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp-basic", default-features = false } +# Crypto sha2 = { version = "0.10", default-features = false } -rustc-hash = { version = "2.1", default-features = false } # For hash in the OTA SFTP server TODO: For the optimisation task: Remove this dependency and use sha2 instead? -log = "0.4" +rustc-hash = { version = "2.1", default-features = false } +bcrypt = { version = "0.17", default-features = false } +subtle = { version = "2", default-features = false } +hmac = { version = "0.12", default-features = false } +digest = { version = "0.10", default-features = false, features = [ + "rand_core", + "subtle", +] } -esp-hal = { version = "1", features = ["unstable", "log-04"] } +# Storage embedded-storage = "0.3.1" +embedded-storage-async = "0.4" esp-storage = { version = "0.8.0" } esp-bootloader-esp-idf = { version = "0.4.0" } +# Networking +edge-dhcp = "0.6" +edge-nal = "0.5" +edge-nal-embassy = "0.7" +embassy-net = { version = "0.7", features = [ + "tcp", + "udp", + "dhcpv4", + "medium-ethernet", +] } +smoltcp = { version = "0.12", default-features = false, features = [ + "medium-ethernet", + "socket-raw", +] } + +# Utilities +log = "0.4" static_cell = { version = "2.1" } -# Used for storage::flash singleton once_cell = { version = "1", features = [ "critical-section", ], default-features = false } +portable-atomic = "1" +snafu = { version = "0.8", default-features = false } +paste = "1" +pretty-hex = { version = "0.4", default-features = false } +embedded-io-async = "0.6.1" +embassy-embedded-hal = "0.3" [dependencies] -storage = { path = "storage" } ota = { path = "ota" } sha2 = { workspace = true } @@ -51,27 +93,20 @@ rustc-hash = { workspace = true } cfg-if = "1" ed25519-dalek = { version = "2", default-features = false } -embassy-executor = { version = "0.9.0" } -embassy-net = { version = "0.7", features = [ - "tcp", - "udp", - "dhcpv4", - "medium-ethernet", -] } -smoltcp = { version = "0.12", default-features = false, features = [ - "medium-ethernet", - "socket-raw", -] } -embassy-time = { version = "0.5.0" } -embedded-io-async = "0.6.1" +embassy-executor = { workspace = true } +embassy-net = { workspace = true } +smoltcp = { workspace = true } +embassy-time = { workspace = true } +embedded-io-async = { workspace = true } esp-alloc = { version = "0.9.0" } esp-backtrace = { version = "0.18.0", features = ["panic-handler", "println"] } esp-hal = { workspace = true } +hal-espressif = { path = "hal-espressif" } esp-rtos = { version = "0.2.0", features = ["embassy", "log-04", "esp-radio"] } esp-radio = { version = "0.17.0", features = ["wifi", "log-04"] } esp-println = { version = "0.16", features = ["log-04"] } hex = { version = "0.4", default-features = false } -log = { version = "0.4" } +log = { workspace = true } static_cell = { workspace = true } ssh-key = { version = "0.6", default-features = false, features = ["ed25519"] } sunset = { workspace = true } @@ -79,40 +114,33 @@ sunset-async = { workspace = true } sunset-sshwire-derive = { workspace = true } sunset-sftp = { workspace = true } getrandom = { version = "0.2.10", features = ["custom"] } -embassy-sync = "0.7" -heapless = "0.8" -embassy-futures = "0.1.2" -edge-dhcp = "0.6" -edge-nal = "0.5" -edge-nal-embassy = "0.7" -#sequential-storage = { version = "4", features = ["heapless"] } +embassy-sync = { workspace = true } +heapless = { workspace = true } +embassy-futures = { workspace = true } +edge-dhcp = { workspace = true } +edge-nal = { workspace = true } +edge-nal-embassy = { workspace = true } esp-storage = { workspace = true } embedded-storage = { workspace = true } -embassy-embedded-hal = "0.3" -bcrypt = { version = "0.17", default-features = false } -subtle = { version = "2", default-features = false } -hmac = { version = "0.12", default-features = false } -# sha2 = { version = "0.10", default-features = false } -digest = { version = "0.10", default-features = false, features = [ - "rand_core", - "subtle", -] } -embedded-storage-async = "0.4" -portable-atomic = "1" +embassy-embedded-hal = { workspace = true } +bcrypt = { workspace = true } +subtle = { workspace = true } +hmac = { workspace = true } +digest = { workspace = true } +embedded-storage-async = { workspace = true } +portable-atomic = { workspace = true } esp-bootloader-esp-idf = { workspace = true } -snafu = { version = "0.8", default-features = false } -paste = "1" -pretty-hex = { version = "0.4", default-features = false } +snafu = { workspace = true } +paste = { workspace = true } +pretty-hex = { workspace = true } [profile.dev] -# Rust debug is too slow. -# For debug builds always builds with some optimization opt-level = 0 debug = 2 debug-assertions = true [profile.release] -codegen-units = 1 # LLVM can perform better optimizations using a single thread +codegen-units = 1 debug = 2 debug-assertions = true incremental = false @@ -122,7 +150,7 @@ overflow-checks = true [profile.esp32s2] inherits = "release" -opt-level = "s" # Optimize for size. +opt-level = "s" [profile.dev.package.esp-storage] opt-level = "s" @@ -133,7 +161,7 @@ opt-level = "s" [features] ipv6 = [] -# Enables the SFTP OTA Subsystem. Use ota-packer to pack a binary and PUT it over sftp +# Enables the SFTP OTA Subsystem. Use packer to pack a binary and PUT it over sftp sftp-ota = [] # MCU options @@ -167,16 +195,6 @@ esp32c3 = [ "esp-storage/esp32c3", "esp-bootloader-esp-idf/esp32c3", ] -#esp32c5 = [ -# "esp-hal/esp32c5", -# "esp-alloc/esp32c5", -# "esp-backtrace/esp32c5", -# "esp-radio/esp32c5", -# "esp-rtos/esp32c5", -# "esp-println/esp32c5", -# "esp-storage/esp32c5", -# "esp-bootloader-esp-idf/esp32c5", -#] esp32c6 = [ "esp-hal/esp32c6", "esp-alloc/esp32c6", diff --git a/src/espressif/buffered_uart.rs b/src/espressif/buffered_uart.rs index 48b58cc..1a23e61 100644 --- a/src/espressif/buffered_uart.rs +++ b/src/espressif/buffered_uart.rs @@ -2,167 +2,41 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -/// Wrapper around bidirectional embassy-sync Pipes, in order to handle UART -/// RX/RX happening in an InterruptExecutor at higher priority. -/// -/// Doesn't implement the InterruptExecutor, in the task in the app should await -/// the 'run' async function. -/// +//! Buffered UART support with app-specific configuration +//! +//! Re-exports from hal-espressif and provides app-specific task wrappers. + +// Re-export core types from HAL +pub use hal_espressif::{BufferedUart, UART_BUF, UART_SIGNAL}; + use crate::config::SSHStampConfig; -use embassy_futures::select::select; -use embassy_sync::pipe::TryWriteError; -use embassy_sync::signal::Signal; -use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pipe::Pipe}; -use esp_hal::Async; use esp_hal::gpio::AnyPin; use esp_hal::peripherals::UART1; use esp_hal::uart::{Config, RxConfig, Uart}; -use portable_atomic::{AtomicUsize, Ordering}; -use static_cell::StaticCell; +use hal_espressif::EspUartPins; use sunset_async::SunsetMutex; -use log::debug; - -// Sizes of the software buffers. Inward is more -// important as an overrun here drops bytes. A full outward -// buffer will only block the executor. -const INWARD_BUF_SZ: usize = 512; -const OUTWARD_BUF_SZ: usize = 256; - -// Size of the buffer for hardware read/write ops. -const UART_BUF_SZ: usize = 64; - -/// Bidirectional pipe buffer for UART communications -pub struct BufferedUart { - outward: Pipe, - inward: Pipe, - dropped_rx_bytes: AtomicUsize, -} - -pub struct UartConfig {} - -impl BufferedUart { - pub fn new() -> Self { - BufferedUart { - outward: Pipe::new(), - inward: Pipe::new(), - dropped_rx_bytes: AtomicUsize::from(0), - } - } - - /// Transfer data between the UART and the buffer struct. - /// - /// This should be awaited from an Embassy task that's run - /// in an InterruptExecutor for lower latency. - pub async fn run(&self, uart: Uart<'_, Async>) { - let (mut uart_rx, mut uart_tx) = uart.split(); - let mut uart_rx_buf = [0u8; UART_BUF_SZ]; - let mut uart_tx_buf = [0u8; UART_BUF_SZ]; - - loop { - let rd_from = async { - loop { - // Note: println! is intentionally avoided here as this runs in an - // InterruptExecutor at high priority. Blocking I/O would cause scheduler panics. - let Ok(n) = uart_rx.read_async(&mut uart_rx_buf).await else { - continue; - }; - - let mut rx_slice = &uart_rx_buf[..n]; - - // Write rx_slice to 'inward' pipe, dropping bytes rather than blocking if - // the pipe is full - while !rx_slice.is_empty() { - rx_slice = match self.inward.try_write(rx_slice) { - Ok(w) => &rx_slice[w..], - Err(TryWriteError::Full) => { - // If the receive buffer is full (no SSH client, or network congestion) then - // drop the oldest bytes from the pipe so we can still write the newest ones. - let mut drop_buf = [0u8; UART_BUF_SZ]; - let dropped = self - .inward - .try_read(&mut drop_buf[..rx_slice.len()]) - .unwrap_or_default(); - let _ = self.dropped_rx_bytes.fetch_update( - Ordering::Relaxed, - Ordering::Relaxed, - |d| Some(d.saturating_add(dropped)), - ); - rx_slice - } - }; - } - } - }; - let rd_to = async { - loop { - let n = self.outward.read(&mut uart_tx_buf).await; - // TODO: handle write errors - let _ = uart_tx.write_async(&uart_tx_buf[..n]).await; - } - }; - select(rd_from, rd_to).await; - } - } - - pub async fn read(&self, buf: &mut [u8]) -> usize { - self.inward.read(buf).await - } - - pub async fn write(&self, buf: &[u8]) { - self.outward.write_all(buf).await; - } - - /// Return the number of dropped bytes (if any) since the last check, - /// and reset the internal count to 0. - pub fn check_dropped_bytes(&self) -> usize { - self.dropped_rx_bytes.swap(0, Ordering::Relaxed) - } - - pub fn reconfigure(&self, _config: &'static SunsetMutex) { - todo!(); - } -} - -impl Default for BufferedUart { - fn default() -> Self { - Self::new() - } -} - -pub async fn uart_buffer_disable() -> () { - // disable uart buffer - debug!("UART buffer disabled: WIP"); - // TODO: Correctly disable/restart UART buffer and/or send messsage to user over SSH -} -// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; - -pub async fn uart_disable() -> () { - // disable uart - debug!("UART disabled: WIP"); - // TODO: Correctly disable/restart UART and/or send messsage to user over SSH +/// Wait for UART buffer initialization +pub async fn uart_buffer_wait_for_initialisation() -> &'static BufferedUart { + hal_espressif::uart_buffer_wait_for_initialisation().await } -/// UART pins for the buffered UART task. -/// -/// Pins are selected at compile time based on the target chip. -/// Each target only populates the pins it actually uses.l -/// -/// SECURITY: In the future, the pins could be selected at runtime -/// via SSH Env variables, think and evaluate if that could pose -/// a security threat. +/// UART pins wrapper for app compatibility pub struct UartPins<'a> { pub rx: AnyPin<'a>, pub tx: AnyPin<'a>, } -pub static UART_BUF: StaticCell = StaticCell::new(); -pub static UART_SIGNAL: Signal = Signal::new(); - -pub async fn uart_buffer_wait_for_initialisation() -> &'static BufferedUart { - UART_BUF.init_with(BufferedUart::new) +impl<'a> From> for EspUartPins<'a> { + fn from(pins: UartPins<'a>) -> Self { + EspUartPins { + rx: pins.rx, + tx: pins.tx, + } + } } +/// UART task for Embassy executor #[embassy_executor::task] pub async fn uart_task( uart_buf: &'static BufferedUart, @@ -170,13 +44,10 @@ pub async fn uart_task( _config: &'static SunsetMutex, pins: UartPins<'static>, ) { - // Note: dbg!/println! avoided throughout as this task runs in an InterruptExecutor - // at high priority where blocking I/O can cause scheduler panics. - - // Wait until ssh shell complete + // Wait until SSH shell is ready UART_SIGNAL.wait().await; - // Hardware UART setup - pins are already selected at compile time + // Hardware UART setup - pins are selected at compile time let uart_config = Config::default().with_rx( RxConfig::default() .with_fifo_full_threshold(16) @@ -187,7 +58,8 @@ pub async fn uart_task( let Ok(uart) = Uart::new(uart1, uart_config) else { return; }; - let uart = uart.with_rx(pins.rx).with_tx(pins.tx).into_async(); + let hal_pins: EspUartPins = pins.into(); + let uart = uart.with_rx(hal_pins.rx).with_tx(hal_pins.tx).into_async(); // Run the main buffered TX/RX loop uart_buf.run(uart).await; diff --git a/src/espressif/mod.rs b/src/espressif/mod.rs index 4370f04..19dccfe 100644 --- a/src/espressif/mod.rs +++ b/src/espressif/mod.rs @@ -2,9 +2,17 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! ESP32 platform support +//! +//! This module provides app-specific wrappers around the HAL implementations. + pub mod buffered_uart; pub mod net; -pub mod rng; -// TODO: Specialise for Espressif, tricky since it seems to require burning eFuses?: -// https://github.com/esp-rs/esp-hal/blob/main/examples/src/bin/hmac.rs -//pub mod hash; + +// Re-export RNG registration from hal-espressif +pub use hal_espressif::EspRng; + +/// Register the hardware RNG for use with getrandom +pub fn register_custom_rng(rng: esp_hal::rng::Rng) { + hal_espressif::EspRng::register(rng); +} diff --git a/src/espressif/net.rs b/src/espressif/net.rs index 02107e7..be563f4 100644 --- a/src/espressif/net.rs +++ b/src/espressif/net.rs @@ -2,41 +2,48 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -use log::{debug, error, info, warn}; +//! Network support with app-specific configuration +//! +//! Re-exports from hal-espressif and provides app-specific network initialization. use crate::config::SSHStampConfig; -use crate::settings::{DEFAULT_IP, DEFAULT_SSID, WIFI_PASSWORD_CHARS}; +use crate::settings::DEFAULT_IP; +use crate::store; + use core::net::Ipv4Addr; use core::net::SocketAddrV4; -use edge_dhcp; -use edge_dhcp::{ - io::{self, DEFAULT_SERVER_PORT}, - server::{Server, ServerOptions}, -}; + +use edge_dhcp::io::{self, DEFAULT_SERVER_PORT}; +use edge_dhcp::server::{Server, ServerOptions}; use edge_nal::UdpBind; use edge_nal_embassy::{Udp, UdpBuffers}; use embassy_executor::Spawner; -use embassy_net::{IpListenEndpoint, Ipv4Cidr, Runner, StaticConfigV4}; -use embassy_net::{Stack, StackResources, tcp::TcpSocket}; +use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; use embassy_time::{Duration, Timer}; use esp_hal::efuse::Efuse; use esp_hal::peripherals::WIFI; use esp_hal::rng::Rng; use esp_radio::Controller; -use esp_radio::wifi::WifiEvent; use esp_radio::wifi::{ - AccessPointConfig, AuthMethod::Wpa2Wpa3Personal, ModeConfig, WifiApState, WifiController, + AccessPointConfig, AuthMethod, ModeConfig, WifiApState, WifiController, WifiEvent, }; +use hal_espressif::flash; use heapless::String; +use log::{debug, error, info, warn}; +use sunset_async::SunsetMutex; + extern crate alloc; -use crate::store; use alloc::string::String as AllocString; -use storage::flash; -use sunset_async::SunsetMutex; -// When you are okay with using a nightly compiler it's better to use https://docs.rs/static_cell/2.1.0/static_cell/macro.make_static.html +use hal_espressif::{DEFAULT_SSID, WIFI_PASSWORD_CHARS}; + +// Re-export functions from hal-espressif +pub use hal_espressif::{ + accept_requests, ap_stack_disable, tcp_socket_disable, wifi_controller_disable, +}; + macro_rules! mk_static { - ($t:ty,$val:expr) => {{ + ($t:ty, $val:expr) => {{ static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); #[deny(unused_attributes)] let x = STATIC_CELL.uninit().write($val); @@ -44,6 +51,7 @@ macro_rules! mk_static { }}; } +/// Bring up WiFi interface with app-specific configuration pub async fn if_up( spawner: Spawner, controller: Controller<'static>, @@ -97,13 +105,13 @@ pub async fn if_up( let ap_config = ModeConfig::AccessPoint( AccessPointConfig::default() .with_ssid(AllocString::from(wifi_ssid(config).await.as_str())) - .with_auth_method(Wpa2Wpa3Personal) + .with_auth_method(AuthMethod::Wpa2Wpa3Personal) .with_password(AllocString::from(wifi_password(config).await.as_str())), ); let res = wifi_controller.set_config(&ap_config); debug!("wifi_set_configuration returned {:?}", res); - let gw_ip_addr_ipv4 = *DEFAULT_IP; + let gw_ip_addr_ipv4 = DEFAULT_IP; let net_config = embassy_net::Config::ipv4_static(StaticConfigV4 { address: Ipv4Cidr::new(gw_ip_addr_ipv4, 24), @@ -113,7 +121,6 @@ pub async fn if_up( let seed = (rng.random() as u64) << 32 | rng.random() as u64; - // Init network stack let (ap_stack, runner) = embassy_net::new( interfaces.ap, net_config, @@ -121,7 +128,18 @@ pub async fn if_up( seed, ); - spawner.spawn(wifi_up(wifi_controller, config)).ok(); + let ssid = wifi_ssid(config).await; + let password = wifi_password(config).await; + + // Convert to static strings for the task + let ssid_static: &'static str = + alloc::boxed::Box::leak(alloc::string::String::from(ssid.as_str()).into_boxed_str()); + let password_static: &'static str = + alloc::boxed::Box::leak(alloc::string::String::from(password.as_str()).into_boxed_str()); + + spawner + .spawn(wifi_up(wifi_controller, ssid_static, password_static)) + .ok(); spawner.spawn(net_up(runner)).ok(); spawner.spawn(dhcp_server(ap_stack, gw_ip_addr_ipv4)).ok(); @@ -141,42 +159,6 @@ pub async fn if_up( Ok(ap_stack) } -pub async fn ap_stack_disable() -> () { - // drop ap_stack - debug!("AP Stack disabled: WIP"); - // TODO: Correctly disable/restart AP Stack and/or send messsage to user over SSH -} - -pub async fn tcp_socket_disable() -> () { - // drop tcp stack - debug!("TCP socket disabled: WIP"); - // TODO: Correctly disable/restart tcp socket and/or send messsage to user over SSH -} - -pub async fn accept_requests<'a>( - tcp_stack: Stack<'a>, - rx_buffer: &'a mut [u8], - tx_buffer: &'a mut [u8], -) -> TcpSocket<'a> { - let mut tcp_socket = TcpSocket::new(tcp_stack, rx_buffer, tx_buffer); - - debug!("Waiting for SSH client..."); - if let Err(e) = tcp_socket - .accept(IpListenEndpoint { - addr: None, - port: 22, - }) - .await - { - error!("connect error: {:?}", e); - // continue; - tcp_socket_disable().await; - } - debug!("Connected, port 22"); - - tcp_socket -} - /// Returns the configured WiFi SSID from the config, or the default SSID if not set. pub async fn wifi_ssid(config: &'static SunsetMutex) -> String<63> { let guard = config.lock().await; @@ -197,39 +179,30 @@ pub async fn wifi_ssid(config: &'static SunsetMutex) -> String<6 /// Panics if wifi_pw is not set in the config. pub async fn wifi_password(config: &'static SunsetMutex) -> String<63> { let guard = config.lock().await; - match &guard.wifi_pw { - Some(pw) => String::<63>::try_from(pw.as_str()).unwrap_or_else(|_| { - panic!("wifi_pw stored value exceeds 63 characters"); - }), - None => panic!("wifi_pw must be set before calling wifi_password()"), - } + let pw_src = guard.wifi_pw.as_ref().expect("wifi_pw should be set"); + String::<63>::try_from(pw_src.as_str()).expect("wifi_pw too long") } -/// Manages the WiFi access point lifecycle. -/// Starts the AP with the configured SSID and password from the config. -/// Handles reconnection if the AP stops. +/// WiFi task for Embassy executor #[embassy_executor::task] pub async fn wifi_up( mut wifi_controller: WifiController<'static>, - config: &'static SunsetMutex, + ssid: &'static str, + password: &'static str, ) { debug!("Device capabilities: {:?}", wifi_controller.capabilities()); - debug!("Starting wifi"); - loop { - let ssid_string = wifi_ssid(config).await; - let pw_string = wifi_password(config).await; let client_config = ModeConfig::AccessPoint( AccessPointConfig::default() - .with_ssid(AllocString::from(ssid_string.as_str())) - .with_auth_method(Wpa2Wpa3Personal) - .with_password(AllocString::from(pw_string.as_str())), + .with_ssid(AllocString::from(ssid)) + .with_auth_method(AuthMethod::Wpa2Wpa3Personal) + .with_password(AllocString::from(password)), ); if esp_radio::wifi::ap_state() == WifiApState::Started { wifi_controller.wait_for_event(WifiEvent::ApStop).await; - Timer::after(Duration::from_millis(5000)).await + Timer::after(Duration::from_millis(5000)).await; } if !matches!(wifi_controller.is_started(), Ok(true)) { if let Err(e) = wifi_controller.set_config(&client_config) { @@ -249,27 +222,17 @@ pub async fn wifi_up( } } -pub async fn wifi_controller_disable() -> () { - // TODO: Correctly disable wifi controller - // pub async fn wifi_disable(wifi_controller: EspWifiController<'_>) -> (){ - // drop wifi controller - // esp_wifi::deinit_unchecked() - // wifi_controller.deinit_unchecked() - debug!("Disabling wifi: WIP"); - //software_reset(); -} - -use esp_radio::wifi::WifiDevice; +/// Network task for Embassy executor #[embassy_executor::task] -async fn net_up(mut runner: Runner<'static, WifiDevice<'static>>) { - debug!("Bringing up network stack...\n"); +pub async fn net_up(mut runner: Runner<'static, esp_radio::wifi::WifiDevice<'static>>) { + debug!("Bringing up network stack..."); runner.run().await } +/// DHCP server task for Embassy executor #[embassy_executor::task] -async fn dhcp_server(stack: Stack<'static>, ip: Ipv4Addr) { +pub async fn dhcp_server(stack: Stack<'static>, ip: Ipv4Addr) { let mut buf = [0u8; 1500]; - let mut gw_buf = [Ipv4Addr::UNSPECIFIED]; let buffers = UdpBuffers::<3, 1024, 1024, 10>::new(); diff --git a/src/handle.rs b/src/handle.rs index 6639de6..2478e27 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -11,8 +11,8 @@ use crate::store; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::channel::Channel; use esp_hal::system::software_reset; +use hal_espressif::flash; use heapless::String; -use storage::flash; use core::fmt::Debug; use core::option::Option::None; @@ -396,8 +396,8 @@ pub async fn ssh_client<'a, 'b>( SessionType::Sftp(ch) => { debug!("Handling SFTP session"); let stdio = ssh_server.stdio(ch).await?; - let ota_writer = storage::esp_ota::OtaWriter::new(); - ota::run_ota_server::(stdio, ota_writer).await? + let ota_writer = hal_espressif::EspOtaWriter::new(); + ota::run_ota_server::(stdio, ota_writer).await? } }; Ok(()) diff --git a/src/main.rs b/src/main.rs index 6291981..41763bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,15 +10,15 @@ use ssh_stamp::{ config::SSHStampConfig, espressif::{ buffered_uart::{BufferedUart, UART_BUF, UartPins, uart_task}, - net, rng, + net, }, handle, serve, settings::UART_BUFFER_SIZE, }; +use hal_espressif::flash; #[cfg(feature = "sftp-ota")] -use ota::otatraits::OtaActions; -use storage::flash; +use ota::traits::OtaActions; use sunset_async::{SSHServer, SunsetMutex}; @@ -80,14 +80,14 @@ async fn main(spawner: Spawner) -> ! { // System init let peripherals = esp_hal::init(esp_hal::Config::default()); let rng = Rng::new(); - rng::register_custom_rng(rng); + ssh_stamp::espressif::register_custom_rng(rng); debug!("Initialising flash "); flash::init(peripherals.FLASH); #[cfg(feature = "sftp-ota")] { - storage::esp_ota::OtaWriter::try_validating_current_ota_partition() + hal_espressif::EspOtaWriter::try_validating_current_ota_partition() .await .expect("Failed to validate the current ota partition"); } diff --git a/src/settings.rs b/src/settings.rs index f871e59..d745f47 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -9,7 +9,7 @@ use core::net::Ipv4Addr; pub(crate) const DEFAULT_SSID: &str = "ssh-stamp"; //pub(crate) const SSH_SERVER_ID: &str = "SSH-2.0-ssh-stamp-0.1"; pub(crate) const KEY_SLOTS: usize = 1; // TODO: Document whether this a "reasonable default"? Justify why? -pub(crate) const DEFAULT_IP: &Ipv4Addr = &Ipv4Addr::new(192, 168, 4, 1); +pub(crate) const DEFAULT_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 4, 1); // WiFi password generation pub(crate) const WIFI_PASSWORD_CHARS: &[u8; 62] = From b1b01a780cd1505e11d0bb77cac859007a91bae4 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:13:06 +0100 Subject: [PATCH 08/14] General architecture document for this refactor, following Matklad's lead: https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html --- docs/ARCHITECTURE.md | 223 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b1f7b06 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,223 @@ +# ARCHITECTURE.md + +## Bird's Eye View + +SSH-Stamp is firmware for turning microcontrollers into SSH-accessible serial bridges. You connect via SSH to the device, and your terminal session is bridged directly to the device's UART—perfect for debugging embedded systems remotely. + +The firmware runs on ESP32 family microcontrollers (ESP32, ESP32-C3, ESP32-C6, ESP32-S2, ESP32-S3) and creates a WiFi access point. When you SSH into the device, you get a shell that's directly connected to the target's serial port. + +## Architecture Philosophy + +**Platform-agnostic core, platform-specific HAL.** The main firmware (`src/`) knows nothing about ESP32 hardware. All hardware concerns flow through the Hardware Abstraction Layer (`hal/`). This separation means porting to other microcontrollers (i.e: Nordic nRF, STM32, RP2040) requires only implementing the HAL traits in a new `hal-*` crate. + +**Traits define contracts.** Each peripheral category (UART, WiFi, Flash, etc.) has a trait in `hal/src/traits/`. Implementations live in platform-specific crates like `hal-espressif/`. + +**State machine drives the application.** The firmware uses a Hierarchical State Machine (HSM). States like "WaitingForWiFi", "SSHConnected", and "Bridging" encode what's configured and what's running at any moment. + +## Crate Structure + +``` +ssh-stamp/ +├── hal/ # Platform-agnostic trait definitions +├── hal-espressif/ # ESP32 implementation of hal traits +├── ota/ # SFTP-based OTA update server +└── src/ # Main firmware (platform-agnostic) +``` + +### `hal/` — Trait Definitions + +The `hal` crate defines *what* a hardware platform must provide, not *how*. It contains traits and configuration structs, no implementations. + +Key entities: +- `HalPlatform` — Bundles all peripherals together. Entry point for hardware initialization. +- `UartHal` — Async read/write for serial communication. +- `WifiHal` — WiFi access point creation and TCP socket management. +- `FlashHal` — Raw flash read/write/erase operations. +- `OtaActions` — OTA partition management (validate, write, finalize, reset). +- `HashHal` — HMAC-SHA256 for secure operations. +- `RngHal` — Hardware random number generation. +- `ExecutorHal` — Async runtime with interrupt priority management. + +Files to read first: +- `hal/src/lib.rs` — The `HalPlatform` trait that ties everything together. +- `hal/src/traits/uart.rs` — Example of a simple trait. +- `hal/src/traits/network/wifi.rs` — Example of a more complex trait with associated types. + +### `hal-espressif/` — ESP32 Implementation + +Implements all `hal` traits for ESP32 family chips using `esp-hal`, `esp-radio`, and related crates. + +Key entities: +- `EspHalPlatform` — Implements `HalPlatform`. Contains the full hardware bundle. +- `EspUart` — Uses `esp-hal::uart` with DMA buffering. +- `EspWifi` — Uses `esp-radio::wifi` for AP mode. +- `EspFlash` — Uses `esp-storage` for flash access. + +Files to read first: +- `hal-espressif/src/lib.rs` — See how peripherals are bundled. +- `hal-espressif/src/config.rs` — Per-target default UART/WiFi pin configurations. + +### `ota/` — OTA Update Server + +Platform-agnostic SFTP server implementation that receives firmware updates. Uses `OtaActions` trait from `hal` to write to flash and reboot. + +Key entities: +- `OtaWriter` — Implements `OtaActions` for the platform. +- `run_ota_server()` — Main OTA server loop. + +Files to read first: +- `ota/src/lib.rs` — Public API. + +### `src/` — Main Firmware + +Platform-agnostic application code. Contains the HSM state machine, SSH handling, and configuration management. + +Key entities: +- `main.rs` — HSM state definitions and transitions. Start here. +- `handle.rs` — SSH event handlers (authentication, channels, environment variables). +- `serve.rs` — SSH connection loop orchestration. +- `config.rs` — Configuration struct stored in flash. +- `serial.rs` — Serial bridge logic (SSH ↔ UART). + +Files to read first: +- `src/main.rs` — See the HSM states and how they transition. +- `src/handle.rs` — Understand what happens when you send SSH commands. + +## Configuration System + +Configuration is stored in flash and loaded at boot: + +``` +SSHStampConfig { + hostkey: Ed25519PrivateKey, // Generated on first boot + pubkeys: [Option; N], // Allowed SSH public keys + wifi_ssid: String, // AP name + wifi_pw: Option, // AP password (None = open) + mac: [u8; 6], // MAC address (or 0xFF for random) + first_login: bool, // First boot? Enables key provisioning +} +``` + +On first boot (`first_login = true`), the device accepts any SSH connection with an empty password. The client sends their public key via SSH environment variables, which gets stored. Subsequent connections require that key for authentication. + +## Architectural Invariants + +**No direct hardware dependencies in `src/`.** The main firmware crate never imports `esp-hal`, `esp-radio`, or similar. All hardware access goes through `hal::HalPlatform` or its trait methods. + +**State machine owns all peripherals.** Peripherals are passed into the HSM at initialization and flow through states. No global mutable state for hardware resources. + +**UART is exclusive resource.** Only one `UartHal` instance exists. The serial bridge takes ownership when SSH session starts. + +**Flash operations are async.** All flash read/write/erase operations return futures. The `FlashHal` trait abstracts platform-specific blocking or non-blocking implementations. + +**OTA is optional.** The `sftp-ota` feature flag enables SFTP-based firmware updates. Without it, the firmware is smaller and simpler. + +## Boundaries Between Layers + +### `hal/` ↔ `hal-espressif/` + +The boundary is defined by traits. `hal/` contains trait definitions and configuration structs. `hal-espressif/` contains implementations. Adding a new platform (e.g., `hal-nordic/`) requires implementing all traits but no changes to `hal/`. + +### `hal` ↔ `ota` + +The `ota` crate depends on `hal::OtaActions` trait. It knows nothing about flash partitions, ESP-IDF, or ESP-specific OTA. The platform implementation (`hal-espressif/src/flash.rs`) handles partition management. + +### `src/handle.rs` ↔ `sunset` + +`handle.rs` contains handlers for `ServEvent` enums from the `sunset` SSH library. The boundary is defined by the `ServEvent` type. Handlers extract SSH payloads and route to appropriate subsystems (config update, UART bridge, etc.). + +## Cross-Cutting Concerns + +### Error Handling + +All HAL operations return `Result`. The `HalError` enum in `hal/src/error.rs` aggregates all platform error types. Platform-specific error details are converted to common variants (e.g., `UartError::Read` → `HalError::Uart(UartError::Read)`). + +### Async Runtime + +Uses `embassy-executor` for async task scheduling. The `ExecutorHal` trait provides access to the spawner and handles interrupt priority configuration. ESP32 uses `esp-rtos` as the Embassy runtime backend. + +### Logging + +Uses `log` crate facade. Platform implementations wire up appropriate backends (`esp-println` for ESP32). Log levels are configurable at compile time. + +### Feature Flags + +Root `Cargo.toml` defines unified feature flags: +- `target-esp32`, `target-esp32c6`, etc. — Select target MCU +- `sftp-ota` — Enable SFTP-based OTA updates + +Target selection propagates to `hal-espressif` which enables corresponding `esp-hal` features. + +## Adding New Hardware Support + +To port to a new microcontroller family: + +1. Create `hal-yourplatform/` crate +2. Implement all traits from `hal/src/traits/` +3. Create `YourPlatformHalPlatform` struct implementing `HalPlatform` +4. Add feature flag in root `Cargo.toml` +5. Add per-target default configs in `hal-yourplatform/src/config.rs` + +No changes needed in `src/` or `hal/`. + +## Common Tasks + +### Adding a new SSH environment variable handler + +Edit `src/handle.rs`. Find `session_env()` function. Add a new match arm for your variable name: + +```rust +"SSH_STAMP_NEW_VAR" => { + let mut config_guard = config.lock().await; + // Handle the new variable + a.succeed()?; + *ctx.config_changed = true; +} +``` + +### Adding a new UART configuration option + +1. Add field to `UartConfig` in `hal/src/config.rs` +2. Update `UartHal` trait if needed (`hal/src/traits/uart.rs`) +3. Implement in `hal-espressif/src/uart.rs` +4. Update per-target defaults in `hal-espressif/src/config.rs` + +### Adding a new HSM state + +1. Define state struct in `src/main.rs` implementing a state trait +2. Add transition logic to parent state +3. Handle entry/exit conditions +4. Wire up any event handlers needed + +## Key Files Quick Reference + +| What | Where | +|------|-------| +| SSH event handling | `src/handle.rs` | +| HSM states | `src/main.rs` | +| Hardware traits | `hal/src/traits/*.rs` | +| ESP32 implementations | `hal-espressif/src/*.rs` | +| Default pin configs | `hal-espressif/src/config.rs` | +| Configuration struct | `src/config.rs` | +| Serial bridge logic | `src/serial.rs` | +| OTA server | `ota/src/lib.rs` | + +## Dependencies + +Key external crates: +- `sunset` / `sunset-async` — SSH protocol implementation +- `embassy-sync` / `embassy-executor` — Async primitives and runtime +- `esp-hal` — ESP32 peripheral access (only in `hal-espressif/`) +- `esp-radio` — ESP32 WiFi (only in `hal-espressif/`) +- `heapless` — No-std collections (String, Vec) +- `heapless` — Stack-allocated collections + +## Testing + +Currently no automated tests. Manual testing requires: +1. Hardware target (ESP32 dev board) +2. WiFi client to connect to AP +3. SSH client to test connection +4. Serial device connected to UART pins for bridge testing + +Focus on architecture correctness over test coverage for now. \ No newline at end of file From 86cefd2d96c8bf97de58e382cde28fe71383e2b9 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:31:11 +0100 Subject: [PATCH 09/14] Add Rustdocs to public hal methods, use async traits... --- hal/src/config.rs | 28 ++++++ hal/src/error.rs | 42 +++++++++ hal/src/lib.rs | 83 +++++++++++++++++- hal/src/traits/executor.rs | 49 +++++++++-- hal/src/traits/flash.rs | 133 ++++++++++++++++++++++++++--- hal/src/traits/hash.rs | 46 +++++++++- hal/src/traits/mod.rs | 7 +- hal/src/traits/network/ethernet.rs | 31 ++++++- hal/src/traits/network/mod.rs | 2 + hal/src/traits/network/wifi.rs | 34 +++++++- hal/src/traits/rng.rs | 39 ++++++++- hal/src/traits/timer.rs | 35 +++++++- hal/src/traits/uart.rs | 68 +++++++++++++-- 13 files changed, 552 insertions(+), 45 deletions(-) diff --git a/hal/src/config.rs b/hal/src/config.rs index 33f8ba3..ce76c85 100644 --- a/hal/src/config.rs +++ b/hal/src/config.rs @@ -2,18 +2,36 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Hardware configuration types. +//! +//! This module provides configuration structures for initializing HAL peripherals. + +/// Top-level hardware configuration. +/// +/// Contains all peripheral-specific configurations needed to initialize +/// a complete hardware platform. #[derive(Clone, Debug)] pub struct HardwareConfig { + /// UART configuration. pub uart: UartConfig, + /// WiFi access point configuration. pub wifi: WifiApConfigStatic, } +/// UART peripheral configuration. +/// +/// Defines pin assignments and baud rate for a UART interface. #[derive(Clone, Debug)] pub struct UartConfig { + /// TX pin number. pub tx_pin: u8, + /// RX pin number. pub rx_pin: u8, + /// CTS (Clear To Send) pin for hardware flow control. pub cts_pin: Option, + /// RTS (Ready To Send) pin for hardware flow control. pub rts_pin: Option, + /// Baud rate in bits per second. pub baud_rate: u32, } @@ -29,11 +47,19 @@ impl Default for UartConfig { } } +/// WiFi access point configuration (static). +/// +/// Contains settings for running the device as a WiFi access point. +/// Uses `heapless::String` for `no_std` compatibility. #[derive(Clone, Debug)] pub struct WifiApConfigStatic { + /// Network name (SSID), max 32 characters. pub ssid: heapless::String<32>, + /// Optional WPA2 password, max 63 characters. pub password: Option>, + /// WiFi channel (1-14 for 2.4GHz). pub channel: u8, + /// MAC address for the access point interface. pub mac: [u8; 6], } @@ -48,8 +74,10 @@ impl Default for WifiApConfigStatic { } } +/// Ethernet interface configuration. #[derive(Clone, Debug)] pub struct EthernetConfig { + /// MAC address for the Ethernet interface. pub mac: [u8; 6], } diff --git a/hal/src/error.rs b/hal/src/error.rs index 1772a77..4ee613e 100644 --- a/hal/src/error.rs +++ b/hal/src/error.rs @@ -2,54 +2,96 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! HAL error types. +//! +//! This module defines error enums for all HAL operations. Each error type +//! corresponds to a specific peripheral or operation category. + use core::fmt; +/// Unified HAL error type. +/// +/// Aggregates all peripheral-specific errors into a single enum for +/// ergonomic error handling across the HAL. #[derive(Debug)] pub enum HalError { + /// Configuration error (invalid settings). Config, + /// UART peripheral error. Uart(UartError), + /// WiFi peripheral error. Wifi(WifiError), + /// Flash storage error. Flash(FlashError), + /// Random number generator error. Rng, + /// Hash/HMAC computation error. Hash(HashError), + /// Timer error. Timer, + /// Async executor error. Executor, } +/// UART-specific errors. #[derive(Debug)] pub enum UartError { + /// Invalid UART configuration. Config, + /// Receive buffer overflow (data lost). BufferOverflow, + /// Read operation failed. Read, + /// Write operation failed. Write, } +/// WiFi-specific errors. #[derive(Debug)] pub enum WifiError { + /// WiFi hardware initialization failed. Initialization, + /// Failed to create socket. SocketCreate, + /// Failed to accept connection. SocketAccept, + /// Socket read failed. SocketRead, + /// Socket write failed. SocketWrite, + /// Socket close failed. SocketClose, + /// DHCP client error. Dhcpc, } +/// Flash storage errors. #[derive(Debug)] pub enum FlashError { + /// Read operation failed. Read, + /// Write operation failed. Write, + /// Erase operation failed. Erase, + /// Requested partition not found. PartitionNotFound, + /// OTA partition validation failed. ValidationFailed, + /// Failed to load configuration from flash. ConfigLoad, + /// Failed to save configuration to flash. ConfigSave, + /// Internal flash controller error. InternalError, } +/// Hash/HMAC computation errors. #[derive(Debug)] pub enum HashError { + /// Invalid hash/HMAC configuration. Config, + /// Hash computation failed. Compute, } diff --git a/hal/src/lib.rs b/hal/src/lib.rs index f49bd8d..ea288d5 100644 --- a/hal/src/lib.rs +++ b/hal/src/lib.rs @@ -2,6 +2,33 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! # Hardware Abstraction Layer (HAL) +//! +//! This crate provides platform-agnostic hardware abstraction traits for embedded systems. +//! Each supported platform (ESP32, nRF52, etc.) implements these traits in separate crates. +//! +//! ## Overview +//! +//! The HAL is organized around several key concepts: +//! +//! - [`HalPlatform`]: Top-level trait bundling all peripherals together +//! - Peripheral traits: [`UartHal`], [`WifiHal`], [`RngHal`], [`FlashHal`], [`HashHal`], [`TimerHal`], [`ExecutorHal`] +//! - Configuration: [`HardwareConfig`], [`UartConfig`], [`WifiApConfigStatic`], [`EthernetConfig`] +//! - Error handling: [`HalError`] with variants for each peripheral type +//! +//! ## Usage +//! +//! Platform implementations (e.g., `hal-espressif`) implement these traits, and applications +//! use the trait bounds to write platform-agnostic code. +//! +//! ```ignore +//! use hal::{HalPlatform, HardwareConfig}; +//! +//! async fn init_peripherals(config: HardwareConfig) -> Result { +//! P::init(config, spawner).await +//! } +//! ``` + #![no_std] pub mod config; @@ -15,20 +42,64 @@ pub use traits::*; use core::future::Future; use embassy_executor::Spawner; -/// Platform abstraction bundling all HAL peripherals +/// Platform abstraction bundling all HAL peripherals. /// /// Each platform implementation (ESP32, Nordic nRF, etc.) implements this trait /// to provide access to all hardware peripherals needed by the firmware. +/// +/// # Example Implementation +/// +/// ```ignore +/// struct EspPlatform { +/// uart: EspUart, +/// wifi: EspWifi, +/// // ... +/// } +/// +/// impl HalPlatform for EspPlatform { +/// type Uart = EspUart; +/// // ... +/// +/// async fn init(config: HardwareConfig, spawner: Spawner) -> Result { +/// // Initialize hardware... +/// } +/// } +/// ``` pub trait HalPlatform { + /// UART peripheral implementation. type Uart: UartHal; + + /// WiFi peripheral implementation. type Wifi: WifiHal; + + /// Random number generator implementation. type Rng: RngHal; + + /// Flash storage implementation. type Flash: FlashHal; + + /// Hash/HMAC implementation. type Hash: HashHal; + + /// Timer implementation. type Timer: TimerHal; + + /// Async executor implementation. type Executor: ExecutorHal; - /// Initialize all peripherals with given configuration + /// Initialize all peripherals with given configuration. + /// + /// This method should be called once at startup to configure and instantiate + /// all hardware peripherals. + /// + /// # Arguments + /// + /// * `config` - Hardware configuration including pin assignments and settings + /// * `spawner` - Embassy executor spawner for spawning async tasks + /// + /// # Returns + /// + /// Returns `Ok(Self)` on successful initialization, or `Err(HalError)` on failure. fn init( config: HardwareConfig, spawner: Spawner, @@ -36,9 +107,13 @@ pub trait HalPlatform { where Self: Sized; - /// Perform hardware reset + /// Perform hardware reset. + /// + /// This function never returns as it triggers a system restart. fn reset() -> !; - /// Get MAC address from hardware (eFuse or similar) + /// Get MAC address from hardware (eFuse or similar). + /// + /// Returns the factory-programmed MAC address for the device. fn mac_address() -> [u8; 6]; } diff --git a/hal/src/traits/executor.rs b/hal/src/traits/executor.rs index e3de2c6..33eda10 100644 --- a/hal/src/traits/executor.rs +++ b/hal/src/traits/executor.rs @@ -2,24 +2,61 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Async runtime/executor operations trait. + use core::future::Future; use embassy_executor::Spawner; -/// Async runtime/executor operations +/// Async executor hardware abstraction. /// /// Provides access to the async runtime and interrupt management. -/// Implementations wrap platform-specific executors (e.g., esp-rtos for ESP32). +/// Implementations wrap platform-specific executors (e.g., embassy-executor for ESP32). +/// +/// # Example +/// +/// ```ignore +/// use embassy_executor::Spawner; +/// +/// async fn run_app(executor: &E) { +/// let spawner = executor.spawner(); +/// spawner.spawn(my_task()).ok(); +/// executor.run(async { +/// // main application loop +/// }); +/// } +/// ``` pub trait ExecutorHal { - /// Get the spawner for spawning async tasks + /// Get the spawner for spawning async tasks. + /// + /// Returns a reference to the executor's spawner, which can be used + /// to spawn additional async tasks. fn spawner(&self) -> &Spawner; - /// Run the executor with the main future (blocking) + /// Run the executor with the main future. + /// + /// Blocks forever, running the executor and processing the main future. + /// This method never returns. + /// + /// # Arguments + /// + /// * `main_future` - The main async task to run (typically the application entry point). fn run>(&self, main_future: F) -> !; - /// Set interrupt priority + /// Set interrupt priority. + /// + /// Configures the priority level for a specific interrupt. Lower priority + /// numbers typically mean higher priority (platform-specific). + /// + /// # Arguments + /// + /// * `irq` - Interrupt number/identifier. + /// * `priority` - Priority level (platform-specific interpretation). fn set_interrupt_priority(&self, irq: usize, priority: u8); - /// Get current core ID (for multi-core systems) + /// Get current core ID. + /// + /// Returns the ID of the currently executing core. Useful for + /// multi-core systems where different cores may need different handling. fn core_id(&self) -> u8; } diff --git a/hal/src/traits/flash.rs b/hal/src/traits/flash.rs index 94ca7cd..c5ab1f1 100644 --- a/hal/src/traits/flash.rs +++ b/hal/src/traits/flash.rs @@ -2,49 +2,156 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Flash storage and OTA update traits. + use core::future::Future; use crate::HalError; -/// Flash storage operations +/// Flash storage operations. +/// +/// Provides read/write/erase operations for non-volatile flash memory. +/// Flash must be erased before writing; see [`Self::erase`] for details. +/// +/// # Example +/// +/// ```ignore +/// async fn write_config(flash: &F, config: &[u8]) -> Result<(), HalError> { +/// let offset = 0x1000; +/// flash.erase(offset, config.len() as u32).await?; +/// flash.write(offset, config).await?; +/// Ok(()) +/// } +/// ``` pub trait FlashHal { - /// Read from flash at offset + /// Read from flash at offset. + /// + /// Reads `buf.len()` bytes starting at `offset` into the buffer. + /// + /// # Arguments + /// + /// * `offset` - Byte offset within flash to read from. + /// * `buf` - Destination buffer for read data. + /// + /// # Returns + /// + /// `Ok(())` on success, or a [`HalError::Flash`] error on failure. fn read(&self, offset: u32, buf: &mut [u8]) -> impl Future>; - /// Write to flash at offset (must be erased first) + /// Write to flash at offset. + /// + /// Writes data to flash. The target region must be erased first + /// (flash cannot transition from 0 to 1 bits without erasing). + /// + /// # Arguments + /// + /// * `offset` - Byte offset within flash to write to. + /// * `buf` - Data to write. + /// + /// # Returns + /// + /// `Ok(())` on success, or a [`HalError::Flash`] error on failure. fn write(&self, offset: u32, buf: &[u8]) -> impl Future>; - /// Erase flash region + /// Erase flash region. + /// + /// Sets all bits in the region to 1 (ready for writing). + /// Flash must be erased before writing; writes can only change 1s to 0s. + /// + /// # Arguments + /// + /// * `offset` - Byte offset within flash to start erasing. + /// * `len` - Number of bytes to erase (will be rounded up to erase block size). + /// + /// # Returns + /// + /// `Ok(())` on success, or a [`HalError::Flash`] error on failure. fn erase(&self, offset: u32, len: u32) -> impl Future>; - /// Get flash storage size in bytes + /// Get flash storage size in bytes. + /// + /// Returns the total size of the flash storage region available + /// for application use. fn size(&self) -> u32; } -/// OTA update operations +/// OTA update operations. /// /// Implementations handle platform-specific partition management /// for Over-The-Air firmware updates. +/// +/// # Safety +/// +/// OTA updates can brick the device if interrupted or corrupted. +/// Implementations should validate firmware before marking as bootable. +/// +/// # Example +/// +/// ```ignore +/// async fn perform_ota(ota: &mut O, firmware: &[u8]) -> Result<(), HalError> { +/// O::try_validating_current_ota_partition().await?; +/// let size = O::get_ota_partition_size().await?; +/// +/// for (offset, chunk) in firmware.chunks(4096).enumerate() { +/// ota.write_ota_data((offset * 4096) as u32, chunk).await?; +/// } +/// +/// ota.finalize_ota_update().await?; +/// ota.reset_device(); +/// } +/// ``` pub trait OtaActions { - /// Validate the current OTA partition + /// Validate the current OTA partition. /// - /// Mark the current OTA slot as VALID - this is only needed if the bootloader - /// was built with auto-rollback support. + /// Marks the current OTA slot as VALID. This is only needed if the bootloader + /// was built with auto-rollback support to prevent reverting to a failed update. + /// + /// # Returns + /// + /// `Ok(())` on successful validation, or an error if validation fails. fn try_validating_current_ota_partition() -> impl Future> + Send; - /// Get size of OTA partition in bytes + /// Get size of OTA partition in bytes. + /// + /// Returns the total size available for new firmware in the update partition. + /// + /// # Returns + /// + /// Partition size in bytes on success, or an error if partition not found. fn get_ota_partition_size() -> impl Future> + Send; - /// Write data to OTA partition at offset + /// Write data to OTA partition at offset. + /// + /// Writes a chunk of firmware data to the update partition. + /// Call this repeatedly for each chunk of the firmware image. + /// + /// # Arguments + /// + /// * `offset` - Byte offset within the partition. + /// * `data` - Firmware data chunk to write. + /// + /// # Returns + /// + /// `Ok(())` on success, or an error on write failure. fn write_ota_data( &self, offset: u32, data: &[u8], ) -> impl Future> + Send; - /// Finalize OTA update and mark for boot + /// Finalize OTA update and mark for boot. + /// + /// Validates the written firmware and marks the partition as bootable. + /// After calling this, [`Self::reset_device`] should be called to boot into the new firmware. + /// + /// # Returns + /// + /// `Ok(())` on success, or an error if validation or marking fails. fn finalize_ota_update(&mut self) -> impl Future> + Send; - /// Reset device to boot into new partition + /// Reset device to boot into new partition. + /// + /// Triggers a system reset. The bootloader will boot from the newly marked + /// OTA partition. This function never returns. fn reset_device(&self) -> !; } diff --git a/hal/src/traits/hash.rs b/hal/src/traits/hash.rs index 89dede2..8ade8f1 100644 --- a/hal/src/traits/hash.rs +++ b/hal/src/traits/hash.rs @@ -2,13 +2,42 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Hash/HMAC operations trait. + use core::future::Future; use crate::HalError; -/// Hash/HMAC operations +/// Hash/HMAC hardware abstraction. +/// +/// Provides cryptographic hash functions. Implementations may use +/// hardware accelerators or software implementations depending on +/// platform capabilities. +/// +/// # Example +/// +/// ```ignore +/// async fn compute_hash(hash: &mut H, data: &[u8]) -> Result<[u8; 32], HalError> { +/// let mut output = [0u8; 32]; +/// hash.sha256(data, &mut output).await?; +/// Ok(output) +/// } +/// ``` pub trait HashHal { - /// Compute HMAC-SHA256 + /// Compute HMAC-SHA256. + /// + /// Computes Hash-based Message Authentication Code using SHA256. + /// Useful for message authentication and key derivation. + /// + /// # Arguments + /// + /// * `key` - Secret key for HMAC. + /// * `message` - Data to authenticate. + /// * `output` - Output buffer for 32-byte HMAC result. + /// + /// # Returns + /// + /// `Ok(())` on success with result in `output`, or an error on failure. fn hmac_sha256( &mut self, key: &[u8], @@ -16,7 +45,18 @@ pub trait HashHal { output: &mut [u8; 32], ) -> impl Future>; - /// Compute SHA256 + /// Compute SHA256. + /// + /// Computes the SHA256 hash of the input message. + /// + /// # Arguments + /// + /// * `message` - Data to hash. + /// * `output` - Output buffer for 32-byte hash result. + /// + /// # Returns + /// + /// `Ok(())` on success with result in `output`, or an error on failure. fn sha256( &mut self, message: &[u8], diff --git a/hal/src/traits/mod.rs b/hal/src/traits/mod.rs index 1f0ac30..d4510f9 100644 --- a/hal/src/traits/mod.rs +++ b/hal/src/traits/mod.rs @@ -2,6 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Peripheral trait definitions. +//! +//! This module re-exports all HAL trait definitions. Each trait provides +//! an abstract interface for a specific hardware peripheral. + mod executor; mod flash; mod hash; @@ -16,4 +21,4 @@ pub use hash::HashHal; pub use network::{EthernetHal, WifiHal}; pub use rng::RngHal; pub use timer::TimerHal; -pub use uart::UartHal; +pub use uart::UartHal; \ No newline at end of file diff --git a/hal/src/traits/network/ethernet.rs b/hal/src/traits/network/ethernet.rs index 3e21068..146777a 100644 --- a/hal/src/traits/network/ethernet.rs +++ b/hal/src/traits/network/ethernet.rs @@ -2,12 +2,39 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Ethernet trait for wired networking. + use core::future::Future; use crate::{EthernetConfig, HalError}; -/// Ethernet hardware abstraction +/// Ethernet hardware abstraction. +/// +/// Provides configuration and control of Ethernet PHY/MAC. +/// Implementations manage the underlying Ethernet controller. +/// +/// # Example +/// +/// ```ignore +/// async fn setup_ethernet(eth: &mut E) -> Result<(), HalError> { +/// let config = EthernetConfig { +/// mac: [0x02, 0x03, 0x04, 0x05, 0x06, 0x07], +/// }; +/// eth.init(config).await +/// } +/// ``` pub trait EthernetHal { - /// Initialize Ethernet with given configuration + /// Initialize Ethernet with given configuration. + /// + /// Configures the Ethernet peripheral with the specified MAC address + /// and brings up the network interface. + /// + /// # Arguments + /// + /// * `config` - Ethernet configuration (primarily MAC address). + /// + /// # Returns + /// + /// `Ok(())` on success, or a [`HalError`] error on failure. fn init(&mut self, config: EthernetConfig) -> impl Future>; } diff --git a/hal/src/traits/network/mod.rs b/hal/src/traits/network/mod.rs index 2879403..e97139e 100644 --- a/hal/src/traits/network/mod.rs +++ b/hal/src/traits/network/mod.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Network peripheral traits. + mod ethernet; mod wifi; diff --git a/hal/src/traits/network/wifi.rs b/hal/src/traits/network/wifi.rs index a329d1c..ba42a5e 100644 --- a/hal/src/traits/network/wifi.rs +++ b/hal/src/traits/network/wifi.rs @@ -2,13 +2,43 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! WiFi trait for access point mode. + use core::future::Future; use crate::{HalError, WifiApConfigStatic}; -/// WiFi hardware abstraction for access point mode +/// WiFi hardware abstraction for access point mode. +/// +/// Provides configuration and control of WiFi hardware in AP mode. +/// Implementations manage the underlying WiFi radio and TCP/IP stack. +/// +/// # Example +/// +/// ```ignore +/// async fn setup_wifi(wifi: &mut W) -> Result<(), HalError> { +/// let config = WifiApConfigStatic { +/// ssid: heapless::String::from("MyDevice"), +/// password: Some(heapless::String::from("secretpass")), +/// channel: 6, +/// mac: [0x02, 0x03, 0x04, 0x05, 0x06, 0x07], +/// }; +/// wifi.start_ap(config).await +/// } +/// ``` pub trait WifiHal { - /// Start WiFi access point with given configuration + /// Start WiFi access point with given configuration. + /// + /// Initializes the WiFi radio and starts broadcasting an access point + /// with the specified SSID, password, and channel. + /// + /// # Arguments + /// + /// * `config` - AP configuration including SSID, password, channel, and MAC. + /// + /// # Returns + /// + /// `Ok(())` on success, or a [`HalError::Wifi`] error on failure. fn start_ap( &mut self, config: WifiApConfigStatic, diff --git a/hal/src/traits/rng.rs b/hal/src/traits/rng.rs index d3b06ab..3deb5a1 100644 --- a/hal/src/traits/rng.rs +++ b/hal/src/traits/rng.rs @@ -2,16 +2,47 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Random number generation trait. + use core::future::Future; use crate::HalError; -/// Random number generation +/// Random number generation hardware abstraction. +/// +/// Provides cryptographically secure random number generation. +/// Implementations typically wrap hardware TRNG peripherals. +/// +/// # Example +/// +/// ```ignore +/// async fn generate_nonce(rng: &mut R) -> Result<[u8; 16], HalError> { +/// let mut buf = [0u8; 16]; +/// rng.fill_bytes(&mut buf).await?; +/// Ok(buf) +/// } +/// ``` pub trait RngHal { - /// Fill buffer with random bytes + /// Fill buffer with random bytes. + /// + /// Generates cryptographically secure random bytes and fills the buffer. + /// + /// # Arguments + /// + /// * `buf` - Buffer to fill with random bytes. + /// + /// # Returns + /// + /// `Ok(())` on success, or an error if generation fails. fn fill_bytes(&mut self, buf: &mut [u8]) -> impl Future>; - /// Generate a random u32 + /// Generate a random u32. + /// + /// Convenience method that generates 4 random bytes and converts to u32. + /// + /// # Returns + /// + /// Random u32 value on success, or an error if generation fails. fn random_u32(&mut self) -> impl Future> { async { let mut buf = [0u8; 4]; @@ -19,4 +50,4 @@ pub trait RngHal { Ok(u32::from_le_bytes(buf)) } } -} +} \ No newline at end of file diff --git a/hal/src/traits/timer.rs b/hal/src/traits/timer.rs index baa645f..acacf39 100644 --- a/hal/src/traits/timer.rs +++ b/hal/src/traits/timer.rs @@ -2,18 +2,45 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! Timer operations trait. + use core::future::Future; -/// Timer operations +/// Timer hardware abstraction. +/// +/// Provides time measurement and delays. Implementations typically wrap +/// system tick timers or RTOS timer facilities. +/// +/// # Example +/// +/// ```ignore +/// async fn measure_time(timer: &T) -> u64 { +/// let start = timer.now_millis(); +/// some_operation().await; +/// timer.now_millis() - start +/// } +/// ``` pub trait TimerHal { - /// Get current time in microseconds since boot + /// Get current time in microseconds since boot. + /// + /// Returns a monotonically increasing counter of microseconds since + /// system startup. May wrap around on long-running systems. fn now_micros(&self) -> u64; - /// Get current time in milliseconds since boot + /// Get current time in milliseconds since boot. + /// + /// Convenience wrapper around [`Self::now_micros`] with millisecond resolution. fn now_millis(&self) -> u64 { self.now_micros() / 1000 } - /// Wait for specified duration in milliseconds + /// Wait for specified duration. + /// + /// Asynchronously waits for the specified number of milliseconds. + /// This is an async operation that yields to the executor. + /// + /// # Arguments + /// + /// * `millis` - Duration to wait in milliseconds. fn delay(&self, millis: u64) -> impl Future; } diff --git a/hal/src/traits/uart.rs b/hal/src/traits/uart.rs index 741efd1..9811e11 100644 --- a/hal/src/traits/uart.rs +++ b/hal/src/traits/uart.rs @@ -2,26 +2,82 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +//! UART hardware abstraction trait. + use core::future::Future; use crate::{HalError, UartConfig}; -/// UART hardware abstraction +/// UART hardware abstraction. +/// +/// Provides asynchronous read/write operations for UART peripherals. +/// Implementations should handle buffering and interrupt management internally. +/// +/// # Example +/// +/// ```ignore +/// async fn echo(uart: &mut U) -> Result<(), HalError> { +/// let mut buf = [0u8; 64]; +/// let n = uart.read(&mut buf).await?; +/// uart.write(&buf[..n]).await?; +/// Ok(()) +/// } +/// ``` pub trait UartHal { - /// Read bytes into buffer, returns number of bytes read + /// Read bytes into buffer. + /// + /// Fills the buffer with received data and returns the number of bytes read. + /// This method waits until at least one byte is available. + /// + /// # Arguments + /// + /// * `buf` - Destination buffer for received data. + /// + /// # Returns + /// + /// Number of bytes read on success, or an error on failure. fn read(&mut self, buf: &mut [u8]) -> impl Future>; - /// Write bytes from buffer, returns number of bytes written + /// Write bytes from buffer. + /// + /// Transmits all bytes from the buffer. This method returns once all + /// bytes have been queued for transmission (may still be in hardware buffers). + /// + /// # Arguments + /// + /// * `buf` - Data to transmit. + /// + /// # Returns + /// + /// Number of bytes written on success (always equal to `buf.len()`), or an error. fn write(&mut self, buf: &[u8]) -> impl Future>; - /// Check if data is available to read + /// Check if data is available to read. + /// + /// Returns `true` if at least one byte is available in the receive buffer. + /// This is a non-blocking check useful for polling patterns. fn can_read(&self) -> bool; - /// Signal for async notification when data is available + /// Get async notification signal. + /// + /// Returns a signal that is signalled when data becomes available for reading. + /// This enables efficient async/await patterns where the caller can wait + /// for the signal instead of polling [`Self::can_read`]. fn signal( &self, ) -> &embassy_sync::signal::Signal; - /// Reconfigure UART with new settings + /// Reconfigure UART with new settings. + /// + /// Changes the UART configuration (baud rate, pins, etc.) at runtime. + /// This may temporarily disrupt ongoing transfers. + /// + /// # Arguments + /// + /// * `config` - New UART configuration to apply. + /// + /// # Returns + /// + /// `Ok(())` on success, or an error if configuration fails. fn reconfigure(&mut self, config: UartConfig) -> impl Future>; } From be343aff546c6184e6db17d33867668a15330a1c Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:31:26 +0100 Subject: [PATCH 10/14] Broken UART, need to fix and verify on-device. --- src/espressif/buffered_uart.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/espressif/buffered_uart.rs b/src/espressif/buffered_uart.rs index 57a06f2..08d583a 100644 --- a/src/espressif/buffered_uart.rs +++ b/src/espressif/buffered_uart.rs @@ -15,6 +15,7 @@ use esp_hal::peripherals::UART1; use esp_hal::system::software_reset; use esp_hal::uart::{Config, RxConfig, Uart}; use hal_espressif::EspUartPins; +use log::error; use sunset_async::SunsetMutex; /// Wait for UART buffer initialization @@ -66,10 +67,5 @@ pub async fn uart_task( error!("Uart config error {e}. Resetting."); software_reset(); } - }; - let hal_pins: EspUartPins = pins.into(); - let uart = uart.with_rx(hal_pins.rx).with_tx(hal_pins.tx).into_async(); - - // Run the main buffered TX/RX loop - uart_buf.run(uart).await; + } } From 48b20f2f35027cefc5087b3199a8b8e3eaae5f35 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:31:43 +0100 Subject: [PATCH 11/14] Cargo lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 154141c..aaf3196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,7 +1340,7 @@ dependencies = [ "embassy-futures", "embassy-net", "embassy-sync 0.7.2", - "embassy-time 0.5.0", + "embassy-time 0.5.1", "embedded-storage", "embedded-storage-async", "esp-bootloader-esp-idf", From fa7bd540833cdaacd23c140b12c907f127356d1a Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 15:31:55 +0100 Subject: [PATCH 12/14] fmt --- hal/src/traits/mod.rs | 2 +- hal/src/traits/rng.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hal/src/traits/mod.rs b/hal/src/traits/mod.rs index d4510f9..560d01a 100644 --- a/hal/src/traits/mod.rs +++ b/hal/src/traits/mod.rs @@ -21,4 +21,4 @@ pub use hash::HashHal; pub use network::{EthernetHal, WifiHal}; pub use rng::RngHal; pub use timer::TimerHal; -pub use uart::UartHal; \ No newline at end of file +pub use uart::UartHal; diff --git a/hal/src/traits/rng.rs b/hal/src/traits/rng.rs index 3deb5a1..aa09e8d 100644 --- a/hal/src/traits/rng.rs +++ b/hal/src/traits/rng.rs @@ -50,4 +50,4 @@ pub trait RngHal { Ok(u32::from_le_bytes(buf)) } } -} \ No newline at end of file +} From c91adcb726041c1683c5bf00b9a09296814a7257 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 16:14:34 +0100 Subject: [PATCH 13/14] Try to keep clippy happy and fix the chip definitions issue... --- Cargo.toml | 12 +++++++++--- hal-espressif/Cargo.toml | 2 +- hal/src/config.rs | 8 +------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 85f8f2d..bc15dfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ digest = { version = "0.10", default-features = false, features = [ # Storage embedded-storage = "0.3.1" embedded-storage-async = "0.4" -esp-storage = { version = "0.8.0" } +esp-storage = { version = "0.8" } esp-bootloader-esp-idf = { version = "0.4.0" } # Networking @@ -99,9 +99,9 @@ smoltcp = { workspace = true } embassy-time = { workspace = true } embedded-io-async = { workspace = true } esp-alloc = { version = "0.9.0" } -esp-backtrace = { version = "0.18.0", features = ["panic-handler", "println"] } +esp-backtrace = { version = "0.18", features = ["panic-handler", "println"] } esp-hal = { workspace = true } -hal-espressif = { path = "hal-espressif" } +hal-espressif = { path = "hal-espressif", default-features = false } esp-rtos = { version = "0.2.0", features = ["embassy", "log-04", "esp-radio"] } esp-radio = { version = "0.17.0", features = ["wifi", "log-04"] } esp-println = { version = "0.16", features = ["log-04"] } @@ -174,6 +174,7 @@ esp32 = [ "esp-println/esp32", "esp-storage/esp32", "esp-bootloader-esp-idf/esp32", + "hal-espressif/esp32", ] esp32c2 = [ "esp-hal/esp32c2", @@ -184,6 +185,7 @@ esp32c2 = [ "esp-println/esp32c2", "esp-storage/esp32c2", "esp-bootloader-esp-idf/esp32c2", + "hal-espressif/esp32c2", ] esp32c3 = [ "esp-hal/esp32c3", @@ -194,6 +196,7 @@ esp32c3 = [ "esp-println/esp32c3", "esp-storage/esp32c3", "esp-bootloader-esp-idf/esp32c3", + "hal-espressif/esp32c3", ] esp32c6 = [ "esp-hal/esp32c6", @@ -204,6 +207,7 @@ esp32c6 = [ "esp-println/esp32c6", "esp-storage/esp32c6", "esp-bootloader-esp-idf/esp32c6", + "hal-espressif/esp32c6", ] esp32s2 = [ "esp-hal/esp32s2", @@ -214,6 +218,7 @@ esp32s2 = [ "esp-println/esp32s2", "esp-storage/esp32s2", "esp-bootloader-esp-idf/esp32s2", + "hal-espressif/esp32s2", ] esp32s3 = [ "esp-hal/esp32s3", @@ -224,4 +229,5 @@ esp32s3 = [ "esp-println/esp32s3", "esp-storage/esp32s3", "esp-bootloader-esp-idf/esp32s3", + "hal-espressif/esp32s3", ] diff --git a/hal-espressif/Cargo.toml b/hal-espressif/Cargo.toml index 33c3b05..167d4c7 100644 --- a/hal-espressif/Cargo.toml +++ b/hal-espressif/Cargo.toml @@ -16,7 +16,7 @@ embassy-futures = { workspace = true } embassy-net = { workspace = true } heapless = { workspace = true } esp-hal = { workspace = true } -esp-storage = { version = "0.8.0" } +esp-storage = { version = "0.8" } esp-bootloader-esp-idf = { version = "0.4.0" } esp-radio = { version = "0.17.0", features = ["wifi", "log-04"] } embedded-storage = { workspace = true } diff --git a/hal/src/config.rs b/hal/src/config.rs index ce76c85..1abba89 100644 --- a/hal/src/config.rs +++ b/hal/src/config.rs @@ -75,14 +75,8 @@ impl Default for WifiApConfigStatic { } /// Ethernet interface configuration. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct EthernetConfig { /// MAC address for the Ethernet interface. pub mac: [u8; 6], } - -impl Default for EthernetConfig { - fn default() -> Self { - Self { mac: [0; 6] } - } -} From 6f26e9dc3628e4145e417ba4ab71fa609a211d51 Mon Sep 17 00:00:00 2001 From: brainstorm Date: Sat, 28 Mar 2026 16:30:41 +0100 Subject: [PATCH 14/14] Try to appease clippy ... --- hal-espressif/src/executor.rs | 4 +++- hal-espressif/src/network/wifi.rs | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/hal-espressif/src/executor.rs b/hal-espressif/src/executor.rs index 9c77e54..7f4ab45 100644 --- a/hal-espressif/src/executor.rs +++ b/hal-espressif/src/executor.rs @@ -29,7 +29,9 @@ impl ExecutorHal for EspExecutor { } fn run>(&self, _main_future: F) -> ! { - loop {} + loop { + core::hint::black_box(&_main_future); + } } fn set_interrupt_priority(&self, _irq: usize, _priority: u8) {} diff --git a/hal-espressif/src/network/wifi.rs b/hal-espressif/src/network/wifi.rs index 5405303..b016ab0 100644 --- a/hal-espressif/src/network/wifi.rs +++ b/hal-espressif/src/network/wifi.rs @@ -186,9 +186,7 @@ pub async fn init_wifi_ap( controller: Controller<'static>, wifi: WIFI<'static>, rng: Rng, - ssid: String<32>, - password: String<63>, - mac: [u8; 6], + config: WifiApConfigStatic, gw_ip: Ipv4Addr, ) -> Result, HalError> { let wifi_init = &*mk_static!(Controller<'static>, controller); @@ -197,13 +195,15 @@ pub async fn init_wifi_ap( .map_err(|_| HalError::Wifi(WifiError::Initialization))?; // Set MAC address - Efuse::set_mac_address(mac).map_err(|_| HalError::Config)?; + Efuse::set_mac_address(config.mac).map_err(|_| HalError::Config)?; let ap_config = ModeConfig::AccessPoint( AccessPointConfig::default() - .with_ssid(AllocString::from(ssid.as_str())) + .with_ssid(AllocString::from(config.ssid.as_str())) .with_auth_method(AuthMethod::Wpa2Wpa3Personal) - .with_password(AllocString::from(password.as_str())), + .with_password(AllocString::from( + config.password.as_ref().unwrap_or(&String::new()).as_str(), + )), ); let _res = wifi_controller.set_config(&ap_config); @@ -224,8 +224,16 @@ pub async fn init_wifi_ap( // Convert ssid and password to static - caller must ensure they live long enough // For now we use leak to make them 'static - let ssid_static: &'static str = Box::leak(ssid.as_str().to_string().into_boxed_str()); - let password_static: &'static str = Box::leak(password.as_str().to_string().into_boxed_str()); + let ssid_static: &'static str = Box::leak(config.ssid.as_str().to_string().into_boxed_str()); + let password_static: &'static str = Box::leak( + config + .password + .as_ref() + .unwrap_or(&String::new()) + .as_str() + .to_string() + .into_boxed_str(), + ); spawner .spawn(wifi_up(wifi_controller, ssid_static, password_static))