Files
IT6500D/it6500/driver.py
grabowski fcb1e1db2a 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>
2026-03-11 11:36:39 +07:00

469 lines
15 KiB
Python

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