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`

## Unreleased

- `generate` and `replay` default to `dac_path` 3v3 instead of 5v, so an agent that leaves the
output out can no longer put 5 V on a 3.3 V pin. With `route=false` they scale for the output
last routed (`analog_path` cal1 = 5v, cal2 = 12v), else 5v. `replay_waveform` falls back to 3v3
for a waveform stored without a path. The instructions tell agents to pick the output the DUT
pin tolerates. `generate` reports the output it used.

## 2.13.0

- `generate` plays the calibrated 16-bit waveform on firmware 3.9.1+ (through the SDK's
Expand Down
4 changes: 2 additions & 2 deletions packages/embeddedci-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,9 @@ the pod's second UART, so `uart_open` keeps the console. Needs gateware v48 (`gp

| Tool | Purpose | Parameters |
| --- | --- | --- |
| `generate` | Drive a sine, square or sawtooth on a DAC output (volts, or amps on `current_out`). | **`waveform`**, **`freq_hz`**, **`amplitude`**, `offset`, `dac_path=5v`, `duration`, `sample_rate_hz`, `on_capture=false`, `route=true` |
| `generate` | Drive a sine, square or sawtooth on a DAC output (volts, or amps on `current_out`). | **`waveform`**, **`freq_hz`**, **`amplitude`**, `offset`, `dac_path=3v3`, `duration`, `sample_rate_hz`, `on_capture=false`, `route=true` |
| `dac_stop` | Stop any DAC output: generator, replay or control loop. | none |
| `replay` | Loop your own samples, or the last ADC capture, out of the DAC. | `volts`, `from_last_capture=false`, `dac_path=5v`, `mapping=faithful`, `sample_rate_hz`, `fault`, `on_capture=false`, `route=true`, `switch_image=true` |
| `replay` | Loop your own samples, or the last ADC capture, out of the DAC. | `volts`, `from_last_capture=false`, `dac_path=3v3`, `mapping=faithful`, `sample_rate_hz`, `fault`, `on_capture=false`, `route=true`, `switch_image=true` |
| `list_waveforms` | The organization's cloud waveform library. | none |
| `replay_waveform` | Loop a cloud-library waveform out of the DAC. | **`waveform_id`**, `dac_path`, `mapping=faithful`, `sample_rate_hz`, `window_start=0`, `window_len=0`, `target_samples=0`, `fault`, `on_capture=false`, `switch_image=true` |
| `save_capture_as_recording` | Save the last ADC capture to the cloud waveform library. | **`name`**, `full_scale_v` |
Expand Down
2 changes: 2 additions & 0 deletions packages/embeddedci-mcp/src/embeddedci_mcp/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@

Analog:
DAC outputs '3v3', '5v', '12v' (bipolar +/-12 V) — dac_output, generate, replay
generate and replay default to 3v3: pass the output the DUT pin tolerates (5v and 12v
can damage a 3.3 V pin; ask the user which SMA is wired if unsure).
ADC sources 'ext' front SMA (true volts, the /12 divider is applied), 'cal1' (5 V DAC loopback),
'cal2' (12 V DAC loopback), 'current_in' (4-20 mA input, J8) — adc_read, capture_adc
4-20 mA output J9 (current_out): loop powered, pin 1 plus to an external floating supply (8 V plus
Expand Down
27 changes: 20 additions & 7 deletions packages/embeddedci-mcp/src/embeddedci_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1535,14 +1535,25 @@ def op() -> m.LaTimingResult:

# -- DAC ------------------------------------------------------------------------------

def _dac_path_arg(dac_path: Optional[str], route: bool) -> Dict[str, Any]:
"""The dac_path keyword for generate/replay. Left out and routed: 3v3, the lowest voltage
output, passed explicitly so an older SDK (whose default is 5v) plays there too. Left out with
route false: omitted, so the SDK scales for the output already routed (cal1 = 5v)."""
if dac_path is not None:
return {"dac_path": dac_path}
return {"dac_path": "3v3"} if route else {}


@mcp.tool(annotations=_ann("Generate a waveform", destructive=True))
async def generate(
waveform: Waveshape,
freq_hz: Annotated[float, Field(gt=0)],
amplitude: Annotated[float, Field(gt=0, description="Peak volts. On dac_path current_out: peak AMPS (0.006 = 6 mA).")],
offset: Annotated[Optional[float], Field(description=(
"Centre volts; default mid-range of dac_path. On current_out: centre AMPS (0.012 = 12 mA)."))] = None,
dac_path: DacPath = "5v",
"Center volts; default mid-range of dac_path. On current_out: center AMPS (0.012 = 12 mA)."))] = None,
dac_path: Annotated[Optional[DacPath], Field(description=(
"DAC output. Omitted: 3v3, the lowest voltage output (with route false: the output last routed, "
"cal1 = 5v). Pick the one the DUT pin tolerates: 5v and 12v can damage a 3.3 V pin."))] = None,
duration: Annotated[Optional[float], Field(description="Seconds; omit to run until dac_stop.")] = None,
sample_rate_hz: RateHz = None,
on_capture: Annotated[bool, Field(description="Start on the next capture's hardware t0 (phase-locked).")] = False,
Expand All @@ -1561,10 +1572,10 @@ async def generate(
current_out(0.004) to go back to 4 mA.
"""
handle = await _call(lambda: SESSION.require().generate(
waveform, freq_hz=freq_hz, amplitude=amplitude, offset=offset, dac_path=dac_path,
waveform, freq_hz=freq_hz, amplitude=amplitude, offset=offset, **_dac_path_arg(dac_path, route),
duration=duration, sample_rate_hz=sample_rate_hz, on_capture=on_capture, route=route))
bits = 16 if (handle.data or {}).get("bits") == 16 else 8
return m.GenerateResult(waveform=waveform, freq_hz=freq_hz, dac_path=dac_path, cotrig=handle.cotrig,
return m.GenerateResult(waveform=waveform, freq_hz=freq_hz, dac_path=handle.dac_path, cotrig=handle.cotrig,
bits=bits)


Expand Down Expand Up @@ -1597,7 +1608,9 @@ async def replay(
volts: Annotated[Optional[List[float]], Field(description=(
"Waveform in volts, one value per sample. On dac_path current_out the values are AMPS (0.004 to 0.020)."))] = None,
from_last_capture: Annotated[bool, Field(description="Replay the last capture_adc instead of volts.")] = False,
dac_path: DacPath = "5v",
dac_path: Annotated[Optional[DacPath], Field(description=(
"DAC output. Omitted: 3v3, the lowest voltage output (with route false: the output last routed, "
"cal1 = 5v). Pick the one the DUT pin tolerates: 5v and 12v can damage a 3.3 V pin."))] = None,
mapping: Annotated[ReplayMapping, Field(description="faithful = reproduce volts (clip); fit = auto-scale.")] = "faithful",
sample_rate_hz: Annotated[Optional[float], Field(description="Replay rate; defaults to the capture's own rate.")] = None,
fault: Optional[m.FaultSpec] = None,
Expand All @@ -1623,7 +1636,7 @@ def op() -> m.ReplayResult:
if SESSION.last_adc is None:
raise SessionStateError("no ADC capture yet — run capture_adc first")
source = SESSION.last_adc
return _replay_result(pod.replay(source, dac_path=dac_path, mapping=mapping,
return _replay_result(pod.replay(source, **_dac_path_arg(dac_path, route), mapping=mapping,
sample_rate_hz=sample_rate_hz, fault=_fault(fault),
on_capture=on_capture, route=route,
switch_image=switch_image))
Expand Down Expand Up @@ -1677,7 +1690,7 @@ def op() -> m.CloudDeviceList:
@mcp.tool(annotations=_ann("Replay a library waveform", destructive=True, cloud=True))
async def replay_waveform(
waveform_id: str,
dac_path: Annotated[Optional[DacPath], Field(description="Output path; default the waveform's own (else 5v).")] = None,
dac_path: Annotated[Optional[DacPath], Field(description="Output path; default the waveform's own, else 3v3.")] = None,
mapping: ReplayMapping = "faithful",
sample_rate_hz: RateHz = None,
window_start: Annotated[int, Field(ge=0, description="First recording sample to replay.")] = 0,
Expand Down
15 changes: 10 additions & 5 deletions packages/embeddedci-mcp/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,14 +526,19 @@ def test_capture_correlated(connected):

def test_generate_and_stop(connected):
result = call("generate", waveform="sine", freq_hz=1000, amplitude=1.0, on_capture=True)
assert result == {"waveform": "sine", "freq_hz": 1000.0, "dac_path": "5v", "cotrig": True, "bits": 8}
assert connected.requests[-2] == {"cmd": "dac_out", "path": "5v"}
assert connected.requests[-1]["amplitude"] == 51
# No dac_path: the 3.3 V output, the lowest voltage one.
assert result == {"waveform": "sine", "freq_hz": 1000.0, "dac_path": "3v3", "cotrig": True, "bits": 8}
assert connected.requests[-2] == {"cmd": "dac_out", "path": "3v3"}
assert connected.requests[-1]["amplitude"] == 77
assert call("dac_stop") == {"stopped": True}
# route=false keeps a loopback set up with analog_path: no dac_out before the generator.
# route=false keeps a loopback set up with analog_path: no dac_out before the generator, and
# the volts scale for the routed DAC (cal1 is the 5 V one).
call("analog_path", path="cal1")
call("generate", waveform="square", freq_hz=100, amplitude=1.0, route=False)
res = call("generate", waveform="square", freq_hz=100, amplitude=1.0, route=False)
assert [r["cmd"] for r in connected.requests[-2:]] == ["analog_path", "generate"]
assert res["dac_path"] == "5v" and connected.requests[-1]["amplitude"] == 51
res = call("generate", waveform="sine", freq_hz=100, amplitude=1.0, dac_path="12v")
assert res["dac_path"] == "12v" and connected.requests[-2] == {"cmd": "dac_out", "path": "12v"}


def test_replay_sources(connected):
Expand Down
52 changes: 34 additions & 18 deletions packages/embeddedci-mcp/tests/tools_surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -1910,15 +1910,23 @@
"type": "number"
},
"dac_path": {
"default": "5v",
"enum": [
"3v3",
"5v",
"12v",
"current_out"
"anyOf": [
{
"enum": [
"3v3",
"5v",
"12v",
"current_out"
],
"type": "string"
},
{
"type": "null"
}
],
"title": "Dac Path",
"type": "string"
"default": null,
"description": "DAC output. Omitted: 3v3, the lowest voltage output (with route false: the output last routed, cal1 = 5v). Pick the one the DUT pin tolerates: 5v and 12v can damage a 3.3 V pin.",
"title": "Dac Path"
},
"duration": {
"anyOf": [
Expand Down Expand Up @@ -1948,7 +1956,7 @@
}
],
"default": null,
"description": "Centre volts; default mid-range of dac_path. On current_out: centre AMPS (0.012 = 12 mA).",
"description": "Center volts; default mid-range of dac_path. On current_out: center AMPS (0.012 = 12 mA).",
"title": "Offset"
},
"on_capture": {
Expand Down Expand Up @@ -3085,15 +3093,23 @@
},
"properties": {
"dac_path": {
"default": "5v",
"enum": [
"3v3",
"5v",
"12v",
"current_out"
"anyOf": [
{
"enum": [
"3v3",
"5v",
"12v",
"current_out"
],
"type": "string"
},
{
"type": "null"
}
],
"title": "Dac Path",
"type": "string"
"default": null,
"description": "DAC output. Omitted: 3v3, the lowest voltage output (with route false: the output last routed, cal1 = 5v). Pick the one the DUT pin tolerates: 5v and 12v can damage a 3.3 V pin.",
"title": "Dac Path"
},
"fault": {
"anyOf": [
Expand Down Expand Up @@ -3249,7 +3265,7 @@
}
],
"default": null,
"description": "Output path; default the waveform's own (else 5v).",
"description": "Output path; default the waveform's own, else 3v3.",
"title": "Dac Path"
},
"fault": {
Expand Down
6 changes: 6 additions & 0 deletions packages/embeddedci-openhtf/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog — `embeddedci-openhtf`

## Unreleased

- `signal_generate`, `signal_generate_phase` and `loopback_measure_phase` default to
`dac_path="3v3"` instead of `"5v"`; `dac_replay_phase` now defaults to the path stored with the
waveform, else `"3v3"`. Pass `dac_path="5v"` to keep the old output.

## 2.3.0

- Needs `embeddedci` 2.12.0 or later (was 2.4), the SDK whose `usb` connection reaches the USB
Expand Down
2 changes: 1 addition & 1 deletion packages/embeddedci-openhtf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ from embeddedci_openhtf import (
bench = benchpod_plug("192.168.1.50:8080")

test = htf.Test(
# drive a 1 V-peak sine centred on 2.5 V (5V DAC path), capture the front ADC SMA
# drive a 1 V-peak sine centered on 2.5 V (5V DAC path), capture the front ADC SMA
# while it runs, stop the DAC, and assert the round trip in volts
loopback_measure_phase(bench, waveform="sine", freq_hz=100, amplitude=1.0,
samples=4096, sample_rate_hz=50_000,
Expand Down
4 changes: 2 additions & 2 deletions packages/embeddedci-openhtf/examples/analog_loopback.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def dac_stop(test, bench):
return htf.Test(
htf.PhaseGroup(
main=[
# 1) loopback: a 100 Hz, 1 V-peak sine centred on 2.5 V (5V path),
# 1) loopback: a 100 Hz, 1 V-peak sine centered on 2.5 V (5V path),
# captured while it runs; expect ~2 V peak-to-peak around 2.5 V.
loopback_measure_phase(
bench, waveform="sine", freq_hz=100, amplitude=1.0, offset=2.5,
Expand All @@ -52,7 +52,7 @@ def dac_stop(test, bench):
),
# 2) free-run a square wave for 2 s, then snapshot the ADC on its own.
signal_generate_phase(bench, waveform="square", freq_hz=100, amplitude=1.0,
duration=2.0),
dac_path="5v", duration=2.0),
adc_capture_phase(bench, samples=4096, sample_rate_hz=ADC_RATE_HZ,
source="ext", pp_range=(1.8, 2.2)),
],
Expand Down
15 changes: 8 additions & 7 deletions packages/embeddedci-openhtf/src/embeddedci_openhtf/analog.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ def stim(test, bench):
# -- low-level helpers (operate on a BenchPod or BenchPodPlug) ---------------

def signal_generate(bench: Any, *, waveform: Waveshape, freq_hz: float, amplitude: float,
offset: Optional[float] = None, dac_path: DacPath = "5v",
offset: Optional[float] = None, dac_path: DacPath = "3v3",
duration: Optional[float] = None,
sample_rate_hz: Optional[float] = None) -> DacHandle:
"""Start a parametric DAC waveform (``sine``/``square``/``sawtooth``) on ``dac_path``.

``amplitude`` is the peak and ``offset`` the centre, both in **volts** (``offset`` defaults to
``amplitude`` is the peak and ``offset`` the center, both in **volts** (``offset`` defaults to
the middle of the path's range). ``duration`` (seconds) stops it by itself; omitted, it runs
until :func:`signal_stop` or the returned handle's ``stop()``. See
:meth:`BenchPod.generate <embeddedci.benchpod.BenchPod.generate>`.
Expand Down Expand Up @@ -223,7 +223,7 @@ def _record_volts(test: Any, cap: Capture, *, prefix: str, attachment: Optional[
# -- phase factories ---------------------------------------------------------

def signal_generate_phase(plug: type, *, waveform: Waveshape, freq_hz: float, amplitude: float,
offset: Optional[float] = None, dac_path: DacPath = "5v",
offset: Optional[float] = None, dac_path: DacPath = "3v3",
duration: Optional[float] = None,
sample_rate_hz: Optional[float] = None,
name: str = "signal_generate") -> object:
Expand Down Expand Up @@ -362,7 +362,7 @@ def _cap(test, bench):

def loopback_measure_phase(plug: type, *, waveform: Waveshape = "sine", freq_hz: float,
amplitude: float, offset: Optional[float] = None,
dac_path: DacPath = "5v", samples: int = 4096,
dac_path: DacPath = "3v3", samples: int = 4096,
sample_rate_hz: Optional[float] = None,
source: Optional[AdcSource] = "ext", settle: float = 0.1,
prefix: str = "adc", mean_range: _Range = None,
Expand All @@ -380,7 +380,7 @@ def loopback_measure_phase(plug: type, *, waveform: Waveshape = "sine", freq_hz:
``_rms_v`` / ``_min_v`` / ``_max_v``.

Wire the DAC output SMA to the front ADC SMA (directly, or through the DUT whose response you
are checking) and keep ``source="ext"``. E.g. a 1 V-peak sine centred on 2.5 V straight back
are checking) and keep ``source="ext"``. E.g. a 1 V-peak sine centered on 2.5 V straight back
in: ``pp_range=(1.8, 2.2), mean_range=(2.4, 2.6)``.
"""
_check_source(source)
Expand Down Expand Up @@ -454,12 +454,13 @@ def _loop(test, bench):
return _loop


def dac_replay_phase(plug: type, *, waveform_id: str, dac_path: DacPath = "5v",
def dac_replay_phase(plug: type, *, waveform_id: str, dac_path: Optional[DacPath] = None,
mapping: ReplayMapping = "faithful", target_samples: int = 4096,
stop_after: bool = False, switch_image: bool = True,
name: str = "dac_replay") -> object:
"""A phase that loads a cloud-stored waveform and replays it (looping) on the DAC.

``dac_path`` left out is the path stored with the waveform, else ``3v3``.
Needs server access (``BENCHPOD_API_KEY``). By default the replay keeps looping after the
phase (so a later capture phase can observe it); set ``stop_after=True`` to stop it at the end
of this phase. A replay that needs the deep-replay gateware image switches the pod to it first
Expand All @@ -474,7 +475,7 @@ def _replay(test, bench):
if handle.switched_image is not None:
test.logger.warning("switched the FPGA to the deep-replay gateware image")
test.logger.info("replaying waveform %s on %s (%d samples)",
waveform_id, dac_path, handle.samples)
waveform_id, handle.dac_path, handle.samples)
if stop_after:
handle.stop()

Expand Down
11 changes: 6 additions & 5 deletions packages/embeddedci-openhtf/tests/test_analog.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ def test_signal_generate_phase_converts_volts_through_generate():
freq_hz=1000, amplitude=1.0, duration=0.5))
assert rec.outcome == tr.Outcome.PASS
i = _cmds(tx).index("generate")
# generate routes the DAC path first, then sends 8-bit generator codes:
# 1.0 V / (5 V / 255) = 51, offset defaults to mid-range 2.5 V -> 128.
assert tx.commands[i - 1] == {"cmd": "dac_out", "path": "5v"}
# generate routes the DAC path first (3v3 when none is given), then sends 8-bit generator
# codes: 1.0 V / (3.3 V / 255) = 77, offset defaults to mid-range 1.65 V -> 127.
assert tx.commands[i - 1] == {"cmd": "dac_out", "path": "3v3"}
assert tx.commands[i] == {"cmd": "generate", "waveform": "sine", "freq": 1000.0,
"amplitude": 51, "offset": 128, "duration_ms": 500}
"amplitude": 77, "offset": 127, "duration_ms": 500}


def test_signal_generate_helper_path_offset_and_rate():
Expand Down Expand Up @@ -169,8 +169,9 @@ def test_loopback_measure_phase_generates_captures_then_stops():
assert rec.outcome == tr.Outcome.PASS
# DAC first (its path re-applies and would open a loopback relay), then route the ADC.
assert _cmds(tx) == ["dac_out", "generate", "analog_path", "capture", "dac_stop"]
assert tx.commands[0] == {"cmd": "dac_out", "path": "3v3"}
assert tx.commands[1] == {"cmd": "generate", "waveform": "sine", "freq": 100.0,
"amplitude": 51, "offset": 128}
"amplitude": 77, "offset": 127}
assert tx.commands[2] == {"cmd": "analog_path", "path": "adc_ext"}
assert tx.commands[3] == {"cmd": "capture", "samples": 8, "sample_rate_mhz": 0.05}
p = _phase(rec, "loopback_measure")
Expand Down
5 changes: 5 additions & 0 deletions packages/embeddedci/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

- `Capabilities.scope` is False on the digital-only board. Current firmware lists `signal` on every
board; it only stands for the scope on firmware without the `analog` flag.
- `generate`, `replay`, `Waveforms.preview` and `replay_waveform` (for a waveform stored without
a path) default to `dac_path="3v3"` instead of `"5v"`, so a call that leaves the output out can
no longer put 5 V on a 3.3 V pin. Pass `dac_path="5v"` to keep the old output. With
`route=False` (a loopback set up with `analog_path`), `generate` and `replay` scale for the
output this connection last routed (`cal1` = 5 V, `cal2` = 12 V), else `5v` as before.

## 2.13.0

Expand Down
Loading