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 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 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) - Console log panel (STM32 fault flags are reported here as they latch)
- PSU capability guard: the HV supply can source at most 20 A - Input-current guard: the sweep "I limit" field is the maximum input
(`PSU_MAX_CURRENT_A` in `gui.py`). Sweep steps whose estimated input draw current allowed for the test environment (set it to whatever the PSU on
`I_in = P_out / (0.90 * V_in)` exceeds that are rejected (skipped and 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 reported per voltage), regardless of the requested step range; a measured
backstop additionally drops any point where the supply actually exceeded backstop additionally drops any point where the supply actually exceeded
the limit and backs the load off. The programmed supply current limit the limit and backs the load off. Manual CC/CP load setpoints are checked
(sweep "I limit" field and manual supply controls) is clamped to 20 A, and against the same limit using live Vin/Vout readings.
manual CC/CP load setpoints are checked against live Vin/Vout readings.
- Load range pinning: a mid-sweep auto-range transition on the Prodigit - Load range pinning: a mid-sweep auto-range transition on the Prodigit
momentarily unloads the converter, so at sweep start the CC range is 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 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 MIN_REDRAW_S = 0.4 # throttle canvas redraws
STM_EMA_TAU_S = 2.0 # EMA time constant for displayed telemetry values 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. # Input-current guard: the sweep "I limit" field holds the maximum input
# Load steps whose estimated input draw exceeds it are rejected, regardless # current allowed for the test environment (whatever PSU is on the bench --
# of what the sweep asks for. The estimate is conservative (low assumed # no hardcoded ceiling). Load steps whose estimated input draw exceeds it
# efficiency) so rejection kicks in before the PSU actually current-limits. # are rejected, regardless of what the sweep asks for. The estimate is
PSU_MAX_CURRENT_A = 20.0 # 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 VOUT_NOM_V = 48.0 # nominal converter output, fallback when unmeasured
EFF_ASSUMED = 0.90 # conservative efficiency for input-current estimates 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) row.pack(fill=tk.X, pady=1)
ttk.Label(row, text="I limit:", width=10).pack(side=tk.LEFT) ttk.Label(row, text="I limit:", width=10).pack(side=tk.LEFT)
self._svi_ilimit = ttk.Entry(row, width=7) 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) self._svi_ilimit.pack(side=tk.LEFT, padx=2)
ttk.Label(row, text="settle:").pack(side=tk.LEFT) ttk.Label(row, text="settle:").pack(side=tk.LEFT)
self._svi_settle = ttk.Entry(row, width=5) 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) self._send_float(Cmd.SET_VOLTAGE, self._sup_voltage)
def _set_supply_current(self) -> None: def _set_supply_current(self) -> None:
self._clamp_supply_current_entry()
self._send_float(Cmd.SET_CURRENT, self._sup_current) self._send_float(Cmd.SET_CURRENT, self._sup_current)
def _clamp_supply_current_entry(self) -> None: def _input_limit_a(self) -> float | None:
"""Keep the programmed supply current within PSU capability.""" """Max input current allowed for the test environment (I limit
field). None when the field does not parse (gate inactive)."""
try: try:
val = float(self._sup_current.get()) return float(self._svi_ilimit.get())
except ValueError: except ValueError:
return return None
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}")
def _apply_supply(self) -> None: def _apply_supply(self) -> None:
self._clamp_supply_current_entry()
try: try:
v = float(self._sup_voltage.get()) v = float(self._sup_voltage.get())
i = float(self._sup_current.get()) i = float(self._sup_current.get())
@@ -1490,18 +1485,20 @@ class TestbenchGUI(tk.Tk):
self._load_value.config(foreground="red") self._load_value.config(foreground="red")
self.after(1000, lambda: self._load_value.config(foreground="")) self.after(1000, lambda: self._load_value.config(foreground=""))
return return
# PSU capability gate (CC/CP; live readings when available) # Input-current gate (CC/CP; live readings when available), against
if mode in ("CC", "CP"): # 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) vin = self._latest_data.get("supply_V", 0.0)
vout = self._latest_data.get("load_V", 0.0) vout = self._latest_data.get("load_V", 0.0)
if vout < 5.0: if vout < 5.0:
vout = VOUT_NOM_V vout = VOUT_NOM_V
if vin > 5.0: if vin > 5.0:
iin_est = _est_input_current(mode, val, vin, vout) iin_est = _est_input_current(mode, val, vin, vout)
if iin_est > PSU_MAX_CURRENT_A: if iin_est > imax:
self._console( self._console(
f"Load {mode}={val:g} rejected: est. input current " 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") f"at Vin={vin:.1f}V / Vout={vout:.1f}V", "error")
self._load_value.config(foreground="red") self._load_value.config(foreground="red")
self.after(1000, lambda: self._load_value.config(foreground="")) self.after(1000, lambda: self._load_value.config(foreground=""))
@@ -1650,14 +1647,6 @@ class TestbenchGUI(tk.Tk):
messagebox.showerror("Invalid Input", "Check sweep parameters.") messagebox.showerror("Invalid Input", "Check sweep parameters.")
return 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 # Ask for output file
default_name = time.strftime("sweep_vi_%Y%m%d_%H%M%S.csv") default_name = time.strftime("sweep_vi_%Y%m%d_%H%M%S.csv")
path = filedialog.asksaveasfilename( path = filedialog.asksaveasfilename(
@@ -1808,9 +1797,10 @@ class TestbenchGUI(tk.Tk):
"STM32 not linked - thermal pause guard inactive for this " "STM32 not linked - thermal pause guard inactive for this "
"sweep", "warn") "sweep", "warn")
# PSU capability gate: never command an operating point whose # Input-current gate: the I limit field IS the maximum input current
# estimated input draw exceeds what the supply can source. # allowed for this test environment -- never command an operating
psu_imax = min(current_limit, PSU_MAX_CURRENT_A) # point whose estimated input draw exceeds it.
psu_imax = current_limit
vout_est = VOUT_NOM_V # refined from measured load voltage as we go 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) 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: 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 ll += l_step
if rejected: if rejected:
self._console( self._console(
f"V={v:.1f}V: rejected {rejected} step(s) - PSU " f"V={v:.1f}V: rejected {rejected} step(s) - input "
f"input limit {psu_imax:g}A / load range", "warn") f"limit {psu_imax:g}A / load range", "warn")
v += v_step v += v_step
finally: finally:
# Ramp load down gradually to avoid sudden transients # Ramp load down gradually to avoid sudden transients