Skip to content

Commit 0a1c7ae

Browse files
committed
esp32: default to native espflash with esptool fallback
1 parent dab5a0c commit 0a1c7ae

5 files changed

Lines changed: 132 additions & 59 deletions

File tree

crates/fbuild-daemon/Cargo.toml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@ name = "fbuild_daemon"
1515
path = "src/lib.rs"
1616

1717
[features]
18-
# Issue #66 spike. Forwards to `fbuild-deploy/espflash-native` so
19-
# `FBUILD_USE_ESPFLASH_VERIFY=1` / `FBUILD_USE_ESPFLASH_WRITE=1` can
20-
# actually route through espflash at runtime. Default-off: the daemon
21-
# binary we ship keeps the conservative esptool-subprocess path unless
22-
# the CI/release pipeline flips this on explicitly.
23-
default = []
18+
# Compile native espflash-backed ESP32 verify/write support into the daemon
19+
# by default. Runtime fallback still routes through esptool automatically if
20+
# the native path fails on a board/host combination.
21+
default = ["espflash-native"]
2422
espflash-native = ["fbuild-deploy/espflash-native"]
2523

2624
[dependencies]
@@ -58,7 +56,7 @@ tempfile = { workspace = true }
5856
socket2 = "0.6"
5957

6058
# libc::kill(pid, 0) is used as a probe to detect dead daemon PIDs at
61-
# startup (stale-PID cleanup). Unix-only Windows uses a manual
59+
# startup (stale-PID cleanup). Unix-only; Windows uses a manual
6260
# OpenProcess FFI in main.rs.
6361
[target.'cfg(unix)'.dependencies]
6462
libc = "0.2"

crates/fbuild-daemon/src/handlers/operations.rs

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,13 @@ use std::sync::Arc;
1818
/// pre-checks through the native [`espflash`] crate (issue #66) instead
1919
/// of the Python `esptool` subprocess.
2020
///
21-
/// Controlled by the `FBUILD_USE_ESPFLASH_VERIFY` environment variable
22-
/// (set to `1`, `true`, `yes`, or `on` to enable — case-insensitive).
23-
/// Any other value — including unset — keeps the default esptool path,
24-
/// so users on unusual setups retain the existing escape hatch until
25-
/// the native path has bench time on every ESP32 family member.
21+
/// Controlled by the `FBUILD_USE_ESPFLASH_VERIFY` environment variable.
22+
/// Native verify is enabled by default when compiled in.
23+
/// Set this variable to `0`, `false`, `no`, or `off` (case-insensitive)
24+
/// to force esptool.
2625
#[cfg(feature = "espflash-native")]
2726
pub(crate) fn native_verify_enabled() -> bool {
28-
match std::env::var("FBUILD_USE_ESPFLASH_VERIFY") {
29-
Ok(v) => matches!(
30-
v.trim().to_ascii_lowercase().as_str(),
31-
"1" | "true" | "yes" | "on"
32-
),
33-
Err(_) => false,
34-
}
27+
env_default_enabled("FBUILD_USE_ESPFLASH_VERIFY")
3528
}
3629

3730
/// Returns `true` when the daemon should trust an in-memory firmware
@@ -137,20 +130,23 @@ pub(crate) fn compute_esp32_image_hash(
137130
/// through the native [`espflash`] crate (issue #66) instead of the
138131
/// Python `esptool` subprocess.
139132
///
140-
/// Controlled by the `FBUILD_USE_ESPFLASH_WRITE` environment variable
141-
/// (set to `1`, `true`, `yes`, or `on` to enable — case-insensitive).
142-
/// Independent of `FBUILD_USE_ESPFLASH_VERIFY` — either toggle can be
143-
/// flipped without the other. Default off so esptool remains the safe
144-
/// fallback while the native write path accumulates bench time across
145-
/// every ESP32 family member.
133+
/// Controlled by the `FBUILD_USE_ESPFLASH_WRITE` environment variable.
134+
/// Native write is enabled by default when compiled in. Independent of
135+
/// `FBUILD_USE_ESPFLASH_VERIFY`. Set it to `0`, `false`, `no`, or `off`
136+
/// (case-insensitive) to force esptool.
146137
#[cfg(feature = "espflash-native")]
147138
pub(crate) fn native_write_enabled() -> bool {
148-
match std::env::var("FBUILD_USE_ESPFLASH_WRITE") {
149-
Ok(v) => matches!(
139+
env_default_enabled("FBUILD_USE_ESPFLASH_WRITE")
140+
}
141+
142+
#[cfg(feature = "espflash-native")]
143+
fn env_default_enabled(name: &str) -> bool {
144+
match std::env::var(name) {
145+
Ok(v) => !matches!(
150146
v.trim().to_ascii_lowercase().as_str(),
151-
"1" | "true" | "yes" | "on"
147+
"0" | "false" | "no" | "off"
152148
),
153-
Err(_) => false,
149+
Err(_) => true,
154150
}
155151
}
156152

@@ -1260,14 +1256,11 @@ pub async fn deploy(
12601256
} else {
12611257
deployer
12621258
};
1263-
// Issue #66: opt-in native `verify-flash` + `write-flash`
1264-
// via the `espflash` crate. Only compiled in when the
1265-
// daemon is built with `--features espflash-native`;
1266-
// default builds keep the esptool-subprocess path and
1267-
// skip pulling espflash + its ~30 transitive deps. At
1268-
// runtime, further gated by the `FBUILD_USE_ESPFLASH_*`
1269-
// env vars so operators can still fall back to esptool
1270-
// on an espflash-native build.
1259+
// Issue #66: native `verify-flash` + `write-flash` via
1260+
// the `espflash` crate. This is compiled in by default;
1261+
// `FBUILD_USE_ESPFLASH_*` are opt-out switches, and the
1262+
// deployer falls back to esptool automatically when a
1263+
// native operation fails.
12711264
#[cfg(feature = "espflash-native")]
12721265
let deployer = deployer
12731266
.with_native_verify(native_verify_enabled())
@@ -2243,6 +2236,44 @@ mod deploy_message_tests {
22432236
}
22442237
}
22452238

2239+
#[cfg(all(test, feature = "espflash-native"))]
2240+
mod espflash_env_tests {
2241+
use super::{native_verify_enabled, native_write_enabled};
2242+
2243+
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2244+
2245+
#[test]
2246+
fn native_verify_defaults_on_and_allows_opt_out() {
2247+
let _guard = ENV_LOCK.lock().unwrap();
2248+
std::env::remove_var("FBUILD_USE_ESPFLASH_VERIFY");
2249+
assert!(native_verify_enabled());
2250+
2251+
std::env::set_var("FBUILD_USE_ESPFLASH_VERIFY", "0");
2252+
assert!(!native_verify_enabled());
2253+
2254+
std::env::set_var("FBUILD_USE_ESPFLASH_VERIFY", "false");
2255+
assert!(!native_verify_enabled());
2256+
2257+
std::env::set_var("FBUILD_USE_ESPFLASH_VERIFY", "1");
2258+
assert!(native_verify_enabled());
2259+
std::env::remove_var("FBUILD_USE_ESPFLASH_VERIFY");
2260+
}
2261+
2262+
#[test]
2263+
fn native_write_defaults_on_and_allows_opt_out() {
2264+
let _guard = ENV_LOCK.lock().unwrap();
2265+
std::env::remove_var("FBUILD_USE_ESPFLASH_WRITE");
2266+
assert!(native_write_enabled());
2267+
2268+
std::env::set_var("FBUILD_USE_ESPFLASH_WRITE", "off");
2269+
assert!(!native_write_enabled());
2270+
2271+
std::env::set_var("FBUILD_USE_ESPFLASH_WRITE", "yes");
2272+
assert!(native_write_enabled());
2273+
std::env::remove_var("FBUILD_USE_ESPFLASH_WRITE");
2274+
}
2275+
}
2276+
22462277
#[cfg(test)]
22472278
mod image_hash_memo_tests {
22482279
//! Memo-cache correctness for [`compute_esp32_image_hash`]: the

crates/fbuild-deploy/Cargo.toml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@ rust-version.workspace = true
77
license.workspace = true
88

99
[features]
10-
# Default is the conservative esptool-subprocess path. The native
11-
# espflash-backed verify/write paths compile in only when the caller
12-
# explicitly opts in via this feature — keeps the default dep graph slim
13-
# (no espflash/strum/deku/miette/... pulled in for consumers that don't
14-
# need it) and gives issue #66's spike a clean kill-switch while it
15-
# accumulates bench time on every ESP32 family member.
16-
default = []
10+
# Compile the native espflash-backed verify/write paths by default. The
11+
# ESP32 deployer keeps the esptool subprocess path as an automatic runtime
12+
# fallback when native connect/verify/write fails.
13+
default = ["espflash-native"]
1714
espflash-native = ["dep:espflash", "dep:md-5"]
1815

1916
[dependencies]
@@ -31,15 +28,12 @@ tracing = { workspace = true }
3128
async-trait = { workspace = true }
3229
object = { workspace = true }
3330
# Native ESP32 flasher protocol (issue #66). The `serialport` feature
34-
# gates the `Flasher` type (the real transport entry point) so we enable
35-
# it; default features stay off to keep the CLI/TUI surface out of our
36-
# dependency graph. Optional so default builds don't pull espflash +
37-
# its ~30 transitive deps (strum, deku, miette, ...); enabled via the
38-
# `espflash-native` feature.
31+
# gates the `Flasher` type (the real transport entry point), so it is
32+
# enabled while espflash's CLI/TUI defaults stay off.
3933
espflash = { version = "4", default-features = false, features = ["serialport"], optional = true }
4034
# Local MD5 of firmware regions for comparison against espflash's on-chip
4135
# `FLASH_MD5SUM` command. Optional and gated behind `espflash-native`
42-
# since it's only used by the native verify path.
36+
# since it is only used by the native verify path.
4337
md-5 = { version = "0.10", optional = true }
4438

4539
[dev-dependencies]

crates/fbuild-deploy/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
# fbuild-deploy
22

3-
Firmware deployment to embedded devices via platform-specific upload tools (esptool, avrdude, teensy_loader_cli), and device reset sequences.
3+
Firmware deployment to embedded devices via platform-specific upload tools (espflash/esptool, avrdude, teensy_loader_cli), and device reset sequences.
44

55
## Key Types
66

77
- `Deployer` -- trait for platform-specific firmware upload (`deploy` method)
88
- `DeploymentResult` -- success/failure with message and optional port
9-
- `Esp32Deployer` -- esptool-based deployer with chip-specific flash offsets and modes
9+
- `Esp32Deployer` -- ESP32 deployer with native espflash fast path and esptool fallback
1010
- `AvrDeployer` -- avrdude-based deployer for Arduino boards
1111
- `TeensyDeployer` -- teensy_loader_cli-based deployer via USB HID
1212

1313
## Modules
1414

1515
- **esp32** -- `Esp32Deployer`, `EsptoolParams`; handles bootloader/partitions/firmware offsets per MCU
16-
- **esp32_native** -- native espflash-backed `verify-flash` and `write-flash` (issue #66); opt-in via `FBUILD_USE_ESPFLASH_VERIFY=1` / `FBUILD_USE_ESPFLASH_WRITE=1` (independent toggles), falls back to esptool subprocess by default
16+
- **esp32_native** -- native espflash-backed `verify-flash` and `write-flash` (issue #66); enabled by default when compiled in, with automatic esptool subprocess fallback
1717
- **avr** -- `AvrDeployer`, `AvrdudeParams`; flashes firmware.hex via serial
1818
- **teensy** -- `TeensyDeployer`, `TeensyLoaderParams`; flashes firmware.hex via USB
1919
- **reset** -- `reset_device`, `detect_platform_for_reset`; DTR/RTS toggle sequences per platform
2020

2121
Skip-redeploy is handled authoritatively by the daemon's device-side `verify-flash` pre-check (see `handlers/operations.rs`), which asks the ESP32 stub flasher for a per-region MD5 via `FLASH_MD5SUM` before writing. The previous client-side `FirmwareLedger` was removed (issue #18) because it could not detect flashes performed outside fbuild.
2222

23-
### Native verify-flash and write-flash (issue #66, opt-in)
23+
### Native verify-flash and write-flash (issue #66)
2424

25-
Setting `FBUILD_USE_ESPFLASH_VERIFY=1` routes the ESP32 verify pre-check through the native [`espflash`](https://crates.io/crates/espflash) crate instead of the Python `esptool` subprocess, avoiding ~1 s of interpreter startup and ~0.5 s of subprocess spawn per invocation. Setting `FBUILD_USE_ESPFLASH_WRITE=1` routes `write-flash` through the same in-process path both the full deploy and the selective post-verify-mismatch rewrite. The two toggles are independent, so you can opt into native verify without native write (or vice versa). The daemon pre-empts any active serial monitor via `SharedSerialManager::preempt_for_deploy` before opening the port, so the native path never contends with the existing lease.
25+
The ESP32 verify pre-check uses the native [`espflash`](https://crates.io/crates/espflash) crate by default instead of the Python `esptool` subprocess, avoiding ~1 s of interpreter startup and ~0.5 s of subprocess spawn per invocation. `write-flash` uses the same in-process path for both full deploys and selective post-verify-mismatch rewrites. If native verify/write fails, the deployer logs a warning and retries the same operation through esptool. Set `FBUILD_USE_ESPFLASH_VERIFY=0` and/or `FBUILD_USE_ESPFLASH_WRITE=0` to force esptool for either phase.
2626

27-
Progress from espflash's `ProgressCallbacks` is bridged into `tracing` and throttled to roughly one log line per 10% of each region, which the daemon's existing log broadcaster surfaces. Structured WebSocket progress frames on the deploy channel are a follow-up.
27+
The daemon pre-empts any active serial monitor via `SharedSerialManager::preempt_for_deploy` before opening the port, so neither path contends with the existing lease. Progress from espflash's `ProgressCallbacks` is bridged into `tracing` and throttled to roughly one log line per 10% of each region, which the daemon's existing log broadcaster surfaces. Structured WebSocket progress frames on the deploy channel are a follow-up.
2828

2929
See `docs/architecture/deploy-preemption.md` for architecture details.

crates/fbuild-deploy/src/esp32.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,26 @@ impl Esp32Deployer {
235235
pub fn try_verify_deployment(&self, firmware_path: &Path, port: &str) -> Result<VerifyOutcome> {
236236
#[cfg(feature = "espflash-native")]
237237
if self.use_native_verify {
238-
return self.try_verify_deployment_native(firmware_path, port);
238+
match self.try_verify_deployment_native(firmware_path, port) {
239+
Ok(outcome) => return Ok(outcome),
240+
Err(e) => {
241+
tracing::warn!(
242+
port,
243+
"native verify-flash failed ({}); falling back to esptool",
244+
e
245+
);
246+
}
247+
}
239248
}
249+
250+
self.try_verify_deployment_esptool(firmware_path, port)
251+
}
252+
253+
fn try_verify_deployment_esptool(
254+
&self,
255+
firmware_path: &Path,
256+
port: &str,
257+
) -> Result<VerifyOutcome> {
240258
let args = self.build_verify_flash_args(firmware_path, port);
241259
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
242260

@@ -1126,7 +1144,23 @@ impl Esp32Deployer {
11261144

11271145
#[cfg(feature = "espflash-native")]
11281146
if self.use_native_write {
1129-
return self.try_deploy_regions_native(firmware_path, port, regions);
1147+
match self.try_deploy_regions_native(firmware_path, port, regions) {
1148+
Ok(result) if result.success => return Ok(result),
1149+
Ok(result) => {
1150+
tracing::warn!(
1151+
port,
1152+
"native selective write-flash failed ({}); falling back to esptool",
1153+
result.message
1154+
);
1155+
}
1156+
Err(e) => {
1157+
tracing::warn!(
1158+
port,
1159+
"native selective write-flash failed ({}); falling back to esptool",
1160+
e
1161+
);
1162+
}
1163+
}
11301164
}
11311165

11321166
let args = self.build_write_flash_args(firmware_path, port, Some(regions));
@@ -1197,7 +1231,23 @@ impl Deployer for Esp32Deployer {
11971231

11981232
#[cfg(feature = "espflash-native")]
11991233
if self.use_native_write {
1200-
return self.try_deploy_native(firmware_path, port);
1234+
match self.try_deploy_native(firmware_path, port) {
1235+
Ok(result) if result.success => return Ok(result),
1236+
Ok(result) => {
1237+
tracing::warn!(
1238+
port,
1239+
"native write-flash failed ({}); falling back to esptool",
1240+
result.message
1241+
);
1242+
}
1243+
Err(e) => {
1244+
tracing::warn!(
1245+
port,
1246+
"native write-flash failed ({}); falling back to esptool",
1247+
e
1248+
);
1249+
}
1250+
}
12011251
}
12021252

12031253
let args = self.build_write_flash_args(firmware_path, port, None);

0 commit comments

Comments
 (0)