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
+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