Skip to content

Accept a single validated ROM SYNC reply on USB-Serial-JTAG devices - #1084

Open
Massimiliano-solutiontech wants to merge 1 commit into
esp-rs:mainfrom
Massimiliano-solutiontech:01-usb-jtag-sync
Open

Massimiliano-solutiontech wants to merge 1 commit into
esp-rs:mainfrom
Massimiliano-solutiontech:01-usb-jtag-sync

Conversation

@Massimiliano-solutiontech

@Massimiliano-solutiontech Massimiliano-solutiontech commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

The ESP ROM answers one SYNC command with eight identical replies, and Connection::sync() requires all of them. When an ESP32-C6's USB-Serial-JTAG port is used through USB-over-IP passthrough (VirtualHere, SEH UTN) on macOS, most of the burst never reaches the host. The CDC-ACM driver keeps a single 64-byte IN request queued, and every refill costs a network round trip. With VirtualHere, exactly 2 of 8 replies arrived in 100/100 attempts, so espflash can't connect.

For ports whose USB VID/PID is 303a:1001, sync() now:

  • waits 100 ms after the reset sequence before sending SYNC;
  • accepts the first complete ROM reply, after checking direction, opcode, payload length, value 0x20120707 and both status bytes. Stub-style replies and malformed frames are rejected;
  • drains further duplicates for at most 300 ms, and stops as soon as all 8 have arrived, so a directly attached board doesn't wait;
  • keeps its SLIP decoder state across read timeouts, so a reply split across reads still counts.

Other VID/PIDs keep the current loop unchanged. Tests cover frame validation, the VID/PID gate, split reads, malformed escapes, and two valid replies followed by a truncated duplicate tail.

Over VirtualHere this is necessary but not sufficient. The currently bundled C6 stub then fails to start (timeout after Using flash stub), and with the v1.3.0 ESP Flasher Stub from #1060 flashing works (see the table below).

Tested (macOS 26.7, Apple M1 Max):

Connection Before After
VirtualHere 6.0.3 → Linux server, Wi‑Fi board-info 0/5 5/5 (with the v1.3 C6 stub); full write + independent MD5 ✔
SEH utnserver Pro, UTN Manager 4.0.11, LAN 5/5 5/5; full write 18.1 s + independent MD5 ✔
Local USB, ESP32-C6FH4 rev v0.2 5/5, 0.75 s 5/5, 0.85 s
Local USB, ESP32-C6 QFN40 rev v0.2 5/5, 0.75 s 5/5, 0.85 s
Local USB, C6FH4 with no valid bootloader (ROM prints invalid header continuously) 3/3, 2.7 s 3/3, 0.83 s
cargo xtask run-tests --chip esp32c6, local C6FH4 pass, 57 s pass, 29 s

A side effect on directly attached boards: when the chip has no valid bootloader (a blank board, or partway through the hardware suite) the ROM floods the port, and the current loop needs ~2.7 s to find eight clean replies. The validated first-reply path connects in 0.83 s, which halves the suite's runtime on a C6FH4.

The extra ~0.1 s per connect on local boards is the post-reset settle. I can drop it if you'd prefer: it was added for remote links, and the drain logic doesn't depend on it.

cargo test -p espflash --lib: 46 passed, 1 ignored. cargo +nightly fmt --check clean, no clippy warnings in src/.

@SergioGasquez SergioGasquez left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Thanks for the contribution, here are some comments

reset_strategy.reset(&mut self.serial)?;

if self.is_native_usb_sync_profile() {
sleep(NATIVE_USB_SYNC_SETTLE);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would drop this unless you have an scenario where VirtualHere/SEH fails without it

} else {
Err(Error::RomError(Box::new(RomError::new(
CommandType::Sync,
RomErrorKind::InvalidMessage,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are losing ROM error detail and always returning InvalidMessage.

Comment thread CHANGELOG.md

### Fixed

- Connecting to USB-Serial-JTAG devices (`303a:1001`) no longer requires all eight ROM `SYNC` replies; one strictly validated reply is accepted and the duplicates are drained, which fixes connecting through USB-over-IP passthrough such as VirtualHere or SEH UTN

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Connecting to USB-Serial-JTAG devices (`303a:1001`) no longer requires all eight ROM `SYNC` replies; one strictly validated reply is accepted and the duplicates are drained, which fixes connecting through USB-over-IP passthrough such as VirtualHere or SEH UTN
- Connecting to USB-Serial-JTAG devices (`303a:1001`) no longer requires all eight ROM `SYNC` replies; one strictly validated reply is accepted and the duplicates are drained, which fixes connecting through USB-over-IP passthrough such as VirtualHere or SEH UTN (#1084)

self.write_command(Command::Sync)?;
self.serial.flush()?;

let first_deadline = std::time::Instant::now() + NATIVE_USB_SYNC_TIMEOUT;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NATIVE_USB_SYNC_TIMEOUT makes each sync attempt wait up to 1 s for the first reply. Before, it was 100 ms (SYNC_TIMEOUT). begin() runs 7 connect attempts with 5 syncs each. So when the ROM doesn't answer (the reset didn't enter download mode, the wrong port was picked, or an app is running), the time spent waiting before ConnectionFailed goes from about 3.5 s to about 35 s. This hits every chip on 303a:1001, not only remote links.

Could you keep the first-reply wait at SYNC_TIMEOUT and only start the drain once a valid reply arrives? If remote links need a longer first-reply wait, could you share the first-reply latency you measured over VirtualHere so we can choose a value from that?

Ok(())
}

fn is_native_usb_sync_profile(&self) -> bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reset strategy selection and is_using_usb_serial_jtag() only check the PID, while this gate also checks the VID. So a xxxx:1001 port would get the USB-JTAG reset but the old sync. Could you reuse is_using_usb_serial_jtag() here? Please also rename native_usb_* to usb_serial_jtag_*, because "native USB" also covers USB-OTG (S2/S3/P4), which this path doesn't handle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants