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
40 changes: 0 additions & 40 deletions .github/workflows/run-templates.yml

This file was deleted.

74 changes: 0 additions & 74 deletions .github/workflows/test-scripts.yml

This file was deleted.

75 changes: 75 additions & 0 deletions dvt_excel_migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# DVT Excel migration

Moving a design-verification report out of a spreadsheet and onto the bench, in
two steps.

## Step 1 — Import the reports you already have

`xcu_import.py` reads a DVT workbook and creates one TofuPilot run from it.

TofuPilot imports Excel natively, but two shapes common in hand-written reports
defeat the column mapper:

- **Stacked cells.** A cell holding `22.33 - 10.11 / 22.34 - 10.10 / 22.35 - 10.11`
is three samples of a two-rail measurement, not one value.
- **Embedded plots.** Screenshots live in the drawing layer, so a mapper that
reads cells never sees them. Their anchor row is what ties a plot to the test
it proves.

The script flattens both: it expands stacked cells to one measurement per
sample per temperature block, and pulls the images out of the `.xlsx` zip keyed
by anchor row.

```bash
pip install tofupilot openpyxl
export TOFUPILOT_API_KEY=...

python xcu_import.py "DVT report.xlsx" \
--procedure-id <uuid> \
--serial-number SN-0001 \
--part-number PCB-REV-A
```

Add `--dry-run` to see what it would create without uploading, and
`--waveforms <dir>` to promote curves to multi-dimensional measurements when
scope CSVs are available (named `<testnr>_<sample>.csv`).

On a real 5-sheet report this produced one run with 13 phases, 111
measurements and 49 attached plots in about 30 seconds.

## Step 2 — Stop writing the report

`smps-dvt/` is the same tests as a TofuPilot procedure, so values are captured
as they are measured rather than read off a screen and typed into a sheet.

```
smps-dvt/
├── procedure.yaml phases, measurements and limits
├── phases/ the Python each phase runs
└── plugs/ one class per instrument
```

```bash
tofupilot run ./smps-dvt
```

The plugs return representative data so the procedure runs anywhere. Every
method keeps its real SCPI call directly above, commented out — swap the two
and the same procedure drives the bench:

- `plugs/scope.py` — Tektronix MSO5-series, 12-bit High Res, AC-coupled ripple
with a 20 MHz bandwidth limit, `CURVe?` waveform readback with
`YMUlt`/`YOFf`/`YZEro`/`XINcr` scaling
- `plugs/ac_source.py` — programmable AC source for the mains sweep

What the spreadsheet cannot do:

- The eleven crossed-regulation rows become **one sweep**, recorded as a
regulation curve indexed by input voltage, with `min`/`max` aggregations
validated against the rail limits.
- Each ripple test keeps **the waveform itself**, not a picture of it, and the
400 mV ceiling is checked against the trace rather than a transcribed number.

Because the curve is data, the same measurement stays comparable across samples
and across temperature setpoints. Run one per chamber setpoint and the drift is
visible directly.
12 changes: 12 additions & 0 deletions dvt_excel_migration/smps_dvt/phases/crossed_regulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Both rails at nominal input: one row of the report."""

CH_23V, CH_9V = 1, 2


def crossed_regulation(measurements, scope, ac_source):
ac_source.set_voltage(230)
scope.configure_channel(CH_23V, 5.0)
scope.configure_channel(CH_9V, 2.0)

measurements.rail_23v = scope.measure_dc(CH_23V)
measurements.rail_9v = scope.measure_dc(CH_9V)
39 changes: 39 additions & 0 deletions dvt_excel_migration/smps_dvt/phases/crossed_regulation_sweep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""The rows a report fills in one at a time, as a single sweep.

Each rail becomes a curve indexed by input voltage, so regulation is something
to compare across samples rather than a column of numbers to read.
"""

# Nominal 230 Vac +/-10%.
MAINS_SWEEP_VAC = (207, 216, 230, 244, 253)

CH_23V, CH_9V = 1, 2


def crossed_regulation_sweep(measurements, scope, ac_source):
mains = []
rail_23v = []
rail_9v = []

for volts in MAINS_SWEEP_VAC:
ac_source.set_voltage(volts)
mains.append(volts)
rail_23v.append(scope.measure_dc(CH_23V))
rail_9v.append(scope.measure_dc(CH_9V))

measurements.rail_23v_vs_mains.x_axis = mains
measurements.rail_23v_vs_mains.y_axis.rail_23v = rail_23v
# The sweep passes when the rail stays inside its limits at every point.
measurements.rail_23v_vs_mains.y_axis.rail_23v.aggregations.min = min(
rail_23v)
measurements.rail_23v_vs_mains.y_axis.rail_23v.aggregations.max = max(
rail_23v)

measurements.rail_9v_vs_mains.x_axis = mains
measurements.rail_9v_vs_mains.y_axis.rail_9v = rail_9v
measurements.rail_9v_vs_mains.y_axis.rail_9v.aggregations.min = min(
rail_9v)
measurements.rail_9v_vs_mains.y_axis.rail_9v.aggregations.max = max(
rail_9v)

ac_source.set_voltage(230)
20 changes: 20 additions & 0 deletions dvt_excel_migration/smps_dvt/phases/ripple_23v.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""23V ripple: the two transcribed figures, plus the trace they came from."""

CH_23V = 1


def ripple_23v(measurements, scope):
scope.configure_channel(CH_23V, 0.02) # 20 mV/div
scope.set_timebase(0.01) # 10 ms/div: ~20 cycles of 100 Hz ripple
scope.acquire()

peak_mv, rms_mv = scope.measure_ripple(CH_23V)
measurements.ripple_23v_peak_to_peak = peak_mv
measurements.ripple_23v_rms = rms_mv

times, values = scope.capture_waveform(CH_23V)
measurements.ripple_23v_waveform.x_axis = times
measurements.ripple_23v_waveform.y_axis.ripple = values
# The same ceiling as the transcribed figure, checked against the trace.
measurements.ripple_23v_waveform.y_axis.ripple.aggregations.peak_to_peak = max(
values) - min(values)
20 changes: 20 additions & 0 deletions dvt_excel_migration/smps_dvt/phases/ripple_9v.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""9V ripple: the two transcribed figures, plus the trace they came from."""

CH_9V = 2


def ripple_9v(measurements, scope):
scope.configure_channel(CH_9V, 0.02) # 20 mV/div
scope.set_timebase(0.01) # 10 ms/div: ~20 cycles of 100 Hz ripple
scope.acquire()

peak_mv, rms_mv = scope.measure_ripple(CH_9V)
measurements.ripple_9v_peak_to_peak = peak_mv
measurements.ripple_9v_rms = rms_mv

times, values = scope.capture_waveform(CH_9V)
measurements.ripple_9v_waveform.x_axis = times
measurements.ripple_9v_waveform.y_axis.ripple = values
# The same ceiling as the transcribed figure, checked against the trace.
measurements.ripple_9v_waveform.y_axis.ripple.aggregations.peak_to_peak = max(
values) - min(values)
31 changes: 31 additions & 0 deletions dvt_excel_migration/smps_dvt/plugs/ac_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Programmable AC source driving the mains side of the regulation sweep."""

# VISA resource of the AC source on the bench LAN.
AC_SOURCE_RESOURCE = "TCPIP0::192.0.2.11::inst0::INSTR"

LINE_FREQUENCY_HZ = 50.0


class ACSource:
def __init__(self):
self._source = None

# import pyvisa
#
# self._source = pyvisa.ResourceManager().open_resource(AC_SOURCE_RESOURCE)
# self._source.timeout = 10_000
# self._source.write(f"SOURce:FREQuency {LINE_FREQUENCY_HZ}")
# self._source.write("OUTPut:STATe ON")

def __del__(self):
# self._source.write("OUTPut:STATe OFF")
pass

def identity(self) -> str:
# return self._source.query("*IDN?").strip()
return "GW-INSTEK,APS-7100,SIMULATED,1.0"

def set_voltage(self, volts: float) -> None:
# self._source.write(f"SOURce:VOLTage {volts}")
# self._source.query("*OPC?")
pass
Loading