Document QEMU emulation in README and improve tool descriptions

Add QEMU section to README with install instructions and tool reference.
Enrich MCP tool docstrings with workflow context, cross-references to
related tools, and guidance on when to use each tool — these descriptions
are read by the calling LLM to decide tool selection.
This commit is contained in:
Ryan Malloy 2026-01-28 16:48:39 -07:00
parent 740164f582
commit cb4822a0a4
2 changed files with 70 additions and 20 deletions

View File

@ -10,6 +10,7 @@ FastMCP server providing AI-powered ESP32/ESP8266 development workflows through
- **Production Tools**: Factory programming and batch operations - **Production Tools**: Factory programming and batch operations
- **Middleware System**: Universal CLI tool integration with bidirectional MCP communication - **Middleware System**: Universal CLI tool integration with bidirectional MCP communication
- **ESP-IDF Integration**: Host application support for hardware-free development - **ESP-IDF Integration**: Host application support for hardware-free development
- **QEMU Emulation**: Virtual ESP32 devices for testing without physical hardware
## Quick Start ## Quick Start
@ -64,6 +65,29 @@ The server implements a component-based architecture with middleware for CLI too
- `OTAManager`: Over-the-air update workflows - `OTAManager`: Over-the-air update workflows
- `ProductionTools`: Factory programming and quality control - `ProductionTools`: Factory programming and quality control
- `Diagnostics`: Memory dumps and performance profiling - `Diagnostics`: Memory dumps and performance profiling
- `QemuManager`: QEMU-based ESP32 emulation with download mode, efuse, and flash support
## QEMU Emulation
Run virtual ESP32 devices without physical hardware. Requires [Espressif's QEMU fork](https://github.com/espressif/qemu):
```bash
# Install via ESP-IDF tools
source /path/to/esp-idf/export.sh
python3 $IDF_PATH/tools/idf_tools.py install qemu-xtensa qemu-riscv32
```
The server auto-detects QEMU binaries from `~/.espressif/tools/`. Once available, five tools are exposed:
| Tool | Description |
|------|-------------|
| `esp_qemu_start` | Launch a virtual ESP device (supports esp32, esp32s2, esp32s3, esp32c3) |
| `esp_qemu_stop` | Stop a running instance |
| `esp_qemu_list` | List all running instances |
| `esp_qemu_status` | Detailed instance info |
| `esp_qemu_flash` | Write firmware to a virtual device's flash |
Virtual devices appear in `esp_scan_ports` alongside physical hardware, connected via `socket://localhost:<port>`.
## Configuration ## Configuration

View File

@ -149,14 +149,30 @@ class QemuManager:
boot_mode: str = "download", boot_mode: str = "download",
extra_args: list[str] | None = None, extra_args: list[str] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """Start a virtual ESP device using QEMU emulation. No physical hardware needed.
Start a QEMU ESP32 emulation instance
Returns a socket URI (socket://localhost:PORT) that works with all
esptool operations: esp_detect_chip, esp_flash_firmware, esp_scan_ports, etc.
Virtual devices also appear automatically in esp_scan_ports results.
Boot modes:
- "download" (default): Device starts in serial bootloader. Use this for
esptool interactions like flashing, chip identification, and flash reading.
- "normal": Device boots from flash and runs firmware. Use this to observe
application output after flashing.
Typical workflow:
1. esp_qemu_start (download mode) -> get socket URI
2. esp_flash_firmware with socket URI -> flash your firmware
3. esp_qemu_stop -> stop the instance
4. esp_qemu_start (normal mode, same flash image) -> boot firmware
Args: Args:
chip_type: Target chip (esp32, esp32s2, esp32s3, esp32c3) chip_type: Target chip (esp32, esp32s2, esp32s3, esp32c3)
flash_image: Path to flash image file (creates blank if not specified) flash_image: Path to existing flash image file. Creates a blank erased
flash_size_mb: Flash size in MB for blank images (default: 4) flash (all 0xFF) if not specified.
tcp_port: TCP port for virtual serial (auto-assigned if not specified) flash_size_mb: Flash size in MB when creating blank images (default: 4)
tcp_port: TCP port for virtual serial (auto-assigned from pool if not specified)
boot_mode: "download" for esptool interaction (default), "normal" to boot from flash boot_mode: "download" for esptool interaction (default), "normal" to boot from flash
extra_args: Additional QEMU command-line arguments extra_args: Additional QEMU command-line arguments
""" """
@ -168,28 +184,34 @@ class QemuManager:
async def qemu_stop( async def qemu_stop(
context: Context, instance_id: str | None = None context: Context, instance_id: str | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """Stop a running QEMU virtual device. Terminates the QEMU process and frees the TCP port.
Stop a running QEMU instance
The flash image is preserved on disk, so the instance can be restarted
with esp_qemu_start using the same flash_image path (e.g., to switch
from download mode to normal boot mode after flashing).
Args: Args:
instance_id: Instance ID to stop (stops all if not specified) instance_id: Instance ID to stop (stops all running instances if not specified)
""" """
return await self._stop_impl(context, instance_id) return await self._stop_impl(context, instance_id)
@self.app.tool("esp_qemu_list") @self.app.tool("esp_qemu_list")
async def qemu_list(context: Context) -> dict[str, Any]: async def qemu_list(context: Context) -> dict[str, Any]:
"""List all QEMU instances with status""" """List all QEMU virtual device instances with their status, chip type, port, and boot mode.
Returns running and stopped instances. Use this to find instance IDs
for esp_qemu_stop or esp_qemu_status, or to get socket URIs for esptool operations."""
return await self._list_impl(context) return await self._list_impl(context)
@self.app.tool("esp_qemu_status") @self.app.tool("esp_qemu_status")
async def qemu_status( async def qemu_status(
context: Context, instance_id: str | None = None context: Context, instance_id: str | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """Get detailed status of a QEMU virtual device including uptime, PID, socket URI,
Get detailed status of a QEMU instance boot mode, and flash/efuse image paths.
Args: Args:
instance_id: Instance to inspect (first running if not specified) instance_id: Instance to inspect (returns first running instance if not specified)
""" """
return await self._status_impl(context, instance_id) return await self._status_impl(context, instance_id)
@ -200,17 +222,21 @@ class QemuManager:
firmware_path: str, firmware_path: str,
address: str = "0x0", address: str = "0x0",
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """Write a firmware binary directly into a QEMU instance's flash image file.
Flash a firmware binary to a QEMU instance's flash image
The instance must be stopped first. This writes the binary at the This is an offline operation the instance must be stopped first.
given offset into the raw flash image file, then you can restart It patches the raw flash image at the given offset, then you can restart
the instance. with esp_qemu_start in "normal" boot mode to run the firmware.
For most use cases, prefer using esp_flash_firmware with the instance's
socket URI while it's running in download mode — that uses esptool's
full flash protocol including verification. Use this tool only when you
need direct image manipulation (e.g., pre-loading a merged binary).
Args: Args:
instance_id: Target QEMU instance instance_id: Target QEMU instance (must be stopped)
firmware_path: Path to firmware binary to write firmware_path: Path to firmware binary to write into the flash image
address: Flash address offset (hex string, default: 0x0) address: Flash address offset as hex string (default: "0x0")
""" """
return await self._flash_impl(context, instance_id, firmware_path, address) return await self._flash_impl(context, instance_id, firmware_path, address)