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
2 changes: 2 additions & 0 deletions packages/embeddedci-mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 2.12.0

- New tool `usb_network`: read the pod's USB network setting, or turn the adapter off over the USB
link (`set="off"`). Turning it back on is `benchpod usb-network on` on the USB console.
- Needs `embeddedci` 2.12.0 or later (was 2.9): its `usb` connection is what reaches the USB
network adapter the instructions describe.
- Connection `usb` reaches the full API over the pod's USB network adapter on firmware 3.9.0 and
Expand Down
1 change: 1 addition & 0 deletions packages/embeddedci-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ the pod's second UART, so `uart_open` keeps the console. Needs gateware v48 (`gp
| --- | --- | --- |
| `set_pull` | Engage or release the bias resistors: LA1-LA6 pull up, LA7/LA8 pull down. | **`las`**, **`enabled`** |
| `pull_status` | State, direction and value of the bias resistor on LA1-LA8. | none |
| `usb_network` | Read the pod's USB network setting (firmware 3.9.0+), or turn the adapter off over the USB link. Only the device changes it; turning it back on is `benchpod usb-network on` on the USB console. | `set=off` |

### GPIO on the LA pins

Expand Down
6 changes: 6 additions & 0 deletions packages/embeddedci-mcp/src/embeddedci_mcp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,9 @@ class StepResult(BaseModel):
steps: int
delay: float
status: str = "started"


class UsbNetworkResult(BaseModel):
"""The pod's USB network setting (firmware 3.9.0+)."""
setting: str = Field(description='"on" or "off": whether the pod\'s USB port is also a network adapter.')
note: str = Field("", description="What happens next, after a change.")
22 changes: 22 additions & 0 deletions packages/embeddedci-mcp/src/embeddedci_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,28 @@ def op() -> m.PullStatusResult:
return await _call(op)


@mcp.tool(annotations=_ann("USB network setting", destructive=True))
async def usb_network(
set: Annotated[Optional[Literal["off"]], Field(description=(
'Omit to read the setting. "off" turns the pod\'s USB network adapter off (only over the '
"pod's USB network link: connection 'usb'); the pod then re-enumerates as its USB console "
"only and this connection ends. Turning it back on needs the USB console: the user runs "
"`benchpod usb-network on`."))] = None,
) -> m.UsbNetworkResult:
"""Read the pod's USB network setting (firmware 3.9.0+), or turn the adapter off. Only the device
changes it: Ethernet, Wi-Fi and the cloud may only read it."""
def op() -> m.UsbNetworkResult:
pod = SESSION.require()
on = pod.usb_network(None if set is None else False)
note = ""
if set == "off":
note = ("The pod re-enumerates as its USB console only; this connection ends. Ask the user to run "
"`benchpod usb-network on` over the USB console to turn it back on.")
return m.UsbNetworkResult(setting="on" if on else "off", note=note)

return await _call(op)


@mcp.tool(annotations=_ann("Pull resistor status", read_only=True))
async def pull_status() -> m.PullStatusResult:
"""State, direction and value of the bias resistor on LA1-LA8."""
Expand Down
12 changes: 11 additions & 1 deletion packages/embeddedci-mcp/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"enable_i2c_sensor", "set_i2c_sensor", "disable_i2c_sensor", "i2c_sensor_status",
"i2c_sensor_regs", "i2c_sensor_capture", "i2c_sensor_types",
"enable_gps", "set_gps", "disable_gps", "gps_status",
"set_pull", "pull_status",
"set_pull", "pull_status", "usb_network",
"analog_path", "dac_output", "current_out", "adc_read", "calibration", "calibrate",
"capture_adc", "capture_la", "capture_correlated", "decode_la", "la_timing",
"generate", "dac_stop", "replay", "list_waveforms", "replay_waveform",
Expand Down Expand Up @@ -605,3 +605,13 @@ def test_la_step_and_command(connected):
assert call("command", request={"cmd": "adc_read", "source": "cal1"})["reply"]["source"] == "cal1"
with pytest.raises(ToolError):
call("command", request={"samples": 1})


def test_usb_network_reads_and_turns_off(fake_open, fake_transport, monkeypatch):
call("connect", connection="usb", la_voltage=3.3)
pod = SESSION.require()
seen = []
monkeypatch.setattr(pod, "usb_network", lambda on=None: seen.append(on) or (on is not False), raising=False)
assert call("usb_network")["setting"] == "on"
res = call("usb_network", set="off")
assert res["setting"] == "off" and "usb-network on" in res["note"] and seen == [None, False]
29 changes: 29 additions & 0 deletions packages/embeddedci-mcp/tests/tools_surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -4411,6 +4411,35 @@
"type": "object"
}
},
"usb_network": {
"annotations": {
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": false,
"readOnlyHint": false,
"title": "USB network setting"
},
"input": {
"properties": {
"set": {
"anyOf": [
{
"const": "off",
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Omit to read the setting. \"off\" turns the pod's USB network adapter off (only over the pod's USB network link: connection 'usb'); the pod then re-enumerates as its USB console only and this connection ends. Turning it back on needs the USB console: the user runs `benchpod usb-network on`.",
"title": "Set"
}
},
"title": "usb_networkArguments",
"type": "object"
}
},
"wiring": {
"annotations": {
"idempotentHint": false,
Expand Down
7 changes: 7 additions & 0 deletions packages/embeddedci/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## 2.12.0

- `BenchPod.usb_network(on=None)`: read the pod's USB network setting (firmware 3.9.0+), or turn
the adapter off over the USB link (`False`). Ethernet, Wi-Fi and the cloud get
`PodLockedError`; turning it back on is `benchpod usb-network on` on the USB console.
- Connection `usb` finds the pod from this computer's own end of its USB network link (Linux and
macOS, `getifaddrs`) before opening the console, so it works for a Linux user outside the
`dialout` group; `usb:<name>` picks among several links by identity. `TcpTransport.usb_net`
marks a USB network transport.
- On firmware 3.9.0 and later the USB console no longer has `la-voltage` or `power` (they are JSON
commands, reached over the USB network adapter): a serial device path then fails with an error
that says to connect with `usb` or the pod's network address (the console answers those with a
Expand Down
12 changes: 10 additions & 2 deletions packages/embeddedci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,21 @@ The USB network adapter works with the in-box drivers of macOS, Linux and Window
Windows 10 (1903 or newer) is supported but not tested: if the adapter shows up without a driver,
update its driver in Device Manager and pick "UsbNcm Host Device".

**Turning the adapter off.** The pod keeps a USB network setting, on by default and separate from
its external LAN policy. `bp.usb_network()` reads it (`True` = on) over any connection;
`bp.usb_network(False)` turns the adapter off, which only works over the USB link itself: the pod
replies, then re-enumerates as its USB console only, which ends the connection. Over Ethernet,
Wi-Fi or the cloud the pod refuses the change (`PodLockedError`). Turning it back on needs the USB
console: `benchpod usb-network on`. Status reports it as `usb.setting` and, when the adapter is
off, `usb.off_reason` (`"setting"` or `"wifi"`).

**Compatibility.** The SDK decides per pod, from what the pod reports (the `usb` line of its
console `status`), never from its own version:

| | Pod firmware before 3.9.0 | Pod firmware 3.9.0 and later |
|---|---|---|
| **This SDK (2.12.0+)** | As before: `usb` is the console (status, ping, LA voltage, target power); the rest needs the network or the cloud. Console auto-detect still finds `0483:5740` and `2E8A`. | `usb` is the full API over the USB network adapter when it is up. When USB networking is off (Wi-Fi configured) or down, it is the full API over the pod's Ethernet or Wi-Fi address from its console status, when this computer reaches it (logged as a warning; the pod's external LAN policy applies there; `transport.usb_lan_fallback` says why); else the console. |
| **An older SDK (2.11.x)** | Unchanged. | `usb` is the console. Status and ping work; LA voltage and target power, which 3.9.0 moved to the JSON API, fail with the pod's "moved to the network API ... update" line. A network address or `embeddedci:<name>` works fully. |
| **An older SDK (2.11.x)** | Unchanged. | `usb` is the console: its auto-detect probes every USB serial port, so it finds a 3.9.0 pod under its new USB id `1209:0001` (tested). Status and ping work; LA voltage and target power, which 3.9.0 moved to the JSON API, fail with the pod's "moved to the network API ... update" line. A network address or `embeddedci:<name>` works fully. |

**Several pods on USB.** Auto-detect reads each pod's name (its identity short id, `baea06` for
`benchpod-baea06`) from the console's `identity` command. Connection `usb:<name>` opens the pod
Expand Down Expand Up @@ -1520,7 +1528,7 @@ lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is

| Area | Methods and properties |
|---|---|
| Connection and device | `ping()`, `status()`, `capabilities`, `refresh_capabilities()` (drop the cached capabilities and read them again), `close()`, `leased` (true while this client holds the cloud lease), `wiring`, `save_wiring(wiring=None)` (store a cloud device's profile on embeddedci.com), `signal(name)`, `fpga_image(image)`, `usb_cc()` |
| Connection and device | `ping()`, `status()`, `capabilities`, `refresh_capabilities()` (drop the cached capabilities and read them again), `close()`, `leased` (true while this client holds the cloud lease), `wiring`, `save_wiring(wiring=None)` (store a cloud device's profile on embeddedci.com), `signal(name)`, `fpga_image(image)`, `usb_cc()`, `usb_network(on=None)` (the USB network setting; `False` turns the adapter off over the USB link) |
| LA voltage | `set_la_voltage(volts)`, `get_la_voltage()` |
| Power and reset | `power_on(efuse=None, *, delay=None)`, `power_off(efuse=None, *, delay=None)`, `target_power(efuse=None, *, on, delay=None)`, `target_status()`, `power_status()`, `measure_power(duration, *, efuse=None, rate_hz=500.0, keep_samples=0)`, `power_profile(*, efuse=None, rate_hz=500.0, keep_samples=4096, max_duration=60.0)`, `reset_target(*, pulse=0.1)`, `set_reset(asserted)`, `reset_state()` |
| Flashing | `flash(...)`, `spi_flash(image, addr=0, *, erase=True, verify=True, hold_reset=False, sck=, mosi=, miso=, cs=)`, `open_spi(...)`, `open_motor_emulator(...)` |
Expand Down
16 changes: 16 additions & 0 deletions packages/embeddedci/src/embeddedci/benchpod/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,22 @@ def usb_cc(self) -> UsbCcStatus:
"""The pod's USB-C cable orientation and the current the upstream source advertises (rev3)."""
return UsbCcStatus.from_reply(self.command({"cmd": "usb_cc"}))

def usb_network(self, on: Optional[bool] = None) -> bool:
"""The pod's USB network setting (firmware 3.9.0+): whether its USB port is also a network
adapter. Returns the stored setting (``True`` = on).

``on=False`` turns the adapter off. Only the device may change it: this works over the
pod's USB network link itself (connection ``usb``); over Ethernet, Wi-Fi or the cloud the
pod refuses with :class:`PodLockedError`. The pod replies, then re-enumerates as its USB
console only, which ends this connection. Turning it back on needs the USB console:
``benchpod usb-network on``.
"""
req: Dict[str, Any] = {"cmd": "usb_network"}
if on is not None:
req["set"] = "on" if on else "off"
data = self.command(req)
return isinstance(data, dict) and data.get("setting") == "on"

# -- flash --------------------------------------------------------------

def flash(
Expand Down
1 change: 1 addition & 0 deletions packages/embeddedci/src/embeddedci/benchpod/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ def __init__(self, message: str, *, cmd: Optional[str] = None,
#: Policy changes the LAN may not make (pod_policy.c).
_POLICY_ELSEWHERE = ("change it from the cloud or the USB console",
"change it from the cloud or over USB",
"change it on the device",
"only the USB console can loosen")
#: Older firmware's answer to a command it does not have (command_handler.c).
_UNKNOWN_CMD = "unknown cmd"
Expand Down
1 change: 1 addition & 0 deletions packages/embeddedci/tests/api_surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"target_status": "(self) -> 'TargetStatus'",
"transport": "property",
"usb_cc": "(self) -> 'UsbCcStatus'",
"usb_network": "(self, on: 'Optional[bool]' = None) -> 'bool'",
"wait_for_level": "(self, la: 'Union[Pin, int, str]', level: 'int', *, timeout: 'float', poll: 'float' = 0.005) -> 'bool'",
"waveforms": "property",
"wiring": "property"
Expand Down
1 change: 1 addition & 0 deletions packages/embeddedci/tests/test_refusal_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"lan_policy: change it from the cloud or the USB console",
"cloud_ca: change it from the cloud or the USB console",
"cloud_proxy: change it from the cloud or over USB",
"usb_network: change it on the device (the USB console, or over the USB link)",
"sig_policy: only the USB console can loosen the policy (now required)",
])
def test_locked(text):
Expand Down
15 changes: 15 additions & 0 deletions packages/embeddedci/tests/test_usb_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,18 @@ def test_local_usb_links_reads_this_computers_interfaces():
from embeddedci.benchpod.transport.usblink import local_usb_links
for host, pod in local_usb_links(): # whatever this machine has; each is a /30 in 10.237/16
assert host.startswith("10.237.") and pod.startswith("10.237.")


def test_usb_network_method_reads_and_turns_off():
from embeddedci.benchpod import BenchPod
sent = []

class T:
def command(self, req, **kw):
sent.append(req)
return {"setting": "off" if req.get("set") == "off" else "on"}

pod = BenchPod.__new__(BenchPod)
pod.command = T().command # the method only needs command()
assert pod.usb_network() is True and sent[-1] == {"cmd": "usb_network"}
assert pod.usb_network(False) is False and sent[-1] == {"cmd": "usb_network", "set": "off"}