# Copyright (c) ETH Zurich, SIS ID and HVL D-ITET
#
"""
The very special "AdvancedCube" with its two single implementations "SynthCube" and
"FPCSCube".
"""
import logging
from collections.abc import Iterator
from hvl_ccb.configuration import configdataclass
from hvl_ccb.utils.enum import BoolEnum
from hvl_ccb.utils.typing import Number
from .base import BaseCube, BaseCubeConfiguration
from .constants import SafetyStatus, _Door
from .errors import (
AdvancedCubeModuleError,
ChargerSwitchError,
DischargeError,
ShortCircuitError,
)
from .switches import SwitchOperation, _Switch
logger = logging.getLogger(__name__)
[docs]
class ActivationStatus(BoolEnum):
"""
Boolean activation status of an advanced cube output bit
"""
ACTIVATED = True
DEACTIVATED = False
[docs]
class PressurizationStatus(BoolEnum):
"""
Boolean status of an advanced cube pressure sensor
"""
PRESSURIZED = True
DEPRESSURIZED = False
class _DischargeSwitch(_Switch):
"""
Discharge switch within a module of the advanced cube.
"""
_ERROR_CLS = DischargeError
_STATES_FOR_SWITCHING = (
SafetyStatus.RED_READY,
SafetyStatus.RED_OPERATE,
)
def __init__(self, handle, module_number: int) -> None:
super().__init__(handle, device_name=f"Discharge Switch {module_number}")
self._CMD_STATUS = (
f'"DB_Safety_Circuit"."Module_{module_number}".'
'"Discharge_Circuit"."si_HMI_Status"'
)
self._CMD_OPERATING_STATUS = (
f'"DB_Safety_Circuit"."Module_{module_number}"."sx_able_to_discharge"'
)
self._CMD_MANUAL = (
f'"DB_Safety_Circuit"."Module_{module_number}"."sx_discharge"'
)
class _ShortCircuitSwitch(_Switch):
"""
Short-circuit switch within a module of the advanced cube.
"""
_ERROR_CLS = ShortCircuitError
def __init__(self, handle, module_number: int) -> None:
identifier = (
"Central_Earthing" if module_number == 0 else f"Module_{module_number}"
)
super().__init__(handle, device_name=f"Short-Circuit Switch {identifier}")
identifier_extended = (
identifier if module_number == 0 else identifier + '"."Short_Circuit'
)
self._CMD_STATUS = (
f'"DB_Safety_Circuit"."{identifier_extended}"."si_HMI_Status"'
)
self._CMD_OPERATING_STATUS = (
f'"DB_Safety_Circuit"."{identifier}"."sx_able_to_short_circuit"'
)
self._CMD_MANUAL = None
class _ChargerSwitch(_Switch):
"""
Charger switch within a module of the advanced cube.
"""
_ERROR_CLS = ChargerSwitchError
_STATES_FOR_SWITCHING = (SafetyStatus.RED_OPERATE,)
def __init__(self, handle, module_number: int) -> None:
identifier = (
"Central_Charger" if module_number == 0 else f"Module_{module_number}"
)
super().__init__(handle, device_name=f"Charger Switch {identifier}")
identifier_extended = (
identifier if module_number == 0 else identifier + '"."Charger_Circuit'
)
self._CMD_STATUS = (
f'"DB_Safety_Circuit"."{identifier_extended}"."si_HMI_Status"'
)
self._CMD_OPERATING_STATUS = (
f'"DB_Safety_Circuit"."{identifier}"."sx_able_to_charge"'
)
self._CMD_MANUAL = f'"DB_Safety_Circuit"."{identifier}"."sx_charge"'
class _Module:
"""
Module level interface between AdvancedCube and current sources.
"""
_ACTIVATABLE_STATES = (
SafetyStatus.GREEN_NOT_READY,
SafetyStatus.GREEN_READY,
)
def __init__(self, handle, index: int) -> None:
self._handle: AdvancedCube = handle
self._index: int = index
self.discharge_switch: _DischargeSwitch = _DischargeSwitch(handle, index)
self.short_circuit_switch: _ShortCircuitSwitch = _ShortCircuitSwitch(
handle, index
)
self.charger_switch: _ChargerSwitch = _ChargerSwitch(handle, index)
self._CMD_ACTIVE: str = f'"DB_Safety_Circuit"."Module_{index}"."sx_dial"'
self._CMD_VOLTAGE: str = f'"DB_Safety_Circuit"."Module_{index}"."si_voltage"'
self._CMD_MAX_SC_SWITCHING_VOLTAGE: str = (
f'"DB_Safety_Circuit"."Module_{index}".'
'"Ir_max_short_circuit_switching_voltage"'
)
def __repr__(self) -> str:
return f"Module {self._index}"
@property
def active(self) -> ActivationStatus:
"""
Module activation status
:return: boolean module activation status
"""
activation_status = ActivationStatus(self._handle.read(self._CMD_ACTIVE))
logger.info(f"Module {self._index} is {activation_status.name}")
return activation_status
@active.setter
def active(self, new_activation_status: ActivationStatus):
new_activation_status = ActivationStatus(new_activation_status)
if self._handle.status in self._ACTIVATABLE_STATES:
self._handle.write(self._CMD_ACTIVE, new_activation_status)
logger.info(f"Module {self._index} set to {new_activation_status.name}")
else:
msg = (
f"Tried to set Module {self._index} to {new_activation_status.name}. "
"Safety circuit must be in green state to change the activation "
f"status of a module, but is in {self._handle.status.name}"
)
logger.error(msg)
raise AdvancedCubeModuleError(msg)
@property
def voltage(self) -> int:
"""
Measured voltage of a module.
:return: measured voltage in V
"""
voltage = int(self._handle.read(self._CMD_VOLTAGE))
logger.info(f"Module {self._index} voltage: {voltage} V")
return voltage
@property
def max_short_circuit_switching_voltage(self) -> int:
"""
Maximum switching voltage of a module.
:return: maximum switching voltage in V
"""
voltage = int(self._handle.read(self._CMD_MAX_SC_SWITCHING_VOLTAGE))
logger.info(
f"Maximum short-circuit switching voltage of Module {self._index}"
f" is set to {voltage} V"
)
return voltage
class _Pneumatics:
"""
Pneumatic circuit of a current source controlled by an AdvancedCube
"""
_CMD_STATUS = '"Ix_Allg_pressure_ok"'
_CMD_ACTIVE = '"Qx_Allg_Main_Pressure"'
def __init__(self, handle) -> None:
self._handle: AdvancedCube = handle
@property
def status(self) -> PressurizationStatus:
"""
Pneumatic circuit pressure status
:return: boolean pressurization status
"""
status = PressurizationStatus(self._handle.read(self._CMD_STATUS))
logger.info(f"Pneumatic circuit is {status.name}")
return status
@property
def active(self) -> ActivationStatus:
"""
Pressure valve activation status
:return: boolean pressure valve activation status
"""
activation_status = ActivationStatus(self._handle.read(self._CMD_ACTIVE))
logger.info(f"Main pressure valve is {activation_status.name}")
return activation_status
@active.setter
def active(self, new_activation_status: ActivationStatus):
new_activation_status = ActivationStatus(new_activation_status)
self._handle.write(self._CMD_ACTIVE, new_activation_status)
logger.info(f"Main pressure valve {new_activation_status.name}")
class _Siren:
"""
Acoustic alert device controlled by an AdvancedCube
"""
def __init__(self, handle, index: int, name: str) -> None:
self._handle: AdvancedCube = handle
self._name = name
self._CMD_ACTIVE: str = f'"Qx_Allg_Acoustic_{index}"'
@property
def active(self) -> ActivationStatus:
"""
Siren activation status
:return: boolean siren activation status
"""
active = ActivationStatus(self._handle.read(self._CMD_ACTIVE))
logger.info(f"{self._name} siren is {active.name}")
return active
@active.setter
def active(self, active: ActivationStatus):
active = ActivationStatus(active)
self._handle.write(self._CMD_ACTIVE, active)
logger.info(f"{self._name} siren has been {active.name}")
[docs]
@configdataclass
class AdvancedCubeConfiguration(BaseCubeConfiguration):
"""Adaption of base configuration to accept longer time to wait for a discharge"""
timeout_status_change: Number = 20
[docs]
class AdvancedCube(BaseCube):
"""
The AdvancedCube is the template for its two special implementations
"""
_N_MODULES: int | None = None
def __init__(self, com, dev_config=None) -> None:
super().__init__(com, dev_config)
self.modules: dict[int, _Module] = {}
self.pneumatics: _Pneumatics | None = None
self.charging_siren: _Siren | None = None
self.triggering_siren: _Siren | None = None
[docs]
@staticmethod
def config_cls():
return AdvancedCubeConfiguration
@property
def number_of_modules(self) -> int:
"""
Queries the number of installed modules (active plus inactive)
"""
if self._N_MODULES is None:
msg = "No number of modules was set in implementation."
logger.error(msg)
raise AdvancedCubeModuleError(msg)
val = self._N_MODULES
logger.info(f"{self.__class__.__name__} has {val} modules installed.")
return val
[docs]
def start(self) -> None:
"""
Start the connection to the AdvancedCube and set up the modules
"""
super().start()
try:
_ = self.number_of_modules
except AdvancedCubeModuleError:
self.stop()
raise
for ii in range(self.number_of_modules):
self.modules[ii + 1] = _Module(self, ii + 1)
self.pneumatics = _Pneumatics(self)
self.charging_siren = _Siren(self, 1, name="Charging")
self.triggering_siren = _Siren(self, 2, name="Triggering")
@property
def active_modules(self) -> Iterator[_Module]:
"""
Iterator for all active modules
"""
for module in self.modules.values():
if module.active == ActivationStatus.ACTIVATED:
yield module
@property
def operate_discharge_switches(self) -> list[SwitchOperation]:
"""
Property to query all discharge switches of the installed/active modules
:return: `list[SwitchOperation]` of the individual discharge switches
"""
return [module.discharge_switch.operate for module in self.active_modules]
@operate_discharge_switches.setter
def operate_discharge_switches(self, operation: SwitchOperation) -> None:
"""
Operate all discharge switches of the installed/active modules
:param operation: a `SwitchOperation` to `OPEN` or `CLOSE`
"""
for module in self.active_modules:
module.discharge_switch.operate = operation
[docs]
class SynthCube(AdvancedCube):
"""
The SynthCube is the special Cube for the Synthetic Circuit
"""
_N_MODULES = 4
def __init__(self, com, dev_config=None) -> None:
super().__init__(com, dev_config)
self.hcc_loop_1: _Module | None = None
self.hcc_loop_2: _Module | None = None
self.hcc_loop_3: _Module | None = None
self.cic: _Module | None = None
[docs]
def start(self) -> None:
super().start()
self.hcc_loop_1 = self.modules[1]
self.hcc_loop_2 = self.modules[2]
self.hcc_loop_3 = self.modules[3]
self.cic = self.modules[4]
@property
def active_hcc_loops(self) -> Iterator[_Module]:
"""
Iterator for all active high-current modules
"""
for module in self.active_modules:
if module in (self.hcc_loop_1, self.hcc_loop_2, self.hcc_loop_3):
yield module
[docs]
class FPCSCube(AdvancedCube):
"""
The FPCSCube is the special Cube for the FPCS
"""
_N_MODULES = 8
door_fpcs_status = _Door("FPCS", "door status")
def __init__(self, com, dev_config=None) -> None:
super().__init__(com, dev_config)
self.central_earthing: _ShortCircuitSwitch | None = None
self.central_charger: _ChargerSwitch | None = None
self.module: dict[str, _Module] = {}
[docs]
def start(self) -> None:
super().start()
for ii in range(self._N_MODULES):
self.module[chr(ord("A") + ii)] = self.modules[ii + 1]
self.module[chr(ord("a") + ii)] = self.modules[ii + 1]
self.central_charger = _ChargerSwitch(self, 0)
self.central_earthing = _ShortCircuitSwitch(self, 0)