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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

WaveBench 是一个用 Python 编写的实验室自动测量台,面向电子设计竞赛调试和日常实验。它把仪器控制、实验步骤和采集证据放在同一条命令链中,支持先离线检查 plan,再决定是否连接硬件。

当前仓库开发线为 `0.8.25`,最新稳定 tag 为 `v0.8.0`。不同版本的命令和能力可能不同,以对应 tag 中的文档为准。
当前仓库开发线为 `0.8.26`,最新稳定 tag 为 `v0.8.0`。不同版本的命令和能力可能不同,以对应 tag 中的文档为准。

## 🌟 特别鸣谢

Expand Down Expand Up @@ -67,6 +67,26 @@ flowchart LR
dmm --> artifacts
```

#### 示波器联合视图

`scope focus` 用一个事务配置一个或多个目标模拟通道,并可同时设置完整横向时间范围、各目标通道的
V/div,以及是否隐藏插件 profile 声明的其他模拟通道:

```bash
wavebench scope focus \
--channel 1 \
--channel 2 \
--time-range 0.01 \
--vertical-scale 1=0.2 \
--vertical-scale 2=0.5 \
--hide-others
```

Core 不定义仪器型号、通道数量或数值范围;这些 guard 由当前插件的 descriptor profile 声明。
操作会先读取 profile 全部模拟通道及受保护的时基、位置和偏置字段。成功后保留目标视图;任一写入
或回读失败时恢复完整 baseline 并重新查询,恢复不完整则停止该 session 的后续写入。该命令不启动
采集、不调用 autoscale,也不修改耦合或输入终端。

## 先在没有仪器时跑通

下面的命令只生成和检查 plan,不会连接仪器,也不会打开输出。
Expand Down
14 changes: 12 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[English](README_EN.md) · 中文

WaveBench 是一个用 Python 编写的实验室自动测量台,提供 CLI、实验性 TUI、显式 run plan、采集包和离线报告。当前开发线为 `0.8.25`,最新稳定 tag 为 `v0.8.0`。版本变化见 [更新日志](../CHANGELOG.md);旧版本原始文档可切换到对应 Git tag 查看。
WaveBench 是一个用 Python 编写的实验室自动测量台,提供 CLI、实验性 TUI、显式 run plan、采集包和离线报告。当前开发线为 `0.8.26`,最新稳定 tag 为 `v0.8.0`。版本变化见 [更新日志](../CHANGELOG.md);旧版本原始文档可切换到对应 Git tag 查看。

> [!WARNING]
> 部分命令会连接并控制真实仪器。示例会区分离线检查、连接读取和硬件写入;执行写入前,应确认接线和限制值。
Expand All @@ -26,6 +26,16 @@ wavebench run check --plan /tmp/wavebench-demo.toml
- [配置文件格式](project/reference/WaveBench_配置文件格式.md):TOML 查找顺序、字段和安全限制。
- 仪器型号命令和编程手册由 [仪器插件仓库](https://github.com/Scaxlibur/wavebench-instrument-plugins) 维护;本仓库只记录 WaveBench 的接入边界。

### 配置示波器联合视图

`wavebench scope focus` 接受重复的 `--channel`、可选的 `--time-range`、重复的
`--vertical-scale CHANNEL=V_PER_DIV` 和 `--hide-others`。Core 只定义可移植事务:插件 profile
声明模拟通道、数值范围、容差和 I/O 预算;Core 读取完整联合 baseline,成功后保留目标视图,失败时
恢复并重新查询。未声明 `scope.focus_configure_v2` 的插件会在仪器 I/O 前拒绝操作。

该命令会修改仪器状态,但不会启动采集、调用 autoscale、修改耦合或切换输入终端。执行前应核对
接线、输入状态和插件 capability。

### 使用 RF 信号源

`rf_source` 不复用普通 `source` 的 Vpp、offset 或数字 channel 模型。先从 [RF 信号源使用指南](project/guides/WaveBench_RF信号源使用指南.md) 确认当前 production capability 和端接声明;DSG830 已开放固定 profile 的调制输出,以及唯一受验证的后面板 `pulse_in_out` output 路径。后者不代表 Pulse input、`TRIGGER IN` 或同步能力。需要实现新型号或查看证据门时,再阅读 [领域设计](project/design/WaveBench_RF信号源设计.md) 与 [开发里程碑](project/design/WaveBench_RF信号源开发里程碑.md)。
Expand Down Expand Up @@ -68,7 +78,7 @@ wavebench run check --plan /tmp/wavebench-demo.toml
| --- | --- | --- |
| 离线 | `run schema`、`run template`、`run check`、`run report`、`capture inspect`、`tui --fake` | 不连接仪器;TUI 可能写本地日志 |
| 连接读取 | `doctor`、`idn`、`status`、`run verify` | 是,仅读取或做预检 |
| 修改状态 | `scope fetch/capture/autoscale`、source/power setter、output、`run plan` | 是,可能写入、触发或改变输出 |
| 修改状态 | `scope focus/fetch/capture/autoscale`、source/power setter、output、`run plan` | 是,可能写入、触发或改变输出 |

`run check` 不代表 plan 可以安全执行。它只检查 TOML 和字段;真正执行前,还要核对接线、scope coupling、输出状态、保护限值和 restore 条款。

Expand Down
17 changes: 15 additions & 2 deletions docs/README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[中文文档](README.md) · English

WaveBench is a Python measurement bench for laboratory debugging. It combines explicit instrument commands, run plans, capture packages, and offline reports. It requires Python 3.11 or newer. The current development line is `0.8.25`; the latest stable tag is `v0.8.0`.
WaveBench is a Python measurement bench for laboratory debugging. It combines explicit instrument commands, run plans, capture packages, and offline reports. It requires Python 3.11 or newer. The current development line is `0.8.26`; the latest stable tag is `v0.8.0`.

> [!WARNING]
> Some commands connect to and change real instruments. Check wiring, input impedance, output state, and voltage/current limits before running a hardware action.
Expand Down Expand Up @@ -51,6 +51,19 @@ For the terminal UI, install `.[tui]` and run `wavebench tui --fake`. The fake m
| TUI | Power, DMM, and source panels | Experimental manual control |
| Plugins | `wavebench.instruments` drivers | Optional, explicitly selected extensions |

## Configure a joint oscilloscope view

`wavebench scope focus` accepts repeated `--channel` arguments, optional `--time-range`, repeated
`--vertical-scale CHANNEL=V_PER_DIV`, and `--hide-others`. Core defines only the portable
transaction. Each plugin descriptor profile owns its analog-channel set, numeric request guards,
tolerances, and I/O budgets. Core reads the complete joint baseline, preserves the requested view
after success, and restores then freshly verifies that baseline after failure. A plugin that does
not declare `scope.focus_configure_v2` is rejected before instrument I/O.

This command changes front-panel state. It does not start acquisition, invoke autoscale, change
coupling, or switch input termination. Wiring, input state, and the selected plugin capability must
be checked before execution.

## Find a guide

- Setup and configuration: [configuration format](project/reference/WaveBench_配置文件格式.md)
Expand All @@ -69,7 +82,7 @@ Most detailed pages are currently maintained in Chinese. Commands, identifiers,
| --- | --- | --- |
| Offline | `run schema`, `run template`, `run check`, `run report`, `capture inspect`, `tui --fake` | No instrument I/O; TUI may write a local log |
| Connected read/preflight | `doctor`, `idn`, `status`, `run verify` | Yes, for queries and checks |
| State-changing | `scope fetch/capture/autoscale`, source/power setters, output commands, `run plan` | Yes; may change setup, trigger acquisition, or switch output |
| State-changing | `scope focus/fetch/capture/autoscale`, source/power setters, output commands, `run plan` | Yes; may change setup, trigger acquisition, or switch output |

WaveBench does not implicitly reset instruments, enable outputs, or change oscilloscope input impedance. `power set` and `power output` are separate operations. When enabled, source restoration covers only the documented basic fields; it is not a full channel snapshot.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "wavebench"
version = "0.8.25"
version = "0.8.26"
description = "Lightweight VISA/SCPI measurement bench for contest debugging"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
62 changes: 62 additions & 0 deletions src/wavebench/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@
from .instruments.registry import resolve_instrument_descriptor
from .instruments.scope_extensions import (
ErrorCheckSpec,
ScopeChannelDisplayRequest,
ScopeContinuousAcquisitionRequest,
ScopeFocusRequest,
ScopeFocusVerticalScale,
ScopeScreenshot,
ScopeScreenshotRequest,
ScopeTraceData,
Expand Down Expand Up @@ -602,6 +605,51 @@ def _scope_error_check(args: argparse.Namespace) -> ErrorCheckSpec | None:
raise ConfigError(str(exc)) from exc


def _scope_channel_display_request(args: argparse.Namespace) -> ScopeChannelDisplayRequest:
try:
return ScopeChannelDisplayRequest(
channel=args.channel,
enabled=args.state == "on",
)
except (TypeError, ValueError) as exc:
raise ConfigError(str(exc)) from exc


def _scope_focus_request(args: argparse.Namespace) -> ScopeFocusRequest:
raw_channels = tuple(args.channels)
if len(set(raw_channels)) != len(raw_channels):
raise ConfigError("scope focus channels must not contain duplicates")
scales: list[ScopeFocusVerticalScale] = []
scale_channels: set[int] = set()
for raw in args.vertical_scales:
channel_text, separator, scale_text = raw.partition("=")
if not separator:
raise ConfigError("scope focus --vertical-scale must use CHANNEL=V_PER_DIV")
try:
channel = int(channel_text)
scale = float(scale_text)
except ValueError as exc:
raise ConfigError(
"scope focus --vertical-scale must use CHANNEL=V_PER_DIV"
) from exc
if channel in scale_channels:
raise ConfigError("scope focus vertical-scale channels must not contain duplicates")
scale_channels.add(channel)
try:
scales.append(ScopeFocusVerticalScale(channel, scale))
except (TypeError, ValueError) as exc:
raise ConfigError(str(exc)) from exc
try:
return ScopeFocusRequest(
channels=tuple(sorted(raw_channels)),
time_range_s=args.time_range,
vertical_scales=tuple(sorted(scales, key=lambda item: item.channel)),
hide_others=args.hide_others,
)
except (TypeError, ValueError) as exc:
raise ConfigError(str(exc)) from exc


def _scope_trace_ref(args: argparse.Namespace) -> ScopeTraceRef:
try:
return ScopeTraceRef(
Expand Down Expand Up @@ -1882,6 +1930,20 @@ def _main(argv: list[str] | None = None) -> int:
for item in service.errors():
print(item)
return 0
if args.command == "display":
result = service.configure_channel_display_v2(
_scope_channel_display_request(args),
error_check=_scope_error_check(args),
)
_emit_scope_extension_result(result.as_dict(), json_mode=args.json)
return 0
if args.command == "focus":
result = service.configure_focus_v2(
_scope_focus_request(args),
error_check=_scope_error_check(args),
)
_emit_scope_extension_result(result.as_dict(), json_mode=args.json)
return 0
if args.command == "screenshot":
if args.screenshot_command == "profile":
result = service.screenshot_profile()
Expand Down
43 changes: 43 additions & 0 deletions src/wavebench/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,49 @@ def build_parser() -> argparse.ArgumentParser:
errors = scope_sub.add_parser("errors", help="Read SYST:ERR? until empty")
add_runtime_options(errors)

display = scope_sub.add_parser(
"display",
help="Explicitly enable or disable one analog channel display",
)
display.add_argument("--channel", type=int, required=True)
display.add_argument("state", choices=("on", "off"))
add_scope_error_options(display)
add_runtime_options(display)

focus = scope_sub.add_parser(
"focus",
help="Configure one atomic multi-channel observation view",
)
focus.add_argument(
"--channel",
dest="channels",
type=int,
action="append",
required=True,
help="Target analog channel; repeat for a multi-channel focus view",
)
focus.add_argument(
"--time-range",
type=float,
default=None,
help="Optional full horizontal visible range in seconds",
)
focus.add_argument(
"--vertical-scale",
dest="vertical_scales",
action="append",
default=[],
metavar="CHANNEL=V_PER_DIV",
help="Optional target-channel V/div; repeat for different target channels",
)
focus.add_argument(
"--hide-others",
action="store_true",
help="Disable other analog channels declared by the driver profile",
)
add_scope_error_options(focus)
add_runtime_options(focus)

status = scope_sub.add_parser(
"status",
help="Read a typed, non-mutating oscilloscope state snapshot",
Expand Down
24 changes: 24 additions & 0 deletions src/wavebench/instruments/scope_extension_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
SCOPE_EXTENSIONS_MIN_CORE_VERSION = "0.8.23"
SCOPE_WAVEFORM_BINARY_MIN_CORE_VERSION = "0.8.24"
SCOPE_PORTABILITY_V2_MIN_CORE_VERSION = "0.8.24"
SCOPE_FOCUS_V2_MIN_CORE_VERSION = "0.8.26"
SCOPE_STRICT_V2_CAPABILITIES = frozenset(
{
"scope.channel_input_state_v2",
Expand All @@ -25,6 +26,8 @@
"scope.fft_status_v2",
"scope.cursor_readout_v2",
"scope.capture_average_v2",
"scope.channel_display_configure_v2",
"scope.focus_configure_v2",
}
)

Expand Down Expand Up @@ -92,6 +95,16 @@
"restore_average_capture_state",
"verify_average_capture_state_restored",
),
"scope.channel_display_configure_v2": (
"get_channel_display_state_v2",
"configure_channel_display_v2",
"restore_channel_display_v2",
),
"scope.focus_configure_v2": (
"get_focus_state_v2",
"configure_focus_v2",
"restore_focus_v2",
),
}
)

Expand Down Expand Up @@ -138,8 +151,17 @@ def validate_scope_descriptor(
"scope portability V2 capabilities require wavebench_min_version "
f">= {SCOPE_PORTABILITY_V2_MIN_CORE_VERSION}"
)
if "scope.focus_configure_v2" in declared and minimum < Version(
SCOPE_FOCUS_V2_MIN_CORE_VERSION
):
raise ConfigError(
"scope focus V2 capability requires wavebench_min_version "
f">= {SCOPE_FOCUS_V2_MIN_CORE_VERSION}"
)
dependencies = {
"scope.acquisition_control": {"scope.acquisition_run_state"},
"scope.channel_display_configure_v2": {"scope.idn"},
"scope.focus_configure_v2": {"scope.idn"},
"scope.capture_average_v2": {
"scope.idn",
"scope.acquisition_status_v2",
Expand All @@ -166,6 +188,8 @@ def validate_scope_descriptor(
"scope.fft_status_v2": "fft_status_profile_v2",
"scope.cursor_readout_v2": "cursor_readout_profile_v2",
"scope.capture_average_v2": "average_capture_profile_v2",
"scope.channel_display_configure_v2": "channel_display_profile_v2",
"scope.focus_configure_v2": "focus_profile_v2",
}
for capability in sorted(declared):
profile_name = profile_requirements.get(capability)
Expand Down
Loading
Loading