GUI: I limit field is the environment input-current limit, no hardcoded 20A

The 20A PSU ceiling was hardcoded and clamped user entries, which breaks
benches with a different supply. Now the sweep I limit field IS the max
input current allowed for the test environment: used verbatim as the
supply CC limit and as the step-rejection / measured-backstop ceiling.
Manual supply-current clamp removed; manual CC/CP load gate now checks
against the I limit field value. PSU_MAX_CURRENT_A is reduced to
INPUT_LIMIT_DEFAULT_A (initial field value only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-06 12:53:44 +07:00
co-authored by Claude Fable 5
parent 7f8672d7b9
commit 668ce30a1d
2 changed files with 32 additions and 41 deletions
+7 -6
View File
@@ -97,14 +97,15 @@ The GUI provides:
launch auto-starts a log at `logs/data_<timestamp>.csv` (relative to the
working directory); use Stop Log / Start Log to switch to a custom path
- Console log panel (STM32 fault flags are reported here as they latch)
- PSU capability guard: the HV supply can source at most 20 A
(`PSU_MAX_CURRENT_A` in `gui.py`). Sweep steps whose estimated input draw
`I_in = P_out / (0.90 * V_in)` exceeds that are rejected (skipped and
- Input-current guard: the sweep "I limit" field is the maximum input
current allowed for the test environment (set it to whatever the PSU on
the bench can source; defaults to 20 A, no hardcoded ceiling). It is
programmed as the supply CC limit, and sweep steps whose estimated input
draw `I_in = P_out / (0.90 * V_in)` exceeds it are rejected (skipped and
reported per voltage), regardless of the requested step range; a measured
backstop additionally drops any point where the supply actually exceeded
the limit and backs the load off. The programmed supply current limit
(sweep "I limit" field and manual supply controls) is clamped to 20 A, and
manual CC/CP load setpoints are checked against live Vin/Vout readings.
the limit and backs the load off. Manual CC/CP load setpoints are checked
against the same limit using live Vin/Vout readings.
- Load range pinning: a mid-sweep auto-range transition on the Prodigit
momentarily unloads the converter, so at sweep start the CC range is
pinned to Range II for the whole run (auto-ranging restored after, with
+25 -35
View File
@@ -39,11 +39,13 @@ TELEM_DECIMATE = 4 # telemetry display decimation (full rate still recorded
MIN_REDRAW_S = 0.4 # throttle canvas redraws
STM_EMA_TAU_S = 2.0 # EMA time constant for displayed telemetry values
# HV supply capability guard: the PSU can source at most this much current.
# Load steps whose estimated input draw exceeds it are rejected, regardless
# of what the sweep asks for. The estimate is conservative (low assumed
# efficiency) so rejection kicks in before the PSU actually current-limits.
PSU_MAX_CURRENT_A = 20.0
# Input-current guard: the sweep "I limit" field holds the maximum input
# current allowed for the test environment (whatever PSU is on the bench --
# no hardcoded ceiling). Load steps whose estimated input draw exceeds it
# are rejected, regardless of what the sweep asks for. The estimate is
# conservative (low assumed efficiency) so rejection kicks in before the
# PSU actually current-limits.
INPUT_LIMIT_DEFAULT_A = 20.0 # initial I limit field value only
VOUT_NOM_V = 48.0 # nominal converter output, fallback when unmeasured
EFF_ASSUMED = 0.90 # conservative efficiency for input-current estimates
@@ -667,7 +669,7 @@ class TestbenchGUI(tk.Tk):
row.pack(fill=tk.X, pady=1)
ttk.Label(row, text="I limit:", width=10).pack(side=tk.LEFT)
self._svi_ilimit = ttk.Entry(row, width=7)
self._svi_ilimit.insert(0, f"{PSU_MAX_CURRENT_A:g}")
self._svi_ilimit.insert(0, f"{INPUT_LIMIT_DEFAULT_A:g}")
self._svi_ilimit.pack(side=tk.LEFT, padx=2)
ttk.Label(row, text="settle:").pack(side=tk.LEFT)
self._svi_settle = ttk.Entry(row, width=5)
@@ -1451,24 +1453,17 @@ class TestbenchGUI(tk.Tk):
self._send_float(Cmd.SET_VOLTAGE, self._sup_voltage)
def _set_supply_current(self) -> None:
self._clamp_supply_current_entry()
self._send_float(Cmd.SET_CURRENT, self._sup_current)
def _clamp_supply_current_entry(self) -> None:
"""Keep the programmed supply current within PSU capability."""
def _input_limit_a(self) -> float | None:
"""Max input current allowed for the test environment (I limit
field). None when the field does not parse (gate inactive)."""
try:
val = float(self._sup_current.get())
return float(self._svi_ilimit.get())
except ValueError:
return
if val > PSU_MAX_CURRENT_A:
self._console(
f"Supply I {val:g}A clamped to PSU max "
f"{PSU_MAX_CURRENT_A:g}A", "warn")
self._sup_current.delete(0, tk.END)
self._sup_current.insert(0, f"{PSU_MAX_CURRENT_A:g}")
return None
def _apply_supply(self) -> None:
self._clamp_supply_current_entry()
try:
v = float(self._sup_voltage.get())
i = float(self._sup_current.get())
@@ -1490,18 +1485,20 @@ class TestbenchGUI(tk.Tk):
self._load_value.config(foreground="red")
self.after(1000, lambda: self._load_value.config(foreground=""))
return
# PSU capability gate (CC/CP; live readings when available)
if mode in ("CC", "CP"):
# Input-current gate (CC/CP; live readings when available), against
# the user-set I limit for this test environment
imax = self._input_limit_a()
if mode in ("CC", "CP") and imax is not None:
vin = self._latest_data.get("supply_V", 0.0)
vout = self._latest_data.get("load_V", 0.0)
if vout < 5.0:
vout = VOUT_NOM_V
if vin > 5.0:
iin_est = _est_input_current(mode, val, vin, vout)
if iin_est > PSU_MAX_CURRENT_A:
if iin_est > imax:
self._console(
f"Load {mode}={val:g} rejected: est. input current "
f"{iin_est:.1f}A > PSU max {PSU_MAX_CURRENT_A:g}A "
f"{iin_est:.1f}A > I limit {imax:g}A "
f"at Vin={vin:.1f}V / Vout={vout:.1f}V", "error")
self._load_value.config(foreground="red")
self.after(1000, lambda: self._load_value.config(foreground=""))
@@ -1650,14 +1647,6 @@ class TestbenchGUI(tk.Tk):
messagebox.showerror("Invalid Input", "Check sweep parameters.")
return
if params["current_limit"] > PSU_MAX_CURRENT_A:
self._console(
f"I limit {params['current_limit']:g}A clamped to PSU max "
f"{PSU_MAX_CURRENT_A:g}A", "warn")
params["current_limit"] = PSU_MAX_CURRENT_A
self._svi_ilimit.delete(0, tk.END)
self._svi_ilimit.insert(0, f"{PSU_MAX_CURRENT_A:g}")
# Ask for output file
default_name = time.strftime("sweep_vi_%Y%m%d_%H%M%S.csv")
path = filedialog.asksaveasfilename(
@@ -1808,9 +1797,10 @@ class TestbenchGUI(tk.Tk):
"STM32 not linked - thermal pause guard inactive for this "
"sweep", "warn")
# PSU capability gate: never command an operating point whose
# estimated input draw exceeds what the supply can source.
psu_imax = min(current_limit, PSU_MAX_CURRENT_A)
# Input-current gate: the I limit field IS the maximum input current
# allowed for this test environment -- never command an operating
# point whose estimated input draw exceeds it.
psu_imax = current_limit
vout_est = VOUT_NOM_V # refined from measured load voltage as we go
first_v = v_start if v_step > 0 else max(v_start, v_stop)
if _est_input_current(load_mode, l_start, first_v, vout_est) > psu_imax:
@@ -1967,8 +1957,8 @@ class TestbenchGUI(tk.Tk):
ll += l_step
if rejected:
self._console(
f"V={v:.1f}V: rejected {rejected} step(s) - PSU "
f"input limit {psu_imax:g}A / load range", "warn")
f"V={v:.1f}V: rejected {rejected} step(s) - input "
f"limit {psu_imax:g}A / load range", "warn")
v += v_step
finally:
# Ramp load down gradually to avoid sudden transients