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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions boards/ssh-stamp-esp/ssh-stamp-esp32-hil/tests/uart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ mod tests {
use core::array::from_fn;
use embassy_futures::select::{Either, select};
use embassy_sync::pipe::Pipe;
use ssh_stamp::serial::serial_bridge;
use ssh_stamp::serial::{CtrlD, serial_bridge};
use ssh_stamp_esp32::{
BufferedUart, EspUartPins, UART_SIGNAL, boot, spawn_uart, start_interrupt_executor,
};
Expand Down Expand Up @@ -109,7 +109,12 @@ mod tests {

let to_uart: Pipe<_, 512> = Pipe::new();
let from_uart: Pipe<_, 512> = Pipe::new();
let bridge = serial_bridge(ChanRead(&to_uart), ChanWrite(&from_uart), uart_buf);
let bridge = serial_bridge(
ChanRead(&to_uart),
ChanWrite(&from_uart),
uart_buf,
CtrlD::Forward,
);

let receive_function = async {
to_uart.write_all(b"test").await;
Expand Down
2 changes: 1 addition & 1 deletion boards/ssh-stamp-esp/ssh-stamp-esp32/src/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ macro_rules! boot {
($peripherals:ident, $rng:ident, $entropy_source:ident, $sw_int1:ident) => {
$crate::init_heap!();
$crate::esp_bootloader_esp_idf::esp_app_desc!();
$crate::esp_println::logger::init_logger_from_env();
$crate::logger::init();
$crate::bench::log_heap("boot");
$crate::log::debug!("HSM: initialising peripherals");

Expand Down
1 change: 1 addition & 0 deletions boards/ssh-stamp-esp/ssh-stamp-esp32/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod boot;
mod can;
pub mod flash;
mod hash;
pub mod logger;
mod network;
mod platform;
mod rng;
Expand Down
110 changes: 110 additions & 0 deletions boards/ssh-stamp-esp/ssh-stamp-esp32/src/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Console logger printing every record as a pretty-printed JSON `log` event.
//!
//! Installing a logger safely needs `log::set_logger`, which only exists on
//! targets with pointer-sized atomics. The ESP32-C2, C3 and S2 lack them, so
//! they keep esp-println's own logger and plain `LEVEL - message` lines.
//!
//! `@BENCH` records (target [`BENCH_TARGET`]) are printed raw: the benchmark
//! harness parses them as whitespace-separated `key=value` pairs.

#[cfg(target_has_atomic = "ptr")]
use log::{Level, LevelFilter, Log, Metadata, Record};
#[cfg(target_has_atomic = "ptr")]
use ssh_stamp::json;
#[cfg(target_has_atomic = "ptr")]
use ssh_stamp::mem_probe::BENCH_TARGET;

/// Installs the console logger, filtered by `ESP_LOG` at build time.
pub fn init() {
#[cfg(target_has_atomic = "ptr")]
if log::set_logger(&JsonLogger).is_ok() {
log::set_max_level(max_level());
}
#[cfg(not(target_has_atomic = "ptr"))]
esp_println::logger::init_logger_from_env();
}

/// `ESP_LOG`, e.g. `info` or `info,esp_radio=warn`.
#[cfg(target_has_atomic = "ptr")]
const FILTER: &str = match option_env!("ESP_LOG") {
Some(f) => f,
None => "info",
};

/// The level for `target`: the longest matching `prefix=level` directive,
/// else the bare `level` one, else `info`.
#[cfg(target_has_atomic = "ptr")]
fn level_for(target: &str) -> LevelFilter {
let mut global = LevelFilter::Info;
let mut best: Option<(usize, LevelFilter)> = None;
for d in FILTER.split(',').map(str::trim) {
match d.split_once('=') {
Some((prefix, level)) => {
if let Ok(level) = level.parse()
&& target.starts_with(prefix)
&& best.is_none_or(|(len, _)| prefix.len() > len)
{
best = Some((prefix.len(), level));
}
}
None => {
if let Ok(level) = d.parse() {
global = level;
}
}
}
}
best.map_or(global, |(_, level)| level)
}

/// The most verbose level any directive enables, so `log` can skip
/// formatting everything below it.
#[cfg(target_has_atomic = "ptr")]
fn max_level() -> LevelFilter {
FILTER
.split(',')
.filter_map(|d| d.rsplit('=').next()?.trim().parse().ok())
.max()
.unwrap_or(LevelFilter::Info)
}

#[cfg(target_has_atomic = "ptr")]
struct JsonLogger;

#[cfg(target_has_atomic = "ptr")]
impl Log for JsonLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= level_for(metadata.target())
}

fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
if record.target() == BENCH_TARGET {
esp_println::println!("{}", record.args());
return;
}
let level = match record.level() {
Level::Error => "error",
Level::Warn => "warn",
Level::Info => "info",
Level::Debug => "debug",
Level::Trace => "trace",
};
esp_println::println!(
"{}",
json::Pretty::new(json::Log {
level,
target: record.target(),
msg: record.args(),
})
);
}

fn flush(&self) {}
}
95 changes: 70 additions & 25 deletions boards/ssh-stamp-esp/ssh-stamp-esp32/src/network/wifi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

use core::net::Ipv4Addr;
use core::net::SocketAddrV4;
use ssh_stamp::json;

use edge_dhcp::io::{self, DEFAULT_SERVER_PORT};
use edge_dhcp::server::{Server, ServerOptions};
Expand Down Expand Up @@ -146,10 +147,13 @@ impl NetworkProviderHal for EspWifi {
debug!("Checking if link is up");
if ap_stack.is_link_up() {
if let Some(config) = ap_stack.config_v4() {
info!(
"Connect to the AP `{}` with IP {}",
ap_config.ap_ssid.as_str(),
config.address,
esp_println::println!(
"{}",
json::Pretty::new(json::NetUp {
ssid: ap_config.ap_ssid.as_str(),
role: "ap",
ip: config.address.address(),
})
);
}
break;
Expand All @@ -162,9 +166,13 @@ impl NetworkProviderHal for EspWifi {
debug!("Checking if station has received IP address");
if ap_stack.is_config_up() {
if let Some(config) = ap_stack.config_v4() {
info!(
"Connect to the AP `{}` with IP {}",
sta_ssid_static, config.address,
esp_println::println!(
"{}",
json::Pretty::new(json::NetUp {
ssid: sta_ssid_static,
role: "station",
ip: config.address.address(),
})
);
}
break;
Expand Down Expand Up @@ -300,35 +308,72 @@ pub async fn wifi_up(mut wifi_controller: WifiController<'static>, sta_ssid: &'s
.await;
match ev {
Ok(EventInfo::Connected(info)) => {
info!("Station connected: {info:?}");
esp_println::println!(
"{}",
json::Pretty::new(format_args!(
r#"{},"mac":"{}","aid":{}}}"#,
json::Head("station_connected"),
json::Mac(info.mac),
info.aid
))
);
}
Ok(EventInfo::Disconnected(info)) => {
info!("Station disconnected: {info:?}");
esp_println::println!(
"{}",
json::Pretty::new(format_args!(
r#"{},"mac":"{}","aid":{},"reason":"{}"}}"#,
json::Head("station_disconnected"),
json::Mac(info.mac),
info.aid,
json::Esc(format_args!("{:?}", info.reason))
))
);
}
_ => (),
}
Timer::after(Duration::from_millis(5000)).await;
}
} else {
// Station Mode
// If the connection is lost it will attempt to reconnect.
loop {
debug!("Connecting to access point...");

match wifi_controller.connect_async().await {
Ok(info) => {
info!("Wifi connected to {info:?}");
}

// Wait until we're no longer connected
let info = wifi_controller.wait_for_disconnect_async().await.ok();
info!("Disconnected: {info:?}");
}
Err(e) => {
info!("Failed to connect to wifi: {e:?}");
// Station Mode
// If the connection is lost it will attempt to reconnect.
loop {
debug!("Connecting to access point...");

match wifi_controller.connect_async().await {
Ok(info) => {
esp_println::println!(
"{}",
json::Pretty::new(format_args!(
r#"{},"ssid":"{}","bssid":"{}","channel":{}}}"#,
json::Head("wifi_connected"),
json::Esc(info.ssid.as_str()),
json::Mac(info.bssid),
info.channel
))
);

// Wait until we're no longer connected
match wifi_controller.wait_for_disconnect_async().await {
Ok(info) => esp_println::println!(
"{}",
json::Pretty::new(format_args!(
r#"{},"ssid":"{}","bssid":"{}","reason":"{}"}}"#,
json::Head("wifi_disconnected"),
json::Esc(info.ssid.as_str()),
json::Mac(info.bssid),
json::Esc(format_args!("{:?}", info.reason))
))
),
Err(e) => info!("Disconnected: {e:?}"),
}
}
Timer::after(Duration::from_millis(1000)).await;
Err(e) => {
info!("Failed to connect to wifi: {e:?}");
}
}
Timer::after(Duration::from_millis(1000)).await;
}
}

Expand Down
4 changes: 4 additions & 0 deletions boards/ssh-stamp-esp/ssh-stamp-esp32/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,8 @@ impl PlatformServices for EspPlatform {
fn activate_uart(&self) {
UART_SIGNAL.signal(1);
}

fn console_print(&self, text: core::fmt::Arguments<'_>) {
esp_println::println!("{text}");
}
}
33 changes: 25 additions & 8 deletions docs/EMULATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,30 @@ Verified on `esp-emu` 0.38.0, ESP32-C6, ssh-stamp at sunset 0.6:
(`authmode: Wpa2Personal`) and takes a DHCP lease on `192.168.4.2`.
- **SSH.** With `hostfwd`, an `ssh` client on the host completes a full
handshake, and the server host key it presents matches the fingerprint the
firmware logged at boot.

```
INFO - SSH server ident: SSH-2.0-Sunset-0.6.0-ssh-stamp-0.3.0
INFO - SSH hostkey fingerprint: SHA256:VO+Yvf+tm7o39TlcybTUPNv5NAxc/iQFlt9fjrekW6g
INFO - Wifi connected to ConnectedInfo { ssid: "myssid", ..., authmode: Wpa2Personal }
INFO - Connect to the AP `myssid` with IP 192.168.4.2/24
firmware printed at boot.

```json
{
"schema": 1,
"event": "boot",
"ident": "SSH-2.0-Sunset-0.6.0-ssh-stamp-0.3.0",
...
"hostkey_fingerprint": "SHA256:VO+Yvf+tm7o39TlcybTUPNv5NAxc/iQFlt9fjrekW6g",
...
}
{
"schema": 1,
"event": "wifi_connected",
"ssid": "myssid",
...
}
{
"schema": 1,
"event": "net_up",
"role": "station",
"ssid": "myssid",
"ip": "192.168.4.2"
}
```

```
Expand Down Expand Up @@ -93,7 +110,7 @@ despite the upstream docs describing an `idf.py merge-bin` flow), and boots it.

## Future directions

- Smoke test in CI: boot, assert on `--exit-on "SSH hostkey fingerprint"`,
- Smoke test in CI: boot, assert on `--exit-on '"event": "net_up"'`,
fail on timeout. Cheap, and catches boot regressions no unit test would.
- Config-partition fixture (above), unlocking station mode and therefore full
SSH integration tests against a real client.
Expand Down
Loading
Loading