Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/embeddedci-mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog — `embeddedci-mcp`

## 2.13.13

- I2C master tools (gateware v55+): `i2c_open`, `i2c_transfer` (a NACK returns `ack: false` and
`nack_at`), `i2c_scan` and `i2c_close`.
- `capture_la` takes `send`: one I2C, SPI or gpio transaction right after the capture arms, with
its reply in the result's `send`. Without `trigger_la` it triggers on SDA or CS falling, or the
gpio pin's edge, and keeps 64 samples from before the trigger.
- `la_dir` sets which way each pin's DUT signal goes (the pod then refuses push-pull drivers on a
DUT output); `set_wiring` sends the profile's directions. `la_pins` rows have `dir` and
`warnings`.
- The SPI tools take modes 1 and 2 (gateware v55+).
- Needs embeddedci 2.13.13.

## 2.13.12

- `sensor_capture` waits for the DUT's next I2C transaction (up to 2 s) and captures it from its
Expand Down
20 changes: 18 additions & 2 deletions packages/embeddedci-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ description, ranges and enums, so an agent sees more than this table.
| Tool | Purpose | Parameters |
| --- | --- | --- |
| `wiring` | Which DUT signal is on which LA channel, plus the power rail, UART baud and SWD target. | none |
| `set_wiring` | Replace the wiring profile for this connection, or store it on embeddedci.com. | **`profile`**, `save=false` |
| `set_wiring` | Replace the wiring profile for this connection, or store it on embeddedci.com. Also sends the profile's pin directions to the pod (`la_dir`; `directions` in the result says whether it worked). | **`profile`**, `save=false` |

### Power

Expand Down Expand Up @@ -226,6 +226,21 @@ description, ranges and enums, so an agent sees more than this table.
| `spi_flash_read` | Read flash bytes to a file, or up to 4096 bytes as hex. | **`addr`**, **`length`**, `file`, `sck`, `mosi`, `miso`, `cs`, `hz=6000000`, `mode=0` |
| `spi_transfer` | One full-duplex SPI transaction with any device on the SPI pins. | **`tx_hex`**, `sck`, `mosi`, `miso`, `cs`, `hz=1000000`, `mode=0` |

`mode` is 0 to 3; modes 1 and 2 need gateware v55+.

### I2C master

The pod as an I2C master on two LA pins (gateware v55+), to read and write registers of a chip on
the DUT's board. The bus needs pull-ups: the pod's on LA1-LA6 (`set_pull` first, LA voltage 3.3), or
the board's with `pullups="external"`.

| Tool | Purpose | Parameters |
| --- | --- | --- |
| `i2c_open` | Make the pod an I2C master on two LA pins; `hz` in the result is the SCL rate the pod runs (a real bus is somewhat slower: the rise time adds to each period). | `sda`, `scl`, `hz=100000`, `pullups=pod` |
| `i2c_transfer` | Write, read, or write then read (repeated START); neither is a probe. A NACK returns `ack: false` and `nack_at`. | **`addr`**, `write_hex`, `read=0` |
| `i2c_scan` | Which addresses answer. | `start=8`, `end=119` |
| `i2c_close` | Release the I2C master's pins. | none |

### UART

| Tool | Purpose | Parameters |
Expand Down Expand Up @@ -286,6 +301,7 @@ version the SDK pins (0.1.0, bundled) unless `BENCHPOD_SENSOR_LIBRARY`, the CLI'
| `gpio_wait` | Wait until an LA channel reads a level (`reached: false` on timeout). | **`la`**, `level=1`, `timeout=5` |
| `gpio_pulse` | FPGA-timed pulses on an LA channel: `width` 4 us to 65.535 ms, `count` up to 65535. | **`la`**, **`width`**, `count=1` |
| `gpio_release` | Return channels to high-Z, watched by captures. | `la` |
| `la_dir` | Which way each pin's DUT signal goes (`dut_input`, `dut_output`, `bidir`, `unknown`); on a DUT output the pod refuses push-pull drivers. `from_wiring` sends the wiring profile's directions; no arguments lists every pin. | `la`, `dir`, `from_wiring=false` |

### Analog

Expand All @@ -306,7 +322,7 @@ version the SDK pins (0.1.0, bundled) unless `BENCHPOD_SENSOR_LIBRARY`, the CLI'
| Tool | Purpose | Parameters |
| --- | --- | --- |
| `capture_adc` | Capture the ADC: calibrated statistics, dominant frequency and a min/max envelope; `clipped_samples` counts samples past the range's full scale. `oversample` converts at 400 kS/s and averages down to `sample_rate_hz` for lower noise, about 5 s at most. | `samples=4096`, `sample_rate_hz`, `source`, `points=200`, `trigger_la`, `trigger_edge=rising`, `trigger_timeout=10`, `range`, `oversample` |
| `capture_la` | Capture all 14 LA channels: levels, edge count, first edge and frequency per channel. Up to 48 MS/s on gateware v50+. | `samples=4096`, `sample_rate_hz`, `stop_dac_after`, `trigger_la`, `trigger_edge=rising`, `trigger_timeout=10`, `trigger_pre_samples=0` |
| `capture_la` | Capture all 14 LA channels: levels, edge count, first edge and frequency per channel. Up to 48 MS/s on gateware v50+. `send` (gateware v55+) runs one transaction right after the capture arms: `{"bus":"i2c","addr":118,"write_hex":"d0","read":1}` (the `i2c_open` session, else one from the wiring), `{"bus":"spi","tx_hex":"9f00"}` (SPI pins from the wiring) or `{"bus":"gpio","la":6,"level":0}` (a GPIO output); without `trigger_la` it triggers on SDA or CS falling or the pin's edge, and the result's `send` has the reply. | `samples=4096`, `sample_rate_hz`, `stop_dac_after`, `trigger_la`, `trigger_edge=rising`, `trigger_timeout=10`, `trigger_pre_samples=0`, `send` |
| `capture_correlated` | ADC and LA from one hardware trigger, on one timebase. | `adc_samples=4096`, `adc_sample_rate_hz`, `la_samples=4096`, `la_sample_rate_hz`, `stop_dac_after`, `points=200`, `trigger_la`, `trigger_edge=rising`, `trigger_timeout=10`, `source` |
| `decode_la` | Decode I2C, UART or SPI from the last LA capture (or a new one). | **`protocol`**, `sda`, `scl`, `rx`, `baud`, `sclk`, `mosi`, `miso`, `cs`, `mode=0`, `capture=last`, `samples=16384`, `sample_rate_hz=1000000`, `max_items=200` |
| `la_timing` | Edge timestamps, pulse widths, frequency, duty cycle and channel-to-channel delay from the last capture. | **`la`**, `edge=rising`, `to_la`, `to_edge=rising`, `after=0`, `max_edges=100` |
Expand Down
4 changes: 2 additions & 2 deletions packages/embeddedci-mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "embeddedci-mcp"
version = "2.13.12"
version = "2.13.13"
description = "MCP server that lets AI agents drive an EmbeddedCI BenchPod hardware-in-the-loop bench"
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -26,7 +26,7 @@ classifiers = [
dependencies = [
# cloud: the embeddedci:<device> destination; discovery: connect("discover") over mDNS;
# analysis: dominant frequency in capture summaries; yaml: sensor definitions an agent writes.
"embeddedci[cloud,discovery,analysis,yaml]>=2.13.12,<3",
"embeddedci[cloud,discovery,analysis,yaml]>=2.13.13,<3",
"mcp>=1.12,<2",
"anyio>=4",
"pydantic>=2.7",
Expand Down
4 changes: 2 additions & 2 deletions packages/embeddedci-mcp/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "io.github.embeddedci-com/embeddedci-mcp",
"title": "EmbeddedCI BenchPod",
"description": "Drive a BenchPod hardware-in-the-loop bench: power, flash, UART, I2C, CAN, analog and logic.",
"version": "2.13.12",
"version": "2.13.13",
"websiteUrl": "https://www.embeddedci.com/docs/benchpod-mcp",
"repository": {
"url": "https://github.com/embeddedci-com/embeddedci-python",
Expand All @@ -15,7 +15,7 @@
"registryType": "pypi",
"registryBaseUrl": "https://pypi.org",
"identifier": "embeddedci-mcp",
"version": "2.13.12",
"version": "2.13.13",
"runtimeHint": "uvx",
"transport": {
"type": "stdio"
Expand Down
1 change: 1 addition & 0 deletions packages/embeddedci-mcp/src/embeddedci_mcp/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Typical flows:
- Flash and boot: flash(file) then power_cycle_and_capture(until_regex) — with a wiring profile that is all; otherwise pass swclk, swdio, target, nreset and rx, tx.
- SPI flash (a 25-series chip on four LA pins, wiring spi_sclk/spi_mosi/spi_miso/spi_cs): spi_flash_info to see it answers, then spi_flash_program(file, addr); hold_reset=true keeps the DUT off the bus. spi_transfer talks to any other SPI device.
- I2C master (gateware v55+): set_pull([sda, scl], true), i2c_open, then i2c_transfer(addr, write_hex, read) or i2c_scan; i2c_close. capture_la(send={...}) runs one I2C, SPI or gpio transaction right after the capture arms and triggers on it. la_dir marks DUT outputs so the pod never drives against them (set_wiring sends the profile's directions).
- Interactive console: uart_open, power_on, uart_read(until_regex), uart_write, uart_close.
- Emulate sensors (I2C, SPI, UART, analog; firmware with the 'sensors' capability): the pod plays a sensor from a definition. sensor_library lists ready ones (bme280, bmp280, sht4x, mpu6050, gps_nmea, ...) and sensor_show gives one's YAML and params. For I2C, set_pull([sda, scl], true) on LA1-LA6 (LA7/LA8 pull DOWN) first. sensor_start(definition=<name or YAML>, pins={sda, scl} | {tx} | ...), then sensor_set(instance, values) changes readings, sensor_stimulus_load + sensor_stimulus_start play values over time, sensor_fault injects a fault, sensor_status shows bus activity, sensor_capture waits for the DUT's next transaction on an I2C sensor's bus and decodes it, sensor_stop ends it. A part the library lacks: read sensor_format, write a definition from the datasheet, check it with sensor_validate, then sensor_start it. A GPS module is gps_nmea on pins={tx: <LA wired to the DUT RX>}.
- Analog: dac_output (DC), generate (sine/square/sawtooth) and replay drive the DAC and route its path; adc_read gives one calibrated value, capture_adc a waveform summary; dac_stop ends any DAC output.
Expand Down
57 changes: 55 additions & 2 deletions packages/embeddedci-mcp/src/embeddedci_mcp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
structured content. Units follow the SDK: volts, amps, seconds, hertz.
"""

from typing import Any, Dict, List, Literal, Optional
from typing import Any, Dict, List, Literal, Optional, Union

from pydantic import BaseModel, Field

Expand All @@ -21,6 +21,20 @@ class FaultSpec(BaseModel):
description="Raw DAC code to hold (default per fault type).")


class SendSpec(BaseModel):
"""One transaction capture_la runs right after it arms (gateware v55+)."""

bus: Literal["i2c", "spi", "gpio"]
addr: Optional[int] = Field(None, ge=0, le=127, description="i2c: 7-bit address.")
write_hex: str = Field("", description="i2c: bytes to write, hex (up to 32).")
read: int = Field(0, ge=0, le=32, description="i2c: bytes to read after the write.")
tx_hex: str = Field("", description="spi: one CS frame, hex (1-32 bytes); SPI pins from the wiring.")
spi_mode: int = Field(0, ge=0, le=3, description="spi: mode 0-3.")
spi_hz: int = Field(1_000_000, ge=190_000, le=6_000_000, description="spi: SCK rate.")
la: Optional[Union[int, str]] = Field(None, description="gpio: an LA channel already a GPIO output.")
level: Optional[Literal[0, 1]] = Field(None, description="gpio: the level to set.")


class InputMapSpec(BaseModel):
"""Engineering-units input map for the control loop (gateware >= v30)."""

Expand Down Expand Up @@ -89,6 +103,7 @@ def from_caps(cls, caps: Any) -> "CapabilitiesInfo":
class SessionInfo(BaseModel):
uart_open: bool = False
can_open: bool = False
i2c_open: bool = False
power_profile_running: bool = Field(False, description="power_profile_start ran without power_profile_stop.")
last_adc_capture_samples: Optional[int] = None
last_la_capture_samples: Optional[int] = None
Expand Down Expand Up @@ -157,6 +172,8 @@ class WiringResult(BaseModel):
description="Wiring that works but is risky (e.g. an I2C bus with no pull-up).")
profile: Dict[str, Any] = Field(default_factory=dict, description="The profile as its JSON object.")
saved: bool = Field(False, description="set_wiring stored this profile on embeddedci.com.")
directions: Optional[str] = Field(None, description=(
"set_wiring: 'sent' when the pod now has the profile's pin directions (la_dir), else why not."))


# -- LA pin ownership + GPIO -------------------------------------------------------
Expand All @@ -165,13 +182,16 @@ class LaPinResult(BaseModel):
la: int
function: str = Field(description=(
"What owns the channel: none (free, watched by captures), gpio, uart_rx, uart_tx, swd_clk, "
"swd_dio, i2c_sda, i2c_scl, step or step_dir."))
"swd_dio, i2c_sda, i2c_scl, step, step_dir, spi_*, sensor_*, or i2cm_sda/i2cm_scl (i2c_open)."))
gpio: Optional[str] = Field(None, description="GPIO mode when function is gpio: input, output or open_drain.")
level: Optional[int] = Field(None, description="Commanded level of a GPIO output / open-drain channel.")
pull: Optional[str] = Field(None, description="Bias direction: up, down, or null on LA9-LA14.")
pull_ohms: Optional[str] = None
pull_on: bool = False
in_use: bool = Field(description="A function other than none owns the channel.")
dir: Optional[str] = Field(None, description=(
"The DUT signal's direction set with la_dir: dut_input, dut_output or bidir; null when unknown."))
warnings: Optional[List[str]] = Field(None, description="Pin warnings from the pod, if any.")


class PinLevel(BaseModel):
Expand Down Expand Up @@ -327,6 +347,38 @@ class SpiFlashReadResult(BaseModel):
hex: str = ""


class I2cSessionResult(BaseModel):
open: bool
sda: Optional[int] = None
scl: Optional[int] = None
hz: Optional[int] = Field(None, description=(
"SCL rate the pod runs; a real bus is somewhat slower (the rise time adds to each period)."))
pullups: Optional[str] = None


class I2cTransferResult(BaseModel):
addr: int
ack: bool = Field(description="Every byte was acknowledged.")
rx_hex: str = Field("", description="The bytes read.")
nack_at: Optional[int] = Field(None, description=(
"When ack is false: 0 = the address, 1..N = the Nth written byte, N+1 = the read address."))


class I2cScanResult(BaseModel):
found: List[int] = Field(description="7-bit addresses that answered.")
found_hex: List[str]


class SendResultModel(BaseModel):
bus: str
ack: Optional[bool] = None
rx_hex: str = ""
nack_at: Optional[int] = None
la: Optional[int] = None
level: Optional[int] = None
error: Optional[str] = Field(None, description="Why the transfer failed; the capture still returned.")


class SpiTransferResult(BaseModel):
rx_hex: str
hz: int
Expand Down Expand Up @@ -572,6 +624,7 @@ class LaCaptureResult(BaseModel):
truncated: Optional[str] = Field(None, description=(
"Above 12 MS/s only: why the capture stopped early ('busy': the signals changed too often, "
"'memory'), so it holds fewer samples than asked; null when complete."))
send: Optional[SendResultModel] = Field(None, description="What the capture's send did.")


class PulseStats(BaseModel):
Expand Down
Loading