Source code for ekfsm.devices.pmbus

from enum import IntFlag
from pathlib import Path

from ekfsm.core.components import SysTree

from ..core.sysfs import SysFSDevice, sysfs_root

from .generic import Device
from ..core.probe import ProbeableDevice

from time import sleep
from functools import wraps
from ekfsm.log import ekfsm_logger
from threading import Lock
import re

__all__ = ["PsuStatus", "PmBus", "retry"]

logger = ekfsm_logger(__name__)


[docs] def retry(max_attempts=5, delay=0.5): """ Retry decorator. Decorator that retries a function a number of times before giving up. This is useful for functions that may fail due to transient errors. Note ---- This is needed for certain PMBus commands that may fail due to transient errors because page switching timing is not effectively handled by older kernel versions. Important --------- This decorator is thread-safe, meaning a read attempt is atomic and cannot be interupted by scheduler. Parameters ---------- max_attempts The maximum number of attempts before giving up. delay The delay in seconds between attempts. """ lock = Lock() def decorator(func): @wraps(func) def wrapper(*args, **kwargs): attempts = 0 while attempts < max_attempts: with lock: try: return func(*args, **kwargs) except Exception as e: attempts += 1 if attempts == max_attempts: logger.exception( f"Failed to execute {func.__name__} after {max_attempts} attempts: {e}" ) raise e logger.info( f"Retrying execution of {func.__name__} in {delay}s..." ) sleep(delay) return wrapper return decorator
[docs] class PsuStatus(IntFlag): """ Represents the status of a PSU according to STATUS_BYTE register. See also -------- External Documentation: `PMBus Power System Management Protocol Specification - Part II - Revision 1.4, Fig. 60 <https://pmbus.org/>`_ Example ------- >>> from ekfsm.devices.pmbus import PsuStatus >>> status = PsuStatus(0x1F) >>> status <PsuStatus.OUTPUT_OVERCURRENT|INPUT_UNDERVOLTAGE|TEMP_ANORMALY|COMMUNICATION_ERROR|ERROR: 31> >>> PsuStatus.OUTPUT_OVERCURRENT in status True >>> # OK is always present >>> PsuStatus.OK in status True >>> # Instead, check if status is OK >>> status = PsuStatus(0x00) >>> status <PsuStatus.OK: 0> >>> PsuStatus.OUTPUT_OVERCURRENT in status False """ OUTPUT_OVERVOLTAGE = 0x20 OUTPUT_OVERCURRENT = 0x10 INPUT_UNDERVOLTAGE = 0x08 TEMP_ANORMALY = 0x04 COMMUNICATION_ERROR = 0x02 ERROR = 0x01 OK = 0x00
[docs] class PmBus(Device, ProbeableDevice): """ This class represents a PMBus device (e.g. a PSU). """ def __init__( self, name: str, parent: SysTree | None = None, children: list[Device] | None = None, *args, **kwargs, ): super().__init__(name, parent, children, *args, **kwargs) self.addr = self.get_i2c_chip_addr() self.sysfs_device = self.get_i2c_sysfs_device(self.addr) files = list(Path(self.sysfs_device.path).rglob("hwmon/*/in1_input")) if len(files) == 0: raise FileNotFoundError("No HWMON entries found in sysfs") self.hwmon_sysfs = SysFSDevice(files[0].parent) self.debugfs_root = sysfs_root() / "kernel/debug/pmbus" files = list(self.debugfs_root.rglob("hwmon*/status*_input")) if len(files) == 0: raise FileNotFoundError("No HWMON entries found in debugfs") self.hwmon_debugfs = SysFSDevice(files[0].parent)
[docs] def probe(self, *args, **kwargs) -> bool: from ekfsm.core import HwModule assert isinstance(self.hw_module, HwModule) # compare the regexp from the board yaml file with the model return re.match(self.hw_module.id, self.model()) is not None
# Voltage and Current Interfaces def _conversion(self, in_file: str) -> float: return float(self.hwmon_sysfs.read_attr_utf8(in_file)) / 1000.0
[docs] @retry() def in1_input(self) -> float: """ Get input voltage of PSU page 1. Returns ------- Input voltage in volts """ return self._conversion("in1_input")
[docs] @retry() def in2_input(self) -> float: """ Get input voltage of PSU page 2. Returns ------- Input voltage in volts """ return self._conversion("in2_input")
[docs] @retry() def curr1_input(self) -> float: """ Get input current of PSU page 1. Returns ------- Input current in amperes """ return self._conversion("curr1_input")
[docs] @retry() def curr2_input(self) -> float: """ Get input current of PSU page 2. Returns ------- Input current in amperes """ return self._conversion("curr2_input")
# Status Interface
[docs] @retry() def status0_input(self) -> PsuStatus: """ Get the status of PSU page 1. Returns ------- PSU status as defined in PsuStatus """ status = int(self.hwmon_debugfs.read_attr_utf8("status0_input").strip(), 16) return PsuStatus(status)
[docs] @retry() def status1_input(self) -> PsuStatus: """ Get the status of PSU page 2. Returns ------- PSU status as defined in PsuStatus """ status = int(self.hwmon_debugfs.read_attr_utf8("status1_input").strip(), 16) return PsuStatus(status)
# Temperature Interface
[docs] @retry() def temp1_input(self) -> float: """ Get the PSU temperature. Returns ------- PSU temperature in degrees celsius """ return self._conversion("temp1_input")
# Inventory Interface
[docs] def vendor(self) -> str: """ Get the vendor of the PSU. Returns ------- PSU vendor """ return self.hwmon_sysfs.read_attr_utf8("vendor").strip()
[docs] def model(self) -> str: """ Get the model of the PSU. Returns ------- PSU model """ return self.hwmon_sysfs.read_attr_utf8("model").strip()
[docs] def serial(self) -> str: """ Get the serial number of the PSU. Returns ------- PSU serial number """ return self.hwmon_sysfs.read_attr_utf8("serial").strip()
[docs] def revision(self) -> str: """ Get the revision of the PSU. Returns ------- PSU revision """ return self.hwmon_sysfs.read_attr_utf8("revision").strip()