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
8 changes: 8 additions & 0 deletions packages/embeddedci-mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog — `embeddedci-mcp`

## 2.13.15

- `connect` over the cloud to a pod with firmware 3.10.4 works again: its `status` was too large
for the cloud channel, which failed the connect. `status` now reports the firmware fields as
unknown with a warning to update to firmware 3.10.5. With firmware 3.10.5's short cloud status,
`status` fills `firmware.lan_tls` from the `lan_tls` command.
- Needs embeddedci 2.13.15.

## 2.13.14

- Pin directions are on by default: `connect` and `set_wiring` send the wiring profile's DUT
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.14"
version = "2.13.15"
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.14,<3",
"embeddedci[cloud,discovery,analysis,yaml]>=2.13.15,<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.14",
"version": "2.13.15",
"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.14",
"version": "2.13.15",
"runtimeHint": "uvx",
"transport": {
"type": "stdio"
Expand Down
20 changes: 19 additions & 1 deletion packages/embeddedci-mcp/src/embeddedci_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
from embeddedci.benchpod.cloud_auth import DEFAULT_API_BASE
from embeddedci.benchpod.connection import CLOUD_PREFIX
from embeddedci.benchpod import errors as _errors
from embeddedci.benchpod.errors import STATUS_FITS_FIRMWARE, FirmwareError, status_too_large
from embeddedci.benchpod.errors import BenchPodError
from embeddedci.benchpod.server_api import ServerApi

Expand Down Expand Up @@ -317,8 +318,25 @@ def _status() -> m.StatusResult:
"else pass a host, 'usb' or 'embeddedci:<device>'")
return m.StatusResult(connected=False, warnings=[f"not connected — {hint}"])
pod = SESSION.require()
firmware = pod.status()
warnings: List[str] = []
try:
firmware = pod.status()
except FirmwareError as exc:
# Firmware 3.10.4's status can be too large for the cloud channel. The connection works:
# report the status as unknown with the fix, rather than failing the connect.
if not status_too_large(exc.firmware_message, exc.cmd):
raise
firmware = {}
warnings.append(f"this pod's status is too large for the cloud channel (firmware "
f"{pod.capabilities.firmware_version or 'older'}): update the pod to firmware "
f"{STATUS_FITS_FIRMWARE} or later; the status fields are unknown until then")
if isinstance(firmware, dict) and firmware.get("short_form") and "lan_tls" not in firmware:
# The cloud status is the short form (firmware 3.10.5+): lan_tls is its own command there.
try:
tls = pod.lan_tls()
firmware["lan_tls"] = {"port": tls.port, "https_only": tls.https_only, "sha256": tls.sha256}
except BenchPodError:
pass
# From this status, not the cached capabilities: it describes the boot running now.
boot = Capabilities.from_status(firmware).boot_warning()
if boot:
Expand Down
23 changes: 23 additions & 0 deletions packages/embeddedci-mcp/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,29 @@ def test_status_warns_about_safe_mode_and_crashes(fake_open, fake_transport, mon
assert call("status")["warnings"] == []


def test_status_too_large_for_the_cloud_keeps_the_connection(fake_open, fake_transport, monkeypatch):
# Firmware 3.10.4 over the cloud: its status did not fit the cloud channel and connect failed.
from embeddedci.benchpod.errors import FirmwareError

def too_large():
raise FirmwareError("reply too large for the cloud channel (1838 bytes, limit 1791)",
cmd="status")
monkeypatch.setattr(fake_transport, "status", too_large)
result = call("connect", connection="192.168.1.50", la_voltage=3.3)
assert result["connected"] is True and result["firmware"] == {}
assert any("too large for the cloud channel" in w and "3.10.5" in w for w in result["warnings"])


def test_short_form_status_gets_lan_tls_from_its_command(fake_open, fake_transport, monkeypatch):
# Firmware 3.10.5's cloud status is the short form: lan_tls is its own command there.
base = fake_transport.status
monkeypatch.setattr(fake_transport, "status", lambda: {**base(), "short_form": True})
fake_transport._cmd_lan_tls = lambda req: {"port": 8443, "https_only": True, "sessions": 0,
"sha256": "ab" * 32}
result = call("connect", connection="192.168.1.50", la_voltage=3.3)
assert result["firmware"]["lan_tls"] == {"port": 8443, "https_only": True, "sha256": "ab" * 32}


def test_connect_without_la_voltage_warns(fake_open):
result = call("connect", connection="usb")
assert result["kind"] == "serial" and result["la_voltage"] is None
Expand Down
9 changes: 9 additions & 0 deletions packages/embeddedci/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog — `embeddedci`

## 2.13.15

- A pod whose `status` is too large for the cloud channel (firmware 3.10.4 with the USB network
adapter up and a lease held) no longer leaves the SDK guessing: the error names the fix,
firmware 3.10.5, and `bp.capabilities` uses the server's capability set instead of reading the
status again on every access. Firmware 3.10.5 sends a short `status` over the cloud
(`"short_form": true`); it leaves out `lan_tls` (use `bp.lan_tls()`), `lease`, the diagnostics
and the fixed feature flags the server already has.

## 2.13.14

- Pin directions are on by default. On a pod with firmware 3.10.4+ the SDK sends the wiring
Expand Down
2 changes: 1 addition & 1 deletion packages/embeddedci/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"
version = "2.13.14"
version = "2.13.15"
description = "BenchPod SDK and pytest plugin for EmbeddedCI hardware-in-the-loop testing"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
8 changes: 7 additions & 1 deletion packages/embeddedci/src/embeddedci/benchpod/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
coerce_pin,
)
from .connection import resolve_connection
from .errors import BenchPodError, FirmwareError, UnsupportedFeatureError, classify_firmware_error
from .errors import (BenchPodError, FirmwareError, UnsupportedFeatureError, classify_firmware_error,
status_too_large)
from .flash import FlashResult
from .gpio import LA_DIRS, GpioPin, LaPinState
from .lease import DEFAULT_LEASE_TTL, DEFAULT_LEASE_WAIT, DeviceLease
Expand Down Expand Up @@ -399,6 +400,11 @@ def capabilities(self) -> Capabilities:
status_ok = True
try:
status = self.status()
except FirmwareError as exc:
# Firmware 3.10.4's status can be too large for the cloud channel. Reading it again
# will not help: use the server's capabilities alone, and keep them.
_log.warning("could not read the pod's status for its capabilities: %s", exc)
status, status_ok = {}, status_too_large(exc.firmware_message, exc.cmd)
except BenchPodError as exc:
_log.warning("could not read the pod's status for its capabilities: %s", exc)
status, status_ok = {}, False
Expand Down
14 changes: 14 additions & 0 deletions packages/embeddedci/src/embeddedci/benchpod/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def __init__(self, message: str, *, cmd: Optional[str] = None) -> None:
self.firmware_message = message
self.cmd = cmd
text = _sdk_refusal_text(message)
if status_too_large(message, cmd):
text += (f"; firmware {STATUS_FITS_FIRMWARE} and later send a status that fits: update "
"the pod (`benchpod flash-self`, or the firmware update in the web app)")
if cmd:
super().__init__(f"{cmd}: {text}")
else:
Expand Down Expand Up @@ -269,6 +272,17 @@ def __init__(self, message: str, *, cmd: Optional[str] = None,
#: The firmware's "la voltage not set; set it with la_voltage (mv 1800 or 3300) first"
#: (command_handler.c require_la_voltage). Matched on the leading words only.
_LA_VOLTAGE_UNSET = "la voltage not set"

#: Firmware 3.10.4 and older could answer ``status`` over the cloud with more than the cloud
#: channel takes ("reply too large for the cloud channel (1838 bytes, limit 1791)"). From this
#: version a cloud ``status`` is a short form that always fits.
STATUS_FITS_FIRMWARE = "3.10.5"
_STATUS_TOO_LARGE = "too large for the cloud channel"


def status_too_large(message: str, cmd: Optional[str] = "status") -> bool:
"""Whether a pod refusal is a ``status`` reply that did not fit the cloud channel."""
return cmd == "status" and _STATUS_TOO_LARGE in message
_LA_VOLTAGE_SDK = ("la voltage not set; set the board's I/O voltage first: "
"BenchPod(conn, la_voltage=3.3) (1.8 for a 1.8 V target), the "
"benchpod_la_voltage fixture in conftest.py, or bp.set_la_voltage(3.3)")
Expand Down
9 changes: 9 additions & 0 deletions packages/embeddedci/tests/test_refusal_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ def test_leased_carries_holder_and_time_left():
assert err.firmware_message.startswith("busy:") and err.cmd == "gpio"


def test_status_too_large_for_the_cloud_names_the_fix():
err = firmware_error("reply too large for the cloud channel (1838 bytes, limit 1791)", cmd="status")
assert isinstance(err, FirmwareError)
assert "too large for the cloud channel" in str(err) and "firmware 3.10.5" in str(err)
# Other commands' size refusals are left as they are.
other = firmware_error("reply too large for the cloud channel (1900 bytes, limit 1791)", cmd="la_pins")
assert "3.10.5" not in str(other)


def test_busy_without_a_lease():
err = firmware_error("busy: a capture or upload is running")
assert type(err) is PodBusyError
Expand Down