3 Commits

Author SHA1 Message Date
0c0821d157 Housekeeping - cleaning & Update 2026-05-03 13:53:38 +02:00
d6aa23c293 fixed all bugs known to tracker 2026-04-20 14:31:19 +02:00
81dfc91dd6 Setup Archive and testing 4 Issues for V0.2) 2026-04-18 12:46:56 +02:00
41 changed files with 13345 additions and 0 deletions

1599
script/SDG2042X_V0.2.6.py Normal file

File diff suppressed because it is too large Load Diff

BIN
script/Test_pulse.bin Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=
SLOT1_OUTP=
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT3_OUTP=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=
SLOT1_OUTP=
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT3_OUTP=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=
SLOT1_OUTP=
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT3_OUTP=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

View File

@ -0,0 +1,66 @@
# Test Procedure — v0.2.2 Issue #10 Sweep fallback timing
## Purpose
Verify that sweep fallback no longer depends on an immediate `SYST:ERR?` after the first `SWWV` write.
The new logic should do this sequence:
1. `*CLS`
2. send first sweep command
3. wait on `*OPC?`
4. then read `SYST:ERR?`
5. only if needed, retry with `SPAC`
## Test files
- `SDG2042X_V0.2.2.py`
- `test_v0.2.2_Issue_10_sweep_fallback.py`
## Preconditions
- SDG reachable via LAN on port `5025`
- No other program connected to the instrument
- Output load/test setup safe for a short sweep test
## Command
Example:
```bash
python3 test_v0.2.2_Issue_10_sweep_fallback.py --ip 192.168.178.50 --channel C1
```
Optional custom values:
```bash
python3 test_v0.2.2_Issue_10_sweep_fallback.py \
--ip 192.168.178.50 \
--channel C1 \
--start 1000 \
--stop 5000 \
--time 1.0 \
--type LIN
```
## What the script checks
1. Socket connection and `*IDN?`
2. Safe baseline setup (`BSWV` to sine)
3. Direct one-form check for:
- `WAV,...`
- `SPAC,...`
4. Production-like bug #10 helper
5. Stale-error immunity:
- inject old error
- run helper again
- helper must still succeed because it starts with `*CLS`
## Pass criteria
- `critical tests passed: True`
- exit code `0`
- no final `[ERR]` style failures from the script
## Interpretation
- If only one of the direct forms works, that is acceptable.
The important point is that the production-like helper succeeds.
- If the stale-error test fails, the error-clear / sync sequence is still not robust enough.
- If both direct forms fail, then bug #10 may be masked by a larger sweep syntax compatibility issue.
## Notes
This test script intentionally mirrors the current GUI sweep token style (`WAV`, `STAR`) so it checks the real regression path for Issue #10, not a separate protocol cleanup.

Binary file not shown.

View File

@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""Live instrument test for SDG GUI bug #10.
Bug #10
-------
Sweep fallback from WAV to SPAC was timing-fragile because the code queried
SYST:ERR? immediately after the first SWWV write. This script tests the new
synchronised approach on a real instrument via raw socket SCPI.
What it checks
--------------
1. Instrument identity and basic socket comms.
2. Which direct sweep form the instrument accepts:
- WAV,<LIN|LOG>
- SPAC,<LIN|LOG>
3. The production-style fallback logic:
*CLS -> SWWV write -> *OPC? -> SYST:ERR? -> optional SPAC fallback
4. Stale-error immunity:
an old queued error should not poison the next sweep apply because the
helper clears the error state before the real attempt.
Notes
-----
- This mirrors the current GUI sweep token set (WAV / STAR) on purpose,
because the target is bug #10 regression testing of the GUI path.
- The programming guide documents START and SWMD for sweep. This script does
not try to resolve that wider compatibility question.
"""
from __future__ import annotations
import argparse
import socket
import sys
import time
from dataclasses import dataclass
from typing import List, Optional
DEFAULT_PORT = 5025
SOCKET_TIMEOUT = 4.0
class SDGLan:
def __init__(self) -> None:
self.sock: Optional[socket.socket] = None
def connect(self, host: str, port: int = DEFAULT_PORT) -> None:
self.close()
self.sock = socket.create_connection((host, port), timeout=SOCKET_TIMEOUT)
self.sock.settimeout(SOCKET_TIMEOUT)
def close(self) -> None:
try:
if self.sock is not None:
self.sock.close()
finally:
self.sock = None
def write(self, cmd: str) -> None:
if self.sock is None:
raise RuntimeError("Not connected")
if not cmd.endswith("\n"):
cmd += "\n"
self.sock.sendall(cmd.encode("ascii"))
def drain(self) -> bytes:
if self.sock is None:
raise RuntimeError("Not connected")
data = bytearray()
while True:
try:
chunk = self.sock.recv(65536)
if not chunk:
break
data.extend(chunk)
except socket.timeout:
break
return bytes(data)
def _recv_line(self) -> str:
if self.sock is None:
raise RuntimeError("Not connected")
buf = bytearray()
while True:
chunk = self.sock.recv(4096)
if not chunk:
break
buf.extend(chunk)
if b"\n" in buf:
break
return buf.decode("ascii", errors="ignore")
def query_retry(self, cmd: str, retries: int = 2) -> str:
last_err: Optional[Exception] = None
for _ in range(retries + 1):
try:
self.drain()
self.write(cmd)
return self._recv_line().strip()
except socket.timeout as exc:
last_err = exc
try:
self.write("*CLS")
except Exception:
pass
if last_err is not None:
raise last_err
raise RuntimeError("query_retry failed without exception")
@dataclass
class ApplyResult:
ok: bool
cmd: str
opc: str
err: str
used_fallback: bool = False
fallback_cmd: str = ""
fallback_opc: str = ""
fallback_err: str = ""
note: str = ""
def print_section(title: str) -> None:
print("\n" + "=" * 78)
print(title)
print("=" * 78)
def sync_apply(io: SDGLan, cmd: str) -> ApplyResult:
try:
try:
io.write("*CLS")
except Exception:
pass
io.write(cmd)
opc = io.query_retry("*OPC?")
err = io.query_retry("SYST:ERR?")
return ApplyResult(ok=err.startswith("0"), cmd=cmd, opc=opc, err=err)
except Exception as exc:
return ApplyResult(ok=False, cmd=cmd, opc="", err=str(exc), note="exception during sync_apply")
def apply_with_bug10_logic(io: SDGLan, channel: str, parts: List[str]) -> ApplyResult:
cmd = f"{channel}:SWWV " + ",".join(parts)
first = sync_apply(io, cmd)
if first.ok:
first.note = "primary form accepted"
return first
parts2 = [p.replace("WAV,", "SPAC,") for p in parts]
if parts2 == parts:
first.note = "primary form failed and no fallback token found"
return first
cmd2 = f"{channel}:SWWV " + ",".join(parts2)
second = sync_apply(io, cmd2)
return ApplyResult(
ok=second.ok,
cmd=first.cmd,
opc=first.opc,
err=first.err,
used_fallback=True,
fallback_cmd=cmd2,
fallback_opc=second.opc,
fallback_err=second.err,
note="fallback accepted" if second.ok else "both forms failed",
)
def build_gui_style_parts(start_hz: int, stop_hz: int, sweep_type: str = "LIN", time_s: float = 1.0,
direction: str = "UP", trigger_source: str = "INT") -> List[str]:
return [
"STATE,ON",
f"WAV,{sweep_type}",
f"STAR,{start_hz}",
f"STOP,{stop_hz}",
f"TIME,{time_s}",
f"DIR,{direction}",
f"TRSR,{trigger_source}",
]
def set_safe_basic_wave(io: SDGLan, channel: str) -> None:
io.write(f"{channel}:BSWV WVTP,SINE,FRQ,1000,AMP,2,OFST,0")
io.query_retry("*OPC?")
_ = io.query_retry("SYST:ERR?")
def read_back(io: SDGLan, channel: str) -> str:
try:
return io.query_retry(f"{channel}:SWWV?")
except Exception as exc:
return f"<readback failed: {exc}>"
def inject_stale_error(io: SDGLan, channel: str) -> None:
# Force a known parser error, wait until the instrument has processed it,
# but intentionally do not read SYST:ERR? afterwards. The next helper call
# should clear it with *CLS before doing the real sweep write.
io.write(f"{channel}:SWWV BADTOKEN,1")
try:
io.query_retry("*OPC?")
except Exception:
pass
def cleanup(io: SDGLan, channel: str) -> None:
try:
sync_apply(io, f"{channel}:SWWV STATE,OFF")
except Exception:
pass
def main() -> int:
parser = argparse.ArgumentParser(description="Live regression test for SDG GUI bug #10 sweep fallback")
parser.add_argument("--ip", required=True, help="Instrument IP address")
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Instrument port (default: {DEFAULT_PORT})")
parser.add_argument("--channel", default="C1", choices=["C1", "C2"], help="Target channel")
parser.add_argument("--start", type=int, default=1000, help="Sweep start frequency in Hz")
parser.add_argument("--stop", type=int, default=5000, help="Sweep stop frequency in Hz")
parser.add_argument("--time", type=float, default=1.0, help="Sweep time in seconds")
parser.add_argument("--type", default="LIN", choices=["LIN", "LOG"], help="Sweep type token used by GUI")
args = parser.parse_args()
io = SDGLan()
try:
print_section("CONNECT")
io.connect(args.ip, args.port)
print(f"Connected to {args.ip}:{args.port}")
print(f"*IDN? -> {io.query_retry('*IDN?')}")
print_section("PREPARE")
set_safe_basic_wave(io, args.channel)
print(f"{args.channel}: basic waveform set to safe SINE baseline")
parts = build_gui_style_parts(
start_hz=args.start,
stop_hz=args.stop,
sweep_type=args.type,
time_s=args.time,
direction="UP",
trigger_source="INT",
)
parts_spac = [p.replace("WAV,", "SPAC,") for p in parts]
print_section("INFORMATIONAL DIRECT FORM CHECKS")
direct_wav = sync_apply(io, f"{args.channel}:SWWV " + ",".join(parts))
print(f"WAV form ok : {direct_wav.ok}")
print(f"WAV form cmd : {direct_wav.cmd}")
print(f"WAV form *OPC? : {direct_wav.opc}")
print(f"WAV form SYST:ERR?: {direct_wav.err}")
direct_spac = sync_apply(io, f"{args.channel}:SWWV " + ",".join(parts_spac))
print(f"SPAC form ok : {direct_spac.ok}")
print(f"SPAC form cmd : {direct_spac.cmd}")
print(f"SPAC form *OPC? : {direct_spac.opc}")
print(f"SPAC form SYST:ERR?: {direct_spac.err}")
print_section("TEST 1 - PRODUCTION-LIKE BUG #10 HELPER")
helper = apply_with_bug10_logic(io, args.channel, parts)
print(f"helper ok : {helper.ok}")
print(f"helper note : {helper.note}")
print(f"primary cmd : {helper.cmd}")
print(f"primary *OPC? : {helper.opc}")
print(f"primary SYST:ERR?: {helper.err}")
if helper.used_fallback:
print(f"fallback used : True")
print(f"fallback cmd : {helper.fallback_cmd}")
print(f"fallback *OPC? : {helper.fallback_opc}")
print(f"fallback SYST:ERR?: {helper.fallback_err}")
else:
print("fallback used : False")
print(f"readback : {read_back(io, args.channel)}")
print_section("TEST 2 - STALE ERROR IMMUNITY")
inject_stale_error(io, args.channel)
stale = apply_with_bug10_logic(io, args.channel, parts)
print(f"stale-test ok : {stale.ok}")
print(f"stale-test note : {stale.note}")
print(f"primary cmd : {stale.cmd}")
print(f"primary *OPC? : {stale.opc}")
print(f"primary SYST:ERR?: {stale.err}")
if stale.used_fallback:
print("fallback used : True")
print(f"fallback cmd : {stale.fallback_cmd}")
print(f"fallback *OPC? : {stale.fallback_opc}")
print(f"fallback SYST:ERR?: {stale.fallback_err}")
else:
print("fallback used : False")
print(f"readback : {read_back(io, args.channel)}")
print_section("RESULT")
critical_ok = helper.ok and stale.ok
print(f"critical tests passed: {critical_ok}")
print("Informational note: direct WAV/SPAC one-form checks may differ by model/firmware.")
return 0 if critical_ok else 1
finally:
try:
cleanup(io, args.channel)
finally:
io.close()
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,51 @@
==============================================================================
CONNECT
==============================================================================
Connected to 192.168.178.220:5025
*IDN? -> Siglent Technologies,SDG2042X,SDG2XFBC8R0225,2.01.01.38
==============================================================================
PREPARE
==============================================================================
C1: basic waveform set to safe SINE baseline
==============================================================================
INFORMATIONAL DIRECT FORM CHECKS
==============================================================================
WAV form ok : True
WAV form cmd : C1:SWWV STATE,ON,WAV,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
WAV form *OPC? : 1
WAV form SYST:ERR?: 0,"No error"
SPAC form ok : True
SPAC form cmd : C1:SWWV STATE,ON,SPAC,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
SPAC form *OPC? : 1
SPAC form SYST:ERR?: 0,"No error"
==============================================================================
TEST 1 - PRODUCTION-LIKE BUG #10 HELPER
==============================================================================
helper ok : True
helper note : primary form accepted
primary cmd : C1:SWWV STATE,ON,WAV,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
primary *OPC? : 1
primary SYST:ERR?: 0,"No error"
fallback used : False
readback : C1:SWWV STATE,ON,TIME,1S,STOP,5000HZ,START,1e-06HZ,TRSR,INT,TRMD,OFF,SWMD,LINE,DIR,UP,SYM,1.20072702051015e-47,MARK_STATE,OFF,MARK_FREQ,0HZ,IDLE_FREQ,START_FREQ,CARR,WVTP,SINE,FRQ,2500.000001HZ,AMP,2V,AMPVRMS,0.707Vrms,AMPDBM,0dBm,OFST,0V,PHSE,0
==============================================================================
TEST 2 - STALE ERROR IMMUNITY
==============================================================================
stale-test ok : True
stale-test note : primary form accepted
primary cmd : C1:SWWV STATE,ON,WAV,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
primary *OPC? : 1
primary SYST:ERR?: 0,"No error"
fallback used : False
readback : C1:SWWV STATE,ON,TIME,1S,STOP,5000HZ,START,1e-06HZ,TRSR,INT,TRMD,OFF,SWMD,LINE,DIR,UP,SYM,1.20072702051015e-47,MARK_STATE,OFF,MARK_FREQ,0HZ,IDLE_FREQ,START_FREQ,CARR,WVTP,SINE,FRQ,2500.000001HZ,AMP,2V,AMPVRMS,0.707Vrms,AMPDBM,0dBm,OFST,0V,PHSE,0
==============================================================================
RESULT
==============================================================================
critical tests passed: True
Informational note: direct WAV/SPAC one-form checks may differ by model/firmware.

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=
SLOT1_OUTP=
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=C1:BSWV WVTP,SQUARE,FRQ,1000HZ,PERI,0.001S,AMP,2V,AMPVRMS,1Vrms,AMPDBM,0dBm,MAX_OUTPUT_AMP,20V,OFST,0V,HLEV,1V,LLEV,-1V,PHSE,0,DUTY,50
SLOT3_OUTP=ON
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=
SLOT1_OUTP=
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

Binary file not shown.

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=FILE_A
SLOT1_BSWV=C1:BSWV WVTP,SINE,FRQ,1000,AMP,1,OFST,0
SLOT1_OUTP=OFF
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT3_OUTP=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=C1:BSWV WVTP,SQUARE,FRQ,1000HZ,PERI,0.001S,AMP,2V,AMPVRMS,1Vrms,AMPDBM,13.0103dBm,MAX_OUTPUT_AMP,20V,OFST,0V,HLEV,1V,LLEV,-1V,PHSE,0,DUTY,50
SLOT1_OUTP=ON
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT3_OUTP=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=
SLOT1_OUTP=
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT3_OUTP=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
# SDG2042X presets
SLOT1_NAME=
SLOT1_BSWV=
SLOT1_OUTP=
SLOT2_NAME=
SLOT2_BSWV=
SLOT2_OUTP=
SLOT3_NAME=
SLOT3_BSWV=
SLOT3_OUTP=
SLOT4_NAME=
SLOT4_BSWV=
SLOT4_OUTP=
SLOT5_NAME=
SLOT5_BSWV=
SLOT5_OUTP=
SLOT6_NAME=
SLOT6_BSWV=
SLOT6_OUTP=
SLOT7_NAME=
SLOT7_BSWV=
SLOT7_OUTP=
SLOT8_NAME=
SLOT8_BSWV=
SLOT8_OUTP=
SLOT9_NAME=
SLOT9_BSWV=
SLOT9_OUTP=
SLOT10_NAME=
SLOT10_BSWV=
SLOT10_OUTP=

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,97 @@
# SDG2042X v0.2 — Test Procedure
**Milestone:** v0.2.0 - first bugfix round
**Issues:** #1 · #2 · #6 · #12
**Date:** 2026-04-18
---
## Files Required
| File | Purpose |
|------|---------|
| `SDG2042X_V0.2.py` | Script under test |
| `test_v0.2_all.py` | Automated test suite |
Both files must be in the **same directory**.
---
## Part 1 — Automated Tests (no instrument required)
Covers: **#1** `_recv_line()` · **#6** screenshot path · **#12** `human_to_eng()`
Covers structurally: **#2** ARB thread (AST check)
### Run
```bash
cd /path/to/scripts
python3 test_v0.2_all.py
```
Single issue:
```bash
python3 test_v0.2_all.py --issue 12
```
### Expected output
```
Issue #1 PASS (5/5)
Issue #12 PASS (20/20)
Issue #2 PASS (8/8)
Issue #6 PASS (2/2)
Total: 35/35 passed
✓ All tests passed — safe to close all four issues
```
Exit code `0` = pass, `1` = fail. Suitable for CI.
### Close criteria
All 35 TCs green → close **#1, #6, #12** immediately.
**#2** additionally requires the hardware test below.
---
## Part 2 — Hardware Test (instrument required)
Covers: **#2** ARB download GUI responsiveness
### Setup
- SDG2042X powered and reachable on LAN
- At least one USER waveform stored on the instrument
*(if none: upload any `.bin` via ARB Manager → Upload first)*
### Procedure
| Step | Action | Expected |
|------|--------|---------|
| 1 | Start GUI: `python3 SDG2042X_V0.2.py --ip <addr>` | Window opens |
| 2 | Click **Connect** | Log: `Connected to <addr>:5025` |
| 3 | Tab **ARB Manager** → click **Refresh** | USER list populated |
| 4 | Select a USER waveform → click **Download USER → file** | Log: `ARB download started…` |
| 5 | **While download runs**: click IDN?, change waveform combo, click SYST:ERR? | UI responds immediately — no freeze |
| 6 | Wait for completion | Log: `Downloaded -> <path> (N bytes)` |
| 7 | Verify saved file: `ls -lh <saved_file>` | Non-zero size |
### Close criteria
Steps 5 and 6 both pass → close **#2**.
---
## Summary Checklist
```
[ ] python3 test_v0.2_all.py → 35/35
[ ] #1 close
[ ] #6 close
[ ] #12 close
[ ] Hardware test step 5: GUI responsive during ARB download
[ ] Hardware test step 6: file saved, non-zero size
[ ] #2 close
[ ] Milestone v0.2.0 → mark closed
```

View File

@ -0,0 +1,69 @@
SDG2042X v0.2 — regression test suite
Script under test: /home/tgohle/TGONet_Virtual/tgo-files/git/ToGo-Lab/0003-SDG2042X-PyQt-GUI-for-Linux/script/test_v02_Issue_1-2-6-12/./SDG2042X_V0.2.py
── Issue #12 — human_to_eng() MHz regression ──
[ OK ] BUG-REG: 1.5 MHz → 1500000 (was 0.0000015)
[ OK ] BUG-REG: 30 MHz → 30000000
[ OK ] BUG-REG: 100 MHz → 100000000
[ OK ] 1 kHz
[ OK ] 10 kHz
[ OK ] 100 Hz
[ OK ] 1 GHz
[ OK ] lowercase hz
[ OK ] uppercase KHZ
[ OK ] 500 ms
[ OK ] 1 us
[ OK ] 250 ns
[ OK ] 2.5 V
[ OK ] 500 mV
[ OK ] 1.0 Vpp
[ OK ] bare integer
[ OK ] bare float
[ OK ] scientific notation
[ OK ] non-numeric fallback
[ OK ] empty string
── Issue #1 — _recv_line() handles partial TCP receive ──
[ OK ] TC-1a single chunk
[ OK ] TC-1b newline split into own chunk
[ OK ] TC-1c byte-by-byte
[ OK ] TC-1d split at midpoint
[ OK ] TC-1e empty response returns empty string
── Issue #6 — on_screenshot() uses in-memory cfg, not disk config ──
[ OK ] TC-6a: worker receives self.cfg path, not load_config() path
 disk cfg = /tmp/tmpy6m9nh8m/dir_disk
 self.cfg = /tmp/tmpy6m9nh8m/dir_memory
 received = /tmp/tmpy6m9nh8m/dir_memory
[ OK ] TC-6b: worker path differs from stale disk path
── Issue #2 — ARB download moved to QThread (structural) ──
[ OK ] TC-2a: class ARBDownloadWorker exists
[ OK ] TC-2b: ARBDownloadWorker inherits QThread
[ OK ] TC-2c: ARBDownloadWorker.run() defined
[ OK ] TC-2d: recv() call inside ARBDownloadWorker.run()
[ OK ] TC-2e: on_arb_download() contains no blocking while loop
[ OK ] TC-2f: on_arb_download() calls worker.start()
[ OK ] TC-2g: _on_arb_download_done() handler defined
[ OK ] TC-2h: done signal connected to handler
 NOTE: live GUI responsiveness during download requires
 a connected SDG2042X — run manually per test strategy.
══════════════════════════════════════════════
 SUMMARY
══════════════════════════════════════════════
Issue #1 PASS (5/5)
Issue #12 PASS (20/20)
Issue #2 PASS (8/8)
Issue #6 PASS (2/2)
Total: 35/35 passed
 ✓ All tests passed — safe to close all four issues
Gitea close checklist:
#1 ✓ close
#2 ✓ close
#6 ✓ close
#12 ✓ close

View File

@ -0,0 +1,476 @@
#!/usr/bin/env python3
"""
test_v0.2_all.py -- SDG2042X v0.2 regression test suite
Tests: Issue #1, #2, #6, #12 (milestone: v0.2.0 - first bugfix round finished)
Usage:
python3 test_v0.2_all.py # run all tests
python3 test_v0.2_all.py --issue 12 # run single issue
Place this file next to SDG2042X_V0.2.py and run from that directory.
No instrument required. All tests are offline / mocked.
Author: ToGo-Lab test suite — generated 2026-04-18
"""
import ast
import importlib.util
import os
import re
import socket
import sys
import tempfile
import threading
import time
import types
SCRIPT_NAME = "SDG2042X_V0.2.py"
# ─────────────────────────────────────────────────────────────────────────────
# ANSI colours (disabled on Windows)
# ─────────────────────────────────────────────────────────────────────────────
USE_COLOUR = sys.platform != "win32"
def _c(code, s): return f"\033[{code}m{s}\033[0m" if USE_COLOUR else s
OK = lambda s: _c("32;1", s)
FAIL = lambda s: _c("31;1", s)
HEAD = lambda s: _c("36;1", s)
DIM = lambda s: _c("2", s)
# ─────────────────────────────────────────────────────────────────────────────
# Result collector
# ─────────────────────────────────────────────────────────────────────────────
results: dict[str, list[tuple[bool, str]]] = {}
def record(issue: str, ok: bool, label: str, detail: str = ""):
results.setdefault(issue, []).append((ok, label))
mark = OK(" OK ") if ok else FAIL(" FAIL ")
print(f" [{mark}] {label}")
if detail:
for line in detail.strip().splitlines():
print(DIM(f" {line}"))
def section(title: str):
print()
print(HEAD(f"── {title} ──"))
# ─────────────────────────────────────────────────────────────────────────────
# Locate the script under test
# ─────────────────────────────────────────────────────────────────────────────
def find_script() -> str:
candidate = os.path.join(os.path.dirname(__file__), SCRIPT_NAME)
if not os.path.exists(candidate):
sys.exit(FAIL(f"ERROR: {SCRIPT_NAME} not found next to this test file.\n"
f"Expected: {candidate}"))
return candidate
SCRIPT_PATH = find_script()
SRC = open(SCRIPT_PATH, encoding="utf-8").read()
# ─────────────────────────────────────────────────────────────────────────────
# Minimal PyQt5 mock so the module can be imported without a display
# ─────────────────────────────────────────────────────────────────────────────
def _install_pyqt5_mock():
def _noop(*a, **k): return None
def _cls(name): return type(name, (), {
"__init__": _noop, "__call__": _noop,
"setEnabled": _noop, "addItems": _noop, "setText": _noop,
"text": lambda s: "", "currentText": lambda s: "",
"appendPlainText": _noop, "setReadOnly": _noop,
"setMinimumHeight": _noop, "setPlaceholderText": _noop,
"setWindowTitle": _noop, "resize": _noop, "show": _noop,
"setRange": _noop, "setValue": _noop, "value": lambda s: 1,
"addWidget": _noop, "setLayout": _noop, "addStretch": _noop,
"setColumnStretch": _noop, "setHorizontalHeaderLabels": _noop,
"horizontalHeader": lambda s: type("h",(),{"setStretchLastSection":_noop})(),
"verticalHeader": lambda s: type("v",(),{"setVisible":_noop})(),
"setItem": _noop, "item": lambda s,r,c: type("i",(),{"setFlags":_noop,"setText":_noop,"text":lambda ss:""})(),
"setFlags": _noop, "clicked": type("sig",(),{"connect":_noop})(),
"currentTextChanged": type("sig",(),{"connect":_noop})(),
"returnPressed": type("sig",(),{"connect":_noop})(),
"isRunning": lambda s: False, "start": _noop,
"done": type("sig",(),{"connect":_noop, "emit":_noop})(),
"isNull": lambda s: True,
})
# Build mock modules
for mod in ["PyQt5","PyQt5.QtWidgets","PyQt5.QtCore","PyQt5.QtGui"]:
if mod not in sys.modules:
sys.modules[mod] = types.ModuleType(mod)
qw = sys.modules["PyQt5.QtWidgets"]
qc = sys.modules["PyQt5.QtCore"]
qg = sys.modules["PyQt5.QtGui"]
for name in ["QWidget","QLabel","QLineEdit","QComboBox","QPushButton",
"QPlainTextEdit","QTabWidget","QGridLayout","QHBoxLayout",
"QVBoxLayout","QApplication","QSpinBox","QCheckBox",
"QListWidget","QTableWidget","QTableWidgetItem",
"QFileDialog","QSizePolicy"]:
setattr(qw, name, _cls(name))
# QThread: base for workers
setattr(qc, "QThread", _cls("QThread"))
setattr(qc, "Qt", type("Qt", (), {"WaitCursor": 0, "ItemIsEnabled": 0}))
class _Signal:
def __init__(self, *types): pass
def connect(self, fn): pass
def emit(self, *a): pass
setattr(qc, "pyqtSignal", _Signal)
setattr(qg, "QImage", _cls("QImage"))
setattr(qg, "QPixmap", _cls("QPixmap"))
_install_pyqt5_mock()
# ─────────────────────────────────────────────────────────────────────────────
# Load module under test (SDG2042X_V0.2.py)
# ─────────────────────────────────────────────────────────────────────────────
spec = importlib.util.spec_from_file_location("sdg", SCRIPT_PATH)
sdg = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(sdg)
except Exception as e:
sys.exit(FAIL(f"ERROR: failed to import {SCRIPT_NAME}: {e}"))
# ═════════════════════════════════════════════════════════════════════════════
# ISSUE #12 — human_to_eng() MHz regression
# ═════════════════════════════════════════════════════════════════════════════
def test_issue_12():
section("Issue #12 — human_to_eng() MHz regression")
fn = sdg.human_to_eng
cases = [
# (input, expected, description)
# ── THE BUG: these were silently wrong before v0.2 ──
("1.5 MHz", "1500000", "BUG-REG: 1.5 MHz → 1500000 (was 0.0000015)"),
("30 MHz", "30000000", "BUG-REG: 30 MHz → 30000000"),
("100 MHz", "100000000", "BUG-REG: 100 MHz → 100000000"),
# ── Frequency ──
("1 kHz", "1000", "1 kHz"),
("10 kHz", "10000", "10 kHz"),
("100 Hz", "100", "100 Hz"),
("1 GHz", "1000000000", "1 GHz"),
("1 hz", "1", "lowercase hz"),
("1 KHZ", "1000", "uppercase KHZ"),
# ── Time ──
("500 ms", "0.5", "500 ms"),
("1 us", "1e-06", "1 us"),
("250 ns", repr(250e-9), "250 ns"),
# ── Voltage ──
("2.5 V", "2.5", "2.5 V"),
("500 mV", "0.5", "500 mV"),
("1.0 Vpp", "1", "1.0 Vpp"),
# ── Bare numbers (duty, phase, offset) ──
("50", "50", "bare integer"),
("0.001", "0.001", "bare float"),
("1e-6", "1e-06", "scientific notation"),
# ── Fallback ──
("SINE", "SINE", "non-numeric fallback"),
("", "", "empty string"),
]
def _eq(a, b):
"""String equality first; fall back to float comparison for IEEE 754 noise."""
if a == b:
return True
try:
return abs(float(a) - float(b)) <= 1e-12 * max(abs(float(b)), 1e-30)
except (ValueError, TypeError):
return False
for inp, expected, desc in cases:
result = fn(inp)
ok = _eq(result, expected)
record("12", ok, desc,
f"input={inp!r} expected≈{expected!r} got={result!r}" if not ok else "")
# ═════════════════════════════════════════════════════════════════════════════
# ISSUE #1 — _recv_line() partial TCP receive
# ═════════════════════════════════════════════════════════════════════════════
def _fragmented_server(host, port, response: bytes, chunk_sizes: list, ready: threading.Event):
"""Mini TCP server: sends response in deliberate fragments."""
srv = socket.create_server((host, port))
srv.settimeout(5)
ready.set()
try:
conn, _ = srv.accept()
offset = 0
for size in chunk_sizes:
conn.sendall(response[offset:offset + size])
offset += size
time.sleep(0.012)
if offset < len(response):
conn.sendall(response[offset:])
conn.close()
finally:
srv.close()
def _next_port(base=15025):
"""Find a free port starting from base."""
for p in range(base, base + 100):
try:
s = socket.create_server(("127.0.0.1", p)); s.close(); return p
except OSError:
continue
raise RuntimeError("No free port found")
def test_issue_1():
section("Issue #1 — _recv_line() handles partial TCP receive")
def run_tc(label, response: bytes, chunk_sizes: list):
port = _next_port()
ready = threading.Event()
t = threading.Thread(
target=_fragmented_server,
args=("127.0.0.1", port, response, chunk_sizes, ready),
daemon=True)
t.start()
ready.wait(timeout=2)
io = sdg.SDGLan()
try:
io.connect("127.0.0.1", port)
result = io._recv_line()
finally:
io.close()
t.join(timeout=3)
expected = response.decode("ascii").strip()
ok = result.strip() == expected
record("1", ok, label,
f"expected={expected!r} got={result.strip()!r}" if not ok else "")
# TC-1a: single chunk (baseline)
run_tc("TC-1a single chunk",
b"C1:BSWV WVTP,SINE,FRQ,1000\n", [])
# TC-1b: \n arrives in a separate recv() — the original bug
msg = b"C1:BSWV WVTP,SINE,FRQ,1000\n"
run_tc("TC-1b newline split into own chunk",
msg, [len(msg) - 1])
# TC-1c: byte-by-byte fragmentation
msg = b"*IDN? Siglent,SDG2042X\n"
run_tc("TC-1c byte-by-byte",
msg, [1] * (len(msg) - 1))
# TC-1d: two unequal halves
msg = b"C1:OUTP ON\n"
run_tc("TC-1d split at midpoint",
msg, [len(msg) // 2])
# TC-1e: empty response (server closes immediately — must not hang)
port = _next_port()
ready = threading.Event()
def empty_server():
srv = socket.create_server(("127.0.0.1", port)); srv.settimeout(5)
ready.set()
conn, _ = srv.accept(); conn.close(); srv.close()
t = threading.Thread(target=empty_server, daemon=True); t.start()
ready.wait(timeout=2)
io = sdg.SDGLan()
try:
io.connect("127.0.0.1", port)
result = io._recv_line()
finally:
io.close()
t.join(timeout=3)
record("1", result == "", "TC-1e empty response returns empty string",
f"got={result!r}" if result != "" else "")
# ═════════════════════════════════════════════════════════════════════════════
# ISSUE #6 — Screenshot path uses stale disk config
# ═════════════════════════════════════════════════════════════════════════════
def test_issue_6():
section("Issue #6 — on_screenshot() uses in-memory cfg, not disk config")
with tempfile.TemporaryDirectory() as tmp:
dir_disk = os.path.join(tmp, "dir_disk"); os.makedirs(dir_disk)
dir_memory = os.path.join(tmp, "dir_memory"); os.makedirs(dir_memory)
# Write a config file pointing at dir_disk
cfg_path = os.path.join(tmp, "SDG2042x.config")
with open(cfg_path, "w") as f:
f.write(f"SCREENSHOT_DIR={dir_disk}\nPRESET_DIR={tmp}\n")
captured = {}
# Patch load_config to return disk dir (simulates on-disk state)
_orig_load = sdg.load_config
sdg.load_config = lambda p: {"PRESET_DIR": tmp, "SCREENSHOT_DIR": dir_disk}
# Patch ScreenshotWorker: capture outdir, don't actually run
class FakeWorker:
def __init__(self, io, outdir, parent=None):
captured["outdir"] = outdir
def isRunning(self): return False
def start(self): pass
done = type("sig", (), {"connect": lambda s, f: None})()
sdg.ScreenshotWorker = FakeWorker
# Minimal stand-in for Main instance with in-memory cfg = dir_memory
class FakeMain:
cfg = {"SCREENSHOT_DIR": dir_memory, "PRESET_DIR": tmp}
i = type("io", (), {"sock": object()})()
_scr_worker = None
log = type("log", (), {"appendPlainText": lambda s, t: None})()
def logln(self, s): pass
def setCursor(self, c): pass
def unsetCursor(self): pass
def _on_screenshot_done(self, *a): pass
btn_scr = type("btn", (), {
"setEnabled": lambda s, v: None,
"isRunning": lambda s: False})()
obj = FakeMain()
sdg.Main.on_screenshot(obj)
sdg.load_config = _orig_load
used = captured.get("outdir", "NOT_CAPTURED")
# TC-6a: worker must receive the in-memory dir, not the disk dir
record("6",
used == dir_memory,
"TC-6a: worker receives self.cfg path, not load_config() path",
f"disk cfg = {dir_disk}\n"
f"self.cfg = {dir_memory}\n"
f"received = {used}")
# TC-6b: inverse sanity — must NOT be the disk path
record("6",
used != dir_disk,
"TC-6b: worker path differs from stale disk path",
f"got {used!r}, must not be {dir_disk!r}" if used == dir_disk else "")
# ═════════════════════════════════════════════════════════════════════════════
# ISSUE #2 — ARB download blocks GUI thread (structural AST check)
# ═════════════════════════════════════════════════════════════════════════════
def test_issue_2():
section("Issue #2 — ARB download moved to QThread (structural)")
tree = ast.parse(SRC)
fn_names = {n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)}
# TC-2a: ARBDownloadWorker class exists
worker_nodes = [n for n in ast.walk(tree)
if isinstance(n, ast.ClassDef) and n.name == "ARBDownloadWorker"]
record("2", bool(worker_nodes), "TC-2a: class ARBDownloadWorker exists")
if worker_nodes:
worker = worker_nodes[0]
bases = [getattr(b, "attr", getattr(b, "id", "")) for b in worker.bases]
# TC-2b: inherits QThread
record("2", "QThread" in bases,
"TC-2b: ARBDownloadWorker inherits QThread",
f"bases found: {bases}" if "QThread" not in bases else "")
# TC-2c: has a run() method
run_methods = [n for n in ast.walk(worker)
if isinstance(n, ast.FunctionDef) and n.name == "run"]
record("2", bool(run_methods), "TC-2c: ARBDownloadWorker.run() defined")
if run_methods:
run_src = ast.get_source_segment(SRC, run_methods[0]) or ""
# TC-2d: recv is inside the worker's run(), i.e. the work is off-thread
record("2", "recv" in run_src,
"TC-2d: recv() call inside ARBDownloadWorker.run()",
"No recv() found in run() body" if "recv" not in run_src else "")
# TC-2e: on_arb_download in Main has NO while loop (blocking recv removed)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == "on_arb_download":
has_while = any(isinstance(n, ast.While) for n in ast.walk(node))
record("2", not has_while,
"TC-2e: on_arb_download() contains no blocking while loop",
"while loop still present — BUG NOT FIXED" if has_while else "")
# TC-2f: calls .start() to launch the thread
call_attrs = [getattr(n.func, "attr", "")
for n in ast.walk(node) if isinstance(n, ast.Call)]
record("2", "start" in call_attrs,
"TC-2f: on_arb_download() calls worker.start()",
f"call attrs found: {call_attrs}" if "start" not in call_attrs else "")
# TC-2g: result handler exists
record("2", "_on_arb_download_done" in fn_names,
"TC-2g: _on_arb_download_done() handler defined")
# TC-2h: done signal connected to handler
record("2", "_on_arb_download_done" in SRC and "done.connect" in SRC,
"TC-2h: done signal connected to handler")
print()
print(DIM(" NOTE: live GUI responsiveness during download requires"))
print(DIM(" a connected SDG2042X — run manually per test strategy."))
# ═════════════════════════════════════════════════════════════════════════════
# Summary
# ═════════════════════════════════════════════════════════════════════════════
def print_summary():
print()
print(HEAD("══════════════════════════════════════════════"))
print(HEAD(" SUMMARY"))
print(HEAD("══════════════════════════════════════════════"))
total_ok = total_fail = 0
issue_verdict = {}
for issue, tcs in sorted(results.items()):
ok_count = sum(1 for ok, _ in tcs if ok)
fail_count = sum(1 for ok, _ in tcs if not ok)
total_ok += ok_count
total_fail += fail_count
verdict = "PASS" if fail_count == 0 else "FAIL"
issue_verdict[issue] = verdict
v_str = OK(f"PASS ({ok_count}/{len(tcs)})") if verdict == "PASS" \
else FAIL(f"FAIL ({fail_count} failed)")
print(f" Issue #{issue:2s} {v_str}")
print()
overall = total_fail == 0
total = total_ok + total_fail
print(f" Total: {total_ok}/{total} passed")
if overall:
print(OK(" ✓ All tests passed — safe to close all four issues"))
else:
print(FAIL(f"{total_fail} test(s) failed — do NOT close affected issues"))
print()
print(" Gitea close checklist:")
for iss in ["1", "2", "6", "12"]:
v = issue_verdict.get(iss, "NOT RUN")
box = OK("✓ close") if v == "PASS" else FAIL("✗ keep open")
print(f" #{iss:2s} {box}")
print()
return overall
# ═════════════════════════════════════════════════════════════════════════════
# Entry point
# ═════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
target = None
if "--issue" in sys.argv:
idx = sys.argv.index("--issue")
if idx + 1 < len(sys.argv):
target = sys.argv[idx + 1]
print(HEAD(f"SDG2042X v0.2 — regression test suite"))
print(DIM(f"Script under test: {SCRIPT_PATH}"))
if target in (None, "12"): test_issue_12()
if target in (None, "1"): test_issue_1()
if target in (None, "6"): test_issue_6()
if target in (None, "2"): test_issue_2()
ok = print_summary()
sys.exit(0 if ok else 1)