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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ __pycache__/
*.egg-info/
dist/
build/

# VSCode extension build artifacts
vscode-fbuild/out/
vscode-fbuild/node_modules/
vscode-fbuild/*.vsix
75 changes: 75 additions & 0 deletions crates/fbuild-build/src/esp32/mcu_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,79 @@ mod tests {
assert!(flags.contains(&"-Dmbedtls_sha1_finish_ret=mbedtls_sha1_finish".to_string()));
assert_eq!(flags.len(), 6);
}

// --- ESP32-C3 specific tests (mirrors test_platform_configs.py from main) ---

#[test]
fn test_esp32c3_config_loads() {
let config = get_mcu_config("esp32c3").expect("esp32c3 config should load");
assert_eq!(config.mcu, "esp32c3");
}

#[test]
fn test_esp32c3_is_riscv() {
let config = get_mcu_config("esp32c3").unwrap();
assert!(config.is_riscv(), "esp32c3 should be RISC-V");
assert!(!config.is_xtensa(), "esp32c3 should not be Xtensa");
}

#[test]
fn test_esp32c3_has_riscv_march_flag() {
let config = get_mcu_config("esp32c3").unwrap();
// Should contain RISC-V march flag (rv32imc variant)
let c_flags = &config.compiler_flags.c;
assert!(
c_flags.iter().any(|f| f.starts_with("-march=rv32")),
"esp32c3 should have -march=rv32... flag, got: {:?}",
c_flags
);
}

#[test]
fn test_esp32c3_has_required_fields() {
let config = get_mcu_config("esp32c3").unwrap();
// Must have compiler flags
assert!(!config.compiler_flags.common.is_empty());
// Must have linker flags
assert!(!config.linker_flags.is_empty());
// Must have esptool config for flashing
let _esptool = &config.esptool;
}

#[test]
fn test_esp32c3_toolchain_prefix() {
let config = get_mcu_config("esp32c3").unwrap();
assert_eq!(
config.toolchain_prefix(),
"riscv32-esp-elf-",
"esp32c3 should use riscv32-esp-elf toolchain"
);
}

#[test]
fn test_esp32c3_bootloader_offset() {
let config = get_mcu_config("esp32c3").unwrap();
// C-series uses 0x0 bootloader offset (unlike esp32 which uses 0x1000)
assert_eq!(
config.bootloader_offset(),
"0x0",
"esp32c3 should use 0x0 bootloader offset"
);
}

/// All supported MCUs should be discoverable and loadable — mirrors
/// `test_contains_expected_mcus` and `test_all_configs_loadable` from
/// `test_platform_configs.py`.
#[test]
fn test_all_supported_mcus_are_loadable() {
let mcus = supported_mcus();
assert!(
mcus.contains(&"esp32c3"),
"esp32c3 must be in supported MCU list"
);
for mcu in mcus {
get_mcu_config(mcu)
.unwrap_or_else(|e| panic!("failed to load config for {mcu}: {e}"));
}
}
}
165 changes: 164 additions & 1 deletion crates/fbuild-build/src/esp32/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ impl BuildOrchestrator for Esp32Orchestrator {
// Read user build_flags early — needed for both library and sketch compilation.
// SDK defines (from flags/defines) are prepended so user flags can override them.
let mut user_flags = sdk_defines;
user_flags.extend(config.get_build_flags(&params.env_name)?);
let user_build_flags = config.get_build_flags(&params.env_name)?;
user_flags.extend(user_build_flags.clone());

// Emit a warning if CDC on boot is effectively enabled (may cause Serial to block
// when no USB host is connected).
warn_if_cdc_on_boot(&board.name, board.extra_flags.as_deref(), &user_build_flags);

if !lib_deps.is_empty() {
let libs_dir = build_dir.join("libs");
Expand Down Expand Up @@ -1045,6 +1050,67 @@ pub fn create() -> Box<dyn BuildOrchestrator> {
Box::new(Esp32Orchestrator)
}

/// Determine whether ARDUINO_USB_CDC_ON_BOOT is effectively enabled.
///
/// Combines `board_extra_flags` (a space-separated string from the board JSON) with
/// `user_build_flags` (from platformio.ini `build_flags`). Board flags are applied
/// first; user flags can override them. The **last** definition of
/// `-DARDUINO_USB_CDC_ON_BOOT=N` wins, matching C preprocessor semantics.
///
/// Returns `true` only if the final effective value is `1`.
pub fn cdc_on_boot_enabled(board_extra_flags: Option<&str>, user_build_flags: &[String]) -> bool {
// Collect all flags in application order: board first, then user.
let board_tokens: Vec<String> = board_extra_flags
.unwrap_or("")
.split_whitespace()
.map(|s| s.to_string())
.collect();

let all_flags: Vec<&str> = board_tokens
.iter()
.map(|s| s.as_str())
.chain(user_build_flags.iter().map(|s| s.as_str()))
.collect();

let mut effective: Option<bool> = None;

for flag in &all_flags {
// Normalise: strip leading whitespace and optional `-D` prefix added by some tools.
let stripped = flag.trim();
// Match `-DARDUINO_USB_CDC_ON_BOOT=VALUE` or `ARDUINO_USB_CDC_ON_BOOT=VALUE`
let without_d = stripped
.strip_prefix("-D")
.unwrap_or(stripped);

if let Some(value) = without_d.strip_prefix("ARDUINO_USB_CDC_ON_BOOT=") {
effective = Some(value.trim() == "1");
}
}

effective.unwrap_or(false)
}

/// Emit a `tracing::warn!` if CDC on boot is effectively enabled.
///
/// `ARDUINO_USB_CDC_ON_BOOT=1` initialises the USB CDC port during boot via native
/// USB (ESP32-S3, C3, C6, S2, …). When no USB host is connected at power-on any
/// call to `Serial.print()` will block indefinitely because the CDC TX buffer has no
/// consumer to drain it.
pub fn warn_if_cdc_on_boot(
board_name: &str,
board_extra_flags: Option<&str>,
user_build_flags: &[String],
) {
if cdc_on_boot_enabled(board_extra_flags, user_build_flags) {
tracing::warn!(
"Board '{}' has ARDUINO_USB_CDC_ON_BOOT=1. \
If no USB host is connected at power-on, Serial.print() will block \
indefinitely. Add -DARDUINO_USB_CDC_ON_BOOT=0 to build_flags to suppress this warning.",
board_name
);
}
}

/// Check if a project is configured for ESP32 by reading its platformio.ini.
pub fn is_esp32_project(project_dir: &Path, env_name: &str) -> bool {
let ini_path = project_dir.join("platformio.ini");
Expand Down Expand Up @@ -1090,4 +1156,101 @@ mod tests {
.unwrap();
assert!(!is_esp32_project(tmp.path(), "uno"));
}

// --- CDC on boot warning tests ---

/// Board that enables CDC on boot via extra_flags (e.g. Adafruit Feather ESP32-S3).
#[test]
fn test_cdc_enabled_by_board_extra_flags() {
let board_flags = Some(
"-DARDUINO_ADAFRUIT_FEATHER_ESP32S3 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_RUNNING_CORE=1"
);
assert!(cdc_on_boot_enabled(board_flags, &[]));
}

/// Board that explicitly disables CDC on boot.
#[test]
fn test_cdc_disabled_by_board_extra_flags() {
let board_flags = Some(
"-DARDUINO_FREENOVE_ESP32_S3_WROOM -DARDUINO_USB_CDC_ON_BOOT=0"
);
assert!(!cdc_on_boot_enabled(board_flags, &[]));
}

/// Plain ESP32 dev board with no CDC flag at all — not enabled.
#[test]
fn test_no_cdc_flag_returns_false() {
let board_flags = Some("-DARDUINO_ESP32_DEV");
assert!(!cdc_on_boot_enabled(board_flags, &[]));
}

/// No board flags at all — not enabled.
#[test]
fn test_no_flags_at_all_returns_false() {
assert!(!cdc_on_boot_enabled(None, &[]));
}

/// User build_flags override a board-level enable (last definition wins).
#[test]
fn test_user_flag_overrides_board_enable() {
let board_flags = Some(
"-DARDUINO_USB_CDC_ON_BOOT=1"
);
let user_flags = vec!["-DARDUINO_USB_CDC_ON_BOOT=0".to_string()];
assert!(!cdc_on_boot_enabled(board_flags, &user_flags));
}

/// User build_flags can enable CDC that the board left unconfigured.
#[test]
fn test_user_flag_enables_cdc() {
let board_flags = Some("-DARDUINO_ESP32_DEV");
let user_flags = vec!["-DARDUINO_USB_CDC_ON_BOOT=1".to_string()];
assert!(cdc_on_boot_enabled(board_flags, &user_flags));
}

/// Multiple user flags — last one wins.
#[test]
fn test_last_user_flag_wins() {
let board_flags = Some("-DARDUINO_USB_CDC_ON_BOOT=1");
let user_flags = vec![
"-DARDUINO_USB_CDC_ON_BOOT=0".to_string(),
"-DARDUINO_USB_CDC_ON_BOOT=1".to_string(),
];
assert!(cdc_on_boot_enabled(board_flags, &user_flags));
}

/// Flags provided as whitespace-separated string should be parsed correctly.
#[test]
fn test_multi_flag_string_parsed_correctly() {
// Board flags: the enable flag appears after another flag.
let board_flags = Some("-DSOME_DEFINE -DARDUINO_USB_CDC_ON_BOOT=1 -DANOTHER=1");
assert!(cdc_on_boot_enabled(board_flags, &[]));
}

/// `warn_if_cdc_on_boot` should not panic for any combination of inputs.
#[test]
fn test_warn_if_cdc_on_boot_no_panic() {
// CDC enabled — triggers warning path
warn_if_cdc_on_boot(
"Adafruit Feather ESP32-S3",
Some("-DARDUINO_USB_CDC_ON_BOOT=1"),
&[],
);
// CDC disabled — no warning
warn_if_cdc_on_boot(
"Freenove ESP32-S3-WROOM",
Some("-DARDUINO_USB_CDC_ON_BOOT=0"),
&[],
);
// No flag at all — no warning
warn_if_cdc_on_boot("ESP32 Dev Module", Some("-DARDUINO_ESP32_DEV"), &[]);
// No board flags — no warning
warn_if_cdc_on_boot("Some Board", None, &[]);
// User override suppresses board enable
warn_if_cdc_on_boot(
"Some Board",
Some("-DARDUINO_USB_CDC_ON_BOOT=1"),
&["-DARDUINO_USB_CDC_ON_BOOT=0".to_string()],
);
}
}
74 changes: 74 additions & 0 deletions crates/fbuild-build/tests/esp32_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,80 @@ void loop() {
);
}

/// Build a self-contained ESP32-C3 blink sketch (RISC-V).
///
/// ESP32-C3 uses the rv32imc RISC-V ISA. This test validates the full build
/// pipeline for the C3 variant, including toolchain selection and framework
/// extraction. It requires Internet access (first run only, then cached).
#[test]
#[ignore]
fn build_esp32c3_blink() {
let tmp = tempfile::TempDir::new().unwrap();
let project_dir = tmp.path();

fs::write(
project_dir.join("platformio.ini"),
"[env:esp32c3]\nplatform = espressif32\nboard = esp32-c3-devkitm-1\nframework = arduino\n",
)
.unwrap();

let src_dir = project_dir.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(
src_dir.join("blink.cpp"),
"\
#include <Arduino.h>

void setup() {
// GPIO 8 is commonly available on ESP32-C3 DevKit
pinMode(8, OUTPUT);
}

void loop() {
digitalWrite(8, HIGH);
delay(1000);
digitalWrite(8, LOW);
delay(1000);
}
",
)
.unwrap();

let build_dir = project_dir.join(".fbuild/build");
let params = BuildParams {
project_dir: project_dir.to_path_buf(),
env_name: "esp32c3".to_string(),
clean: true,
profile: BuildProfile::Release,
build_dir,
verbose: true,
jobs: None,
generate_compiledb: false,
compiledb_only: false,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
let result = orchestrator
.build(&params)
.expect("ESP32-C3 build should succeed");

assert!(result.success);
let bin_path = result.hex_path.expect("should produce bin file");
assert!(bin_path.exists());

let size = result.size_info.expect("should have size info");
eprintln!(
"ESP32-C3 blink build: flash={}/{} ({:.1}%) ram={}/{} ({:.1}%) time={:.1}s",
size.total_flash,
size.max_flash.unwrap_or(0),
size.flash_percent().unwrap_or(0.0),
size.total_ram,
size.max_ram.unwrap_or(0),
size.ram_percent().unwrap_or(0.0),
result.build_time_secs
);
}

/// Build a self-contained ESP32-S3 blink sketch (Xtensa, native USB-CDC).
///
/// This test requires Internet access (first run only, then cached).
Expand Down
34 changes: 34 additions & 0 deletions crates/fbuild-config/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,4 +765,38 @@ leonardo.upload.speed=57600
let defines = config.get_defines();
assert_eq!(defines.get("ARDUINO_AVR_UNO"), Some(&"1".to_string()));
}

#[test]
fn test_esp32c3_board_config() {
let config = BoardConfig::from_board_id("esp32c3", &HashMap::new()).unwrap();
assert_eq!(config.mcu, "esp32c3");
assert_eq!(config.core, "esp32");
assert_eq!(config.flash_mode, Some("qio".to_string()));
assert_eq!(config.ldscript, Some("esp32c3_out.ld".to_string()));
// ESP32-C3 DevKit runs at 160 MHz
assert_eq!(config.f_cpu, "160000000L");
}

#[test]
fn test_esp32c3_devkitm1_board_config() {
// Direct look up by full board ID (same underlying JSON)
let config = BoardConfig::from_board_id("esp32-c3-devkitm-1", &HashMap::new()).unwrap();
assert_eq!(config.mcu, "esp32c3");
let flags = config.extra_flags.unwrap_or_default();
assert!(
flags.contains("ARDUINO_ESP32C3_DEV"),
"expected ARDUINO_ESP32C3_DEV in extra_flags, got: {flags}"
);
}

#[test]
fn test_esp32c3_no_psram() {
// The plain C3 DevKit has no PSRAM
let config = BoardConfig::from_board_id("esp32c3", &HashMap::new()).unwrap();
let flags = config.extra_flags.clone().unwrap_or_default();
assert!(
!flags.contains("BOARD_HAS_PSRAM"),
"ESP32-C3 DevKit should not have PSRAM flag, got: {flags}"
);
}
}
Loading