Skip to content
Open
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
4 changes: 2 additions & 2 deletions doc/reference/plugin-catalog-en.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Regenerate it with `python scripts/generate_plugin_catalog.py`; verify drift wit
| [`wavebench-rohde-schwarz-rtm2000`](../../packages/wavebench-rohde-schwarz-rtm2000/README_EN.md) | `0.15.0` | `rohde-schwarz.rtm2032` | scope | RTM2032, RTM2000 | `>=3.11` | `wavebench>=0.8.26,<0.9` |
| [`wavebench-shengpu-sp3000a`](../../packages/wavebench-shengpu-sp3000a/README_EN.md) | `0.2.0` | `shengpu.sp30120` | sweep_analyzer | SP30120 | `>=3.11` | `wavebench>=0.8,<0.9` |
| [`wavebench-siglent-sdg2000x`](../../packages/wavebench-siglent-sdg2000x/README_EN.md) | `0.8.2` | `siglent.sdg2000x` | source | SDG2042X, SDG2082X, SDG2122X | `>=3.11` | `wavebench>=0.8.24,<0.9` |
| [`wavebench-siglent-sds3000`](../../packages/wavebench-siglent-sds3000/README_EN.md) | `0.1.0` | `siglent.sds3000` | scope | SDS3054 | `>=3.11` | `wavebench>=0.8.24,<0.9` |
| [`wavebench-siglent-sds800x-hd`](../../packages/wavebench-siglent-sds800x-hd/README_EN.md) | `0.6.0` | `siglent.sds800x-hd` | scope | SDS802X HD, SDS804X HD, SDS812X HD, SDS814X HD, SDS822X HD, SDS824X HD | `>=3.11` | `wavebench>=0.8.23,<0.9` |
| [`wavebench-siglent-sds3000`](../../packages/wavebench-siglent-sds3000/README_EN.md) | `0.1.1` | `siglent.sds3000` | scope | SDS3054 | `>=3.11` | `wavebench>=0.8.24,<0.9` |
| [`wavebench-siglent-sds800x-hd`](../../packages/wavebench-siglent-sds800x-hd/README_EN.md) | `0.6.1` | `siglent.sds800x-hd` | scope | SDS802X HD, SDS804X HD, SDS812X HD, SDS814X HD, SDS822X HD, SDS824X HD | `>=3.11` | `wavebench>=0.8.23,<0.9` |

## Declared capabilities

Expand Down
4 changes: 2 additions & 2 deletions doc/reference/plugin-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
| [`wavebench-rohde-schwarz-rtm2000`](../../packages/wavebench-rohde-schwarz-rtm2000/README.md) | `0.15.0` | `rohde-schwarz.rtm2032` | 示波器 | RTM2032, RTM2000 | `>=3.11` | `wavebench>=0.8.26,<0.9` |
| [`wavebench-shengpu-sp3000a`](../../packages/wavebench-shengpu-sp3000a/README.md) | `0.2.0` | `shengpu.sp30120` | 扫频仪 | SP30120 | `>=3.11` | `wavebench>=0.8,<0.9` |
| [`wavebench-siglent-sdg2000x`](../../packages/wavebench-siglent-sdg2000x/README.md) | `0.8.2` | `siglent.sdg2000x` | 信号源 | SDG2042X, SDG2082X, SDG2122X | `>=3.11` | `wavebench>=0.8.24,<0.9` |
| [`wavebench-siglent-sds3000`](../../packages/wavebench-siglent-sds3000/README.md) | `0.1.0` | `siglent.sds3000` | 示波器 | SDS3054 | `>=3.11` | `wavebench>=0.8.24,<0.9` |
| [`wavebench-siglent-sds800x-hd`](../../packages/wavebench-siglent-sds800x-hd/README.md) | `0.6.0` | `siglent.sds800x-hd` | 示波器 | SDS802X HD, SDS804X HD, SDS812X HD, SDS814X HD, SDS822X HD, SDS824X HD | `>=3.11` | `wavebench>=0.8.23,<0.9` |
| [`wavebench-siglent-sds3000`](../../packages/wavebench-siglent-sds3000/README.md) | `0.1.1` | `siglent.sds3000` | 示波器 | SDS3054 | `>=3.11` | `wavebench>=0.8.24,<0.9` |
| [`wavebench-siglent-sds800x-hd`](../../packages/wavebench-siglent-sds800x-hd/README.md) | `0.6.1` | `siglent.sds800x-hd` | 示波器 | SDS802X HD, SDS804X HD, SDS812X HD, SDS814X HD, SDS822X HD, SDS824X HD | `>=3.11` | `wavebench>=0.8.23,<0.9` |

## 已声明 capability

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@
本矩阵回答「WaveBench 当前接口能否表达」;[`COMMAND_COVERAGE.md`](COMMAND_COVERAGE.md) 与机器目录回答「手册的每个明确实体如何处置」。两者分母不同:前者是 26 项 capability,后者是 578 个明确手册实体。

因此,100% 覆盖不等于 100% 实机执行。复位、校准、文件、网络、hardcopy、选件激活、关机和任意脚本仍必须隔离;选件缺失、型号不适用、固件未确认和核心模型缺口也必须保留为可审计结论。

## 恢复后的设置验证

波形传输的 CHDR/CFMT/CORD/WFSU,以及采集路径修改的 TRMD/TDIV/VDIV/TRA,在写回原设置后逐项独立查询。枚举按规范化值比较,时基与垂直档位按单位换算后以 `rtol=1e-9, atol=0` 比较;响应格式等价不应误报漂移。读回不符、响应无效或恢复写入失败均使操作失败,并记录具体字段。

结构化传输或会话失败后不继续恢复 I/O。普通主操作错误与恢复错误通过异常原因保留。此次修复经过离线故障测试,包括设备静默忽略写入;未进行新的实机验收,既有实机记录不用于声称新增回读已验收。
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ Disposition meanings:
This matrix answers whether each current WaveBench interface can be represented. [`COMMAND_COVERAGE_EN.md`](COMMAND_COVERAGE_EN.md) and the machine catalog answer how every explicit manual entity is disposed. Their denominators differ: 26 capabilities here and 578 explicit manual entities in the command catalog.

One hundred percent coverage therefore does not mean executing every instruction on hardware. Reset, calibration, filesystem, network, hardcopy, option activation, shutdown, and arbitrary-script paths remain quarantined. Missing options, model exclusions, unverified firmware behavior, and core-model gaps remain auditable coverage outcomes.

## Restoration readback

Restored CHDR/CFMT/CORD/WFSU settings and capture TRMD/TDIV/VDIV/TRA settings are independently queried after each write. Enumerations are normalized; timebase and vertical scales are compared in base units with `rtol=1e-9, atol=0`. A mismatch, malformed response or restore write failure fails the operation and identifies the affected field. Structured transport/session failures stop further restoration I/O. A primary operation failure is retained as the cause when restoration also fails. This change has offline fault coverage, including silently ignored writes; no new hardware acceptance was performed.
2 changes: 1 addition & 1 deletion packages/wavebench-siglent-sds3000/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-siglent-sds3000"
version = "0.1.0"
version = "0.1.1"
description = "SIGLENT SDS3000 oscilloscope plugin for WaveBench"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def descriptor() -> InstrumentDescriptor:
wavebench_min_version="0.8.24",
wavebench_max_version="0.9.0",
distribution="wavebench-siglent-sds3000",
version="0.1.0",
version="0.1.1",
source="entry_point:siglent.sds3000",
scope_coupling_policy="switchable-termination",
config_fields=("connection.resource", "scope.driver", "waveform.*"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from contextlib import contextmanager
from dataclasses import dataclass, field
from math import isfinite
from math import isclose, isfinite
import re
from threading import RLock
from typing import TYPE_CHECKING, Callable, Iterator
Expand Down Expand Up @@ -342,6 +342,35 @@ def _query_waveform_transfer_state(self) -> _WaveformTransferState:
),
)

def _verify_restored_setting(self, name: str, expected: str) -> None:
response = self.transport.query(f"{name}?", replay=ReplayPolicy.NO_REPLAY)
parsers = {"CHDR": self._parse_header_state, "CFMT": self._parse_format_state,
"CORD": self._parse_order_state, "WFSU": self._parse_setup_state,
"TRMD": self._parse_trigger_mode}
if name in parsers:
actual = parsers[name](response)
matches = actual == expected
elif name.endswith(":TRA"):
actual = self._parse_trace_state(response, channel=int(name[1:name.index(":")]))
matches = actual == expected
else:
time_axis = name == "TDIV"
units = frozenset({"", "S", "MS", "US", "NS", "KS"} if time_axis else {"", "V", "MV", "UV", "NV", "KV"})
header = "TIME_DIV" if time_axis else name.replace(":VDIV", ":VOLT_DIV")
actual = self._parse_positive_quantity(response, headers=(name, header), units=units, name=name)
def quantity(value: str) -> float:
match = _QUANTITY_RE.fullmatch(value)
assert match is not None
unit = match.group("unit")
scale = {"M": 1e-3, "U": 1e-6, "N": 1e-9, "K": 1e3}.get(unit[:1], 1.)
return float(match.group("number")) * scale
matches = isclose(quantity(actual), quantity(expected), rel_tol=1e-9, abs_tol=0.)
if not matches:
raise StateDriftError(
f"SDS3000 restore readback mismatch: {name}", expected={name: expected},
actual={name: actual}, diff={name: {"expected": expected, "actual": actual}},
)

@contextmanager
def _temporary_waveform_transfer_state(
self,
Expand Down Expand Up @@ -371,23 +400,24 @@ def _temporary_waveform_transfer_state(
for command, previous in reversed(restore):
try:
self.transport.write(f"{command} {previous}")
self._verify_restored_setting(command, previous)
except _STRUCTURED_IO_ERRORS:
raise
except Exception as exc: # pragma: no branch - all failures are retained
failures.append((command, previous, exc))
if failures:
expected = {command: previous for command, previous, _ in failures}
diff = {
command: {"expected": previous, "actual": "unknown"}
for command, previous, _ in failures
command: {"expected": previous, "actual": getattr(exc, "actual", {}).get(command, "unknown")}
for command, previous, exc in failures
}
names = ", ".join(command for command, _, _ in failures)
raise StateDriftError(
f"failed to restore SDS3000 waveform transfer state: {names}",
expected=expected,
actual={command: "unknown" for command in expected},
actual={name: getattr(exc, "actual", {}).get(name, "unknown") for name, _, exc in failures},
diff=diff,
) from failures[0][2]
) from (operation_failure or failures[0][2])

@staticmethod
def _validate_waveform_points(points: str) -> str:
Expand Down Expand Up @@ -555,23 +585,24 @@ def _temporary_capture_state(
for name, command, expected in reversed(restore):
try:
self.transport.write(command)
self._verify_restored_setting(name, expected)
except _STRUCTURED_IO_ERRORS:
raise
except Exception as exc: # pragma: no branch - all failures are retained
failures.append((name, expected, exc))
if failures:
expected = {name: value for name, value, _ in failures}
diff = {
name: {"expected": value, "actual": "unknown"}
for name, value, _ in failures
name: {"expected": value, "actual": getattr(exc, "actual", {}).get(name, "unknown")}
for name, value, exc in failures
}
names = ", ".join(name for name, _, _ in failures)
raise StateDriftError(
f"failed to restore SDS3000 capture state: {names}",
expected=expected,
actual={name: "unknown" for name in expected},
actual={name: getattr(exc, "actual", {}).get(name, "unknown") for name, _, exc in failures},
diff=diff,
) from failures[0][2]
) from (operation_failure or failures[0][2])

def _acquire_once(self) -> None:
budget_ms = max(min(self.io_timeout_ms, self.opc_timeout_ms), 1_000)
Expand Down
75 changes: 74 additions & 1 deletion packages/wavebench-siglent-sds3000/tests/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ def test_fetch_waveform_uses_existing_capability_and_restores_transfer_state() -
"WFSU?",
"C1:WF? DESC",
"C1:WF? DAT1",
"WFSU?", "CORD?", "CFMT?", "CHDR?",
]
assert transport.writes == [
"CHDR OFF",
Expand Down Expand Up @@ -519,7 +520,7 @@ def _capture_transport(
}
return FakeTransport(
responses=responses,
response_sequences={"TRMD?": ["TRMD AUTO", f"TRMD {final_trigger_mode}"]},
response_sequences={"TRMD?": ["TRMD AUTO", f"TRMD {final_trigger_mode}", "TRMD AUTO"]},
binary_responses={
"C1:WF? DESC": _word_descriptor(),
"C1:WF? DAT1": struct.pack("<4h", -2, 0, 2, 4),
Expand Down Expand Up @@ -647,3 +648,75 @@ def test_capture_rejects_invalid_requests_before_io(

assert transport.queries == []
assert transport.writes == []


def _track_setting_writes(transport, *, ignored):
original = transport.write
def write(command):
original(command)
if command != ignored and " " in command:
name, value = command.split(" ", 1)
transport.responses[name + "?"] = value
transport.write = write


@pytest.mark.parametrize("ignored", ["CHDR SHORT", "CFMT DEF9,BYTE,BIN", "CORD HI", "WFSU SP,4,NP,10,FP,2,SN,0"])
def test_transfer_restore_detects_silently_ignored_write(ignored):
transport = _waveform_transport()
_track_setting_writes(transport, ignored=ignored)
with pytest.raises(StateDriftError) as caught:
SDS3000Scope(transport).fetch_waveform(1, check_errors=False)
name = ignored.split(" ", 1)[0]
assert caught.value.actual[name] != caught.value.expected[name]
assert caught.value.diff[name]["actual"] == caught.value.actual[name]


@pytest.mark.parametrize("ignored", ["TDIV 2 MS", "C1:VDIV 200 MV", "C1:TRA OFF", "TRMD AUTO"])
def test_capture_restore_detects_silently_ignored_write(ignored):
transport = _capture_transport(trace_states={1: "OFF"})
# The first two trigger reads model baseline and completed acquisition.
transport.response_sequences["TRMD?"] = ["AUTO", "STOP"]
original_query = transport.query
def query(command, *, replay):
if command in transport.response_sequences and not transport.response_sequences[command]:
del transport.response_sequences[command]
return original_query(command, replay=replay)
transport.query = query
transport.responses["TRMD?"] = "STOP"
_track_setting_writes(transport, ignored=ignored)
with pytest.raises(StateDriftError) as caught:
SDS3000Scope(transport).capture_waveform(1, check_errors=False, time_range_s=.01, vertical_scale_v_per_div=.5)
assert ignored.split(" ", 1)[0] in caught.value.diff


@pytest.mark.parametrize("name,expected,response", [
("TDIV", "2 MS", "TIME_DIV 0.002 S"),
("C1:VDIV", "200 MV", "C1:VOLT_DIV 0.2 V"),
])
def test_restore_compares_quantities_after_unit_conversion(name, expected, response):
scope = SDS3000Scope(FakeTransport(responses={name + "?": response}))
scope._verify_restored_setting(name, expected)


def test_restore_readback_failure_retains_primary_waveform_error():
primary = TimeoutError("primary waveform failure")
transport = _waveform_transport(data_response=primary)
transport.response_sequences["CHDR?"] = ["SHORT", "OFF"]
with pytest.raises(StateDriftError) as caught:
SDS3000Scope(transport).fetch_waveform(1, check_errors=False)
assert caught.value.__cause__ is primary


def test_restore_verification_query_failure_stops_following_io():
failure = _transport_failure(operation="query", synchronization=Synchronization.LOST)
transport = _waveform_transport()
original = transport.query
def query(command, *, replay):
if command == "WFSU?" and transport.writes:
raise failure
return original(command, replay=replay)
transport.query = query
with pytest.raises(TransportIOError) as caught:
SDS3000Scope(transport).fetch_waveform(1, check_errors=False)
assert caught.value is failure
assert transport.writes[-1] == "WFSU SP,4,NP,10,FP,2,SN,0"
2 changes: 1 addition & 1 deletion packages/wavebench-siglent-sds3000/tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


PACKAGE_ROOT = Path(__file__).resolve().parents[1]
PACKAGE_VERSION = "0.1.0"
PACKAGE_VERSION = "0.1.1"


def _run(command: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,9 @@ x[i] = horizontal_delay - timebase * 10 / 2 + i * sample_interval
- [功能覆盖开发路线](SDS800X_HD_COVERAGE_MILESTONES.md)
- [实机验收记录](SDS800X_HD_HARDWARE_ACCEPTANCE.md)
- [Scope R1.3 conformance](SDS800X_HD_R13_CONFORMANCE.md)

## 波形传输恢复验证

legacy `fetch_waveform` 写回原传输设置后,重新读取 source/start/interval/points/width/byte order 六项并与快照比较。写入被设备忽略、回读无效或不一致时操作失败;不会只凭命令返回就认定恢复成功。结构化传输或会话失败后停止后续恢复 I/O,普通主操作与恢复失败信息均保留。

本次新增验证仅做离线测试和制品检查,不扩展型号/固件实机范围,也不承诺自动恢复 capture 修改的时基、垂直档位或通道显示。测试专用 R1.3 fixture 与此生产路径保持各自的证据边界。
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,7 @@ This list indexes protocol domains from the current driver. The complete source
- [Feature-coverage development roadmap](SDS800X_HD_COVERAGE_MILESTONES_EN.md)
- [Hardware acceptance record](SDS800X_HD_HARDWARE_ACCEPTANCE_EN.md)
- [Scope R1.3 conformance](SDS800X_HD_R13_CONFORMANCE.md) (Chinese)

## Waveform transfer restoration verification

After restoring the legacy `fetch_waveform` transfer settings, the driver freshly reads source, start, interval, points, width and byte order and compares them with the baseline. Ignored writes, malformed readback and mismatches fail the operation. Structured transport/session failures stop subsequent restoration I/O; ordinary primary and restoration errors are both retained. This change is tested offline and does not extend hardware acceptance or promise restoration of capture timebase, vertical scale or channel display. The R1.3 test fixture remains separate from this production path.
2 changes: 1 addition & 1 deletion packages/wavebench-siglent-sds800x-hd/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-siglent-sds800x-hd"
version = "0.6.0"
version = "0.6.1"
description = "SIGLENT SDS800X HD oscilloscope driver plugin for WaveBench"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def descriptor() -> InstrumentDescriptor:
wavebench_min_version="0.8.23",
wavebench_max_version="0.9.0",
distribution="wavebench-siglent-sds800x-hd",
version="0.6.0",
version="0.6.1",
source="entry_point:siglent.sds800x-hd",
scope_coupling_policy="fixed-high-impedance",
config_fields=("connection.resource", "scope.driver", "waveform.*"),
Expand Down
Loading
Loading