diff --git a/build.rs b/build.rs index 3d94ff8..75f1ce7 100644 --- a/build.rs +++ b/build.rs @@ -1,32 +1,48 @@ use minijinja::{context, Environment}; use owo_colors::OwoColorize; +use std::collections::HashMap; use std::fs; fn main() { - // Protocol tests run on the host without a board feature. Cargo doesn't - // expose `cfg(test)` to build scripts, so we rely on an env var set by - // the justfile `test` recipe. See: `just test` + // Protocol tests run on the host without a board feature. if std::env::var_os("DONGLORA_HOST_TEST").is_some() { return; } let board_dir = "src/board"; - let boards: Vec = fs::read_dir(board_dir) - .expect("failed to read src/board directory") - .filter_map(|e| { - let p = e.ok()?.path(); - let stem = p.file_stem()?.to_str()?; - // Exclude non-board files: mod.rs, traits.rs, and platform helpers (esp32s3.rs). - if p.is_file() - && p.extension()? == "rs" - && !matches!(stem, "mod" | "traits" | "esp32s3") - { - return Some(p.file_stem()?.to_str()?.to_string()); - } - None - }) - .collect(); + // ── Discover boards vs helpers by content ─────────────────────── + // + // A .rs file in src/board/ that implements LoRaBoard is a board. + // Everything else (traits.rs, esp32s3.rs, etc.) is a helper module. + // No exclusion lists — detection is based on what the file contains. + let mut boards: Vec = Vec::new(); + + for entry in fs::read_dir(board_dir).expect("failed to read src/board directory") { + let p = match entry { + Ok(e) => e.path(), + Err(_) => continue, + }; + if !p.is_file() || p.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + let stem = match p.file_stem().and_then(|s| s.to_str()) { + Some("mod") => continue, // Skip generated file + Some(s) => s.to_string(), + None => continue, + }; + let content = match fs::read_to_string(&p) { + Ok(c) => c, + Err(_) => continue, + }; + if content.contains("LoRaBoard for Board") { + boards.push(stem); + } + } + + boards.sort(); // Deterministic ordering + + // ── Validate exactly one board feature is enabled ─────────────── let enabled: Vec<_> = std::env::vars() .filter(|(k, _)| k.starts_with("CARGO_FEATURE_")) .map(|(k, _)| k.replace("CARGO_FEATURE_", "").to_lowercase()) @@ -49,6 +65,35 @@ fn main() { ), } + // ── Auto-discover helper modules used by board files ──────────── + // + // Scan each board file for `use super::` to find which helper + // modules they depend on. Generate cfg gates so helpers compile only + // when a dependent board is selected. + let mut helper_boards: HashMap> = HashMap::new(); + + for board in &boards { + let path = format!("{}/{}.rs", board_dir, board); + if let Ok(content) = fs::read_to_string(&path) { + for line in content.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("use super::") { + let helper = rest + .split(|c: char| !c.is_alphanumeric() && c != '_') + .next() + .unwrap_or(""); + if !helper.is_empty() && helper != "traits" { + helper_boards + .entry(helper.to_string()) + .or_default() + .push(board.clone()); + } + } + } + } + } + + // ── Render board module template ──────────────────────────────── let mut env = Environment::new(); let template = fs::read_to_string("src/board/mod.rs.j2").expect("missing src/board/mod.rs.j2"); env.add_template("mod", &template) @@ -56,7 +101,7 @@ fn main() { let rendered = env .get_template("mod") .expect("template not found") - .render(context!(boards => boards)) + .render(context!(boards => boards, helpers => helper_boards)) .expect("failed to render board module"); fs::write("src/board/mod.rs", rendered).expect("failed to write src/board/mod.rs"); @@ -64,9 +109,7 @@ fn main() { println!("cargo:rerun-if-changed={}", board_dir); // cortex-m-rt's link.x does `INCLUDE memory.x`. Copy our nRF layout into - // OUT_DIR (which cortex-m-rt already adds to the linker search path) so it's - // only visible to ARM builds. Keeping it out of the project root prevents - // the linker from shadowing esp-hal's generated memory.x on Xtensa builds. + // OUT_DIR so it's only visible to ARM builds. let target = std::env::var("TARGET").unwrap_or_default(); if !target.starts_with("xtensa") { let out_dir = std::env::var("OUT_DIR").unwrap(); diff --git a/src/board/PORTING.md b/src/board/PORTING.md index caaab6d..7ed5318 100644 --- a/src/board/PORTING.md +++ b/src/board/PORTING.md @@ -5,14 +5,14 @@ ### 1. Create the board file Create `src/board/your_board.rs`. Use an existing board as a template: -- ESP32-S3 boards: copy `heltec_v4.rs` (uses shared helpers from `esp32s3.rs`) -- nRF52840 boards: copy `rak_wisblock_4631.rs` +- ESP32-S3 boards: copy `heltec_v4.rs` (uses shared helpers from `board/esp32s3.rs` and `hal/esp32s3.rs`) +- nRF52840 boards: copy `rak_wisblock_4631.rs` (uses shared helpers from `hal/nrf52840.rs`) +- New MCU family: create a new `hal/.rs` with MCU primitives first -ESP32-S3 boards share common type aliases, peripheral bundles, and init helpers -via `src/board/esp32s3.rs`. Your board file only needs the trait impl and -board-specific init (Vext GPIO, display reset). If you add a new ESP32-S3 board, -add its feature to the `#[cfg(any(...))]` gate on `mod esp32s3` in -`src/board/mod.rs.j2`. +`build.rs` auto-discovers your board file — any `.rs` file in `src/board/` that +implements `LoRaBoard for Board` is automatically detected as a board. Helper +modules (like `esp32s3.rs`) are auto-discovered via `use super::` imports +in board files. No template editing or exclusion lists needed. ### 2. Define concrete types @@ -21,7 +21,9 @@ Your board must export these type aliases: ```rust pub type RadioDriver = ...; // Must work with lora_phy::LoRa pub type UsbDriver = ...; // Must implement embassy_usb_driver::Driver<'static> + // (or UartDriver for UART boards) pub type DisplayI2c = ...; // Must implement embedded_hal_async::i2c::I2c +pub type DisplayDriver = ...; // Must implement DrawTarget ``` ### 3. Define peripheral bundles @@ -32,39 +34,43 @@ pub struct RadioParts { pub delay: embassy_time::Delay, } -pub struct UsbParts { +pub struct UsbParts { // or UartParts for UART boards pub driver: UsbDriver, } pub struct DisplayParts { pub i2c: DisplayI2c, - pub mac: [u8; 6], } ``` ### 4. Implement the LoRaBoard trait ```rust -use super::traits::LoRaBoard; +use super::traits::{BoardParts, LoRaBoard}; impl LoRaBoard for Board { const NAME: &'static str = "Your Board Name"; const TX_POWER_RANGE: (i8, i8) = (-9, 22); // check your radio + PA + + type RadioParts = RadioParts; + type CommParts = UsbParts; // or UartParts + type DisplayParts = DisplayParts; + type DisplayDriver = DisplayDriver; + fn init() -> Self { ... } fn mac_address() -> [u8; 6] { ... } // read from efuse, FICR, etc. + fn into_parts(self) -> BoardParts { + // Initialize buses via hal::, construct drivers, return BoardParts + BoardParts { radio, host, display: Some(display_parts), mac: Self::mac_address() } + } } ``` -The compiler will refuse to build if any of these are missing. - -### 5. Implement `Board::into_parts()` +### 5. Add display init ```rust -impl Board { - pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { - // Initialize SPI, DMA, radio, USB, I2C - // Return peripheral bundles - } +pub async fn create_display(i2c: DisplayI2c) -> Option { + // Construct and initialize your display driver (SSD1306, SH1106, etc.) } ``` @@ -79,7 +85,7 @@ your_board = ["dep:your-hal", ...] ### 7. Add to justfile -Add your board's feature/target/chip to the `boards` list at the top of `justfile`. +Add your board's feature/target/chip to the board definitions at the top of `justfile`. ### 8. Build and test @@ -88,5 +94,3 @@ just check your_board # Must compile just clippy your_board # Must be clean just build your_board # Produces firmware ``` - -`build.rs` auto-discovers your `.rs` file — no template editing needed. diff --git a/src/board/mod.rs.j2 b/src/board/mod.rs.j2 index 063145d..ba6daa7 100644 --- a/src/board/mod.rs.j2 +++ b/src/board/mod.rs.j2 @@ -2,9 +2,10 @@ pub mod traits; pub use traits::*; -#[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] -mod esp32s3; - +{% for helper, helper_features in helpers|items -%} +#[cfg(any({% for f in helper_features %}feature = "{{ f }}"{% if not loop.last %}, {% endif %}{% endfor %}))] +mod {{ helper }}; +{% endfor %} cfg_if::cfg_if! { {%- for board in boards %} {% if loop.first %}if{% else %}else if{% endif %} #[cfg(feature = "{{ board }}")] {