From 9e5bd0537f4ab4be983e77b58cbf7d3c862fc3a6 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Thu, 6 Mar 2025 16:53:11 +1100 Subject: [PATCH 1/6] Experimenting with separating uart logic to a separate embassy task and adding interrupt code later (to avoid overflow errors due to running too much logic on the "main" task) [ci skip] Co-authored-by: Angus Gratton --- src/espressif/mod.rs | 2 +- src/espressif/serial.rs | 60 +++++++++++++++++++++++++++++++++++++++++ src/serial.rs | 5 ++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/espressif/mod.rs b/src/espressif/mod.rs index 70aa075..2735db2 100644 --- a/src/espressif/mod.rs +++ b/src/espressif/mod.rs @@ -1,3 +1,3 @@ pub mod net; pub mod rng; -// pub mod serial; +pub mod serial; diff --git a/src/espressif/serial.rs b/src/espressif/serial.rs index 1b90511..3520308 100644 --- a/src/espressif/serial.rs +++ b/src/espressif/serial.rs @@ -11,3 +11,63 @@ // .with_tx(peripherals.GPIO10) // .into_async() // } + +use embassy_executor::Spawner; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pipe::Pipe}; +use embassy_futures::select::select; +use esp_hal::Async; +use esp_hal::uart::Uart; + +const RD_BUF_SZ: usize = 512; +const WR_BUF_SZ: usize = 256; + +struct BufferedUart<'a> { + uart: Uart<'a, Async>, + from_uart: Pipe, + to_uart: Pipe, +} + +impl BufferedUart<'static> { + pub fn uart_init(spawner: Spawner) -> Self { + + + } + + // Call this from inside the embassy Task + pub async fn run(&mut self) { + let (mut uart_rx, mut uart_tx) = self.uart.split(); + let mut uart_rx_buf = [0u8; 128]; + let mut uart_tx_buf = [0u8; 128]; + loop { + let rd_from = async { + loop { + let n = uart_rx.read(&mut uart_rx_buf).await.unwrap(); + self.from_uart.write_all(&uart_rx_buf[:n]).await.unwrap(); + } + }; + let rd_to = async { + loop { + self.to_uart.read(&mut uart_tx_buf); + uart_tx.write_all() + } + } + match select(rf_from, rd_to).await { + + } + } + } + + pub async fn read(&mut self, &mut buf: [u8]) -> usize { + self.from_uart.read(buf).await + } + + pub async fn write(&mut self, &buf: [u8]) { + self.to_uart.write_all(buf).await + } + + pub fn reconfigure(&mut self, config: Config) { + todo!(); + } + +} + diff --git a/src/serial.rs b/src/serial.rs index f86078c..870da02 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -6,6 +6,11 @@ use embedded_io_async::{Read, Write}; use esp_hal::{uart::{RxError::FifoOverflowed, Uart, UartRx}, Async}; use esp_println::println; +#[embassy_executor::task] +async fn uart_task(instance: BufferedUart<'static>) { + instance.run(); +} + /// Forwards an incoming SSH connection to/from the local UART, until /// the connection drops pub(crate) async fn serial_bridge( From 4d4986d26709b104cf68d34ecfb1f00ed52a9bba Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Sat, 22 Mar 2025 21:08:48 +1100 Subject: [PATCH 2/6] Add (mermaid) gantt chart for nlnet discussion about project dedication/compensation. First pass is majorly out of scale effort for the project (days instead of hours) --- docs/nlnet/gantt.md | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/nlnet/gantt.md diff --git a/docs/nlnet/gantt.md b/docs/nlnet/gantt.md new file mode 100644 index 0000000..7edfcfd --- /dev/null +++ b/docs/nlnet/gantt.md @@ -0,0 +1,53 @@ + + +```mermaid +gantt + title SSH Stamp development plan under NLNet grant + dateFormat YYYY-MM-DD + excludes weekends + tickInterval 1month + weekday monday + todayMarker off + axisFormat %Y-%m-%d + section Prototype + UART <-> SSH working : active, uart_ssh, 2025-04-01, 60d + section Provisioning + Provisioning : prov, after uart_ssh, 20d + OTA updates : ota, after prov, 30d + section Docs + usage docs : usage_docs, after ota, 20d + dev docs : dev_docs, after ota, 15d + section Robustness + #forbid(unsafe) : no_unsafe, after ota, 20d + UART interrupts : uart_intr, after uart_ssh, 30d + section Multi-target + Other espressif targets : all_espressif, after no_unsafe, 30d + Other chip1 : chip1, after all_espressif, 30d + Ohter chip2 : chip2, after chip1, 30d + section Testing + ci : ci, after chip2, 7d + hardware in the loop : HIL, after ci, 25d + user_testing : user_testing, after dev_docs, 30d + section Security + Self security audit : self_sec_audit, after all_espressif, 25d + NLNet security audit : nlnet_sec_audit, after all_espressif, 60d +``` + +Total hours: 200h + +Hourly rate: 40 eur/h \ No newline at end of file From 6d48a37baa3fa7e8d087ef307fe62105c1ff8815 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Sat, 22 Mar 2025 21:40:34 +1100 Subject: [PATCH 3/6] Correct-er? estimates on cost and effort? Will review again tomorrow before sending out --- docs/nlnet/gantt.md | 75 +++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/docs/nlnet/gantt.md b/docs/nlnet/gantt.md index 7edfcfd..a6240a2 100644 --- a/docs/nlnet/gantt.md +++ b/docs/nlnet/gantt.md @@ -1,53 +1,56 @@ - - ```mermaid gantt - title SSH Stamp development plan under NLNet grant + title SSH Stamp (a.k.a esp-ssh-rs) development plan under NLNet grant dateFormat YYYY-MM-DD excludes weekends - tickInterval 1month + tickInterval 1day weekday monday todayMarker off - axisFormat %Y-%m-%d + axisFormat %e section Prototype - UART <-> SSH working : active, uart_ssh, 2025-04-01, 60d + UART <-> SSH working : active, uart_ssh, 2025-04-01, 24h section Provisioning - Provisioning : prov, after uart_ssh, 20d - OTA updates : ota, after prov, 30d + Provisioning : prov, after uart_ssh, 16h + OTA updates : ota, after prov, 12h section Docs - usage docs : usage_docs, after ota, 20d - dev docs : dev_docs, after ota, 15d + usage docs : usage_docs, after ota, 10h + dev docs : dev_docs, after ota, 8h section Robustness - #forbid(unsafe) : no_unsafe, after ota, 20d - UART interrupts : uart_intr, after uart_ssh, 30d + #forbid(unsafe) : no_unsafe, after ota, 12h + UART perf : uart_intr, after uart_ssh, 12h section Multi-target - Other espressif targets : all_espressif, after no_unsafe, 30d - Other chip1 : chip1, after all_espressif, 30d - Ohter chip2 : chip2, after chip1, 30d + Espressif chips : all_espressif, after no_unsafe, 12h + Other chip1 : chip1, after all_espressif, 20h + Other chip2 : chip2, after chip1, 18h section Testing - ci : ci, after chip2, 7d - hardware in the loop : HIL, after ci, 25d - user_testing : user_testing, after dev_docs, 30d + CI/CD : ci, after chip2, 16h + hardware in test loop : HIL, after ci, 21h + Users test : user_tests, after dev_docs, 9h section Security - Self security audit : self_sec_audit, after all_espressif, 25d - NLNet security audit : nlnet_sec_audit, after all_espressif, 60d + Self audit : self_sec_audit, after all_espressif, 10h + NLNet security audit? : nlnet_sec_audit, after all_espressif, 45h ``` +```verbatim Total hours: 200h +Hourly rate: 40 eur/h +``` + + \ No newline at end of file From 7140d351e2e336bb3a41f1d3505509bff2491f7d Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 24 Mar 2025 16:37:43 +1100 Subject: [PATCH 4/6] Add sans-io gantt estimation --- docs/nlnet/gantt.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/nlnet/gantt.md b/docs/nlnet/gantt.md index a6240a2..fa63992 100644 --- a/docs/nlnet/gantt.md +++ b/docs/nlnet/gantt.md @@ -13,11 +13,12 @@ gantt Provisioning : prov, after uart_ssh, 16h OTA updates : ota, after prov, 12h section Docs - usage docs : usage_docs, after ota, 10h - dev docs : dev_docs, after ota, 8h + usage docs : usage_docs, after ota, 4h + dev docs : dev_docs, after ota, 2h section Robustness #forbid(unsafe) : no_unsafe, after ota, 12h UART perf : uart_intr, after uart_ssh, 12h + sans-io refactor : sans_io, after uart_intr, 16h section Multi-target Espressif chips : all_espressif, after no_unsafe, 12h Other chip1 : chip1, after all_espressif, 20h From 16249b2a8a10de1412864677088083ee8500c885 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Thu, 10 Apr 2025 15:56:06 +1000 Subject: [PATCH 5/6] Add sketch/draft of the application flow FSM, only modelling the 'happy path' for now and with quite a few gaps/breakage --- src/espressif/net.rs | 3 +- src/espressif/serial.rs | 25 +++++------- src/fsm/app.rs | 90 +++++++++++++++++++++++++++++++++++++++++ src/fsm/mod.rs | 1 + src/lib.rs | 1 + src/serve.rs | 14 +------ 6 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 src/fsm/app.rs create mode 100644 src/fsm/mod.rs diff --git a/src/espressif/net.rs b/src/espressif/net.rs index 0c77ebe..bb93434 100644 --- a/src/espressif/net.rs +++ b/src/espressif/net.rs @@ -94,7 +94,6 @@ pub async fn if_up( pub async fn accept_requests( stack: Stack<'static>, - uart: Uart<'static, Async>, ) -> Result<(), sunset::Error> { let rx_buffer = mk_static!([u8; 1536], [0; 1536]); let tx_buffer = mk_static!([u8; 1536], [0; 1536]); @@ -114,7 +113,7 @@ pub async fn accept_requests( } println!("Connected, port 22"); - crate::serve::handle_ssh_client(&mut socket, uart).await?; + crate::serve::handle_ssh_client(&mut socket).await?; //} Ok(()) // FIXME: All is fine but not really if we lose connection only once... removed loop to deal with uart copy issues later diff --git a/src/espressif/serial.rs b/src/espressif/serial.rs index 3520308..5cdca27 100644 --- a/src/espressif/serial.rs +++ b/src/espressif/serial.rs @@ -1,22 +1,8 @@ -// use esp_hal::uart::{Config, Uart}; -// use esp_hal::Async; -// use esp_hal::peripherals::Peripherals; - -// pub(crate) fn init_uart(peripherals: UART1) -> Uart<'static, Async> { -// let config = Config::default().with_rx_timeout(1); - -// Uart::new(peripherals.UART1, config) -// .unwrap() -// .with_rx(peripherals.GPIO11) -// .with_tx(peripherals.GPIO10) -// .into_async() -// } - use embassy_executor::Spawner; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pipe::Pipe}; use embassy_futures::select::select; use esp_hal::Async; -use esp_hal::uart::Uart; +use esp_hal::uart::{Config, RxConfig, Uart}; const RD_BUF_SZ: usize = 512; const WR_BUF_SZ: usize = 256; @@ -29,8 +15,15 @@ struct BufferedUart<'a> { impl BufferedUart<'static> { pub fn uart_init(spawner: Spawner) -> Self { + // Espressif-specific UART setup + let uart_config = Config::default() + .with_rx(RxConfig::default().with_fifo_full_threshold(16).with_timeout(1)); - + let uart = Uart::new(peripherals.UART1, uart_config) + .unwrap() + .with_rx(peripherals.GPIO11) + .with_tx(peripherals.GPIO10) + .into_async(); } // Call this from inside the embassy Task diff --git a/src/fsm/app.rs b/src/fsm/app.rs new file mode 100644 index 0000000..7fa18d1 --- /dev/null +++ b/src/fsm/app.rs @@ -0,0 +1,90 @@ +// Inspired by https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=ee3e4df093c136ced7b394dc7ffb78e1 +// Originally described in https://hoverbear.org/blog/rust-state-machine-pattern/ + +// Tenets: +// 1. Lightweight and easy to understand/change. +// 2. Should not interfere in performance, only "big" state transitions should be tracked (not micromanage on bytes sent, etc...). +// 3. Non intrusive in application code. + +pub enum State { + PowerOn, + Reset, + Idle, + Timeout, + BridgeUp, + InitPeripherals, + TcpStackUp, + TaskSpawning { name: &str }, + TaskFailed { name: &str }, + TaskRunning { name: &str }, + AllTasksOk, + ClientConnecting, + ClientConnected, + AuthzChecks, + SshConnEstablished, + ReadEnvVars, + UartReconf, + SshUartBridgeEstablished, +} + +enum Event { + Ok, + Fail, + UartReconf, + ClientConnect, + SshDisconnect, +} + +impl State { + fn next(self, event: Event) -> State { + match (self, event) { + (State::PowerOn, Event::Ok) => State::TaskSpawning, + (State::TaskSpawning { .. }, Event::Ok) => State::TaskRunning { .. }, + (State::AllTasksOk, Event::Ok) => State::BridgeUp, + (State::BridgeUp, Event::Ok) => State::Idle, + (State::Idle, Event::Ok) => State::Idle, + (State::Idle, Event::ClientConnect) => State::ClientConnecting, + (State::ClientConnecting, Event::Ok) => State::ReadEnvVars, + (State::ReadEnvVars, Event::Ok) => State::ClientConnected, + (State::ClientConnected, Event::Ok) => State::SshUartBridgeEstablished, + (s, e) => { + State::Fail(println!("Wrong state, event combination: {:#?} {:#?}", s, e)) + } + } + } + fn run(&self) { + match *self { + State::Idle | + State::Fail(_) => {} + } + } +} + +// fn main() { +// let mut state = State::Idle; +// +// // Sequence of events (might be dynamic based on what State::run did) +// // TODO: Declare this array automatically from the enum definition above. +// let events = [Event::Ok, +// Event::Fail]; +// +// let mut iter = events.iter(); +// +// loop { +// // just a hack to get owned values, because I used an iterator +// let event = iter.next().unwrap().clone(); +// print!("__ Transition from {:?}", state); +// state = state.next(event); +// println!(" to {:?}", state); + +// if let State::Fail(string) = state { +// println!("{}", string); +// break; +// } else { +// // You might want to do somethin while in a state +// // You could also add State::enter() and State::exit() +// state.run(); +// } +// } + +// } \ No newline at end of file diff --git a/src/fsm/mod.rs b/src/fsm/mod.rs new file mode 100644 index 0000000..02c0277 --- /dev/null +++ b/src/fsm/mod.rs @@ -0,0 +1 @@ +pub mod app; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index f32497f..923c1a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod keys; pub mod serial; pub mod serve; pub mod settings; +//pub mod fsm; diff --git a/src/serve.rs b/src/serve.rs index 67c4748..c364a96 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -156,18 +156,8 @@ pub async fn start(spawner: Spawner) -> Result<(), sunset::Error> { // Bring up the network interface and start accepting SSH connections. let tcp_stack = if_up(spawner, wifi_controller, peripherals.WIFI, &mut rng).await?; - // Espressif-specific UART setup - let uart_config = Config::default() - .with_rx(RxConfig::default().with_fifo_full_threshold(16).with_timeout(1)); - - let uart = Uart::new(peripherals.UART1, uart_config) - .unwrap() - .with_rx(peripherals.GPIO11) - .with_tx(peripherals.GPIO10) - .into_async(); - - // Start accepting SSH connections and redirect them to the UART later on - accept_requests(tcp_stack, uart).await?; + // Start accepting SSH connections + accept_requests(tcp_stack).await?; // All is fine :) Ok(()) From 046165ea33fb83ae5c6900e9640789b52a4b63b9 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 21 Apr 2025 16:09:30 +1000 Subject: [PATCH 6/6] Running a few more experiments with this FSM: a bit disappointed still by the code repetition when re-enumerating states... I suspect that https://github.com/CarlKCarlK/dua_blinka/blob/main/src/led_state.rs where state transitions directly call the associated functions might be more maintainable mid/long term? --- src/fsm/app.rs | 101 +++++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/src/fsm/app.rs b/src/fsm/app.rs index 7fa18d1..8e9b3ef 100644 --- a/src/fsm/app.rs +++ b/src/fsm/app.rs @@ -1,22 +1,27 @@ // Inspired by https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=ee3e4df093c136ced7b394dc7ffb78e1 // Originally described in https://hoverbear.org/blog/rust-state-machine-pattern/ +// Resurfaced at HN: https://news.ycombinator.com/item?id=43741051 + +// Playground at: https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=654cde7e18ecce9f5e350fedf27abab9 // Tenets: // 1. Lightweight and easy to understand/change. // 2. Should not interfere in performance, only "big" state transitions should be tracked (not micromanage on bytes sent, etc...). // 3. Non intrusive in application code. -pub enum State { - PowerOn, +pub enum State<'a> { + PowerOn, // Both PowerOn and Reset represent states where peripherals are not initialised yet. Reset, + Start, // Represents state where peripherals and basics are initialised Idle, Timeout, + Failure(&'a str), BridgeUp, InitPeripherals, TcpStackUp, - TaskSpawning { name: &str }, - TaskFailed { name: &str }, - TaskRunning { name: &str }, + TaskSpawning { name: &'a str }, + TaskFailed { name: &'a str }, + TaskRunning { name: &'a str }, AllTasksOk, ClientConnecting, ClientConnected, @@ -27,64 +32,70 @@ pub enum State { SshUartBridgeEstablished, } +#[derive(Clone)] enum Event { - Ok, + AllGood, Fail, UartReconf, ClientConnect, SshDisconnect, } -impl State { - fn next(self, event: Event) -> State { +impl<'a> State<'a> { + fn next(self, event: Event) -> State<'a> { match (self, event) { - (State::PowerOn, Event::Ok) => State::TaskSpawning, - (State::TaskSpawning { .. }, Event::Ok) => State::TaskRunning { .. }, - (State::AllTasksOk, Event::Ok) => State::BridgeUp, - (State::BridgeUp, Event::Ok) => State::Idle, - (State::Idle, Event::Ok) => State::Idle, + (State::PowerOn, Event::AllGood) => State::TaskSpawning { name: "G'day" }, + (State::TaskSpawning { .. }, Event::AllGood) => State::TaskRunning { name: "A task?" }, + (State::AllTasksOk, Event::AllGood) => State::BridgeUp, + (State::BridgeUp, Event::AllGood) => State::Idle, + (State::Idle, Event::AllGood) => State::Idle, (State::Idle, Event::ClientConnect) => State::ClientConnecting, - (State::ClientConnecting, Event::Ok) => State::ReadEnvVars, - (State::ReadEnvVars, Event::Ok) => State::ClientConnected, - (State::ClientConnected, Event::Ok) => State::SshUartBridgeEstablished, - (s, e) => { - State::Fail(println!("Wrong state, event combination: {:#?} {:#?}", s, e)) + (State::ClientConnecting, Event::AllGood) => State::ReadEnvVars, + (State::ReadEnvVars, Event::AllGood) => State::ClientConnected, + (State::ClientConnected, Event::AllGood) => State::SshUartBridgeEstablished, + (_s, _e) => { + // TODO: Implement appropriate formatters/display trait + //State::Start(println!("Wrong state, event combination: {} {}", s, e)) + State::Start } } } fn run(&self) { match *self { State::Idle | - State::Fail(_) => {} + State::Failure(_) => {} + _ => todo!() } } } -// fn main() { -// let mut state = State::Idle; -// -// // Sequence of events (might be dynamic based on what State::run did) -// // TODO: Declare this array automatically from the enum definition above. -// let events = [Event::Ok, -// Event::Fail]; -// -// let mut iter = events.iter(); -// -// loop { -// // just a hack to get owned values, because I used an iterator -// let event = iter.next().unwrap().clone(); -// print!("__ Transition from {:?}", state); -// state = state.next(event); -// println!(" to {:?}", state); +fn main() { + let mut state = State::Idle; + + // Sequence of events (might be dynamic based on what State::run did) + // TODO: Declare this array automatically from the enum definition above. + let events = [Event::AllGood, + Event::Fail]; + + let mut iter = events.iter(); -// if let State::Fail(string) = state { -// println!("{}", string); -// break; -// } else { -// // You might want to do somethin while in a state -// // You could also add State::enter() and State::exit() -// state.run(); -// } -// } + loop { + // TODO: Find a better solution to this "just a hack" that does not involve + // clone(). + // just a hack to get owned values, because I used an iterator + let event = iter.next().unwrap().clone(); + //print!("__ Transition from {:?}", state); + state = state.next(event); + //println!(" to {}", state); + + if let State::Failure(string) = state { + println!("{}", string); + break; + } else { + // You might want to do somethin while in a state + // You could also add State::enter() and State::exit() + state.run(); + } + } -// } \ No newline at end of file +} \ No newline at end of file