Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Advanced Agent Workflows

Once you stop thinking “one board, one firmware”, espctl unlocks two patterns that change what an ESP32 development bench can do. Both are fully agent-driven — the user describes intent in natural language and the AI assistant orchestrates the MCP tool calls. The user’s job is limited to plugging in the boards and watching the dashboard.

PatternAxisWhat it gives you
One board, many rolesTimeA single ESP32-S3 rotates through several firmwares as the day’s task changes.
Many boards, one operatorSpaceA mini-PC or Pi drives four boards in parallel, each running a different role.

Time: one board, many roles

A single ESP32-S3 with USB has enough flash and RAM to host any of:

  • a USB logic analyzer (sigrok-compatible firmware — see PulseView & Sigrok Firmware)
  • a Wi-Fi gateway with MQTT or HTTP telemetry
  • a USB-to-serial debug bridge for probing another target’s UART
  • whatever custom project you’re iterating on right now

espctl makes the role swap a single conversational step. The agent fetches the right project, builds it, flashes the bound port, and verifies the new firmware booted — typically 60–120 seconds end to end for a small project that is already cached on the build server.

Conversation transcript

User:  Switch the board on /dev/cu.usbmodem1101 to logic-analyzer mode.

Agent: → build  { target: "esp32s3",
                  repo_url: "https://github.com/.../sigrok-fx2lafw-esp",
                  git_ref: "v0.2",
                  build_type: "release" }
       ← { task_id: "8a2f...", status: "running" }

       → build.status { task_id: "8a2f..." }   (poll every 2s)
       ← { status: "succeeded", firmware_path: ".../firmware.bin" }

       → flash.run  { firmware_path: ".../firmware.bin",
                      port:          "/dev/cu.usbmodem1101",
                      baud:          460800 }
       ← { success: true, chip: "ESP32-S3", flashed_bytes: 487424 }

       → monitor.run { port:          "/dev/cu.usbmodem1101",
                       duration_sec:  10,
                       filter:        "fx2lafw" }
       ← { lines_captured: 4, output: "I (321) fx2lafw: ready, 24 MHz max\n..." }

Agent: ✓ Logic analyzer is running on /dev/cu.usbmodem1101 at 24 MHz max
       sample rate. Open PulseView and select that serial port to capture.

What the agent uses

StepMCP toolWhy
Fetch + compile the new projectbuildNeeds no local toolchain; build server has every IDF version cached.
Track progressbuild.statusReturns pendingrunningsucceeded/failed.
Write the new imageflash.runPure-Rust espflash; no Python.
Confirm bootmonitor.runOptional but cheap — 10 s capture proves the role swap took effect.

The same ESP32-S3 can rotate through several roles in a single working session without any rewiring. If you keep a list of project repos (sigrok-fx2lafw-esp, your-esphome-gateway, usb-uart-bridge, my-current-project) the agent learns the rotation after the first prompt and you can shorthand subsequent swaps:

User:  Now make it a serial bridge.
Agent: [refers to its prior context, picks the bridge project, builds, flashes]

When this is the right pattern

  • Hardware-poor environments (workshops, classrooms, single-board labs).
  • Field demos where you carry one board and reconfigure on demand.
  • Bring-up sequences where you need to probe a behavior with a logic analyzer firmware, then switch the same board into the production role.

When it isn’t

  • Anything that needs two roles simultaneously. Use the spatial pattern below.
  • Production fleets — flashing is destructive to the previous role’s state. The temporal pattern is for development, not deployed workloads.

Space: many boards, one operator

A mini-PC or a Raspberry Pi 4/5 has enough USB ports to host four ESP boards. Each board enumerates as a distinct serial port (/dev/ttyUSB0..3 on Linux, COM3..6 on Windows, /dev/cu.usbmodem* on macOS). Because every espctl tool that touches a board accepts an explicit port argument, the agent treats each port as an independent target.

A useful four-role bench:

SlotRoleFirmware project
/dev/ttyUSB0USB logic analyzersigrok-compatible (see PulseView & Sigrok Firmware)
/dev/ttyUSB1Target under test (latest build)your project, branch main
/dev/ttyUSB2Target under test (previous build, A/B compare)your project, branch release-1.4
/dev/ttyUSB3Gateway + carrier-wave samplerESPHome Wi-Fi sink with ADC sampling

Conversation transcript

User:  Spin up the four-board bench. Latest main on USB1, last release
       on USB2, gateway on USB3, logic analyzer on USB0.

Agent: → espctl ports                              (CLI invoked via shell)
       ← /dev/ttyUSB0..3 enumerated, all ESP32-S3

       (parallel — two builds dispatched concurrently)
       → build  { target: "esp32s3", git_ref: "main",        ... }
       → build  { target: "esp32s3", git_ref: "release-1.4", ... }
       ← { task_id: "main-9c1a...", status: "running" }
       ← { task_id: "rel--7b22...", status: "running" }

       → build.status { task_id: "main-9c1a..." }
       → build.status { task_id: "rel--7b22..." }
       ← both succeeded

       (parallel — four flashes, each pinned to its port)
       → flash.run { firmware_path: <sigrok>,    port: "/dev/ttyUSB0" }
       → flash.run { firmware_path: <main.bin>,  port: "/dev/ttyUSB1" }
       → flash.run { firmware_path: <rel.bin>,   port: "/dev/ttyUSB2" }
       → flash.run { firmware_path: <gateway>,   port: "/dev/ttyUSB3" }
       ← all succeeded

       (parallel — four monitors capturing first 30 s of boot)
       → monitor.run { port: "/dev/ttyUSB0", duration_sec: 30, filter: "fx2lafw" }
       → monitor.run { port: "/dev/ttyUSB1", duration_sec: 30, filter: "heartbeat" }
       → monitor.run { port: "/dev/ttyUSB2", duration_sec: 30, filter: "heartbeat" }
       → monitor.run { port: "/dev/ttyUSB3", duration_sec: 30, filter: "wifi" }

Agent: ✓ Bench is up.
       USB0: logic analyzer, 24 MHz ready
       USB1: main @ a4f2c1e, 1 Hz heartbeat
       USB2: release-1.4 @ 9d20b5a, 1 Hz heartbeat
       USB3: gateway online, joined Wi-Fi as "lab-bench-gw"

       PulseView is configured to read USB0; Wireshark over USB3 will
       show MQTT publishes from USB1 and USB2.

What the agent uses

StepMCP tool / CLIWhy
Enumerate boardsespctl portsConfirms which ports actually have an ESP attached and their VID:PID.
Compile two firmwares in paralleltwo concurrent build callsEach returns its own task_id; the agent polls both. Build server runs them on independent sandboxes.
Flash four boards in parallelfour flash.run calls, each with port: pinnedLocal USB serial is per-port; no contention as long as ports differ.
Verify each rolefour monitor.run calls, each filter:-scopedSame parallelism rule — different ports, no contention.

Operator’s job

Watching, not driving:

  1. Plug in the four boards.
  2. Tell the agent the role for each port.
  3. Watch the agent’s stream-summary as it works.
  4. Open the application surfaces — PulseView, Wireshark/MQTT explorer, ADC visualizer — that consume what the bench produces.

The user never types espctl build, flash, or monitor themselves.

When this is the right pattern

  • A/B firmware regression testing (USB1 vs USB2).
  • Active capture during bring-up (analyzer + DUT in the same session).
  • Lab benches where one operator coordinates several test rigs.
  • Continuous-integration mules — a Pi running this pattern as a scheduled task can validate every PR against multiple boards unattended.

When it isn’t

  • Production manufacturing flashing — for that, scale espctl across hosts and use a queue, not one Pi orchestrating four ports.
  • Anything requiring sub-millisecond synchronization between boards. USB serial is not real-time.

Why this works in espctl

The two patterns rely on three properties of the espctl MCP surface:

  1. Stateless tools. flash.run and monitor.run accept the port as an explicit argument; nothing in the agent’s history binds a tool call to a particular board. Calling the same tool twice with two different ports is a clean parallel operation.
  2. Independent build tasks. build returns a task_id immediately and runs in the background. Two build calls produce two task_ids and the agent polls each build.status independently.
  3. Local execution for hardware. flash.run and monitor.run run on the user’s machine (or the operator’s mini-PC/Pi), talking directly to USB. Build is remote; hardware is local. The agent moves between the two transparently.

See also