- 2D sweep now supports CC (constant current) and CP (constant power) load modes via --load-mode flag (CLI) and combobox (GUI) - Supply capability check before all sweeps: validates max voltage against supply range and prints V/I/P summary - Renamed sweep-vi args from --i-start/stop/step to --l-start/stop/step to reflect that the load setpoint can be current or power - GUI labels update dynamically based on selected load mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
728 lines
27 KiB
Python
728 lines
27 KiB
Python
"""CLI for the MPPT Tracker Testbench.
|
||
|
||
Orchestrates IT6500D (supply) + Prodigit 3366G (load) + HIOKI 3193-10 (meter).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import csv
|
||
import sys
|
||
import time
|
||
|
||
from it6500.driver import IT6500
|
||
from prodigit3366g.driver import Prodigit3366G
|
||
from hioki3193.driver import Hioki3193
|
||
from testbench.bench import MPPTTestbench
|
||
|
||
|
||
# ── Instrument connection ─────────────────────────────────────────────
|
||
|
||
|
||
def connect_bench(args: argparse.Namespace) -> MPPTTestbench:
|
||
"""Create and return a connected MPPTTestbench from CLI args."""
|
||
supply_addr = MPPTTestbench.find_supply(args.supply_address)
|
||
meter_addr = MPPTTestbench.find_meter(args.meter_address)
|
||
load_port = args.load_port
|
||
|
||
print(f"Supply: {supply_addr}")
|
||
print(f"Load: {load_port}")
|
||
print(f"Meter: {meter_addr}")
|
||
print()
|
||
|
||
supply = IT6500(supply_addr, timeout_ms=args.timeout)
|
||
load = Prodigit3366G(load_port, baudrate=args.load_baud)
|
||
meter = Hioki3193(meter_addr, timeout_ms=args.timeout)
|
||
|
||
return MPPTTestbench(supply, load, meter)
|
||
|
||
|
||
# ── Commands ──────────────────────────────────────────────────────────
|
||
|
||
|
||
def cmd_identify(bench: MPPTTestbench, _args: argparse.Namespace) -> None:
|
||
"""Identify all three instruments."""
|
||
print("=== DC Power Supply (IT6500D) ===")
|
||
print(f" Identity: {bench.supply.idn()}")
|
||
print(f" Output: {'ON' if bench.supply.get_output_state() else 'OFF'}")
|
||
print(f" V set: {bench.supply.get_voltage():.4f} V")
|
||
print(f" I set: {bench.supply.get_current():.4f} A")
|
||
|
||
print()
|
||
print("=== DC Electronic Load (Prodigit 3366G) ===")
|
||
print(f" Model: {bench.load.name()}")
|
||
print(f" Load: {'ON' if bench.load.get_load_state() else 'OFF'}")
|
||
print(f" Mode: {bench.load.get_mode()}")
|
||
|
||
print()
|
||
print("=== Power Analyzer (HIOKI 3193-10) ===")
|
||
print(f" Identity: {bench.meter.idn()}")
|
||
print(f" Options: {bench.meter.options()}")
|
||
print(f" Wiring: {bench.meter.get_wiring_mode()}")
|
||
|
||
|
||
def cmd_setup(bench: MPPTTestbench, _args: argparse.Namespace) -> None:
|
||
"""Configure all instruments for MPPT testing."""
|
||
print("Setting up all instruments for MPPT testing...")
|
||
bench.setup_all()
|
||
print()
|
||
print("Supply: remote mode")
|
||
print("Load: remote mode")
|
||
print("Meter: 1P2W, DC coupling, auto-range, EFF1=P6/P5")
|
||
print("Display: SELECT16 — U5,I5,P5,EFF1 (left) | U6,I6,P6 (right)")
|
||
print()
|
||
print("Ready. Use 'measure' or 'monitor' to start reading data.")
|
||
|
||
|
||
def cmd_measure(bench: MPPTTestbench, _args: argparse.Namespace) -> None:
|
||
"""Take a single measurement from all instruments."""
|
||
data = bench.measure_all()
|
||
|
||
print("=== Supply (IT6500D) ===")
|
||
print(f" Voltage = {data['supply_V']:>10.4f} V")
|
||
print(f" Current = {data['supply_I']:>10.4f} A")
|
||
print(f" Power = {data['supply_P']:>10.4f} W")
|
||
print()
|
||
print("=== Load (Prodigit 3366G) ===")
|
||
print(f" Voltage = {data['load_V']:>10.4f} V")
|
||
print(f" Current = {data['load_I']:>10.4f} A")
|
||
print(f" Power = {data['load_P']:>10.4f} W")
|
||
print()
|
||
print("=== Meter (HIOKI 3193-10) ===")
|
||
print(f" Input: U5={data['meter_U5']:>+12.4E} V "
|
||
f"I5={data['meter_I5']:>+12.4E} A "
|
||
f"P5={data['meter_P5']:>+12.4E} W")
|
||
print(f" Output: U6={data['meter_U6']:>+12.4E} V "
|
||
f"I6={data['meter_I6']:>+12.4E} A "
|
||
f"P6={data['meter_P6']:>+12.4E} W")
|
||
print(f" EFF1 = {data['meter_EFF1']:>+12.4E} %")
|
||
|
||
|
||
def cmd_monitor(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Continuously monitor all instruments."""
|
||
interval = args.interval
|
||
|
||
writer = None
|
||
outfile = None
|
||
columns = [
|
||
"timestamp",
|
||
"supply_V", "supply_I", "supply_P",
|
||
"load_V", "load_I", "load_P",
|
||
"meter_U5", "meter_I5", "meter_P5",
|
||
"meter_U6", "meter_I6", "meter_P6",
|
||
"meter_EFF1",
|
||
]
|
||
if args.output:
|
||
outfile = open(args.output, "w", newline="")
|
||
writer = csv.writer(outfile)
|
||
writer.writerow(columns)
|
||
print(f"Logging to {args.output}")
|
||
|
||
print(
|
||
f"{'Time':>10s} "
|
||
f"{'Sup_V':>8s} {'Sup_I':>8s} {'Sup_P':>8s} "
|
||
f"{'Ld_V':>8s} {'Ld_I':>8s} {'Ld_P':>8s} "
|
||
f"{'P_in':>10s} {'P_out':>10s} {'EFF%':>8s}"
|
||
)
|
||
print("-" * 105)
|
||
|
||
try:
|
||
count = 0
|
||
while args.count == 0 or count < args.count:
|
||
data = bench.measure_all()
|
||
ts = time.strftime("%H:%M:%S")
|
||
print(
|
||
f"{ts:>10s} "
|
||
f"{data['supply_V']:8.3f} {data['supply_I']:8.3f} {data['supply_P']:8.2f} "
|
||
f"{data['load_V']:8.3f} {data['load_I']:8.3f} {data['load_P']:8.2f} "
|
||
f"{data['meter_P5']:>+10.3E} {data['meter_P6']:>+10.3E} {data['meter_EFF1']:8.2f}"
|
||
)
|
||
|
||
if writer:
|
||
writer.writerow(
|
||
[time.strftime("%Y-%m-%d %H:%M:%S")]
|
||
+ [f"{data[c]:.6f}" for c in columns[1:]]
|
||
)
|
||
outfile.flush()
|
||
|
||
count += 1
|
||
if args.count == 0 or count < args.count:
|
||
time.sleep(interval)
|
||
except KeyboardInterrupt:
|
||
print("\nMonitoring stopped.")
|
||
finally:
|
||
if outfile:
|
||
outfile.close()
|
||
print(f"Data saved to {args.output}")
|
||
|
||
|
||
def cmd_live(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Live monitor with real-time graphs."""
|
||
import matplotlib.pyplot as plt
|
||
from matplotlib.animation import FuncAnimation
|
||
from collections import deque
|
||
|
||
max_points = args.history
|
||
interval_ms = int(args.interval * 1000)
|
||
|
||
timestamps: deque[float] = deque(maxlen=max_points)
|
||
series: dict[str, deque[float]] = {
|
||
k: deque(maxlen=max_points)
|
||
for k in [
|
||
"supply_P", "load_P",
|
||
"meter_P5", "meter_P6", "meter_EFF1",
|
||
"meter_U5", "meter_U6",
|
||
"meter_I5", "meter_I6",
|
||
]
|
||
}
|
||
t0 = time.time()
|
||
|
||
writer = None
|
||
outfile = None
|
||
columns = [
|
||
"timestamp",
|
||
"supply_V", "supply_I", "supply_P",
|
||
"load_V", "load_I", "load_P",
|
||
"meter_U5", "meter_I5", "meter_P5",
|
||
"meter_U6", "meter_I6", "meter_P6",
|
||
"meter_EFF1",
|
||
]
|
||
if args.output:
|
||
outfile = open(args.output, "w", newline="")
|
||
writer = csv.writer(outfile)
|
||
writer.writerow(columns)
|
||
print(f"Logging to {args.output}")
|
||
|
||
fig, axes = plt.subplots(4, 1, figsize=(14, 12), squeeze=False)
|
||
fig.suptitle("MPPT Testbench Live Monitor", fontsize=14, fontweight="bold")
|
||
axes = axes.flatten()
|
||
|
||
# Subplot 0: Power
|
||
axes[0].set_ylabel("Power (W)")
|
||
axes[0].set_title("Power")
|
||
axes[0].grid(True, alpha=0.3)
|
||
line_p5, = axes[0].plot([], [], label="P_in (meter)", linewidth=1.5)
|
||
line_p6, = axes[0].plot([], [], label="P_out (meter)", linewidth=1.5)
|
||
line_sp, = axes[0].plot([], [], label="Supply P", linewidth=1, linestyle="--", alpha=0.6)
|
||
line_lp, = axes[0].plot([], [], label="Load P", linewidth=1, linestyle="--", alpha=0.6)
|
||
axes[0].legend(loc="upper left", fontsize=9)
|
||
|
||
# Subplot 1: Efficiency
|
||
axes[1].set_ylabel("Efficiency (%)")
|
||
axes[1].set_title("Efficiency")
|
||
axes[1].grid(True, alpha=0.3)
|
||
line_eff, = axes[1].plot([], [], label="EFF1", linewidth=1.5, color="green")
|
||
axes[1].legend(loc="upper left", fontsize=9)
|
||
|
||
# Subplot 2: Voltage
|
||
axes[2].set_ylabel("Voltage (V)")
|
||
axes[2].set_title("Voltage")
|
||
axes[2].grid(True, alpha=0.3)
|
||
line_u5, = axes[2].plot([], [], label="U5 (input)", linewidth=1.5)
|
||
line_u6, = axes[2].plot([], [], label="U6 (output)", linewidth=1.5)
|
||
axes[2].legend(loc="upper left", fontsize=9)
|
||
|
||
# Subplot 3: Current
|
||
axes[3].set_ylabel("Current (A)")
|
||
axes[3].set_title("Current")
|
||
axes[3].set_xlabel("Time (s)")
|
||
axes[3].grid(True, alpha=0.3)
|
||
line_i5, = axes[3].plot([], [], label="I5 (input)", linewidth=1.5)
|
||
line_i6, = axes[3].plot([], [], label="I6 (output)", linewidth=1.5)
|
||
axes[3].legend(loc="upper left", fontsize=9)
|
||
|
||
fig.tight_layout()
|
||
|
||
ERROR_THRESHOLD = 1e90
|
||
all_lines = [line_p5, line_p6, line_sp, line_lp, line_eff, line_u5, line_u6, line_i5, line_i6]
|
||
|
||
def _clean(val: float) -> float:
|
||
return float("nan") if abs(val) > ERROR_THRESHOLD else val
|
||
|
||
def update(_frame):
|
||
try:
|
||
data = bench.measure_all()
|
||
except Exception as e:
|
||
print(f"Read error: {e}")
|
||
return all_lines
|
||
|
||
now = time.time() - t0
|
||
timestamps.append(now)
|
||
|
||
for key in series:
|
||
series[key].append(_clean(data[key]))
|
||
|
||
ts = time.strftime("%H:%M:%S")
|
||
print(
|
||
f"{ts} P_in={data['meter_P5']:+.2E} "
|
||
f"P_out={data['meter_P6']:+.2E} "
|
||
f"EFF={data['meter_EFF1']:.1f}%"
|
||
)
|
||
|
||
if writer:
|
||
writer.writerow(
|
||
[time.strftime("%Y-%m-%d %H:%M:%S")]
|
||
+ [f"{data[c]:.6f}" for c in columns[1:]]
|
||
)
|
||
outfile.flush()
|
||
|
||
t_list = list(timestamps)
|
||
line_p5.set_data(t_list, list(series["meter_P5"]))
|
||
line_p6.set_data(t_list, list(series["meter_P6"]))
|
||
line_sp.set_data(t_list, list(series["supply_P"]))
|
||
line_lp.set_data(t_list, list(series["load_P"]))
|
||
line_eff.set_data(t_list, list(series["meter_EFF1"]))
|
||
line_u5.set_data(t_list, list(series["meter_U5"]))
|
||
line_u6.set_data(t_list, list(series["meter_U6"]))
|
||
line_i5.set_data(t_list, list(series["meter_I5"]))
|
||
line_i6.set_data(t_list, list(series["meter_I6"]))
|
||
|
||
for ax in axes:
|
||
ax.relim()
|
||
ax.autoscale_view()
|
||
|
||
return all_lines
|
||
|
||
_anim = FuncAnimation(
|
||
fig, update, interval=interval_ms, blit=False, cache_frame_data=False
|
||
)
|
||
|
||
try:
|
||
plt.show()
|
||
except KeyboardInterrupt:
|
||
pass
|
||
finally:
|
||
if outfile:
|
||
outfile.close()
|
||
print(f"Data saved to {args.output}")
|
||
|
||
|
||
def cmd_sweep(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Run a voltage sweep to characterize MPPT tracking."""
|
||
print(
|
||
f"Voltage sweep: {args.v_start:.1f}V -> {args.v_stop:.1f}V, "
|
||
f"step={args.v_step:.2f}V, I_limit={args.current_limit:.1f}A, "
|
||
f"settle={args.settle:.1f}s"
|
||
)
|
||
|
||
# Ensure load is configured
|
||
if args.load_mode:
|
||
bench.load.set_mode(args.load_mode)
|
||
if args.load_value is not None:
|
||
mode = bench.load.get_mode()
|
||
if mode == "CC":
|
||
bench.load.set_cc_current(args.load_value)
|
||
elif mode == "CR":
|
||
bench.load.set_cr_resistance(args.load_value)
|
||
elif mode == "CV":
|
||
bench.load.set_cv_voltage(args.load_value)
|
||
elif mode == "CP":
|
||
bench.load.set_cp_power(args.load_value)
|
||
bench.load.load_on()
|
||
|
||
print()
|
||
results = bench.sweep_voltage(
|
||
v_start=args.v_start,
|
||
v_stop=args.v_stop,
|
||
v_step=args.v_step,
|
||
current_limit=args.current_limit,
|
||
settle_time=args.settle,
|
||
load_setpoint=args.load_value if args.load_value is not None else 0.0,
|
||
)
|
||
|
||
bench.load.load_off()
|
||
|
||
_write_sweep_csv(results, args.output)
|
||
_print_sweep_summary(results)
|
||
|
||
|
||
def _write_sweep_csv(results: list, output: str | None) -> None:
|
||
"""Write sweep results to CSV."""
|
||
if not output:
|
||
return
|
||
with open(output, "w", newline="") as f:
|
||
writer = csv.writer(f)
|
||
writer.writerow([
|
||
"voltage_set", "current_limit", "load_setpoint",
|
||
"supply_V", "supply_I", "supply_P",
|
||
"load_V", "load_I", "load_P",
|
||
"input_power", "output_power", "efficiency",
|
||
])
|
||
for pt in results:
|
||
writer.writerow([
|
||
f"{pt.voltage_set:.4f}",
|
||
f"{pt.current_limit:.4f}",
|
||
f"{pt.load_setpoint:.4f}",
|
||
f"{pt.supply_voltage:.4f}",
|
||
f"{pt.supply_current:.4f}",
|
||
f"{pt.supply_power:.4f}",
|
||
f"{pt.load_voltage:.4f}",
|
||
f"{pt.load_current:.4f}",
|
||
f"{pt.load_power:.4f}",
|
||
f"{pt.input_power:.4f}",
|
||
f"{pt.output_power:.4f}",
|
||
f"{pt.efficiency:.4f}",
|
||
])
|
||
print(f"\nSweep data saved to {output}")
|
||
|
||
|
||
def _print_sweep_summary(results: list) -> None:
|
||
"""Print best-efficiency point from sweep results."""
|
||
if results:
|
||
best = max(results, key=lambda p: p.efficiency)
|
||
print(f"\nBest efficiency: {best.efficiency:.2f}% "
|
||
f"at V_set={best.voltage_set:.2f}V "
|
||
f"I_load={best.load_setpoint:.2f}A "
|
||
f"(P_in={best.input_power:.2f}W, P_out={best.output_power:.2f}W)")
|
||
|
||
|
||
def cmd_sweep_load(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Run a load current sweep at a fixed supply voltage."""
|
||
print(
|
||
f"Load current sweep: {args.i_start:.2f}A -> {args.i_stop:.2f}A, "
|
||
f"step={args.i_step:.2f}A, V={args.voltage:.1f}V, "
|
||
f"I_limit={args.current_limit:.1f}A, settle={args.settle:.1f}s"
|
||
)
|
||
print()
|
||
|
||
results = bench.sweep_load_current(
|
||
voltage=args.voltage,
|
||
current_limit=args.current_limit,
|
||
i_start=args.i_start,
|
||
i_stop=args.i_stop,
|
||
i_step=args.i_step,
|
||
settle_time=args.settle,
|
||
)
|
||
|
||
_write_sweep_csv(results, args.output)
|
||
_print_sweep_summary(results)
|
||
|
||
|
||
def cmd_efficiency(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Measure efficiency at a fixed operating point."""
|
||
# Configure load
|
||
if args.load_mode:
|
||
bench.load.set_mode(args.load_mode)
|
||
if args.load_value is not None:
|
||
mode = bench.load.get_mode()
|
||
if mode == "CC":
|
||
bench.load.set_cc_current(args.load_value)
|
||
elif mode == "CR":
|
||
bench.load.set_cr_resistance(args.load_value)
|
||
elif mode == "CV":
|
||
bench.load.set_cv_voltage(args.load_value)
|
||
elif mode == "CP":
|
||
bench.load.set_cp_power(args.load_value)
|
||
bench.load.load_on()
|
||
|
||
print(
|
||
f"Measuring efficiency at {args.voltage:.1f}V, "
|
||
f"{args.current_limit:.1f}A limit, "
|
||
f"{args.samples} samples..."
|
||
)
|
||
print()
|
||
|
||
result = bench.measure_efficiency(
|
||
voltage=args.voltage,
|
||
current_limit=args.current_limit,
|
||
settle_time=args.settle,
|
||
samples=args.samples,
|
||
sample_interval=args.interval,
|
||
)
|
||
|
||
bench.load.load_off()
|
||
|
||
print()
|
||
print(f"Average input power: {result['avg_input_power']:.4f} W")
|
||
print(f"Average output power: {result['avg_output_power']:.4f} W")
|
||
print(f"Average efficiency: {result['avg_efficiency']:.2f} %")
|
||
|
||
|
||
def cmd_sweep_vi(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Run a 2D voltage × load sweep."""
|
||
mode = args.load_mode
|
||
unit = "A" if mode == "CC" else "W"
|
||
print(
|
||
f"2D sweep: V={args.v_start:.1f}-{args.v_stop:.1f}V (step {args.v_step:.1f}), "
|
||
f"{mode}={args.l_start:.2f}-{args.l_stop:.2f}{unit} (step {args.l_step:.2f}), "
|
||
f"I_limit={args.current_limit:.1f}A, settle={args.settle:.1f}s"
|
||
)
|
||
print()
|
||
|
||
results = bench.sweep_vi(
|
||
v_start=args.v_start,
|
||
v_stop=args.v_stop,
|
||
v_step=args.v_step,
|
||
l_start=args.l_start,
|
||
l_stop=args.l_stop,
|
||
l_step=args.l_step,
|
||
current_limit=args.current_limit,
|
||
settle_time=args.settle,
|
||
load_mode=mode,
|
||
)
|
||
|
||
_write_sweep_csv(results, args.output)
|
||
_print_sweep_summary(results)
|
||
|
||
|
||
def cmd_shade_profile(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Run a shade / irradiance profile from a CSV file."""
|
||
steps = MPPTTestbench.load_shade_profile(args.profile)
|
||
duration = steps[-1]["time"] if steps else 0
|
||
|
||
print(f"Shade profile: {args.profile}")
|
||
print(f" {len(steps)} steps over {duration:.0f}s, settle={args.settle:.1f}s")
|
||
|
||
# Show what modes/values are used
|
||
modes = {s.get("load_mode", "?") for s in steps}
|
||
voltages = [s["voltage"] for s in steps]
|
||
currents = [s["current_limit"] for s in steps]
|
||
print(
|
||
f" Voltage: {min(voltages):.1f} - {max(voltages):.1f}V, "
|
||
f"I_limit: {min(currents):.1f} - {max(currents):.1f}A, "
|
||
f"Load modes: {', '.join(sorted(modes))}"
|
||
)
|
||
print()
|
||
|
||
results = bench.run_shade_profile(steps, settle_time=args.settle)
|
||
|
||
_write_sweep_csv(results, args.output)
|
||
_print_sweep_summary(results)
|
||
|
||
|
||
def cmd_supply(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Control the DC supply directly."""
|
||
if args.action == "on":
|
||
bench.supply.output_on()
|
||
print("Supply output ON")
|
||
elif args.action == "off":
|
||
bench.supply.output_off()
|
||
print("Supply output OFF")
|
||
elif args.action == "set":
|
||
if args.voltage is not None and args.current is not None:
|
||
bench.supply.apply(args.voltage, args.current)
|
||
print(f"Supply set: {args.voltage:.4f} V, {args.current:.4f} A")
|
||
elif args.voltage is not None:
|
||
bench.supply.set_voltage(args.voltage)
|
||
print(f"Supply voltage: {args.voltage:.4f} V")
|
||
elif args.current is not None:
|
||
bench.supply.set_current(args.current)
|
||
print(f"Supply current: {args.current:.4f} A")
|
||
else:
|
||
print("Specify --voltage and/or --current")
|
||
|
||
|
||
def cmd_load(bench: MPPTTestbench, args: argparse.Namespace) -> None:
|
||
"""Control the electronic load directly."""
|
||
if args.action == "on":
|
||
bench.load.load_on()
|
||
print("Load ON")
|
||
elif args.action == "off":
|
||
bench.load.load_off()
|
||
print("Load OFF")
|
||
elif args.action == "set":
|
||
if args.mode:
|
||
bench.load.set_mode(args.mode)
|
||
if args.value is not None:
|
||
mode = bench.load.get_mode()
|
||
if mode == "CC":
|
||
bench.load.set_cc_current(args.value)
|
||
print(f"Load CC: {args.value:.4f} A")
|
||
elif mode == "CR":
|
||
bench.load.set_cr_resistance(args.value)
|
||
print(f"Load CR: {args.value:.4f} ohm")
|
||
elif mode == "CV":
|
||
bench.load.set_cv_voltage(args.value)
|
||
print(f"Load CV: {args.value:.4f} V")
|
||
elif mode == "CP":
|
||
bench.load.set_cp_power(args.value)
|
||
print(f"Load CP: {args.value:.4f} W")
|
||
elif args.mode:
|
||
print(f"Load mode: {args.mode}")
|
||
|
||
|
||
def cmd_safe_off(bench: MPPTTestbench, _args: argparse.Namespace) -> None:
|
||
"""Emergency shutdown: turn off load, then supply."""
|
||
print("Shutting down...")
|
||
bench.safe_off()
|
||
print(" Load OFF")
|
||
print(" Supply OFF")
|
||
print("Done.")
|
||
|
||
|
||
# ── Main ──────────────────────────────────────────────────────────────
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(
|
||
description="MPPT Tracker Testbench: IT6500D + Prodigit 3366G + HIOKI 3193-10",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""\
|
||
examples:
|
||
%(prog)s identify
|
||
%(prog)s setup
|
||
%(prog)s measure
|
||
%(prog)s monitor --interval 1.0 --output data.csv
|
||
%(prog)s live --interval 0.5
|
||
%(prog)s sweep --v-start 10 --v-stop 50 --v-step 1 --current-limit 10 -o sweep.csv
|
||
%(prog)s sweep-load --voltage 75 --current-limit 10 --i-start 1 --i-stop 20 --i-step 1 -o load.csv
|
||
%(prog)s efficiency --voltage 36 --current-limit 10 --samples 10
|
||
%(prog)s sweep-vi --v-start 35 --v-stop 100 --v-step 5 --l-start 0.5 --l-stop 30 --l-step 1 --current-limit 35 -o map.csv
|
||
%(prog)s sweep-vi --v-start 35 --v-stop 100 --v-step 5 --l-start 50 --l-stop 500 --l-step 50 --load-mode CP --current-limit 35 -o map_cp.csv
|
||
%(prog)s shade-profile --profile cloud_pass.csv --settle 2.0 -o shade_results.csv
|
||
%(prog)s supply set --voltage 24 --current 10
|
||
%(prog)s supply on
|
||
%(prog)s load set --mode CC --value 5.0
|
||
%(prog)s load on
|
||
%(prog)s safe-off
|
||
""",
|
||
)
|
||
|
||
# Global instrument connection args
|
||
parser.add_argument(
|
||
"--supply-address",
|
||
help="IT6500D VISA address (auto-detect if omitted)",
|
||
)
|
||
parser.add_argument(
|
||
"--load-port", default="COM1",
|
||
help="Prodigit 3366G serial port (default: COM1)",
|
||
)
|
||
parser.add_argument(
|
||
"--load-baud", type=int, default=115200,
|
||
help="Prodigit 3366G baud rate (default: 115200)",
|
||
)
|
||
parser.add_argument(
|
||
"--meter-address",
|
||
help="HIOKI 3193-10 VISA address (auto-detect if omitted)",
|
||
)
|
||
parser.add_argument(
|
||
"--timeout", type=int, default=5000,
|
||
help="VISA timeout in ms (default: 5000)",
|
||
)
|
||
|
||
sub = parser.add_subparsers(dest="command", required=True)
|
||
|
||
# identify
|
||
sub.add_parser("identify", help="Identify all connected instruments")
|
||
|
||
# setup
|
||
sub.add_parser("setup", help="Configure all instruments for MPPT testing")
|
||
|
||
# measure
|
||
sub.add_parser("measure", help="Single measurement from all instruments")
|
||
|
||
# monitor
|
||
p_mon = sub.add_parser("monitor", help="Continuous monitoring of all instruments")
|
||
p_mon.add_argument("-i", "--interval", type=float, default=1.0)
|
||
p_mon.add_argument("-n", "--count", type=int, default=0, help="0=infinite")
|
||
p_mon.add_argument("-o", "--output", help="CSV output file")
|
||
|
||
# live
|
||
p_live = sub.add_parser("live", help="Live real-time graph of all instruments")
|
||
p_live.add_argument("-i", "--interval", type=float, default=1.0)
|
||
p_live.add_argument("-o", "--output", help="CSV output file")
|
||
p_live.add_argument("--history", type=int, default=300)
|
||
|
||
# sweep
|
||
p_sweep = sub.add_parser("sweep", help="Voltage sweep to characterize MPPT tracking")
|
||
p_sweep.add_argument("--v-start", type=float, required=True, help="Start voltage (V)")
|
||
p_sweep.add_argument("--v-stop", type=float, required=True, help="Stop voltage (V)")
|
||
p_sweep.add_argument("--v-step", type=float, required=True, help="Voltage step (V)")
|
||
p_sweep.add_argument("--current-limit", type=float, required=True, help="Current limit (A)")
|
||
p_sweep.add_argument("--settle", type=float, default=1.0, help="Settle time per step (s)")
|
||
p_sweep.add_argument("--load-mode", choices=["CC", "CR", "CV", "CP"], help="Set load mode before sweep")
|
||
p_sweep.add_argument("--load-value", type=float, help="Set load value before sweep")
|
||
p_sweep.add_argument("-o", "--output", help="CSV output file")
|
||
|
||
# sweep-load
|
||
p_swl = sub.add_parser("sweep-load", help="Load current sweep at fixed supply voltage")
|
||
p_swl.add_argument("--voltage", type=float, required=True, help="Fixed supply voltage (V)")
|
||
p_swl.add_argument("--current-limit", type=float, required=True, help="Supply current limit (A)")
|
||
p_swl.add_argument("--i-start", type=float, required=True, help="Start load current (A)")
|
||
p_swl.add_argument("--i-stop", type=float, required=True, help="Stop load current (A)")
|
||
p_swl.add_argument("--i-step", type=float, required=True, help="Current step (A)")
|
||
p_swl.add_argument("--settle", type=float, default=1.0, help="Settle time per step (s)")
|
||
p_swl.add_argument("-o", "--output", help="CSV output file")
|
||
|
||
# efficiency
|
||
p_eff = sub.add_parser("efficiency", help="Measure efficiency at fixed operating point")
|
||
p_eff.add_argument("--voltage", type=float, required=True, help="Supply voltage (V)")
|
||
p_eff.add_argument("--current-limit", type=float, required=True, help="Current limit (A)")
|
||
p_eff.add_argument("--samples", type=int, default=5, help="Number of readings to average")
|
||
p_eff.add_argument("--settle", type=float, default=2.0, help="Initial settle time (s)")
|
||
p_eff.add_argument("-i", "--interval", type=float, default=1.0, help="Interval between samples")
|
||
p_eff.add_argument("--load-mode", choices=["CC", "CR", "CV", "CP"])
|
||
p_eff.add_argument("--load-value", type=float)
|
||
|
||
# sweep-vi (2D)
|
||
p_svi = sub.add_parser("sweep-vi", help="2D voltage × load sweep (efficiency map)")
|
||
p_svi.add_argument("--v-start", type=float, required=True, help="Start voltage (V)")
|
||
p_svi.add_argument("--v-stop", type=float, required=True, help="Stop voltage (V)")
|
||
p_svi.add_argument("--v-step", type=float, required=True, help="Voltage step (V)")
|
||
p_svi.add_argument("--l-start", type=float, required=True, help="Start load setpoint (A for CC, W for CP)")
|
||
p_svi.add_argument("--l-stop", type=float, required=True, help="Stop load setpoint")
|
||
p_svi.add_argument("--l-step", type=float, required=True, help="Load step size")
|
||
p_svi.add_argument("--load-mode", choices=["CC", "CP"], default="CC", help="Load mode: CC (current) or CP (power)")
|
||
p_svi.add_argument("--current-limit", type=float, required=True, help="Supply current limit (A)")
|
||
p_svi.add_argument("--settle", type=float, default=2.0, help="Settle time per step (s)")
|
||
p_svi.add_argument("-o", "--output", help="CSV output file")
|
||
|
||
# shade-profile
|
||
p_shade = sub.add_parser("shade-profile", help="Run a shade/irradiance profile from CSV")
|
||
p_shade.add_argument("--profile", required=True, help="Profile CSV file (time,voltage,current_limit,...)")
|
||
p_shade.add_argument("--settle", type=float, default=2.0, help="Settle time per step (s)")
|
||
p_shade.add_argument("-o", "--output", help="CSV output file for results")
|
||
|
||
# supply (direct control)
|
||
p_sup = sub.add_parser("supply", help="Direct supply control")
|
||
p_sup_sub = p_sup.add_subparsers(dest="action", required=True)
|
||
p_sup_sub.add_parser("on", help="Turn supply output ON")
|
||
p_sup_sub.add_parser("off", help="Turn supply output OFF")
|
||
p_sup_set = p_sup_sub.add_parser("set", help="Set supply voltage/current")
|
||
p_sup_set.add_argument("-v", "--voltage", type=float)
|
||
p_sup_set.add_argument("-c", "--current", type=float)
|
||
|
||
# load (direct control)
|
||
p_ld = sub.add_parser("load", help="Direct load control")
|
||
p_ld_sub = p_ld.add_subparsers(dest="action", required=True)
|
||
p_ld_sub.add_parser("on", help="Turn load ON")
|
||
p_ld_sub.add_parser("off", help="Turn load OFF")
|
||
p_ld_set = p_ld_sub.add_parser("set", help="Set load mode and value")
|
||
p_ld_set.add_argument("-m", "--mode", choices=["CC", "CR", "CV", "CP"])
|
||
p_ld_set.add_argument("-v", "--value", type=float, help="Setpoint value")
|
||
|
||
# safe-off
|
||
sub.add_parser("safe-off", help="Emergency: turn off load and supply")
|
||
|
||
args = parser.parse_args()
|
||
|
||
dispatch = {
|
||
"identify": cmd_identify,
|
||
"setup": cmd_setup,
|
||
"measure": cmd_measure,
|
||
"monitor": cmd_monitor,
|
||
"live": cmd_live,
|
||
"sweep": cmd_sweep,
|
||
"sweep-load": cmd_sweep_load,
|
||
"efficiency": cmd_efficiency,
|
||
"sweep-vi": cmd_sweep_vi,
|
||
"shade-profile": cmd_shade_profile,
|
||
"supply": cmd_supply,
|
||
"load": cmd_load,
|
||
"safe-off": cmd_safe_off,
|
||
}
|
||
|
||
bench = connect_bench(args)
|
||
try:
|
||
bench.supply.remote()
|
||
bench.load.remote()
|
||
dispatch[args.command](bench, args)
|
||
except KeyboardInterrupt:
|
||
print("\nInterrupted.")
|
||
finally:
|
||
bench.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|