ekfsm 1.1.0a15.post1__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ekfsm might be problematic. Click here for more details.

ekfsm/devices/mux.py CHANGED
@@ -1,8 +1,11 @@
1
1
  from ekfsm.core.components import SysTree
2
+ from ekfsm.log import ekfsm_logger
2
3
 
3
- from ..core.sysfs import SysFSDevice
4
+ from ..core.sysfs import SysfsDevice
4
5
  from .generic import Device
5
6
 
7
+ logger = ekfsm_logger(__name__)
8
+
6
9
 
7
10
  class MuxChannel(Device):
8
11
  """
@@ -25,20 +28,21 @@ class MuxChannel(Device):
25
28
  def __init__(
26
29
  self,
27
30
  name: str,
28
- channel_id: int,
29
31
  parent: "I2CMux",
30
32
  children: list[Device] | None = None,
33
+ abort=False,
34
+ channel_id: int | None = None,
31
35
  *args,
32
36
  **kwargs,
33
37
  ) -> None:
34
- super().__init__(name=name, parent=parent, children=children)
38
+ super().__init__(name, parent, children, abort, *args, **kwargs)
35
39
  self.channel_id = channel_id
36
40
 
37
41
  assert parent.sysfs_device is not None
38
42
  assert isinstance(self.parent, I2CMux)
39
43
 
40
44
  path = parent.sysfs_device.path / f"channel-{self.channel_id}"
41
- self.sysfs_device = SysFSDevice(path)
45
+ self.sysfs_device = SysfsDevice(path, False)
42
46
 
43
47
 
44
48
  class I2CMux(Device):
@@ -59,11 +63,12 @@ class I2CMux(Device):
59
63
  self,
60
64
  name: str,
61
65
  parent: SysTree | None = None,
62
- children: list[MuxChannel] | None = None,
66
+ children: list[Device] | None = None,
67
+ abort=False,
63
68
  *args,
64
69
  **kwargs,
65
70
  ):
66
- super().__init__(name, parent, children, **kwargs)
71
+ super().__init__(name, parent, children, abort, *args, **kwargs)
67
72
 
68
73
  self.addr = self.get_i2c_chip_addr()
69
74
  self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
ekfsm/devices/pmbus.py CHANGED
@@ -1,20 +1,17 @@
1
+ import re
1
2
  from enum import IntFlag
2
- from pathlib import Path
3
+ from functools import wraps
4
+ from time import sleep
3
5
 
4
6
  from ekfsm.core.components import SysTree
7
+ from ekfsm.exceptions import HWMonError
8
+ from ekfsm.log import ekfsm_logger
5
9
 
6
- from ..core.sysfs import SysFSDevice, sysfs_root
7
-
8
- from .generic import Device
9
10
  from ..core.probe import ProbeableDevice
11
+ from ..core.sysfs import list_sysfs_attributes, sysfs_root
12
+ from .generic import Device
10
13
 
11
- from time import sleep
12
- from functools import wraps
13
- from ekfsm.log import ekfsm_logger
14
- from threading import Lock
15
- import re
16
-
17
- __all__ = ["PsuStatus", "PmBus", "retry"]
14
+ __all__ = ["PSUStatus", "PMBus", "retry"]
18
15
 
19
16
  logger = ekfsm_logger(__name__)
20
17
 
@@ -34,8 +31,8 @@ def retry(max_attempts=5, delay=0.5):
34
31
 
35
32
  Important
36
33
  ---------
37
- This decorator is thread-safe, meaning a read attempt is atomic and cannot
38
- be interupted by scheduler.
34
+ This decorator is _not_ thread-safe across multiple ekfsm processes. Unfortunately,
35
+ we cannot use fcntl or flock syscalls with files on virtual filesystems like sysfs.
39
36
 
40
37
  Parameters
41
38
  ----------
@@ -45,26 +42,19 @@ def retry(max_attempts=5, delay=0.5):
45
42
  The delay in seconds between attempts.
46
43
  """
47
44
 
48
- lock = Lock()
49
-
50
45
  def decorator(func):
51
46
  @wraps(func)
52
47
  def wrapper(*args, **kwargs):
53
48
  attempts = 0
54
49
  while attempts < max_attempts:
55
- with lock:
56
- try:
57
- return func(*args, **kwargs)
58
- except Exception as e:
59
- attempts += 1
60
- if attempts == max_attempts:
61
- logger.exception(
62
- f"Failed to execute {func.__name__} after {max_attempts} attempts: {e}"
63
- )
64
- raise e
65
- logger.info(
66
- f"Retrying execution of {func.__name__} in {delay}s..."
67
- )
50
+ try:
51
+ return func(*args, **kwargs)
52
+ except Exception as e:
53
+ attempts += 1
54
+ if attempts == max_attempts:
55
+ logger.exception(f"Failed to execute {func.__name__} after {max_attempts} attempts: {e}")
56
+ raise e
57
+ logger.info(f"Retrying execution of {func.__name__} in {delay}s...")
68
58
  sleep(delay)
69
59
 
70
60
  return wrapper
@@ -72,7 +62,7 @@ def retry(max_attempts=5, delay=0.5):
72
62
  return decorator
73
63
 
74
64
 
75
- class PsuStatus(IntFlag):
65
+ class PSUStatus(IntFlag):
76
66
  """
77
67
  Represents the status of a PSU according to STATUS_BYTE register.
78
68
 
@@ -93,9 +83,8 @@ class PsuStatus(IntFlag):
93
83
  >>> PsuStatus.OK in status
94
84
  True
95
85
  >>> # Instead, check if status is OK
96
- >>> status = PsuStatus(0x00)
97
- >>> status
98
- <PsuStatus.OK: 0>
86
+ >>> status == PsuStatus(0x00)
87
+ False
99
88
  >>> PsuStatus.OUTPUT_OVERCURRENT in status
100
89
  False
101
90
  """
@@ -109,44 +98,47 @@ class PsuStatus(IntFlag):
109
98
  OK = 0x00
110
99
 
111
100
 
112
- class PmBus(Device, ProbeableDevice):
113
- """
114
- This class represents a PMBus device (e.g. a PSU).
115
- """
101
+ logger = ekfsm_logger(__name__)
102
+
116
103
 
104
+ class PMBus(Device, ProbeableDevice):
117
105
  def __init__(
118
106
  self,
119
107
  name: str,
120
108
  parent: SysTree | None = None,
121
109
  children: list[Device] | None = None,
110
+ abort: bool = False,
122
111
  *args,
123
112
  **kwargs,
124
113
  ):
125
- super().__init__(name, parent, children, *args, **kwargs)
114
+ super().__init__(name, parent, children, abort, *args, **kwargs)
126
115
  self.addr = self.get_i2c_chip_addr()
127
- self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
128
-
129
- files = list(Path(self.sysfs_device.path).rglob("hwmon/*/in1_input"))
130
- if len(files) == 0:
131
- raise FileNotFoundError("No HWMON entries found in sysfs")
132
- self.hwmon_sysfs = SysFSDevice(files[0].parent)
133
-
134
- self.debugfs_root = sysfs_root() / "kernel/debug/pmbus"
135
- files = list(self.debugfs_root.rglob("hwmon*/status*_input"))
136
- if len(files) == 0:
137
- raise FileNotFoundError("No HWMON entries found in debugfs")
138
- self.hwmon_debugfs = SysFSDevice(files[0].parent)
116
+ self.sysfs_device = self.get_i2c_sysfs_device(self.addr, driver_required=True)
117
+
118
+ try:
119
+ for entry in self.sysfs_device.path.glob("hwmon/hwmon*"):
120
+ if entry.is_dir():
121
+ attrs = list_sysfs_attributes(entry)
122
+ self.sysfs_device.extend_attributes(attrs)
123
+
124
+ debug_attrs_path = sysfs_root().joinpath(f"kernel/debug/pmbus/{entry.name}")
125
+ debug_attrs = list_sysfs_attributes(debug_attrs_path)
126
+ self.sysfs_device.extend_attributes(debug_attrs)
127
+ except FileNotFoundError:
128
+ logger.debug("Expected sysfs attribute not found")
129
+ except StopIteration:
130
+ raise HWMonError("Device is not managed by hwmon subsystem")
139
131
 
140
132
  def probe(self, *args, **kwargs) -> bool:
141
- from ekfsm.core import HwModule
133
+ from ekfsm.core import HWModule
142
134
 
143
- assert isinstance(self.hw_module, HwModule)
135
+ assert isinstance(self.hw_module, HWModule)
144
136
  # compare the regexp from the board yaml file with the model
145
137
  return re.match(self.hw_module.id, self.model()) is not None
146
138
 
147
139
  # Voltage and Current Interfaces
148
- def _conversion(self, in_file: str) -> float:
149
- return float(self.hwmon_sysfs.read_attr_utf8(in_file)) / 1000.0
140
+ def __convert_and_scale(self, attr: str) -> float:
141
+ return self.sysfs.read_float(attr) / 1000.0
150
142
 
151
143
  @retry()
152
144
  def in1_input(self) -> float:
@@ -157,7 +149,7 @@ class PmBus(Device, ProbeableDevice):
157
149
  -------
158
150
  Input voltage in volts
159
151
  """
160
- return self._conversion("in1_input")
152
+ return self.__convert_and_scale("in1_input")
161
153
 
162
154
  @retry()
163
155
  def in2_input(self) -> float:
@@ -168,7 +160,7 @@ class PmBus(Device, ProbeableDevice):
168
160
  -------
169
161
  Input voltage in volts
170
162
  """
171
- return self._conversion("in2_input")
163
+ return self.__convert_and_scale("in2_input")
172
164
 
173
165
  @retry()
174
166
  def curr1_input(self) -> float:
@@ -179,7 +171,7 @@ class PmBus(Device, ProbeableDevice):
179
171
  -------
180
172
  Input current in amperes
181
173
  """
182
- return self._conversion("curr1_input")
174
+ return self.__convert_and_scale("curr1_input")
183
175
 
184
176
  @retry()
185
177
  def curr2_input(self) -> float:
@@ -190,32 +182,32 @@ class PmBus(Device, ProbeableDevice):
190
182
  -------
191
183
  Input current in amperes
192
184
  """
193
- return self._conversion("curr2_input")
185
+ return self.__convert_and_scale("curr2_input")
194
186
 
195
187
  # Status Interface
196
188
  @retry()
197
- def status0_input(self) -> PsuStatus:
189
+ def status0_input(self) -> PSUStatus:
198
190
  """
199
191
  Get the status of PSU page 1.
200
192
 
201
193
  Returns
202
194
  -------
203
- PSU status as defined in PsuStatus
195
+ PSU status as defined in PSUStatus
204
196
  """
205
- status = int(self.hwmon_debugfs.read_attr_utf8("status0_input").strip(), 16)
206
- return PsuStatus(status)
197
+ status = self.sysfs.read_int("status0_input")
198
+ return PSUStatus(status)
207
199
 
208
200
  @retry()
209
- def status1_input(self) -> PsuStatus:
201
+ def status1_input(self) -> PSUStatus:
210
202
  """
211
203
  Get the status of PSU page 2.
212
204
 
213
205
  Returns
214
206
  -------
215
- PSU status as defined in PsuStatus
207
+ PSU status as defined in PSUStatus
216
208
  """
217
- status = int(self.hwmon_debugfs.read_attr_utf8("status1_input").strip(), 16)
218
- return PsuStatus(status)
209
+ status = self.sysfs.read_int("status1_input")
210
+ return PSUStatus(status)
219
211
 
220
212
  # Temperature Interface
221
213
  @retry()
@@ -227,7 +219,7 @@ class PmBus(Device, ProbeableDevice):
227
219
  -------
228
220
  PSU temperature in degrees celsius
229
221
  """
230
- return self._conversion("temp1_input")
222
+ return self.__convert_and_scale("temp1_input")
231
223
 
232
224
  # Inventory Interface
233
225
  def vendor(self) -> str:
@@ -238,7 +230,7 @@ class PmBus(Device, ProbeableDevice):
238
230
  -------
239
231
  PSU vendor
240
232
  """
241
- return self.hwmon_sysfs.read_attr_utf8("vendor").strip()
233
+ return self.sysfs.read_utf8("vendor")
242
234
 
243
235
  def model(self) -> str:
244
236
  """
@@ -248,7 +240,7 @@ class PmBus(Device, ProbeableDevice):
248
240
  -------
249
241
  PSU model
250
242
  """
251
- return self.hwmon_sysfs.read_attr_utf8("model").strip()
243
+ return self.sysfs.read_utf8("model")
252
244
 
253
245
  def serial(self) -> str:
254
246
  """
@@ -258,7 +250,7 @@ class PmBus(Device, ProbeableDevice):
258
250
  -------
259
251
  PSU serial number
260
252
  """
261
- return self.hwmon_sysfs.read_attr_utf8("serial").strip()
253
+ return self.sysfs.read_utf8("serial")
262
254
 
263
255
  def revision(self) -> str:
264
256
  """
@@ -268,4 +260,4 @@ class PmBus(Device, ProbeableDevice):
268
260
  -------
269
261
  PSU revision
270
262
  """
271
- return self.hwmon_sysfs.read_attr_utf8("revision").strip()
263
+ return self.sysfs.read_utf8("revision")
ekfsm/devices/smbios.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from pathlib import Path
2
- from ekfsm.core.components import HwModule
3
- from ekfsm.core.sysfs import SysFSDevice, sysfs_root
2
+
3
+ from ekfsm.core.components import HWModule
4
+ from ekfsm.core.sysfs import SysfsDevice, sysfs_root
5
+
4
6
  from .generic import Device
5
7
 
6
8
 
@@ -18,15 +20,15 @@ class SMBIOS(Device):
18
20
  def __init__(
19
21
  self,
20
22
  name: str,
21
- parent: HwModule | None = None,
23
+ parent: HWModule | None = None,
24
+ children: list["Device"] | None = None,
25
+ abort: bool = False,
22
26
  *args,
23
27
  **kwargs,
24
28
  ):
25
- self.sysfs_device: SysFSDevice = SysFSDevice(
26
- sysfs_root() / Path("devices/virtual/dmi/id")
27
- )
29
+ self.sysfs_device: SysfsDevice | None = SysfsDevice(sysfs_root() / Path("devices/virtual/dmi/id"), False)
28
30
 
29
- super().__init__(name, parent, None, *args, **kwargs)
31
+ super().__init__(name, parent, None, abort, *args, **kwargs)
30
32
 
31
33
  def revision(self) -> str:
32
34
  """
@@ -37,4 +39,4 @@ class SMBIOS(Device):
37
39
  str
38
40
  The board revision.
39
41
  """
40
- return self.sysfs_device.read_attr_utf8("board_version").strip()
42
+ return self.sysfs.read_utf8("board_version")
ekfsm/devices/smbus.py CHANGED
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from typing import List
3
3
 
4
4
 
5
- class SimSmbus(ABC):
5
+ class SimSMBus(ABC):
6
6
  @abstractmethod
7
7
  def read_word_data(self, cmd: int) -> int:
8
8
  pass
ekfsm/devices/utils.py CHANGED
@@ -1,13 +1,4 @@
1
1
  from crcmod.predefined import Crc
2
- from typing import Sequence
3
-
4
-
5
- def compute_int_from_bytes(data: Sequence[int]) -> int:
6
- # Combine the bytes into a single integer
7
- result = 0
8
- for num in data:
9
- result = (result << 8) | num
10
- return result
11
2
 
12
3
 
13
4
  def get_crc16_xmodem(data: bytes) -> int:
ekfsm/exceptions.py CHANGED
@@ -3,6 +3,7 @@ from enum import Enum
3
3
 
4
4
  class EkfSmException(Exception):
5
5
  """Base class for all exceptions in the EKFSM Library"""
6
+
6
7
  pass
7
8
 
8
9
 
@@ -12,7 +13,7 @@ class ConfigError(EkfSmException):
12
13
  pass
13
14
 
14
15
 
15
- class SYSFSError(EkfSmException):
16
+ class SysFSError(EkfSmException):
16
17
  """Error while handling sysfs pseudo file system"""
17
18
 
18
19
  pass
@@ -31,9 +32,31 @@ class GPIOError(EkfSmException):
31
32
  def __init__(self, error_type: ErrorType, details: str | None = None):
32
33
  self.error_type = error_type
33
34
  self.details = details
34
- super().__init__(
35
- f"{error_type.value}: {details}" if details else error_type.value
36
- )
35
+ super().__init__(f"{error_type.value}: {details}" if details else error_type.value)
36
+
37
+
38
+ class DriverError(EkfSmException):
39
+ """No driver found for device"""
40
+
41
+ pass
42
+
43
+
44
+ class HWMonError(EkfSmException):
45
+ """No HwMon entry found for device"""
46
+
47
+ pass
48
+
49
+
50
+ class ConversionError(EkfSmException):
51
+ """Failed to convert"""
52
+
53
+ pass
54
+
55
+
56
+ class UnsupportedModeError(EkfSmException):
57
+ """Format not supported"""
58
+
59
+ pass
37
60
 
38
61
 
39
62
  class FirmwareNodeError(EkfSmException):
@@ -47,9 +70,7 @@ class DataCorruptionError(EkfSmException):
47
70
 
48
71
  def __init__(self, details: str | None = None):
49
72
  self.details = details
50
- super().__init__(
51
- f"Data corruption: {details}" if details else "Data corruption"
52
- )
73
+ super().__init__(f"Data corruption: {details}" if details else "Data corruption")
53
74
 
54
75
 
55
76
  class AcquisitionError(EkfSmException):
ekfsm/lock.py CHANGED
@@ -1,22 +1,23 @@
1
+ """
2
+ Some devices or device functions don't allow concurrent access.
3
+ The locking mechanism is used to ensure that only one process/thread can
4
+ access the device (function) at a time.
5
+
6
+ Locking granularity is defined by the device.
7
+ It may be at the device level or function level.
8
+
9
+ Application can choose to use the locking mechanism or not.
10
+ By default, the locking mechanism is enabled and uses the default
11
+ lockfile root directory ``/var/lock/ekfsm``.
12
+
13
+ Use :func:`locking_configure` to enable or disable the locking mechanism or to change
14
+ the lockfile root directory.
15
+ """
16
+
1
17
  import fcntl
2
18
  import os
3
- from pathlib import Path
4
19
  from contextlib import contextmanager
5
-
6
- #
7
- # Some devices or device functions don't allow concurrent access.
8
- # The locking mechanism is used to ensure that only one process/thread can
9
- # access the device (function) at a time.
10
- #
11
- # Locking granularity is defined by the device.
12
- # It may be at the device level or function level.
13
- #
14
- # Application can choose to use the locking mechanism or not.
15
- # By default, the locking mechanism is enabled and uses the default
16
- # lockfile root directory /var/lock/ekfsm.
17
- #
18
- # Use locking_configure() to enable or disable the locking mechanism or to change
19
- # the lockfile root directory.
20
+ from pathlib import Path
20
21
 
21
22
  USE_LOCK = True
22
23
  LOCKFILE_ROOT = "/var/lock/ekfsm"
@@ -27,8 +28,12 @@ def locking_configure(enable: bool, lockfile_root: str = LOCKFILE_ROOT):
27
28
  """
28
29
  Configures the locking mechanism.
29
30
 
30
- :param enable: Whether to enable or disable locking.
31
- :param lockfile_root: The root directory for lockfiles. (default: /var/lock/ekfsm)
31
+ Parameters
32
+ ----------
33
+ enable
34
+ Whether to enable or disable locking.
35
+ lockfile_root
36
+ The root directory for lockfiles.
32
37
  """
33
38
  global USE_LOCK, LOCKFILE_ROOT
34
39
  USE_LOCK = enable
@@ -55,9 +60,12 @@ class Locker:
55
60
 
56
61
  Example
57
62
  -------
58
- with Locker("mysharedresourcename").lock():
59
- # Access the shared resource here
60
- pass
63
+ .. code-block:: python
64
+
65
+ with Locker("mysharedresourcename").lock():
66
+ # Access the shared resource here
67
+ pass
68
+
61
69
  """
62
70
 
63
71
  def __init__(self, module: str):
@@ -72,6 +80,17 @@ class Locker:
72
80
  ALL_LOCKERS.append(self)
73
81
 
74
82
  def cleanup(self):
83
+ """
84
+ Cleans up the lock file and closes the lock file descriptor.
85
+
86
+ Important
87
+ ---------
88
+ This method should be called when the lock is no longer needed.
89
+
90
+ Note
91
+ ----
92
+ It is automatically called when the context manager exits.
93
+ """
75
94
  if not USE_LOCK:
76
95
  return
77
96
  if self.lock_fd is not None:
@@ -84,6 +103,14 @@ class Locker:
84
103
 
85
104
  @contextmanager
86
105
  def lock(self):
106
+ """
107
+ Locks the resource for exclusive access.
108
+
109
+ Note
110
+ ----
111
+ This method is a context manager that locks the resource when entered
112
+ and releases the lock when exited.
113
+ """
87
114
  if not USE_LOCK:
88
115
  yield
89
116
  self.lock_fd = os.open(self.lockfile_path, os.O_RDWR)