Source code for hvl_ccb.dev.ka3000p.device

#  Copyright (c) ETH Zurich, SIS ID and HVL D-ITET
#

import contextlib
import logging
import re
from collections.abc import Generator
from time import sleep
from typing import Any

from hvl_ccb.configuration import configdataclass
from hvl_ccb.dev.base import SingleCommDevice
from hvl_ccb.utils.validation import validate_bool, validate_number

from .base import KA3000PError, KA3000PStatus, _gen_status
from .comm import KA3000PCommunication

logger = logging.getLogger(__name__)


[docs] @configdataclass class KA3000PConfig: """Device configuration class for a KA3000P power supply""" time_to_wait_before_save_settings: float = 0.1
[docs] class KA3000P(SingleCommDevice): """Class to controll a KA3000P power supply""" com: KA3000PCommunication config: KA3000PConfig
[docs] @staticmethod def default_com_cls(): return KA3000PCommunication
[docs] @staticmethod def config_cls(): return KA3000PConfig
def __init__(self, com, dev_config=None) -> None: super().__init__(com, dev_config) self._brand: str = "" self._comm_version: str = "" self._serial_number: str = "" # maximum output voltage of the hardware (unit V) self._max_voltage_hardware: float = 0 # maximum output current of the hardware (unit A) self._max_current_hardware: float = 0
[docs] def start(self) -> None: super().start() try: self.idenify_device() except KA3000PError as exc: self.stop() msg = ( "Error during identifying the device. " "Either the device is unknown or the connection is not stable." ) logger.exception(msg) raise KA3000PError(msg) from exc
[docs] def idenify_device(self) -> None: identification: str = self.com.query_not_none("*IDN?") logger.debug(f'Identified device: "{identification}"') # Result: KORAD KA3005P V5.9 SN:02738566 regex_model = ( r"(?P<brand>[A-Z]+) " # KORAD r"KA(?P<voltage>\d{2})(?P<current>\d{2})P " # KA3005P r"V(?P<version>[.0-9]+) " # V5.9 r"SN:(?P<sn>[0-9]+)" # SN:02738566 ) m_obj: re.Match[str] | None = re.match(regex_model, identification) if m_obj is None: msg = f"Cannot identify device with identification: {identification}" logger.error(msg) raise KA3000PError(msg) device_values: dict[str, str] = m_obj.groupdict() self._brand = device_values["brand"] self._max_voltage_hardware = float(device_values["voltage"]) self._max_current_hardware = float(device_values["current"]) self._comm_version = f"V{device_values['version']}" self._serial_number = device_values["sn"]
# Static information @property def brand(self) -> str: """Brand of the connected device :return: Band """ return self._brand @property def comm_version(self) -> str: """Version of the communication protocol :return: Version number """ return self._comm_version @property def serial_number(self) -> str: """Serial number of the connected device :return: Serial number """ return self._serial_number @property def status(self) -> KA3000PStatus: with self.com.access_lock: self.com.write("STATUS?") stb_qry = self.com.read_bytes() # A somehow "manual" strip of a single byte, as the result can be a byte which # could be interpreted as whitespace if len(stb_qry) == 0: msg = "Queried status and did not get an answer" logger.error(msg) raise KA3000PError(msg) if len(stb_qry) > 1 and len(stb_qry[1:].strip()) != 0: msg = f"Queried status and answer was not a single byte ({stb_qry!r})" logger.error(msg) raise KA3000PError(msg) return _gen_status(stb_qry[0]) # Voltage and current values @property def voltage(self) -> float: """Return the measured output voltage in V""" value = float(self.com.query_not_none("VOUT1?")) logger.debug(f"Voltage at Output 1 of KA3000P: {value} V") return value @voltage.setter def voltage(self, value: float) -> None: """Set the output voltage""" validate_number("voltage", value, (0, self.max_voltage), logger=logger) value_str = f"VSET1:{value:.2f}" logger.debug(f'Set output voltage ({value} V) with: "{value_str}"') self.com.write(value_str) @property def current(self) -> float: """Return the measured output current in A""" value = float(self.com.query_not_none("IOUT1?")) logger.debug(f"Current at Output 1 of KA3000P: {value} A") return value @current.setter def current(self, value: float) -> None: """Set the output current""" validate_number("current", value, (0, self.max_current), logger=logger) value_str = f"ISET1:{value:.3f}" logger.debug(f'Set output current ({value} A) with: "{value_str}"') self.com.write(value_str) @property def set_voltage(self) -> float: """Return the set voltage (may differ from actual value) in V""" value = float(self.com.query_not_none("VSET1?")) logger.debug(f"Set Voltage at Output 1 of KA3000P: {value} V") return value @set_voltage.setter def set_voltage(self, value: float) -> None: """Set the output voltage""" self.voltage = value @property def set_current(self) -> float: """Return the set current (may differ from actual value) in A""" value = float(self.com.query_not_none("ISET1?")) logger.debug(f"Set Current at Output 1 of KA3000P: {value} A") return value @set_current.setter def set_current(self, value: float) -> None: """Set the output current""" self.current = value @property def max_voltage(self) -> float: """Maximal output voltage of the hardware in V, but user can reset to a lower value""" return self._max_voltage_hardware @property def max_current(self) -> float: """Maximal output current of the hardware in A, but user can reset to a lower value""" return self._max_current_hardware # Output stage @property def output(self) -> bool | None: """State of the high voltage output""" return self.status.output @output.setter def output(self, value: bool) -> None: """ Activates the output of the source :param value: `True` for activation, `False` for deactivation """ validate_bool("output", value, logger=logger) value_str = f"OUT{1 if value else 0}" logger.debug(f'Set output ({"ON" if value else "OFF"}) with: "{value_str}"') self.com.write(value_str) # Beep @property def beep(self) -> bool: """State of the beep""" return self.status.beep @beep.setter def beep(self, value: bool) -> None: """ Activates the beep of the source :param value: `True` for activation, `False` for deactivation """ validate_bool("beep", value, logger=logger) value_str = f"BEEP{1 if value else 0}" logger.debug(f'Set beep ({"ON" if value else "OFF"}) with: "{value_str}"') self.com.write(value_str) # Lock Screen @property def lock(self) -> bool: """State of the button lock""" msg = "The state of the button lock cannot be read. It is write-only..." logger.error(msg) raise NotImplementedError(msg) @lock.setter def lock(self, value: bool) -> None: """Lock or unlock the hardware buttons at the front panel :param value: `True` to lock the buttons, `False` to enable the manuel buttons """ validate_bool("lock", value, logger=logger) value_str = f"LOCK{1 if value else 0}" logger.debug(f'Set lock ({"ON" if value else "OFF"}) with: "{value_str}"') self.com.write(value_str) # Controlled Voltage or Current @property def controlled_voltage(self) -> bool: """If the power supply operates at voltage limiting mode. Opposite to `controlled_current`. :return: Output is voltage limited """ return self.status.ch1_cv @property def controlled_current(self) -> bool: """If the power supply operates at current limiting mode. Opposite to `controlled_voltage`. :return: Output is current limited """ return not self.status.ch1_cv # Save and Recall settings (unfortunately, also write-only) @property def save(self) -> int: """Save a setting""" msg = "The save the settings cannot be read. It is write-only..." logger.error(msg) raise NotImplementedError(msg) @save.setter def save(self, value: int) -> None: """Save the panel settings to a given slot. Available slot: 1...5 :param value: `int` between 1 and 5 to select the slot """ validate_number("save", value, (1, 5), number_type=int, logger=logger) value_str = f"SAV{value}" logger.debug(f'Save settings to slot {value} with: "{value_str}"') self.com.write(value_str) @property def recall(self) -> int: """Recall a setting""" msg = "The recall a setting cannot be read. It is write-only..." logger.error(msg) raise NotImplementedError(msg) @recall.setter def recall(self, value: int) -> None: """Recall the panel settings from a given slot. Available slot: 1...5 :param value: `int` between 1 and 5 to select the slot """ validate_number("recall", value, (1, 5), number_type=int, logger=logger) value_str = f"RCL{value}" logger.debug(f'Recall a setting from slot {value} with: "{value_str}"') self.com.write(value_str)
[docs] @contextlib.contextmanager def save_settings_to(self, slot: int) -> Generator[None, Any, None]: """Method to save settings to a slot. Use this method with a `with`-block. It will recall a slot and save the settings to it. Example: .. code-block:: python slot = 3 with ka3005p.save_settings_to(slot): ka3005p.voltage = 2.5 ka3005p.current = 0.75 :param slot: Slot 1...5 """ self.recall = slot yield sleep(self.config.time_to_wait_before_save_settings) self.save = slot
# OCP @property def ocp(self) -> bool: """State of the over current protection""" return self.status.ocp @ocp.setter def ocp(self, value: bool) -> None: """ Activates the over current protection :param value: `True` for activation, `False` for deactivation """ validate_bool("ocp", value, logger=logger) value_str = f"OCP{1 if value else 0}" logger.debug( f"Set over current protection ({'ON' if value else 'OFF'}) " f'with: "{value_str}"' ) self.com.write(value_str) # OVP @property def ovp(self) -> bool: """State of the over voltage protection""" return self.status.ovp @ovp.setter def ovp(self, value: bool) -> None: """ Activates the over voltage protection :param value: `True` for activation, `False` for deactivation """ validate_bool("ovp", value, logger=logger) value_str = f"OVP{1 if value else 0}" logger.debug( f"Set over voltage protection ({'ON' if value else 'OFF'}) " f'with: "{value_str}"' ) self.com.write(value_str)