espemu: eFuses, flash encryption, esp32c5, and a working hard reset (CII-251) - #431
Conversation
4db9eb5 to
73de499
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The eFuse subprocess can leave the running emulator with stale state and overwrite burned data, while --encrypt false is treated as enabled.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds esp-emu support for eFuse images, flash encryption, ESP32-C5, and control-channel resets.
Changes:
- Adds eFuse CLI configuration and burn support.
- Adds XTS-AES image encryption.
- Adds ESP32-C5 support and
dut.serialdevice-state operations.
File summaries
| File | Description |
|---|---|
pytest_embedded/plugin.py |
Adds the eFuse path option and fixture. |
pytest_embedded/dut_factory.py |
Propagates eFuse and encryption settings. |
tests/test_espemu.py |
Adds eFuse integration coverage. |
README.md |
Documents ESP32-C5 support. |
serial.py |
Implements emulated serial control operations. |
espemu.py |
Adds eFuse handling and control-channel commands. |
dut.py |
Exposes dut.serial. |
app.py |
Adds encrypted image generation. |
__init__.py |
Exports encryption constants and serial support. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
a81a58d to
4432143
Compare
|
@hfudev PTAL! |
d2910c2 to
e1e3f47
Compare
hfudev
left a comment
There was a problem hiding this comment.
Overall LGTM. two more small fixes.
|
btw, please also cleanup the commit history a bit. we're generating the changelog based on the conventional commit history :) |
`--espemu-efuse-path` gives the emulator an eFuse image, and `execute_efuse_command()` burns into it through a second instance in download mode. `--encrypt` and `--keyfile` encrypt the merged image the way the qemu service does.
esp-emu has accepted `--chip esp32c5` for a while, but the service's target list rejected it before launch, so every esp32c5 app was unrunnable under `idf,espemu`.
`EspEmuSerial` is the `serial` fixture, built from the emulator's control port. It resets the chip and erases and writes flash over that channel without restarting the process, and a burned eFuse image is reloaded into the running emulator. Operations that need esptool still raise.
e1e3f47 to
cf330dd
Compare
Done, PTAL again! |
|
LGTM. Thank you :) |
Description
The qemu service can give a test an eFuse image, burn eFuses into it and boot an encrypted image. The esp-emu service could do none of that, and it had no
dut.serialat all, so ESP-IDF tests that operate on device state had nothing to run against.eFuses
--espemu-efuse-pathgives the emulator an eFuse image, creating a blank QEMU-compatible one when the file does not exist.execute_efuse_command()runs espefuse against a second emulator instance started in download mode with its UART on a socket. A second instance is needed because the first one is booted into the firmware, and pyserial'ssocket://handler ignores modem control lines, so esptool cannot reset it into download mode.The second instance writes the burned image back as it exits, and the running emulator then reads it in over the control channel with
efuse-load, which resets the machine. A reset later in the same test therefore sees the burn, as it would on hardware.Flash encryption
--encryptand--keyfileencrypt the merged image the way the qemu service does, with one difference: the command isespsecure encrypt-flash-data --aes-xts, because every target the emulator supports encrypts flash with XTS-AES.esp32c5
esp-emu has accepted
--chip esp32c5for a while, but the service rejected the target before launching it, so every esp32c5 app was unrunnable under--embedded-services idf,espemu. Nothing else in the service is target specific — the flash image comes from the app's own flash arguments — so the target list was the only gap.dut.serial
ESP-IDF tests reach the chip through two channels: the console stream (
dut.expect) and a control channel (dut.serial) that resets it, erases flash and burns eFuses. Withoutdut.serial, 48 cases in an ESP-IDF sweep died at setup onAttributeError: ... has no attribute 'serial', and 40% of those only wanted a reset.esp-emu serves a control channel, and
EspEmuSerialsends these over it:hard_reset()sendsreset. The emulator process keeps running, so the dut's output stream, its expect history and its log all continue across the reset; relaunching the process would break all three.erase_flash()anderase_region(offset, size)map onto the channel's commands directly.erase_partition(name)resolves the offset and size from the app's partition table, the wayIdfSerialdoes, then erases that region.write_flash_no_enc(offset, path)writes a file into flash.EspEmuSerialis registered as theserialfixture, like every other service's serial, and is built from a port rather than from the emulator. The factory picks the control port before either fixture exists and hands the same port to both:EspEmuserves it with--control-tcpandEspEmuSerialconnects to it. Before choosing a port the factory checksesp-emu --helpfor--control-tcp, because an older binary exits on an unknown flag.The operations that need esptool itself —
flash(),bootloader_flash()and the partition-writing helpers esp_tee's tests subclass — raiseNotImplementedErrornaming the operation that was wanted.Validation
Run against ESP-IDF master with
--embedded-services idf,espemu:esp_systemandesp_hw_supportcases as the other targets.esp_driver_uart'stest_uart_single_dev, which used to fail at setup ondut.serial, now resets the chip six times and runs through to its test content.erase_partition('nvs_key')resolves to 0x110000 and erases 0x1000 bytes of the running machine's flash.In this repo,
test_serial_fixture_check.pychecks that theserialfixture anddut.serialare the same object, share the emulator's port, and can reset the chip with the port alone.Dependency
Everything above works with the published esp-emu, which serves
--control-tcp, except theefuse-loadreload: that command is newer than the channel and not in a release yet. The service checks for it, and without it a burn stays in the image for the next start rather than reaching the machine already running. A build with no control channel at all still boots and runs tests; only thedut.serialoperations raise, with an explanation.Checklist
Before submitting a Pull Request, please ensure the following: