Initial ITECH IT6500 series DC PSU control tool
USB-TMC/SCPI driver and CLI for IT6500 series power supplies. Commands: identify, measure, monitor, live, set, output, protection, config, send. Auto-detects instrument via USB VID 0x2EC7 / PID 0x6522. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
46
README.md
Normal file
46
README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# ITECH IT6500 Series DC Power Supply Control Tool
|
||||||
|
|
||||||
|
Python CLI and driver for controlling ITECH IT6500 series programmable DC power supplies via USB (USBTMC/SCPI).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auto-detect and identify the instrument
|
||||||
|
it6500 identify
|
||||||
|
|
||||||
|
# Take a single measurement
|
||||||
|
it6500 measure
|
||||||
|
|
||||||
|
# Continuous monitoring with CSV export
|
||||||
|
it6500 monitor --interval 1.0 --output data.csv
|
||||||
|
|
||||||
|
# Live graph
|
||||||
|
it6500 live --interval 0.5
|
||||||
|
|
||||||
|
# Set voltage and current
|
||||||
|
it6500 set --voltage 12.0 --current 5.0
|
||||||
|
|
||||||
|
# Control output
|
||||||
|
it6500 output on
|
||||||
|
it6500 output off
|
||||||
|
|
||||||
|
# Check protection status
|
||||||
|
it6500 protection
|
||||||
|
|
||||||
|
# Configure OVP, slew rates
|
||||||
|
it6500 config --ovp 60.0 --rise 0.1 --fall 0.1
|
||||||
|
|
||||||
|
# Send raw SCPI commands
|
||||||
|
it6500 send "*IDN?"
|
||||||
|
it6500 send "VOLT 24.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Models
|
||||||
|
|
||||||
|
IT6512, IT6513, IT6512A, IT6513A, IT6522A, IT6502D, IT6512D, IT6532A, IT6533A, IT6523D
|
||||||
5
it6500/__init__.py
Normal file
5
it6500/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""ITECH IT6500 Series Programmable DC Power Supply USB/SCPI driver."""
|
||||||
|
|
||||||
|
from it6500.driver import IT6500
|
||||||
|
|
||||||
|
__all__ = ["IT6500"]
|
||||||
425
it6500/cli.py
Normal file
425
it6500/cli.py
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
"""CLI tool for controlling the ITECH IT6500 Series DC Power Supply via USB."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from it6500.driver import IT6500
|
||||||
|
|
||||||
|
|
||||||
|
def find_instrument(address: str | None) -> str:
|
||||||
|
"""Find the USB-TMC instrument address or use the provided one."""
|
||||||
|
if address:
|
||||||
|
return address
|
||||||
|
|
||||||
|
import pyvisa
|
||||||
|
|
||||||
|
rm = pyvisa.ResourceManager()
|
||||||
|
resources = rm.list_resources()
|
||||||
|
rm.close()
|
||||||
|
|
||||||
|
# Look for ITECH by USB VID/PID
|
||||||
|
itech = [r for r in resources if "2EC7" in r.upper() or "6522" in r.upper()]
|
||||||
|
if itech:
|
||||||
|
print(f"Found ITECH IT6500: {itech[0]}")
|
||||||
|
return itech[0]
|
||||||
|
|
||||||
|
usb = [r for r in resources if "USB" in r]
|
||||||
|
if not usb:
|
||||||
|
print("No ITECH IT6500 or USB instruments found. Available resources:")
|
||||||
|
for r in resources:
|
||||||
|
print(f" {r}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(usb) == 1:
|
||||||
|
print(f"Found USB instrument: {usb[0]}")
|
||||||
|
return usb[0]
|
||||||
|
|
||||||
|
print("Multiple USB instruments found:")
|
||||||
|
for i, r in enumerate(usb):
|
||||||
|
print(f" [{i}] {r}")
|
||||||
|
choice = input("Select instrument number: ")
|
||||||
|
return usb[int(choice)]
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_query(psu: IT6500, method, fallback="N/A"):
|
||||||
|
"""Call a query method, returning fallback on timeout/error."""
|
||||||
|
try:
|
||||||
|
return method()
|
||||||
|
except Exception:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_identify(psu: IT6500, _args: argparse.Namespace) -> None:
|
||||||
|
"""Print instrument identity and status."""
|
||||||
|
print(f"Identity: {psu.idn()}")
|
||||||
|
print(f"SCPI Ver: {_safe_query(psu, psu.get_version)}")
|
||||||
|
print(f"Output: {'ON' if psu.get_output_state() else 'OFF'}")
|
||||||
|
print(f"Voltage set: {psu.get_voltage():.4f} V")
|
||||||
|
print(f"Current set: {psu.get_current():.4f} A")
|
||||||
|
|
||||||
|
op = _safe_query(psu, psu.get_operation_status, {})
|
||||||
|
if isinstance(op, dict):
|
||||||
|
mode = "CV" if op.get("CV") else ("CC" if op.get("CC") else "---")
|
||||||
|
print(f"Mode: {mode}")
|
||||||
|
|
||||||
|
qs = _safe_query(psu, psu.get_questionable_status, {})
|
||||||
|
if isinstance(qs, dict):
|
||||||
|
active = [k for k, v in qs.items() if v]
|
||||||
|
print(f"Protection: {', '.join(active) if active else 'None'}")
|
||||||
|
|
||||||
|
err_code, err_msg = psu.get_error()
|
||||||
|
if err_code != 0:
|
||||||
|
print(f"Error: ({err_code}) {err_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_measure(psu: IT6500, _args: argparse.Namespace) -> None:
|
||||||
|
"""Take a single measurement."""
|
||||||
|
result = psu.measure_all()
|
||||||
|
print(f" Voltage = {result.voltage:>10.4f} V")
|
||||||
|
print(f" Current = {result.current:>10.4f} A")
|
||||||
|
print(f" Power = {result.power:>10.4f} W")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_monitor(psu: IT6500, args: argparse.Namespace) -> None:
|
||||||
|
"""Continuously monitor measurements at an interval."""
|
||||||
|
interval = args.interval
|
||||||
|
|
||||||
|
writer = None
|
||||||
|
outfile = None
|
||||||
|
if args.output:
|
||||||
|
outfile = open(args.output, "w", newline="")
|
||||||
|
writer = csv.writer(outfile)
|
||||||
|
writer.writerow(["timestamp", "voltage_V", "current_A", "power_W"])
|
||||||
|
print(f"Logging to {args.output}")
|
||||||
|
|
||||||
|
print(f"{'Time':>10s} {'Voltage(V)':>12s} {'Current(A)':>12s} {'Power(W)':>12s}")
|
||||||
|
print("-" * 52)
|
||||||
|
|
||||||
|
try:
|
||||||
|
count = 0
|
||||||
|
while args.count == 0 or count < args.count:
|
||||||
|
result = psu.measure_all()
|
||||||
|
ts = time.strftime("%H:%M:%S")
|
||||||
|
print(
|
||||||
|
f"{ts:>10s} {result.voltage:>12.4f} "
|
||||||
|
f"{result.current:>12.4f} {result.power:>12.4f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if writer:
|
||||||
|
writer.writerow([
|
||||||
|
time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
f"{result.voltage:.4f}",
|
||||||
|
f"{result.current:.4f}",
|
||||||
|
f"{result.power:.4f}",
|
||||||
|
])
|
||||||
|
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(psu: IT6500, args: argparse.Namespace) -> None:
|
||||||
|
"""Live monitor with real-time graph."""
|
||||||
|
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)
|
||||||
|
voltages: deque[float] = deque(maxlen=max_points)
|
||||||
|
currents: deque[float] = deque(maxlen=max_points)
|
||||||
|
powers: deque[float] = deque(maxlen=max_points)
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
writer = None
|
||||||
|
outfile = None
|
||||||
|
if args.output:
|
||||||
|
outfile = open(args.output, "w", newline="")
|
||||||
|
writer = csv.writer(outfile)
|
||||||
|
writer.writerow(["timestamp", "voltage_V", "current_A", "power_W"])
|
||||||
|
print(f"Logging to {args.output}")
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(3, 1, figsize=(12, 9), squeeze=False)
|
||||||
|
fig.suptitle("ITECH IT6500 Live Monitor", fontsize=14, fontweight="bold")
|
||||||
|
axes = axes.flatten()
|
||||||
|
|
||||||
|
labels = [("Voltage", "V", voltages), ("Current", "A", currents), ("Power", "W", powers)]
|
||||||
|
lines = []
|
||||||
|
for ax, (title, unit, _) in zip(axes, labels):
|
||||||
|
ax.set_ylabel(f"{title} ({unit})")
|
||||||
|
ax.set_xlabel("Time (s)")
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
ax.set_title(title, fontsize=11)
|
||||||
|
line, = ax.plot([], [], linewidth=1.5)
|
||||||
|
lines.append(line)
|
||||||
|
ax.legend([title], loc="upper left", fontsize=9)
|
||||||
|
|
||||||
|
fig.tight_layout()
|
||||||
|
|
||||||
|
def update(_frame):
|
||||||
|
try:
|
||||||
|
result = psu.measure_all()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Read error: {e}")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
now = time.time() - t0
|
||||||
|
timestamps.append(now)
|
||||||
|
voltages.append(result.voltage)
|
||||||
|
currents.append(result.current)
|
||||||
|
powers.append(result.power)
|
||||||
|
|
||||||
|
ts = time.strftime("%H:%M:%S")
|
||||||
|
print(
|
||||||
|
f"{ts} V={result.voltage:.4f} "
|
||||||
|
f"I={result.current:.4f} P={result.power:.4f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if writer:
|
||||||
|
writer.writerow([
|
||||||
|
time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
f"{result.voltage:.4f}",
|
||||||
|
f"{result.current:.4f}",
|
||||||
|
f"{result.power:.4f}",
|
||||||
|
])
|
||||||
|
outfile.flush()
|
||||||
|
|
||||||
|
t_list = list(timestamps)
|
||||||
|
for line, (_, _, data) in zip(lines, labels):
|
||||||
|
line.set_data(t_list, list(data))
|
||||||
|
|
||||||
|
for ax in axes:
|
||||||
|
ax.relim()
|
||||||
|
ax.autoscale_view()
|
||||||
|
|
||||||
|
return 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_set(psu: IT6500, args: argparse.Namespace) -> None:
|
||||||
|
"""Set voltage and/or current."""
|
||||||
|
if args.voltage is not None and args.current is not None:
|
||||||
|
psu.apply(args.voltage, args.current)
|
||||||
|
print(f"Applied: {args.voltage:.4f} V, {args.current:.4f} A")
|
||||||
|
elif args.voltage is not None:
|
||||||
|
psu.set_voltage(args.voltage)
|
||||||
|
print(f"Voltage set: {args.voltage:.4f} V")
|
||||||
|
elif args.current is not None:
|
||||||
|
psu.set_current(args.current)
|
||||||
|
print(f"Current set: {args.current:.4f} A")
|
||||||
|
else:
|
||||||
|
print("Specify --voltage and/or --current")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_output(psu: IT6500, args: argparse.Namespace) -> None:
|
||||||
|
"""Turn output ON or OFF."""
|
||||||
|
if args.state.upper() in ("ON", "1"):
|
||||||
|
psu.output_on()
|
||||||
|
print("Output ON")
|
||||||
|
else:
|
||||||
|
psu.output_off()
|
||||||
|
print("Output OFF")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_protection(psu: IT6500, args: argparse.Namespace) -> None:
|
||||||
|
"""Check or clear protection status."""
|
||||||
|
if args.clear:
|
||||||
|
try:
|
||||||
|
psu.clear_ovp()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
psu.clear_status()
|
||||||
|
print("Protection cleared.")
|
||||||
|
else:
|
||||||
|
qs = psu.get_questionable_status()
|
||||||
|
for name, active in qs.items():
|
||||||
|
status = "TRIGGERED" if active else "OK"
|
||||||
|
print(f" {name}: {status}")
|
||||||
|
|
||||||
|
ovp_tripped = _safe_query(psu, psu.get_ovp_tripped, False)
|
||||||
|
print(f" OVP tripped: {'YES' if ovp_tripped else 'NO'}")
|
||||||
|
|
||||||
|
ovp_level = _safe_query(psu, psu.get_ovp_level, None)
|
||||||
|
if ovp_level is not None:
|
||||||
|
print(f"\n OVP level: {ovp_level:.2f} V")
|
||||||
|
ovp_state = _safe_query(psu, psu.get_ovp_state, None)
|
||||||
|
if ovp_state is not None:
|
||||||
|
print(f" OVP enabled: {'YES' if ovp_state else 'NO'}")
|
||||||
|
ovp_delay = _safe_query(psu, psu.get_ovp_delay, None)
|
||||||
|
if ovp_delay is not None:
|
||||||
|
print(f" OVP delay: {ovp_delay:.3f} s")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config(psu: IT6500, args: argparse.Namespace) -> None:
|
||||||
|
"""View or change instrument configuration."""
|
||||||
|
if args.ovp is not None:
|
||||||
|
psu.set_ovp_level(args.ovp)
|
||||||
|
psu.set_ovp_state(True)
|
||||||
|
print(f"OVP set to {args.ovp:.2f} V (enabled)")
|
||||||
|
if args.rise is not None:
|
||||||
|
psu.set_rise_time(args.rise)
|
||||||
|
print(f"Rise time: {args.rise:.3f} s")
|
||||||
|
if args.fall is not None:
|
||||||
|
psu.set_fall_time(args.fall)
|
||||||
|
print(f"Fall time: {args.fall:.3f} s")
|
||||||
|
if args.avg is not None:
|
||||||
|
psu.set_averaging(args.avg)
|
||||||
|
print(f"Averaging: {args.avg}")
|
||||||
|
if args.vrange is not None:
|
||||||
|
psu.set_voltage_range(args.vrange)
|
||||||
|
print(f"Voltage range: {args.vrange:.2f} V")
|
||||||
|
|
||||||
|
# Show current config
|
||||||
|
print(f"\nCurrent configuration:")
|
||||||
|
print(f" Voltage set: {psu.get_voltage():.4f} V")
|
||||||
|
print(f" Current set: {psu.get_current():.4f} A")
|
||||||
|
for label, method in [
|
||||||
|
("V range", psu.get_voltage_range),
|
||||||
|
("Rise time", psu.get_rise_time),
|
||||||
|
("Fall time", psu.get_fall_time),
|
||||||
|
("OVP level", psu.get_ovp_level),
|
||||||
|
]:
|
||||||
|
val = _safe_query(psu, method, None)
|
||||||
|
if val is not None:
|
||||||
|
print(f" {label + ':':12s} {val}")
|
||||||
|
ovp_state = _safe_query(psu, psu.get_ovp_state, None)
|
||||||
|
if ovp_state is not None:
|
||||||
|
print(f" OVP enabled: {'YES' if ovp_state else 'NO'}")
|
||||||
|
avg = _safe_query(psu, psu.get_averaging, None)
|
||||||
|
if avg is not None:
|
||||||
|
print(f" Averaging: {avg}")
|
||||||
|
beep = _safe_query(psu, psu.get_beeper, None)
|
||||||
|
if beep is not None:
|
||||||
|
print(f" Beeper: {'ON' if beep else 'OFF'}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_send(psu: IT6500, args: argparse.Namespace) -> None:
|
||||||
|
"""Send a raw SCPI command."""
|
||||||
|
command = " ".join(args.raw_command)
|
||||||
|
result = psu.send(command)
|
||||||
|
if result is not None:
|
||||||
|
print(f"Response: {result}")
|
||||||
|
else:
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="ITECH IT6500 Series DC Power Supply USB Control Tool",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""\
|
||||||
|
examples:
|
||||||
|
%(prog)s identify
|
||||||
|
%(prog)s measure
|
||||||
|
%(prog)s monitor --interval 1.0 --output data.csv
|
||||||
|
%(prog)s live --interval 0.5
|
||||||
|
%(prog)s set --voltage 12.0 --current 5.0
|
||||||
|
%(prog)s output on
|
||||||
|
%(prog)s output off
|
||||||
|
%(prog)s protection
|
||||||
|
%(prog)s protection --clear
|
||||||
|
%(prog)s config --ovp 60.0 --rise 0.1 --fall 0.1
|
||||||
|
%(prog)s send "*IDN?"
|
||||||
|
%(prog)s send "VOLT 24.0"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-a", "--address",
|
||||||
|
help="VISA resource address (e.g., USB0::0x2EC7::0x6522::...::INSTR). Auto-detects if omitted.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout", type=int, default=5000,
|
||||||
|
help="Communication timeout in ms (default: 5000)",
|
||||||
|
)
|
||||||
|
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# identify
|
||||||
|
sub.add_parser("identify", help="Show instrument identity and status")
|
||||||
|
|
||||||
|
# measure
|
||||||
|
sub.add_parser("measure", help="Take a single measurement")
|
||||||
|
|
||||||
|
# monitor
|
||||||
|
p_mon = sub.add_parser("monitor", help="Continuously monitor measurements")
|
||||||
|
p_mon.add_argument("-i", "--interval", type=float, default=1.0, help="Seconds between readings (default: 1.0)")
|
||||||
|
p_mon.add_argument("-n", "--count", type=int, default=0, help="Number of readings (0=infinite)")
|
||||||
|
p_mon.add_argument("-o", "--output", help="CSV output file path")
|
||||||
|
|
||||||
|
# live
|
||||||
|
p_live = sub.add_parser("live", help="Live monitor with real-time graph")
|
||||||
|
p_live.add_argument("-i", "--interval", type=float, default=1.0, help="Seconds between readings (default: 1.0)")
|
||||||
|
p_live.add_argument("-o", "--output", help="CSV output file path")
|
||||||
|
p_live.add_argument("--history", type=int, default=300, help="Max data points to display (default: 300)")
|
||||||
|
|
||||||
|
# set
|
||||||
|
p_set = sub.add_parser("set", help="Set voltage and/or current")
|
||||||
|
p_set.add_argument("-v", "--voltage", type=float, help="Output voltage in Volts")
|
||||||
|
p_set.add_argument("-c", "--current", type=float, help="Output current in Amps")
|
||||||
|
|
||||||
|
# output
|
||||||
|
p_out = sub.add_parser("output", help="Turn output ON or OFF")
|
||||||
|
p_out.add_argument("state", choices=["on", "off", "ON", "OFF", "1", "0"], help="ON or OFF")
|
||||||
|
|
||||||
|
# protection
|
||||||
|
p_prot = sub.add_parser("protection", help="Check or clear protection status")
|
||||||
|
p_prot.add_argument("--clear", action="store_true", help="Clear protection flags")
|
||||||
|
|
||||||
|
# config
|
||||||
|
p_cfg = sub.add_parser("config", help="View or change instrument configuration")
|
||||||
|
p_cfg.add_argument("--ovp", type=float, help="Set OVP level (Volts)")
|
||||||
|
p_cfg.add_argument("--rise", type=float, help="Set voltage rise time (seconds)")
|
||||||
|
p_cfg.add_argument("--fall", type=float, help="Set voltage fall time (seconds)")
|
||||||
|
p_cfg.add_argument("--avg", type=int, help="Set measurement averaging count (0-15)")
|
||||||
|
p_cfg.add_argument("--vrange", type=float, help="Set voltage range upper limit (Volts)")
|
||||||
|
|
||||||
|
# send
|
||||||
|
p_send = sub.add_parser("send", help="Send raw SCPI command")
|
||||||
|
p_send.add_argument("raw_command", nargs="+", help="Command string (queries auto-detected by '?')")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
address = find_instrument(args.address)
|
||||||
|
|
||||||
|
dispatch = {
|
||||||
|
"identify": cmd_identify,
|
||||||
|
"measure": cmd_measure,
|
||||||
|
"monitor": cmd_monitor,
|
||||||
|
"live": cmd_live,
|
||||||
|
"set": cmd_set,
|
||||||
|
"output": cmd_output,
|
||||||
|
"protection": cmd_protection,
|
||||||
|
"config": cmd_config,
|
||||||
|
"send": cmd_send,
|
||||||
|
}
|
||||||
|
|
||||||
|
with IT6500(address, timeout_ms=args.timeout) as psu:
|
||||||
|
psu.remote()
|
||||||
|
dispatch[args.command](psu, args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
468
it6500/driver.py
Normal file
468
it6500/driver.py
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
"""ITECH IT6500 Series Programmable DC Power Supply SCPI driver.
|
||||||
|
|
||||||
|
Communicates via USB-TMC (USBTMC) using PyVISA.
|
||||||
|
|
||||||
|
SCPI commands based on IT6500 Series Programming Guide v1.0.
|
||||||
|
Covers: IT6512/IT6513/IT6512A/IT6513A/IT6522A/IT6502D/IT6512D
|
||||||
|
IT6532A/IT6533A/IT6523D
|
||||||
|
|
||||||
|
USB interface is IEEE 488.2 USB488 compliant.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
import pyvisa
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MeasurementResult:
|
||||||
|
"""Container for a single measurement snapshot."""
|
||||||
|
|
||||||
|
voltage: float
|
||||||
|
current: float
|
||||||
|
power: float
|
||||||
|
timestamp: float = field(default_factory=time.time)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"MeasurementResult(V={self.voltage:.4f}, "
|
||||||
|
f"I={self.current:.4f}, P={self.power:.4f})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IT6500:
|
||||||
|
"""Driver for ITECH IT6500 Series DC Power Supply over USB-TMC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address: VISA resource string, e.g. "USB0::0x2EC7::0x6522::800682011797230003::INSTR"
|
||||||
|
timeout_ms: Communication timeout in milliseconds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# USB identifiers for ITECH IT6500 series
|
||||||
|
USB_VID = 0x2EC7
|
||||||
|
USB_PID = 0x6522
|
||||||
|
|
||||||
|
def __init__(self, address: str, timeout_ms: int = 5000) -> None:
|
||||||
|
self._address = address
|
||||||
|
self._rm = pyvisa.ResourceManager()
|
||||||
|
self._inst = self._rm.open_resource(address)
|
||||||
|
self._inst.timeout = timeout_ms
|
||||||
|
self._inst.read_termination = "\n"
|
||||||
|
self._inst.write_termination = "\n"
|
||||||
|
# Clear any stale errors/status from previous sessions
|
||||||
|
self._inst.write("*CLS")
|
||||||
|
|
||||||
|
# -- Low-level communication --
|
||||||
|
|
||||||
|
def write(self, command: str) -> None:
|
||||||
|
"""Send a SCPI command to the instrument."""
|
||||||
|
self._inst.write(command)
|
||||||
|
|
||||||
|
def query(self, command: str) -> str:
|
||||||
|
"""Send a query and return the response string."""
|
||||||
|
return self._inst.query(command).strip()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close the VISA connection."""
|
||||||
|
try:
|
||||||
|
self.local()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._inst.close()
|
||||||
|
self._rm.close()
|
||||||
|
|
||||||
|
def __enter__(self) -> IT6500:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc) -> None:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
# -- IEEE 488.2 Common Commands --
|
||||||
|
|
||||||
|
def idn(self) -> str:
|
||||||
|
"""Query instrument identity (*IDN?).
|
||||||
|
|
||||||
|
Returns 4 comma-separated fields:
|
||||||
|
Manufacturer, Model, Serial, Firmware version
|
||||||
|
"""
|
||||||
|
return self.query("*IDN?")
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset to factory defaults (*RST).
|
||||||
|
|
||||||
|
Sets: OUTPUT OFF, CURR MIN, VOLT:PROT MAX, VOLT MIN
|
||||||
|
"""
|
||||||
|
self.write("*RST")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def clear_status(self) -> None:
|
||||||
|
"""Clear all status registers and error queue (*CLS)."""
|
||||||
|
self.write("*CLS")
|
||||||
|
|
||||||
|
def operation_complete(self) -> bool:
|
||||||
|
"""Block until all pending operations complete (*OPC?)."""
|
||||||
|
return self.query("*OPC?") == "1"
|
||||||
|
|
||||||
|
def status_byte(self) -> int:
|
||||||
|
"""Read the status byte register (*STB?)."""
|
||||||
|
return int(self.query("*STB?"))
|
||||||
|
|
||||||
|
def event_status(self) -> int:
|
||||||
|
"""Read and clear the standard event status register (*ESR?)."""
|
||||||
|
return int(self.query("*ESR?"))
|
||||||
|
|
||||||
|
def set_event_status_enable(self, mask: int) -> None:
|
||||||
|
"""Set the standard event enable register (*ESE)."""
|
||||||
|
self.write(f"*ESE {mask}")
|
||||||
|
|
||||||
|
def get_event_status_enable(self) -> int:
|
||||||
|
"""Query the standard event enable register (*ESE?)."""
|
||||||
|
return int(self.query("*ESE?"))
|
||||||
|
|
||||||
|
def set_service_request_enable(self, mask: int) -> None:
|
||||||
|
"""Set the service request enable register (*SRE)."""
|
||||||
|
self.write(f"*SRE {mask}")
|
||||||
|
|
||||||
|
def get_service_request_enable(self) -> int:
|
||||||
|
"""Query the service request enable register (*SRE?)."""
|
||||||
|
return int(self.query("*SRE?"))
|
||||||
|
|
||||||
|
def trigger(self) -> None:
|
||||||
|
"""Send a bus trigger (*TRG)."""
|
||||||
|
self.write("*TRG")
|
||||||
|
|
||||||
|
def save(self, slot: int) -> None:
|
||||||
|
"""Save current setup to memory slot (0-9)."""
|
||||||
|
self.write(f"*SAV {slot}")
|
||||||
|
|
||||||
|
def recall(self, slot: int) -> None:
|
||||||
|
"""Recall saved setup from memory slot (0-9)."""
|
||||||
|
self.write(f"*RCL {slot}")
|
||||||
|
|
||||||
|
# -- System Commands --
|
||||||
|
|
||||||
|
def remote(self) -> None:
|
||||||
|
"""Switch to remote control mode (front panel locked)."""
|
||||||
|
self.write("SYSTem:REMote")
|
||||||
|
|
||||||
|
def local(self) -> None:
|
||||||
|
"""Switch to local control mode (front panel active)."""
|
||||||
|
self.write("SYSTem:LOCal")
|
||||||
|
|
||||||
|
def rwlock(self) -> None:
|
||||||
|
"""Remote mode with LOCAL button also locked."""
|
||||||
|
self.write("SYSTem:RWLock")
|
||||||
|
|
||||||
|
def get_error(self) -> tuple[int, str]:
|
||||||
|
"""Read error code and message from the error queue."""
|
||||||
|
resp = self.query("SYSTem:ERRor?")
|
||||||
|
parts = resp.split(",", 1)
|
||||||
|
code = int(parts[0].strip())
|
||||||
|
msg = parts[1].strip().strip('"') if len(parts) > 1 else ""
|
||||||
|
return code, msg
|
||||||
|
|
||||||
|
def get_version(self) -> str:
|
||||||
|
"""Query SCPI version string (e.g. '2009.0')."""
|
||||||
|
return self.query("SYSTem:VERSion?")
|
||||||
|
|
||||||
|
def set_power_on_state(self, state: str) -> None:
|
||||||
|
"""Set power-on state: 'RST' (factory) or 'SAV0' (last saved)."""
|
||||||
|
self.write(f"SYSTem:POSetup {state}")
|
||||||
|
|
||||||
|
def get_power_on_state(self) -> str:
|
||||||
|
"""Query power-on state setting."""
|
||||||
|
return self.query("SYSTem:POSetup?")
|
||||||
|
|
||||||
|
def clear_errors(self) -> None:
|
||||||
|
"""Clear the error information queue."""
|
||||||
|
self.write("SYSTem:CLEar")
|
||||||
|
|
||||||
|
def set_beeper(self, on: bool) -> None:
|
||||||
|
"""Enable/disable the front panel beeper."""
|
||||||
|
self.write(f"SYSTem:BEEPer {'ON' if on else 'OFF'}")
|
||||||
|
|
||||||
|
def get_beeper(self) -> bool:
|
||||||
|
"""Query beeper state."""
|
||||||
|
return self.query("SYSTem:BEEPer?") == "1"
|
||||||
|
|
||||||
|
def set_interface(self, iface: str) -> None:
|
||||||
|
"""Select communication interface: 'GPIB', 'USB', 'RS232', 'RS485'."""
|
||||||
|
self.write(f"SYSTem:INTerface {iface}")
|
||||||
|
|
||||||
|
# -- Output Control --
|
||||||
|
|
||||||
|
def output_on(self) -> None:
|
||||||
|
"""Turn the output ON."""
|
||||||
|
self.write("OUTPut ON")
|
||||||
|
|
||||||
|
def output_off(self) -> None:
|
||||||
|
"""Turn the output OFF."""
|
||||||
|
self.write("OUTPut OFF")
|
||||||
|
|
||||||
|
def get_output_state(self) -> bool:
|
||||||
|
"""Query output state. Returns True if ON."""
|
||||||
|
return self.query("OUTPut?") == "1"
|
||||||
|
|
||||||
|
# -- Voltage Settings --
|
||||||
|
|
||||||
|
def set_voltage(self, volts: float) -> None:
|
||||||
|
"""Set the output voltage (immediate)."""
|
||||||
|
self.write(f"VOLTage {volts}")
|
||||||
|
|
||||||
|
def get_voltage(self) -> float:
|
||||||
|
"""Query the voltage set point."""
|
||||||
|
return float(self.query("VOLTage?"))
|
||||||
|
|
||||||
|
def set_voltage_triggered(self, volts: float) -> None:
|
||||||
|
"""Set voltage to apply on next trigger event."""
|
||||||
|
self.write(f"VOLTage:TRIGgered {volts}")
|
||||||
|
|
||||||
|
def get_voltage_triggered(self) -> float:
|
||||||
|
"""Query the triggered voltage set point."""
|
||||||
|
return float(self.query("VOLTage:TRIGgered?"))
|
||||||
|
|
||||||
|
# -- Voltage Protection (OVP) --
|
||||||
|
|
||||||
|
def set_ovp_level(self, volts: float) -> None:
|
||||||
|
"""Set over-voltage protection level."""
|
||||||
|
self.write(f"VOLTage:PROTection {volts}")
|
||||||
|
|
||||||
|
def get_ovp_level(self) -> float:
|
||||||
|
"""Query OVP level."""
|
||||||
|
return float(self.query("VOLTage:PROTection?"))
|
||||||
|
|
||||||
|
def set_ovp_delay(self, seconds: float) -> None:
|
||||||
|
"""Set OVP delay time (0.001-0.6 s)."""
|
||||||
|
self.write(f"VOLTage:PROTection:DELay {seconds}")
|
||||||
|
|
||||||
|
def get_ovp_delay(self) -> float:
|
||||||
|
"""Query OVP delay time."""
|
||||||
|
return float(self.query("VOLTage:PROTection:DELay?"))
|
||||||
|
|
||||||
|
def set_ovp_state(self, on: bool) -> None:
|
||||||
|
"""Enable/disable software OVP."""
|
||||||
|
self.write(f"VOLTage:PROTection:STATe {'ON' if on else 'OFF'}")
|
||||||
|
|
||||||
|
def get_ovp_state(self) -> bool:
|
||||||
|
"""Query OVP enable state."""
|
||||||
|
return self.query("VOLTage:PROTection:STATe?") == "1"
|
||||||
|
|
||||||
|
def get_ovp_tripped(self) -> bool:
|
||||||
|
"""Check if OVP has been triggered. True = tripped."""
|
||||||
|
return self.query("VOLTage:PROTection:TRIGgered?") == "1"
|
||||||
|
|
||||||
|
def clear_ovp(self) -> None:
|
||||||
|
"""Clear OVP tripped state (reduce voltage or remove source first)."""
|
||||||
|
self.write("VOLTage:PROTection:CLEar")
|
||||||
|
|
||||||
|
# -- Voltage Limits --
|
||||||
|
|
||||||
|
def set_voltage_limit(self, volts: float) -> None:
|
||||||
|
"""Set lower limit of output voltage."""
|
||||||
|
self.write(f"VOLTage:LIMit {volts}")
|
||||||
|
|
||||||
|
def get_voltage_limit(self) -> float:
|
||||||
|
"""Query voltage lower limit."""
|
||||||
|
return float(self.query("VOLTage:LIMit?"))
|
||||||
|
|
||||||
|
def set_voltage_range(self, volts: float) -> None:
|
||||||
|
"""Set upper limit of output voltage."""
|
||||||
|
self.write(f"VOLTage:RANGe {volts}")
|
||||||
|
|
||||||
|
def get_voltage_range(self) -> float:
|
||||||
|
"""Query voltage upper limit."""
|
||||||
|
return float(self.query("VOLTage:RANGe?"))
|
||||||
|
|
||||||
|
# -- Current Settings --
|
||||||
|
|
||||||
|
def set_current(self, amps: float) -> None:
|
||||||
|
"""Set the output current (immediate)."""
|
||||||
|
self.write(f"CURRent {amps}")
|
||||||
|
|
||||||
|
def get_current(self) -> float:
|
||||||
|
"""Query the current set point."""
|
||||||
|
return float(self.query("CURRent?"))
|
||||||
|
|
||||||
|
def set_current_triggered(self, amps: float) -> None:
|
||||||
|
"""Set current to apply on next trigger event."""
|
||||||
|
self.write(f"CURRent:TRIGgered {amps}")
|
||||||
|
|
||||||
|
def get_current_triggered(self) -> float:
|
||||||
|
"""Query the triggered current set point."""
|
||||||
|
return float(self.query("CURRent:TRIGgered?"))
|
||||||
|
|
||||||
|
# -- Slew Rate --
|
||||||
|
|
||||||
|
def set_rise_time(self, seconds: float) -> None:
|
||||||
|
"""Set voltage rising time (0-65.535 s)."""
|
||||||
|
self.write(f"RISe {seconds}")
|
||||||
|
|
||||||
|
def get_rise_time(self) -> float:
|
||||||
|
"""Query voltage rising time."""
|
||||||
|
return float(self.query("RISe?"))
|
||||||
|
|
||||||
|
def set_fall_time(self, seconds: float) -> None:
|
||||||
|
"""Set voltage falling time (0-65.535 s)."""
|
||||||
|
self.write(f"FALL {seconds}")
|
||||||
|
|
||||||
|
def get_fall_time(self) -> float:
|
||||||
|
"""Query voltage falling time."""
|
||||||
|
return float(self.query("FALL?"))
|
||||||
|
|
||||||
|
# -- Compound Command --
|
||||||
|
|
||||||
|
def apply(self, voltage: float, current: float) -> None:
|
||||||
|
"""Set voltage and current simultaneously (APPLy).
|
||||||
|
|
||||||
|
Values must be within the range limits, otherwise an execution error occurs.
|
||||||
|
"""
|
||||||
|
self.write(f"APPLy {voltage},{current}")
|
||||||
|
|
||||||
|
def get_apply(self) -> tuple[float, float]:
|
||||||
|
"""Query the APPLy voltage and current set points."""
|
||||||
|
resp = self.query("APPLy?")
|
||||||
|
parts = resp.split(",")
|
||||||
|
return float(parts[0].strip()), float(parts[1].strip())
|
||||||
|
|
||||||
|
# -- Trigger Control --
|
||||||
|
|
||||||
|
def set_trigger_source(self, source: str) -> None:
|
||||||
|
"""Set trigger source: 'MANUAL' or 'BUS'."""
|
||||||
|
self.write(f"TRIGger:SOURce {source}")
|
||||||
|
|
||||||
|
def get_trigger_source(self) -> str:
|
||||||
|
"""Query trigger source."""
|
||||||
|
return self.query("TRIGger:SOURce?")
|
||||||
|
|
||||||
|
def trigger_immediate(self) -> None:
|
||||||
|
"""Send an immediate trigger signal."""
|
||||||
|
self.write("TRIGger")
|
||||||
|
|
||||||
|
# -- Measurement Queries --
|
||||||
|
|
||||||
|
def measure_voltage(self) -> float:
|
||||||
|
"""Read the actual output voltage in Volts."""
|
||||||
|
return float(self.query("MEASure:VOLTage?"))
|
||||||
|
|
||||||
|
def measure_current(self) -> float:
|
||||||
|
"""Read the actual output current in Amperes."""
|
||||||
|
return float(self.query("MEASure:CURRent?"))
|
||||||
|
|
||||||
|
def measure_power(self) -> float:
|
||||||
|
"""Read the actual output power in Watts."""
|
||||||
|
return float(self.query("MEASure:POWer?"))
|
||||||
|
|
||||||
|
def measure_all(self) -> MeasurementResult:
|
||||||
|
"""Read voltage, current, and power."""
|
||||||
|
voltage = self.measure_voltage()
|
||||||
|
current = self.measure_current()
|
||||||
|
power = self.measure_power()
|
||||||
|
return MeasurementResult(
|
||||||
|
voltage=voltage,
|
||||||
|
current=current,
|
||||||
|
power=power,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Fetch (cached readings, no new measurement triggered) --
|
||||||
|
|
||||||
|
def fetch_voltage(self) -> float:
|
||||||
|
"""Read cached voltage from sample buffer."""
|
||||||
|
return float(self.query("FETCh:VOLTage?"))
|
||||||
|
|
||||||
|
def fetch_current(self) -> float:
|
||||||
|
"""Read cached current from sample buffer."""
|
||||||
|
return float(self.query("FETCh:CURRent?"))
|
||||||
|
|
||||||
|
def fetch_power(self) -> float:
|
||||||
|
"""Read cached power from sample buffer."""
|
||||||
|
return float(self.query("FETCh:POWer?"))
|
||||||
|
|
||||||
|
# -- Measurement Averaging --
|
||||||
|
|
||||||
|
def set_averaging(self, count: int) -> None:
|
||||||
|
"""Set measurement averaging filter count (0-15)."""
|
||||||
|
self.write(f"SENSe:AVERage:COUNt {count}")
|
||||||
|
|
||||||
|
def get_averaging(self) -> int:
|
||||||
|
"""Query measurement averaging count."""
|
||||||
|
return int(self.query("SENSe:AVERage:COUNt?"))
|
||||||
|
|
||||||
|
# -- Display Control --
|
||||||
|
|
||||||
|
def set_display(self, on: bool) -> None:
|
||||||
|
"""Turn VFD display on or off."""
|
||||||
|
self.write(f"DISPlay {'ON' if on else 'OFF'}")
|
||||||
|
|
||||||
|
def get_display(self) -> bool:
|
||||||
|
"""Query display state."""
|
||||||
|
return self.query("DISPlay?") == "1"
|
||||||
|
|
||||||
|
def set_display_text(self, text: str) -> None:
|
||||||
|
"""Show custom text on VFD display."""
|
||||||
|
self.write(f'DISPlay:TEXT "{text}"')
|
||||||
|
|
||||||
|
def clear_display_text(self) -> None:
|
||||||
|
"""Clear custom text from display."""
|
||||||
|
self.write("DISPlay:TEXT:CLEar")
|
||||||
|
|
||||||
|
# -- Configuration --
|
||||||
|
|
||||||
|
def save_config(self) -> None:
|
||||||
|
"""Save current configuration to non-volatile memory."""
|
||||||
|
self.write("CONFigure:SAVe")
|
||||||
|
|
||||||
|
# -- Status Registers --
|
||||||
|
|
||||||
|
def get_questionable_status(self) -> dict[str, bool]:
|
||||||
|
"""Read questionable status condition register.
|
||||||
|
|
||||||
|
Returns dict with protection flags: OV, OC, OP, OT.
|
||||||
|
"""
|
||||||
|
bits = int(self.query("STATus:QUEStionable:CONDition?"))
|
||||||
|
return {
|
||||||
|
"OV": bool(bits & 0x01), # bit 0: Over voltage
|
||||||
|
"OC": bool(bits & 0x02), # bit 1: Over current
|
||||||
|
"OP": bool(bits & 0x08), # bit 3: Over power
|
||||||
|
"OT": bool(bits & 0x10), # bit 4: Over temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_questionable_event(self) -> int:
|
||||||
|
"""Read and clear the questionable event register."""
|
||||||
|
return int(self.query("STATus:QUEStionable?"))
|
||||||
|
|
||||||
|
def set_questionable_enable(self, mask: int) -> None:
|
||||||
|
"""Set questionable status enable mask."""
|
||||||
|
self.write(f"STATus:QUEStionable:ENABle {mask}")
|
||||||
|
|
||||||
|
def get_operation_status(self) -> dict[str, bool]:
|
||||||
|
"""Read operation status condition register.
|
||||||
|
|
||||||
|
Returns dict: CAL, WTG (waiting for trigger), CV, CC.
|
||||||
|
"""
|
||||||
|
bits = int(self.query("STATus:OPERation:CONDition?"))
|
||||||
|
return {
|
||||||
|
"CAL": bool(bits & 0x01), # bit 0: Calibrating
|
||||||
|
"WTG": bool(bits & 0x04), # bit 2: Waiting for trigger (mapped from bit 2 per doc showing WTG=bit3 with weight 4)
|
||||||
|
"CV": bool(bits & 0x10), # bit 4: Constant voltage
|
||||||
|
"CC": bool(bits & 0x20), # bit 5: Constant current (mapped from weight 8 per doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_operation_event(self) -> int:
|
||||||
|
"""Read and clear the operation event register."""
|
||||||
|
return int(self.query("STATus:OPERation?"))
|
||||||
|
|
||||||
|
def set_operation_enable(self, mask: int) -> None:
|
||||||
|
"""Set operation status enable mask."""
|
||||||
|
self.write(f"STATus:OPERation:ENABle {mask}")
|
||||||
|
|
||||||
|
# -- Send Raw Command --
|
||||||
|
|
||||||
|
def send(self, command: str) -> str | None:
|
||||||
|
"""Send a raw SCPI command. Returns response if it's a query."""
|
||||||
|
if "?" in command:
|
||||||
|
return self.query(command)
|
||||||
|
self.write(command)
|
||||||
|
return None
|
||||||
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[project]
|
||||||
|
name = "itech-it6500"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "ITECH IT6500 Series Programmable DC Power Supply USB/SCPI control tools"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"matplotlib>=3.10.8",
|
||||||
|
"pyvisa>=1.16.2",
|
||||||
|
"pyvisa-py>=0.8.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["it6500"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
it6500 = "it6500.cli:main"
|
||||||
Reference in New Issue
Block a user