Skip to content
Draft
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
34 changes: 19 additions & 15 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@

[alias]
# All ESP32 targets build the binary in the ssh-stamp-esp32 crate.
# `--no-default-features` keeps cargo from selecting the default (esp32c6) feature
# alongside another MCU's, which would fail at link time.
build-esp32 = "build --release --target xtensa-esp32-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32 -Z build-std=core,alloc"
build-esp32c2 = "build --release --target riscv32imc-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32c2"
build-esp32c3 = "build --release --target riscv32imc-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32c3"
build-esp32c6 = "build --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32c6"
build-esp32s2 = "build --profile esp32s2 --target xtensa-esp32s2-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32s2 -Z build-std=core,alloc"
build-esp32s3 = "build --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32s3 -Z build-std=core,alloc"
# Board features select a specific PCB and imply the IC feature.
# `--no-default-features` prevents the default board from clashing.
#
# Boards with a BSP entry use a board-<name> feature:
build-esp32c6 = "build --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-devkitc"
build-esp32c6-generic = "build --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-generic"
build-esp32s2 = "build --profile esp32s2 --target xtensa-esp32s2-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32-s2-saola -Z build-std=core,alloc"

run-esp32c6 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-devkitc"
run-esp32c6-generic = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32c6-generic"
run-esp32s2 = "run --profile esp32s2 --target xtensa-esp32s2-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features board-esp32-s2-saola"

run-esp32 = "run --release --target xtensa-esp32-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32"
run-esp32c2 = "run --release --target riscv32imc-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32c2"
run-esp32c3 = "run --release --target riscv32imc-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32c3"
run-esp32c6 = "run --release --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32c6"
run-esp32s2 = "run --profile esp32s2 --target xtensa-esp32s2-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32s2"
run-esp32s3 = "run --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features --features esp32s3"
# IC-only targets (no BSP entry yet) build the library only — the binary hits
# the `compile_error!("No board feature selected.")` guard until a board module
# is added to ssh-stamp-esp32-boards. Use `--lib` so the alias succeeds.
build-esp32 = "build --release --target xtensa-esp32-none-elf -p ssh-stamp-esp32 --lib --no-default-features --features esp32 -Z build-std=core,alloc"
build-esp32c2 = "build --release --target riscv32imc-unknown-none-elf -p ssh-stamp-esp32 --lib --no-default-features --features esp32c2"
build-esp32c3 = "build --release --target riscv32imc-unknown-none-elf -p ssh-stamp-esp32 --lib --no-default-features --features esp32c3"
build-esp32s3 = "build --release --target xtensa-esp32s3-none-elf -p ssh-stamp-esp32 --lib --no-default-features --features esp32s3 -Z build-std=core,alloc"

# Test alias
test-ota = "test --package ota --target x86_64-unknown-linux-gnu"
Expand All @@ -28,7 +32,7 @@ build-packer = "build --package ota --bin packer --target x86_64-unknown-linux-g
packer = "run --package ota --bin packer --target x86_64-unknown-linux-gnu"

# doc aliases (cannot use "doc" — shadows cargo's built-in command)
build-doc = "doc --target riscv32imac-unknown-none-elf --no-deps --lib -p ssh-stamp -p ssh-stamp-hal -p ssh-stamp-esp32 -p ota --no-default-features --features ssh-stamp-esp32/esp32c6"
build-doc = "doc --target riscv32imac-unknown-none-elf --no-deps --lib -p ssh-stamp -p ssh-stamp-hal -p ssh-stamp-esp32 -p ssh-stamp-esp32-boards -p ota --no-default-features --features ssh-stamp-esp32/board-esp32c6-devkitc"

[target.xtensa-esp32-none-elf] # ESP32
runner = "espflash flash --baud=921600 --monitor --chip esp32"
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ jobs:
matrix:
device: [
# RISC-V devices:
{ soc: "esp32c2", toolchain: "stable" },
{ soc: "esp32c3", toolchain: "stable" },
# { soc: "esp32c5", toolchain: "stable" },
{ soc: "esp32c6", toolchain: "stable" },
{ soc: "esp32c2", board: "esp32c2", target: "riscv32imc-unknown-none-elf", toolchain: "stable", buildstd: "", binlib: "--lib" },
{ soc: "esp32c3", board: "esp32c3", target: "riscv32imc-unknown-none-elf", toolchain: "stable", buildstd: "", binlib: "--lib" },
{ soc: "esp32c6", board: "board-esp32c6-devkitc", target: "riscv32imac-unknown-none-elf", toolchain: "stable", buildstd: "", binlib: "--bin ssh-stamp-esp32" },
{ soc: "esp32c6", board: "board-esp32c6-generic", target: "riscv32imac-unknown-none-elf", toolchain: "stable", buildstd: "", binlib: "--bin ssh-stamp-esp32" },
# Xtensa devices:
{ soc: "esp32", toolchain: "esp" },
{ soc: "esp32s2", toolchain: "esp" },
{ soc: "esp32s3", toolchain: "esp" },
{ soc: "esp32", board: "esp32", target: "xtensa-esp32-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--lib" },
{ soc: "esp32s2", board: "board-esp32-s2-saola", target: "xtensa-esp32s2-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--bin ssh-stamp-esp32" },
{ soc: "esp32s3", board: "esp32s3", target: "xtensa-esp32s3-none-elf", toolchain: "esp", buildstd: "-Z build-std=core,alloc", binlib: "--lib" },
]
steps:
- name: Cache
Expand All @@ -53,12 +53,12 @@ jobs:
version: 1.96.0

- name: Build project
run: cargo +${{ matrix.device.toolchain }} build-${{ matrix.device.soc }}
run: cargo +${{ matrix.device.toolchain }} build --release --target ${{ matrix.device.target }} -p ssh-stamp-esp32 ${{ matrix.device.binlib }} --no-default-features --features ${{ matrix.device.board }} ${{ matrix.device.buildstd }}

- name: Check lints and format
if: ${{ contains(fromJson('["esp32c6"]'), matrix.device.soc) }}
if: ${{ matrix.device.board == 'board-esp32c6-devkitc' }}
run: |
cargo +${{ matrix.device.toolchain }} clippy --release --features ${{ matrix.device.soc }} --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features -- -D warnings -A clippy::default_trait_access
cargo +${{ matrix.device.toolchain }} clippy --release --features board-esp32c6-devkitc --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features -- -D warnings -A clippy::default_trait_access
cargo +${{ matrix.device.toolchain }} fmt -- --check
packer:
name: OTA Packer
Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ license = "GPL-3.0-or-later"
[lib]

[workspace]
members = ["ssh-stamp-hal", "ssh-stamp-esp32", "ota"]
members = ["ssh-stamp-hal", "ssh-stamp-esp32", "ssh-stamp-esp32-boards", "ota"]

[workspace.lints.clippy]
mem_forget = "warn"
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,27 @@ If your SSH client doesn't forward environment variables by default, use the `-o

# UART pins

Default UART RX/TX pins vary by target and are defined in the port binary (`ssh-stamp-esp32/src/bin/ssh-stamp-esp32.rs`). To look them up, run:
UART RX/TX pins are defined per-board in the `ssh-stamp-esp32-boards` crate.
Each board feature (e.g. `board-esp32c6-devkitc`) selects a specific PCB and
its pin assignments. The `Board` trait is the single source of truth — no
other file in the repository hard-codes UART pin numbers.

Supported boards:

| Board feature | IC | UART RX | UART TX | Board |
|--------------------------|-----------|---------|---------|------------------------------------------------------------------------------------|
| `board-esp32c6-devkitc` | ESP32-C6 | 10 | 11 | [ESP32-C6-DevKitC-1](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/index.html) |
| `board-esp32c6-generic` | ESP32-C6 | 10 | 11 | Generic ESP32-C6 board |
| `board-esp32-s2-saola` | ESP32-S2 | 10 | 11 | [ESP32-S2-Saola-1](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s2/esp32-s2-saola-1/index.html) |

To see the full list with links, run:

```
cargo build-doc
```

Then open `target/riscv32imac-unknown-none-elf/doc/ssh_stamp/index.html` and navigate to the `ssh_stamp_esp32` crate documentation, which contains a per-target pin assignment table.
Then open `target/riscv32imac-unknown-none-elf/doc/ssh_stamp_esp32_boards/index.html`,
which contains the per-board pin assignment table.

# Example usecases

Expand Down
20 changes: 20 additions & 0 deletions ssh-stamp-esp32-boards/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

[package]
name = "ssh-stamp-esp32-boards"
version = "0.1.0"
edition = "2024"
authors = ["Roman Valls Guimera <brainstorm@nopcode.org>"]
description = "Board Support Package — per-PCB pin mappings for ssh-stamp-esp32"
license = "GPL-3.0-or-later"
repository = "https://github.com/brainstorm/ssh-stamp"

[features]
board-esp32c6-devkitc = []
board-esp32c6-generic = []
board-esp32-s2-saola = []

[lints]
workspace = true
18 changes: 18 additions & 0 deletions ssh-stamp-esp32-boards/src/esp32_s2_saola.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Espressif ESP32-S2-Saola-1 board support.
//!
//! Official board page:
//! <https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s2/esp32-s2-saola-1/index.html>

use crate::Board;

pub struct Esp32s2Saola;

impl Board for Esp32s2Saola {
const NAME: &'static str = "esp32-s2-saola";
const UART_RX: u8 = 10;
const UART_TX: u8 = 11;
}
18 changes: 18 additions & 0 deletions ssh-stamp-esp32-boards/src/esp32c6_devkitc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Espressif ESP32-C6-DevKitC-1 board support.
//!
//! Official board page:
//! <https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/index.html>

use crate::Board;

pub struct Esp32c6Devkitc;

impl Board for Esp32c6Devkitc {
const NAME: &'static str = "esp32c6-devkitc";
const UART_RX: u8 = 10;
const UART_TX: u8 = 11;
}
22 changes: 22 additions & 0 deletions ssh-stamp-esp32-boards/src/esp32c6_generic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Generic ESP32-C6 board support.
//!
//! Intended for custom ESP32-C6 boards that follow the standard C6 pinout
//! but are not the Espressif DevKitC-1. Uses GPIO 10/11 for UART, matching
//! the safe default on the ESP32-C6 family.
//!
//! See the ESP32-C6 datasheet for pin multiplexing constraints:
//! <https://www.espressif.com/en/support/documents/technical-documents>

use crate::Board;

pub struct Esp32c6Generic;

impl Board for Esp32c6Generic {
const NAME: &'static str = "esp32c6-generic";
const UART_RX: u8 = 10;
const UART_TX: u8 = 11;
}
103 changes: 103 additions & 0 deletions ssh-stamp-esp32-boards/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Board Support Package for ssh-stamp-esp32.
//!
//! Each board feature selects a specific PCB and provides both the logical
//! pin numbers (via [`Board::UART_RX`] / [`Board::UART_TX`]) and the
//! hardware GPIO extraction (via [`take_uart_pins!`]).
//!
//! The [`Board`] trait is the single source of truth for pin numbers. The
//! [`take_uart_pins!`] macro verifies at compile time that its GPIO field
//! access matches the trait's consts, so the two can never drift: changing
//! a pin in the trait without updating the macro (or vice versa) is a
//! compile error.
//!
//! # Available boards
//!
//! | Board feature | IC | UART RX | UART TX | Board |
//! |--------------------------|-----------|---------|---------|------------------------------------------------------------------------------------|
//! | `board-esp32c6-devkitc` | ESP32-C6 | 10 | 11 | [ESP32-C6-DevKitC-1](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/index.html) |
//! | `board-esp32c6-generic` | ESP32-C6 | 10 | 11 | Generic ESP32-C6 board |
//! | `board-esp32-s2-saola` | ESP32-S2 | 10 | 11 | [ESP32-S2-Saola-1](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s2/esp32-s2-saola-1/index.html) |

#![no_std]

mod esp32_s2_saola;
mod esp32c6_devkitc;
mod esp32c6_generic;

pub use esp32_s2_saola::Esp32s2Saola;
pub use esp32c6_devkitc::Esp32c6Devkitc;
pub use esp32c6_generic::Esp32c6Generic;

/// Board-level pin assignment.
///
/// Each supported board implements this trait to provide GPIO pin numbers
/// for the UART bridge. These consts are the single source of truth for
/// pin numbers — the [`take_uart_pins!`] macro cross-checks them at
/// compile time against its GPIO field access.
///
/// To add a new board:
/// 1. Create a module in `src/` with a unit struct implementing `Board`.
/// 2. Add a `board-<name>` feature in `Cargo.toml` (implies the IC feature on `esp-hal`).
/// 3. Add the GPIO mapping branch to the `take_uart_pins!` macro in this file,
/// with a `const { assert!(...) }` matching the trait consts.
/// 4. Add the feature gate + `use` in the binary's `cfg_if!` block.
pub trait Board {
/// Human-readable board name (e.g. `"esp32c6-devkitc"`).
const NAME: &'static str;
/// GPIO number for UART RX.
const UART_RX: u8;
/// GPIO number for UART TX.
const UART_TX: u8;
}

/// Extract UART GPIO pins from `peripherals` for the active board.
///
/// Pass the active board type so the macro can verify at compile time that
/// its GPIO field access matches [`Board::UART_RX`] / [`Board::UART_TX`].
/// Returns `(AnyPin, AnyPin)` — the caller wraps these into the appropriate
/// UART pins struct.
///
/// This macro is the **only** place in the codebase where UART GPIO singletons
/// are accessed. The pin numbers in the macro are checked against the `Board`
/// trait consts via `const { assert!(...) }`, so the `Board` trait remains the
/// canonical declaration and the two can never silently drift.
///
/// # Panics
///
/// Compile-time error if no board feature is selected, or if the `Board`
/// trait consts disagree with the macro's GPIO field access.
#[macro_export]
macro_rules! take_uart_pins {
($peripherals:expr, $board:ty) => {{
#[cfg(feature = "board-esp32c6-devkitc")]
{
const { assert!(<$board>::UART_RX == 10) };
const { assert!(<$board>::UART_TX == 11) };
($peripherals.GPIO10.into(), $peripherals.GPIO11.into())
}
#[cfg(feature = "board-esp32c6-generic")]
{
const { assert!(<$board>::UART_RX == 10) };
const { assert!(<$board>::UART_TX == 11) };
($peripherals.GPIO10.into(), $peripherals.GPIO11.into())
}
#[cfg(feature = "board-esp32-s2-saola")]
{
const { assert!(<$board>::UART_RX == 10) };
const { assert!(<$board>::UART_TX == 11) };
($peripherals.GPIO10.into(), $peripherals.GPIO11.into())
}
#[cfg(not(any(
feature = "board-esp32c6-devkitc",
feature = "board-esp32c6-generic",
feature = "board-esp32-s2-saola",
)))]
{
compile_error!("No board feature selected.");
}
}};
}
14 changes: 13 additions & 1 deletion ssh-stamp-esp32/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ esp-println = { version = "0.17", features = ["log-04"] }
embedded-storage = { workspace = true }
embedded-storage-async = { workspace = true }

ssh-stamp-esp32-boards = { path = "../ssh-stamp-esp32-boards", optional = true }

once_cell = { workspace = true }
sunset-async = { workspace = true }
sha2 = { workspace = true }
Expand All @@ -51,9 +53,19 @@ portable-atomic = { version = "1" }
cfg-if = "1"

[features]
default = ["esp32c6"]
default = ["board-esp32c6-devkitc"]
sftp-ota = ["ssh-stamp/sftp-ota"]
ipv6 = ["ssh-stamp/ipv6"]

# Board features — select a specific PCB. Each enables the corresponding
# board definition in ssh-stamp-esp32-boards AND the IC feature it targets.
board-esp32c6-devkitc = ["dep:ssh-stamp-esp32-boards", "ssh-stamp-esp32-boards/board-esp32c6-devkitc", "esp32c6"]
board-esp32c6-generic = ["dep:ssh-stamp-esp32-boards", "ssh-stamp-esp32-boards/board-esp32c6-generic", "esp32c6"]
board-esp32-s2-saola = ["dep:ssh-stamp-esp32-boards", "ssh-stamp-esp32-boards/board-esp32-s2-saola", "esp32s2"]

# IC features — select the chip only; activated automatically by board features.
# Kept for all Espressif MCUs even when no board definition exists yet, so the
# chip remains targetable for custom boards or future BSP entries.
esp32 = ["esp-hal/esp32", "esp-radio/esp32", "esp-storage/esp32", "esp-bootloader-esp-idf/esp32", "esp-alloc/esp32", "esp-backtrace/esp32", "esp-rtos/esp32", "esp-println/esp32"]
esp32c2 = ["esp-hal/esp32c2", "esp-radio/esp32c2", "esp-storage/esp32c2", "esp-bootloader-esp-idf/esp32c2", "esp-alloc/esp32c2", "esp-backtrace/esp32c2", "esp-rtos/esp32c2", "esp-println/esp32c2"]
esp32c3 = ["esp-hal/esp32c3", "esp-radio/esp32c3", "esp-storage/esp32c3", "esp-bootloader-esp-idf/esp32c3", "esp-alloc/esp32c3", "esp-backtrace/esp32c3", "esp-rtos/esp32c3", "esp-println/esp32c3"]
Expand Down
Loading
Loading