Skip to content
Closed
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: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ wavebench.toml
# Python
__pycache__/
*.py[cod]
.venv/
.venv*/
.pytest_cache/
.ruff_cache/
dist/
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ WaveBench 主包长期预装 RTM2000/RTM2032、DS1104Z/DS1000Z、DG4000/DG4202
- LAN VISA 连接
- `scope idn`、`scope errors`;声明相应 capability 的驱动还支持只读 `scope status`、`scope acquisition-status`、`scope history-timestamps` 与 `scope measurement-statistics`
- 显式 `scope auto` / `scope autoscale`
- 显式 `scope display --channel N on|off` 与 `scope focus --channel N`,可审计地调整通道显示、时基窗口和垂直档位;不会控制信号源或电源
- `scope fetch` 与 `scope capture`;默认先只读确认输入为高阻,50 Ω 需显式 `--allow-50ohm`
- 声明 `scope.capture_average` 的驱动可执行受控平均采集;公共结果要求逐项恢复并返回恢复前后配置证据
- 声明 `scope.digital_status` 的驱动可读取既有 MSO 数字通道状态;该能力不读取数字波形,也不隐式配置阈值、显示或传输格式
Expand Down Expand Up @@ -145,6 +146,9 @@ WaveBench 主包长期预装 RTM2000/RTM2032、DS1104Z/DS1000Z、DG4000/DG4202
- `run.schema`:返回 run plan schema
- `run.check`:只解析并检查 `plans/*.toml` 下的 run plan,不连接仪器
- `capture.inspect`:读取 `data/raw/` 下的离线采集包摘要
- `scope.observe`:只读连接配置中的示波器,返回 IDN、状态快照(若驱动支持)和高阻安全判断;支持 CH1-CH4 多通道观察;可传 `fetch_waveform=true` 读取一个或多个通道的当前波形摘要,并对成功读取的通道生成 pairwise `relationships`(频率比、幅值/均值关系、相关性、延迟/相位、交点),但该模式可能改动示波器的波形传输源/模式
- `scope.advise`:基于 `scope.observe` 给出每通道时基/垂直档位/display 建议;只返回建议,不应用调整
- `doctor.config`:结构化返回配置中各仪器的只读可达性、IDN 和型号匹配检查结果
- `/mcp` 与 `/call` 的 JSON 请求体有 1 MiB 上限;路径参数按工具限制在项目内固定目录

## 安全默认值
Expand Down Expand Up @@ -184,6 +188,28 @@ $env:PYTHONPATH = "src"
python -m wavebench scope idn --config wavebench.toml
```

### Windows + WSL 入口

Windows 主机上可用 `scripts/wsl-run.ps1` 从 PowerShell 直接进入 WSL 的项目虚拟环境执行命令,适合把开发、测试和 LAN 仪器访问统一放在 WSL 中:

```powershell
.\scripts\wsl-run.ps1 wavebench scope idn --config wavebench.toml
.\scripts\wsl-run.ps1 wavebench scope fetch --config wavebench.toml --channel 1
.\scripts\wsl-run.ps1 pytest -q
```

脚本默认进入当前仓库对应的 WSL 路径并激活 `.venv-wsl`。如需指定发行版:

```powershell
.\scripts\wsl-run.ps1 -Distro Ubuntu wavebench doctor --config wavebench.toml
```

若只是想在 WSL 内跑系统命令而不激活 `.venv-wsl`:

```powershell
.\scripts\wsl-run.ps1 --no-venv python3 --version
```

## 示例命令

采集示波器波形:
Expand All @@ -193,6 +219,13 @@ python -m wavebench scope capture --config wavebench.toml --channel 1 --label sm
python -m wavebench scope capture --config wavebench.toml --channel 1 --label smoke_with_screen --points def --no-csv --screenshot
```

显式调整示波器显示:

```powershell
python -m wavebench scope display --config wavebench.toml --channel 2 off
python -m wavebench scope focus --config wavebench.toml --channel 1 --time-range 0.01 --vertical-scale 0.2
```

DS1104Z 配置示例:

```toml
Expand Down
48 changes: 48 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# WaveBench AI-Agent Experience TODO

目标:让 AI agent 能以低风险、结构化、可审计的方式观察实验台、理解已有产物、提出建议,并只通过显式 run plan 执行会改变仪器状态的动作。

## P0: 安全只读上下文

- [x] 增加 MCP 工具 `scope.observe`:读取配置中的示波器,返回 IDN、状态快照、通道高阻判断、当前波形摘要;不保存文件、不改仪器状态、不暴露 raw SCPI。
- [x] `scope.observe` 支持 CH1-CH4 多通道观察;显式 `fetch_waveform=true` 且获取到两个以上通道时返回 pairwise relationships。
- [x] 增加 MCP 工具 `doctor.config`:把现有 `doctor` 结果结构化返回给 agent,支持配置可达性和型号匹配判断。
- [ ] 增加 MCP 工具 `net.discover`:结构化返回只读 LAN 仪器发现结果,默认限制网段大小和端口集合。
- [ ] 所有 MCP 工具都标注 `read_only`、`mutates_instrument=false`、`raw_scpi=false` 等安全元数据。

## P1: 离线产物理解

- [ ] 增强 `capture.inspect`:可选返回 FFT 摘要、质量告警、推荐下一步采集参数,但不读取大数组进响应。
- [ ] 增加 `capture.list`:列出 `data/raw` 下最近采集包,供 agent 找上下文。
- [ ] 增加 `run.list` / `run.inspect`:列出和解释 `data/runs` 下的实验记录。
- [ ] 给采集包生成 agent 友好的 `summary.md` 或结构化 `analysis.json`。

## P2: Agent 建议层

- [x] 增加 `scope.advise`:基于 `scope.observe` 的结果给出时基、垂直档位、触发、点数建议;只返回建议,不应用。
- [x] 根据实测或期望频率推荐每通道 focus time-range,默认约 10 个周期。
- [x] 根据实测 Vpp 推荐垂直档位,默认约占 5 格。
- [x] 识别 CH1/CH2 这类大频率跨度,建议分通道/分 profile 观察,避免单时基误判形状。
- [x] 增加 `scope.observe.expectations`:把已知闭环信号作为结构化依据,例如 CH1 1 kHz/1 Vpp/50% 方波、CH2 50 kHz/1 Vpp/500 mVdc/30% 对称三角波,并返回逐项 pass/warn/fail。
- [x] 支持 frequency/Vpp/mean/duty/symmetry_percent 的基础断言。
- [x] 期望断言必须显式 `fetch_waveform=true`,避免 agent 在不知情时触发波形传输状态变化。
- [x] 返回总体 expectation status,并把 fail/warn 加入 agent hints。
- [x] 增加多时基建议:当多个通道的频率跨度较大时,提醒 agent 不要用单个显示时基同时判断所有通道形状,应分通道或分 profile 采集。
- [x] 多通道关系分析增加交点:对每对通道返回交点数量、采样返回点、交点时间/电压和相对斜率方向;交点过多时截断并给 warning。
- [ ] 让 `scope.expect` 也参与多时基建议:即使当前频率估计低置信,也能利用用户给定的期望频率识别 CH1/CH2 这种 50x 频率跨度。
- [ ] 增加 `plan.propose`:从自然语言目标或结构化目标生成保守 run plan 草案,默认写到 `plans/`,不执行。
- [ ] 增加 `plan.explain`:解释某个 run plan 会读写哪些仪器、哪些步骤会改变输出状态、有哪些保护。

## P3: 可控执行边界

- [ ] MCP 继续保持默认只读;会改仪器状态的能力只通过显式 `run plan` 文件和人工确认入口暴露。
- [x] 增加显式示波器显示控制 CLI:通道显示 on/off、focus 单通道、显式 autoscale;用于人类和 agent 做可审计调参。
- [x] focus 动作只调整示波器显示/采集窗口,不改变信号源、电源或被测对象;执行后输出 mutation manifest。
- [ ] 对所有会改状态的 plan 步骤生成 mutation manifest,便于 agent 在执行前向人类说明。
- [ ] 增加跨进程仪器锁,避免 Windows/WSL/多个 agent 同时打开同一台仪器导致响应串线。

## P4: 人类与 Agent 共用体验

- [x] 增加 `scripts/wsl-run.ps1` 作为 Windows 到 WSL 的标准执行入口,并在 README 中记录。
- [ ] 为 WSL/Windows 推荐环境增加 `doctor environment` 或 `env doctor`,检查 Python、PyVISA、WSL、网络可达性。
- [ ] 增加最小可视化:采集包自动生成 waveform/FFT PNG,报告中可直接查看。
7 changes: 6 additions & 1 deletion doc/project/WaveBench_HTTP_MCP_只读接口.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,21 @@ MCP notification 请求没有 `id` 时返回空响应;普通请求返回 JSON-
- `run.schema`:返回 run plan schema 文本和结构化 schema 行。
- `run.check`:参数 `{"plan": "plans/<name>.toml"}`,只解析并检查 `plans/*.toml` 下的 run plan,不连接仪器。
- `capture.inspect`:参数 `{"path": "data/raw/<capture_dir>"}`,读取 `data/raw/` 下的离线采集包摘要。
- `scope.observe`:参数 `{"channels": [1, 2], "fetch_waveform": false}`,只读连接配置中的示波器,返回 IDN、状态快照(若驱动支持)和高阻安全判断。支持 CH1-CH4 多通道观察。显式传 `fetch_waveform=true` 时,会读取一个或多个通道的当前波形摘要;当至少两个通道读取成功时,返回 `relationships`,包含频率比、Vpp/均值关系、公共时间窗、归一化相关性、估计延迟、同频相位和通道间交点。该工具不保存文件,不暴露 raw SCPI,但抓波形可能改动示波器的波形传输源/模式。
- 可选 `expectations`:例如 `{"1": {"frequency_hz": 1000, "vpp_v": 1.0, "duty_percent": 50}, "2": {"frequency_hz": 50000, "vpp_v": 1.0, "mean_v": 0.5, "symmetry_percent": 30}}`。使用 expectations 必须同时传 `fetch_waveform=true`。
- `scope.advise`:参数同 `scope.observe`,另有 `target_cycles` 和 `target_vertical_divisions`。它基于当前观察和可选 expectations 给出 `scope focus` / `scope display` 建议、每通道推荐时基窗口和垂直档位;只返回建议,绝不应用调整。若传 `fetch_waveform=true`,同样可能改动示波器的波形传输源/模式。
- `doctor.config`:参数 `{"timeout_ms": 1000}`,结构化返回配置中各仪器的只读可达性、IDN 和型号匹配检查结果;不执行网段发现。

## 安全边界

- 默认只监听 `127.0.0.1`。
- 拒绝监听 `0.0.0.0`。
- `/mcp`、`/tools` 和 `/call` 强制 Bearer token。
- 当前工具全部只读
- 当前工具不提供 raw SCPI,不应用显示/输出/采集建议;`scope.observe` / `scope.advise` 在 `fetch_waveform=true` 时会显式标注可能的波形传输状态影响
- 不提供 raw SCPI。
- 不提供 power/source output on/off。
- 不提供 run 执行工具。
- `run.check` 只允许项目内 `plans/*.toml`。
- `capture.inspect` 只允许项目内 `data/raw/` 离线采集包。
- `doctor.config` 只检查当前配置中的资源,不扫描网段。
- `/mcp` 和 `/call` 的 JSON 请求体有 1 MiB 上限。
73 changes: 73 additions & 0 deletions scripts/wsl-run.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
$ErrorActionPreference = "Stop"

$Distro = ""
$NoVenv = $false
$Command = @()
for ($i = 0; $i -lt $args.Count; $i++) {
$arg = [string]$args[$i]
if ($arg -eq "-Distro" -or $arg -eq "--distro") {
$i += 1
if ($i -ge $args.Count) {
throw "$arg requires a distro name"
}
$Distro = [string]$args[$i]
continue
}
if ($arg -eq "-NoVenv" -or $arg -eq "--no-venv") {
$NoVenv = $true
continue
}
if ($arg -eq "--") {
if ($i + 1 -lt $args.Count) {
$Command = @($args[($i + 1)..($args.Count - 1)])
}
break
}
$Command = @($args[$i..($args.Count - 1)])
break
}

function ConvertTo-BashLiteral {
param([string]$Value)
return "'" + $Value.Replace("'", "'\''") + "'"
}

$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$repoPath = ""
if ($repoRoot.Path -match "^([A-Za-z]):\\(.*)$") {
$drive = $Matches[1].ToLowerInvariant()
$relative = $Matches[2].Replace("\", "/")
$repoPath = "/mnt/$drive/$relative"
} else {
$wslPathArgs = @("wslpath", "-a", "-u", $repoRoot.Path)
if ($Distro) {
$repoPath = (& wsl.exe -d $Distro -e @wslPathArgs).Trim()
} else {
$repoPath = (& wsl.exe -e @wslPathArgs).Trim()
}
}

if (-not $repoPath) {
throw "failed to resolve repository path inside WSL"
}

if ($Command.Count -eq 0) {
$Command = @("wavebench", "--help")
}

$escapedCommand = ($Command | ForEach-Object { ConvertTo-BashLiteral $_ }) -join " "
$repoLiteral = ConvertTo-BashLiteral $repoPath
$venvLine = ""
if (-not $NoVenv) {
$venvLine = "if [ ! -f .venv-wsl/bin/activate ]; then echo 'missing .venv-wsl; run: python3 -m venv .venv-wsl && source .venv-wsl/bin/activate && pip install -e ''.[dev]''' >&2; exit 2; fi; . .venv-wsl/bin/activate;"
}

$bashScript = "set -e; cd $repoLiteral; $venvLine exec $escapedCommand"

if ($Distro) {
& wsl.exe -d $Distro -e bash -lc $bashScript
} else {
& wsl.exe -e bash -lc $bashScript
}

exit $LASTEXITCODE
21 changes: 21 additions & 0 deletions src/wavebench/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
_print_scope_digital_status,
_print_scope_digital_waveform,
_print_scope_measurement_statistics,
_print_scope_mutation_manifest,
_print_scope_cursor_readout,
_print_scope_derived_waveform_metadata,
_print_scope_fft_status,
Expand Down Expand Up @@ -669,6 +670,26 @@ def main(argv: list[str] | None = None) -> int:
service.autoscale()
print("AUToscale completed")
return 0
if args.command == "display":
result = service.set_channel_display(
channel=args.channel,
enabled=args.state.lower() == "on",
)
_print_scope_mutation_manifest(result)
return 0
if args.command == "focus":
if args.time_range is not None and args.time_range <= 0:
raise ConfigError("--time-range must be > 0")
if args.vertical_scale is not None and args.vertical_scale <= 0:
raise ConfigError("--vertical-scale must be > 0")
result = service.focus_channel(
channel=args.channel,
time_range_s=args.time_range,
vertical_scale_v_per_div=args.vertical_scale,
hide_other_channels=args.hide_other_channels,
)
_print_scope_mutation_manifest(result)
return 0
if args.command == "fetch":
channel = args.channel or service.config.scope.default_channel
service.require_high_impedance(channel, allow_50ohm=args.allow_50ohm)
Expand Down
14 changes: 14 additions & 0 deletions src/wavebench/cli_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,20 @@ def number(value: float | None) -> str:
print(f"cursor.x_ratio={number(readout.x_ratio)}")
print(f"cursor.y_ratio={number(readout.y_ratio)}")


def _print_scope_mutation_manifest(manifest: dict[str, Any]) -> None:
print(f"operation={manifest['operation']}")
print(f"mutates_instrument={str(bool(manifest['mutates_instrument'])).lower()}")
print(f"raw_scpi={str(bool(manifest['raw_scpi'])).lower()}")
print(f"channel={manifest['channel']}")
for key in ("display", "time_range_s", "vertical_scale_v_per_div", "hide_other_channels"):
if key in manifest and manifest[key] is not None:
value = manifest[key]
if isinstance(value, bool):
value = str(value).lower()
print(f"{key}={value}")
print("affected_settings=" + ",".join(str(item) for item in manifest["affected_settings"]))

def _print_dmm_function_status(function: str) -> None:
print(f"功能 / Function: {function}")

Expand Down
32 changes: 32 additions & 0 deletions src/wavebench/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,38 @@ def build_parser() -> argparse.ArgumentParser:
autoscale = scope_sub.add_parser("autoscale", help="Alias of scope auto")
add_runtime_options(autoscale)

display = scope_sub.add_parser(
"display",
help="Explicitly turn one analog channel display on or off",
)
display.add_argument("--channel", type=int, required=True)
display.add_argument("state", choices=["on", "off", "ON", "OFF"])
add_runtime_options(display)

focus = scope_sub.add_parser(
"focus",
help="Explicitly focus the scope display/acquisition window on one channel",
)
focus.add_argument("--channel", type=int, required=True)
focus.add_argument(
"--time-range",
type=float,
default=None,
help="Set total horizontal acquisition/display window in seconds",
)
focus.add_argument(
"--vertical-scale",
type=float,
default=None,
help="Set selected channel vertical scale in V/div",
)
focus.add_argument(
"--hide-other-channels",
action="store_true",
help="Turn CH1-CH4 displays off except the selected channel",
)
add_runtime_options(focus)

fetch = scope_sub.add_parser("fetch", help="Fetch waveform data without creating full package")
fetch.add_argument("--channel", type=int, default=None)
fetch.add_argument("--points", default=None, help="Override waveform points: def, max, or dmax")
Expand Down
Loading