embassy-rs/embassy · critical
heartbeat from esp32 stopped
Error message
heartbeat from esp32 stopped
What it means
embassy-net-esp-hosted panics in its main run loop when the ESP32 co-processor stops sending heartbeat responses within HEARTBEAT_MAX_GAP. The driver polls the esp32 (via ioctl control events) and extends the deadline while the chip is initializing; if it is not initializing and the deadline expires, the firmware link is considered dead and the driver aborts, because it cannot recover the radio link. This is a fatal, unrecoverable condition for the driver by design.
Solutions
- Verify the ESP32 is running a compatible esp-hosted-ng firmware version flashed for the correct chip (esp32, esp32s3, c3) and that its status LED/boot logs show it is up
- Check power supply stability and decoupling capacitors; ESP32 Wi-Fi TX current spikes commonly cause brown-outs and resets
- Inspect SPI wiring (MOSI/MISO/CS/CLK/handshake pins) and confirm they match the embassy-net-esp-hosted Config pin settings
- Ensure the esp32 reset sequence is honored: reset the chip (or power-cycle the board) before/with driver init so ControlState transitions are observed
- Catch the panic in a supervisor task and restart the whole driver + esp32 reset, since the run loop cannot recover once the heartbeat is lost
Example fix
// before: driver started immediately at boot while esp32 still resetting let device = embassy_net_esp_hosted::new(mio, spi, ctrl_pin, rng, config); // after: hold esp32 in reset, then release and wait before init delay.delay(100.millis()); // let esp32 boot fully / recover from reset let device = embassy_net_esp_hosted::new(mio, spi, ctrl_pin, rng, config);
Defensive patterns
Strategy: retry
Validate before calling
// before driver init: verify esp32 responds and is flashed
// (embedded: no pre-call check possible; validate at startup)
fn validate_esp32(ctrl: &mut Control) {
match ctrl.init() {
Ok(_) => info!("esp32 alive"),
Err(e) => panic!("esp32 not responding, check power/spi/firmware: {:?}", e),
}
} Try / catch
// Rust panics cannot be caught in no_std; wrap in a supervisor task
// that resets the esp32 and re-creates the driver on watchdog expiry
loop {
spawn_network_task();
if WATCHDOG.timed_out() {
esp32_reset_pin.set_low(); // hard reset co-processor
Timer::after(100.millis()).await;
esp32_reset_pin.set_high();
}
} Prevention
- Use a compatible esp-hosted-ng firmware version for your driver release
- Provide a stable power supply with adequate decoupling for esp32 Wi-Fi current spikes
- Verify SPI pin assignments in Config against the physical wiring
- Add a hardware watchdog that resets the esp32 (reset pin) if the driver stops
- Wait for esp32 boot completion before initializing the driver
When it happens
Trigger: The run() loop receives EitherMany::Fourth(()) (heartbeat tick) while shared.state() is neither ControlState::Reboot nor ControlState::WaitingForInit, and the time since the last heartbeat from the ESP32 exceeded HEARTBEAT_MAX_GAP. Practically: the esp32 stopped answering init/control/ioctl exchanges (chip crashed, reset, brown-out, or SPI link failure).
Common situations: ESP32 firmware flashed with a mismatched or corrupt esp-hosted firmware image; insufficient power supply causing the esp32 to brown out and reset mid-operation; SPI wiring/soldering issues on custom boards; the esp32 being externally reset (e.g., by another driver or a watchdog); driver started before the esp32 finished booting and it then hung.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Passphrase is too short or too long
- ErevidIsZero
- Boot prepare error
- Boot prepare error
- Boot prepare error
AI-assisted analysis of embassy-rs/embassy@463a07b963 (2026-09-10).
Data as JSON: /api/errors/9ea88d41edfb9741.
Report an issue: GitHub.
Appendix: source
Thrown at embassy-net-esp-hosted/src/lib.rs:324
header.copy(&mut buffer);
tx_len = PayloadHeader::SIZE + packet_len;
} else {
// Backend doesn't support the requested interface type. Drop the
// packet and send nothing this iteration.
buffer[..PayloadHeader::SIZE].fill(0);
}
}
EitherMany::Third(()) => {
buffer[..PayloadHeader::SIZE].fill(0);
tx_len = 0;
}
EitherMany::Fourth(()) => {
// Extend the deadline if initializing
if let ioctl::ControlState::Reboot | ioctl::ControlState::WaitingForInit = self.shared.state() {
self.heartbeat_deadline = Instant::now() + HEARTBEAT_MAX_GAP;
continue;
}
panic!("heartbeat from esp32 stopped")
}
// Bluetooth HCI packet queued by the host stack.
#[cfg(feature = "bluetooth")]
EitherMany::Fifth(slot) => {
if let Some(if_type_and_num) = self.backend.encode_iface_type(InterfaceType::Hci) {
// `slot.buf[0]` is the H4 packet type indicator; it travels in the
// payload header's `hci_priv_packet_type` field, and the remaining
// bytes are the HCI packet body.
let pkt_type = slot.buf[0];
let body = &slot.buf[1..slot.len];
buffer[PayloadHeader::SIZE..][..body.len()].copy_from_slice(body);
let header = PayloadHeader {
if_type_and_num,
len: body.len() as _,
offset: PayloadHeader::SIZE as _,
seq_num: self.next_seq,View on GitHub (pinned to 463a07b963)