# 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, Optional
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) -> Optional[bool]:
"""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)