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
- **Middleware System**: Universal CLI tool integration with bidirectional MCP communication
- **ESP-IDF Integration**: Host application support for hardware-free development
- **QEMU Emulation**: Virtual ESP32 devices for testing without physical hardware
## Quick Start
@ -64,6 +65,29 @@ The server implements a component-based architecture with middleware for CLI too
- `OTAManager`: Over-the-air update workflows
- `ProductionTools`: Factory programming and quality control
- `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

View File

@ -149,14 +149,30 @@ class QemuManager:
boot_mode: str = "download",
extra_args: list[str] | None = None,
) -> dict[str, Any]:
"""
Start a QEMU ESP32 emulation instance
"""Start a virtual ESP device using QEMU emulation. No physical hardware needed.
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:
chip_type: Target chip (esp32, esp32s2, esp32s3, esp32c3)
flash_image: Path to flash image file (creates blank if not specified)
flash_size_mb: Flash size in MB for blank images (default: 4)
tcp_port: TCP port for virtual serial (auto-assigned if not specified)
flash_image: Path to existing flash image file. Creates a blank erased
flash (all 0xFF) 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
extra_args: Additional QEMU command-line arguments
"""
@ -168,28 +184,34 @@ class QemuManager:
async def qemu_stop(
context: Context, instance_id: str | None = None
) -> dict[str, Any]:
"""
Stop a running QEMU instance
"""Stop a running QEMU virtual device. Terminates the QEMU process and frees the TCP port.
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:
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)
@self.app.tool("esp_qemu_list")
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)
@self.app.tool("esp_qemu_status")
async def qemu_status(
context: Context, instance_id: str | None = None
) -> dict[str, Any]:
"""
Get detailed status of a QEMU instance
"""Get detailed status of a QEMU virtual device including uptime, PID, socket URI,
boot mode, and flash/efuse image paths.
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)
@ -200,17 +222,21 @@ class QemuManager:
firmware_path: str,
address: str = "0x0",
) -> dict[str, Any]:
"""
Flash a firmware binary to a QEMU instance's flash image
"""Write a firmware binary directly into a QEMU instance's flash image file.
The instance must be stopped first. This writes the binary at the
given offset into the raw flash image file, then you can restart
the instance.
This is an offline operation the instance must be stopped first.
It patches the raw flash image at the given offset, then you can restart
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:
instance_id: Target QEMU instance
firmware_path: Path to firmware binary to write
address: Flash address offset (hex string, default: 0x0)
instance_id: Target QEMU instance (must be stopped)
firmware_path: Path to firmware binary to write into the flash image
address: Flash address offset as hex string (default: "0x0")
"""
return await self._flash_impl(context, instance_id, firmware_path, address)