ekfsm 0.11.0b1.post3__py3-none-any.whl → 0.13.0a160.post4__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/gpio.py CHANGED
@@ -8,7 +8,7 @@ from gpiod.chip import LineSettings
8
8
  from gpiod.line import Direction, Value
9
9
  from more_itertools import first_true
10
10
 
11
- from ekfsm.core.components import SystemComponent
11
+ from ekfsm.core.components import SysTree
12
12
  from ekfsm.exceptions import GPIOError
13
13
  from ekfsm.log import ekfsm_logger
14
14
 
@@ -55,7 +55,7 @@ class GPIO(Device):
55
55
  def __init__(
56
56
  self,
57
57
  name: str,
58
- parent: SystemComponent | None = None,
58
+ parent: SysTree | None = None,
59
59
  *args,
60
60
  **kwargs,
61
61
  ):
@@ -83,7 +83,7 @@ class GPIO(Device):
83
83
 
84
84
  def _find_gpio_dev(
85
85
  self,
86
- parent: SystemComponent | None = None,
86
+ parent: SysTree | None = None,
87
87
  *args,
88
88
  **kwargs,
89
89
  ) -> tuple[int, int]:
@@ -186,7 +186,7 @@ class GPIOExpander(GPIO):
186
186
  def __init__(
187
187
  self,
188
188
  name: str,
189
- parent: SystemComponent | None,
189
+ parent: SysTree | None,
190
190
  *args,
191
191
  **kwargs,
192
192
  ):
@@ -203,7 +203,7 @@ class EKFIdentificationIOExpander(GPIOExpander, ProbeableDevice):
203
203
  def __init__(
204
204
  self,
205
205
  name: str,
206
- parent: SystemComponent | None,
206
+ parent: SysTree | None,
207
207
  *args,
208
208
  **kwargs,
209
209
  ):
@@ -212,10 +212,11 @@ class EKFIdentificationIOExpander(GPIOExpander, ProbeableDevice):
212
212
  def probe(self, *args, **kwargs) -> bool:
213
213
  from ekfsm.core import HwModule
214
214
 
215
- assert isinstance(self.root, HwModule)
215
+ assert isinstance(self.hw_module, HwModule)
216
216
  id, _ = self.read_board_id_rev()
217
+ self.logger.debug(f"Probing EKFIdentificationIOExpander: {id}")
217
218
 
218
- return self.root.id == id
219
+ return self.hw_module.id == id
219
220
 
220
221
  def read_board_id_rev(self) -> tuple[int, int]:
221
222
  for pin in range(6, 8):
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from ekfsm.core.components import SystemComponent
3
+ from ekfsm.core.components import SysTree
4
4
  from ekfsm.log import ekfsm_logger
5
5
 
6
6
  from ..core.sysfs import SysFSDevice
@@ -18,7 +18,7 @@ class IIOThermalHumidity(Device):
18
18
  def __init__(
19
19
  self,
20
20
  name: str,
21
- parent: SystemComponent | None = None,
21
+ parent: SysTree | None = None,
22
22
  children: list[Device] | None = None,
23
23
  *args,
24
24
  **kwargs,
ekfsm/devices/imu.py ADDED
@@ -0,0 +1,14 @@
1
+ class ImuSample:
2
+ """
3
+ Class to store IMU data sample
4
+
5
+ * accel: list[float] - Accelerometer data in m/s^2, [x, y, z]
6
+ * gyro: list[float] - Gyroscope data in degrees/s, [x, y, z]
7
+ * lost: bool - True if data was lost before that sample
8
+
9
+ """
10
+
11
+ def __init__(self, accel: list[float], gyro: list[float], lost: bool):
12
+ self.accel = accel
13
+ self.gyro = gyro
14
+ self.lost = lost
ekfsm/devices/mux.py CHANGED
@@ -1,4 +1,4 @@
1
- from ekfsm.core.components import SystemComponent
1
+ from ekfsm.core.components import SysTree
2
2
 
3
3
  from ..core.sysfs import SysFSDevice
4
4
  from .generic import Device
@@ -9,7 +9,7 @@ class MuxChannel(Device):
9
9
  self,
10
10
  name: str,
11
11
  channel_id: int,
12
- parent: 'I2CMux',
12
+ parent: "I2CMux",
13
13
  children: list[Device] | None = None,
14
14
  *args,
15
15
  **kwargs,
@@ -28,7 +28,7 @@ class I2CMux(Device):
28
28
  def __init__(
29
29
  self,
30
30
  name: str,
31
- parent: SystemComponent | None = None,
31
+ parent: SysTree | None = None,
32
32
  children: list[MuxChannel] | None = None,
33
33
  *args,
34
34
  **kwargs,
ekfsm/devices/pmbus.py CHANGED
@@ -1,18 +1,100 @@
1
+ from enum import IntFlag
1
2
  from pathlib import Path
2
3
 
3
- from ekfsm.core.components import SystemComponent
4
+ from ekfsm.core.components import SysTree
4
5
 
5
- from ..core.sysfs import SysFSDevice
6
+ from ..core.sysfs import SysFSDevice, sysfs_root
6
7
 
7
8
  from .generic import Device
8
9
  from ..core.probe import ProbeableDevice
9
10
 
11
+ from time import sleep
12
+ from functools import wraps
13
+ from ekfsm.log import ekfsm_logger
14
+ from threading import Lock
15
+
16
+ __all__ = ["PsuStatus", "PmBus", "retry"]
17
+
18
+ logger = ekfsm_logger(__name__)
19
+
20
+
21
+ def retry(max_attempts=5, delay=0.5):
22
+ """
23
+ Retry decorator.
24
+
25
+ Decorator that retries a function a number of times before giving up.
26
+
27
+ This is useful for functions that may fail due to transient errors.
28
+
29
+ Note
30
+ ----
31
+ This is needed for certain PMBus commands that may fail due to transient errors
32
+ because page switching timing is not effectively handled by older kernel versions.
33
+
34
+ Important
35
+ ---------
36
+ This decorator is thread-safe, meaning a read attempt is atomic and cannot
37
+ be interupted by scheduler.
38
+
39
+ Parameters
40
+ ----------
41
+ max_attempts
42
+ The maximum number of attempts before giving up.
43
+ delay
44
+ The delay in seconds between attempts.
45
+ """
46
+
47
+ lock = Lock()
48
+
49
+ def decorator(func):
50
+ @wraps(func)
51
+ def wrapper(*args, **kwargs):
52
+ attempts = 0
53
+ while attempts < max_attempts:
54
+ with lock:
55
+ try:
56
+ return func(*args, **kwargs)
57
+ except Exception as e:
58
+ attempts += 1
59
+ if attempts == max_attempts:
60
+ logger.exception(
61
+ f"Failed to execute {func.__name__} after {max_attempts} attempts: {e}"
62
+ )
63
+ raise e
64
+ logger.info(
65
+ f"Retrying execution of {func.__name__} in {delay}s..."
66
+ )
67
+ sleep(delay)
68
+
69
+ return wrapper
70
+
71
+ return decorator
72
+
73
+
74
+ class PsuStatus(IntFlag):
75
+ """
76
+ Represents the status of a PSU according to STATUS_BYTE register.
77
+
78
+ See Also
79
+ --------
80
+ `PMBus Power System Management Protocol Specification - Part II - Revision 1.4, Fig. 60 <https://pmbus.org/>`_
81
+ """
82
+
83
+ OUTPUT_OVERVOLTAGE = 0x20
84
+ OUTPUT_OVERCURRENT = 0x10
85
+ INPUT_UNDERVOLTAGE = 0x08
86
+ TEMP_ANORMALY = 0x04
87
+ COMMUNICATION_ERROR = 0x02
88
+ ERROR = 0x01
89
+ OK = 0x00
90
+
10
91
 
11
92
  class PmBus(Device, ProbeableDevice):
93
+
12
94
  def __init__(
13
95
  self,
14
96
  name: str,
15
- parent: SystemComponent | None = None,
97
+ parent: SysTree | None = None,
16
98
  children: list[Device] | None = None,
17
99
  *args,
18
100
  **kwargs,
@@ -23,43 +105,143 @@ class PmBus(Device, ProbeableDevice):
23
105
 
24
106
  files = list(Path(self.sysfs_device.path).rglob("hwmon/*/in1_input"))
25
107
  if len(files) == 0:
26
- raise FileNotFoundError("No HWMON entries found")
108
+ raise FileNotFoundError("No HWMON entries found in sysfs")
27
109
  self.hwmon_sysfs = SysFSDevice(files[0].parent)
28
110
 
111
+ self.debugfs_root = sysfs_root() / "kernel/debug/pmbus"
112
+ files = list(self.debugfs_root.rglob("hwmon*/status*_input"))
113
+ if len(files) == 0:
114
+ raise FileNotFoundError("No HWMON entries found in debugfs")
115
+ self.hwmon_debugfs = SysFSDevice(files[0].parent)
116
+
29
117
  def probe(self, *args, **kwargs) -> bool:
30
118
  from ekfsm.core import HwModule
31
119
 
32
- assert isinstance(self.root, HwModule)
33
- return self.root.id == self.model()
120
+ assert isinstance(self.hw_module, HwModule)
121
+ return self.hw_module.id == self.model()
34
122
 
35
123
  # Voltage and Current Interfaces
36
- def _in_conversion(self, in_file: str) -> float:
37
- return float(self.hwmon_sysfs.read_attr_utf8(in_file)) / 1000.0
38
-
39
- def _current_conversion(self, in_file: str) -> float:
124
+ def _conversion(self, in_file: str) -> float:
40
125
  return float(self.hwmon_sysfs.read_attr_utf8(in_file)) / 1000.0
41
126
 
127
+ @retry()
42
128
  def in1_input(self) -> float:
43
- return self._in_conversion("in1_input")
129
+ """
130
+ Get input voltage of PSU page 1.
44
131
 
132
+ Returns
133
+ -------
134
+ Input voltage in volts
135
+ """
136
+ return self._conversion("in1_input")
137
+
138
+ @retry()
45
139
  def in2_input(self) -> float:
46
- return self._in_conversion("in2_input")
140
+ """
141
+ Get input voltage of PSU page 2.
142
+
143
+ Returns
144
+ -------
145
+ Input voltage in volts
146
+ """
147
+ return self._conversion("in2_input")
47
148
 
149
+ @retry()
48
150
  def curr1_input(self) -> float:
49
- return self._current_conversion("curr1_input")
151
+ """
152
+ Get input current of PSU page 1.
50
153
 
154
+ Returns
155
+ -------
156
+ Input current in amperes
157
+ """
158
+ return self._conversion("curr1_input")
159
+
160
+ @retry()
51
161
  def curr2_input(self) -> float:
52
- return self._current_conversion("curr2_input")
162
+ """
163
+ Get input current of PSU page 2.
164
+
165
+ Returns
166
+ -------
167
+ Input current in amperes
168
+ """
169
+ return self._conversion("curr2_input")
170
+
171
+ # Status Interface
172
+ @retry()
173
+ def status0_input(self) -> PsuStatus:
174
+ """
175
+ Get the status of PSU page 1.
176
+
177
+ Returns
178
+ -------
179
+ PSU status as defined in PsuStatus
180
+ """
181
+ status = int(self.hwmon_debugfs.read_attr_utf8("status0_input").strip(), 16)
182
+ return PsuStatus(status)
183
+
184
+ @retry()
185
+ def status1_input(self) -> PsuStatus:
186
+ """
187
+ Get the status of PSU page 2.
188
+
189
+ Returns
190
+ -------
191
+ PSU status as defined in PsuStatus
192
+ """
193
+ status = int(self.hwmon_debugfs.read_attr_utf8("status1_input").strip(), 16)
194
+ return PsuStatus(status)
195
+
196
+ # Temperature Interface
197
+ @retry()
198
+ def temp1_input(self) -> float:
199
+ """
200
+ Get the PSU temperature.
201
+
202
+ Returns
203
+ -------
204
+ PSU temperature in degrees celsius
205
+ """
206
+ return self._conversion("temp1_input")
53
207
 
54
208
  # Inventory Interface
55
209
  def vendor(self) -> str:
210
+ """
211
+ Get the vendor of the PSU.
212
+
213
+ Returns
214
+ -------
215
+ PSU vendor
216
+ """
56
217
  return self.hwmon_sysfs.read_attr_utf8("vendor").strip()
57
218
 
58
219
  def model(self) -> str:
220
+ """
221
+ Get the model of the PSU.
222
+
223
+ Returns
224
+ -------
225
+ PSU model
226
+ """
59
227
  return self.hwmon_sysfs.read_attr_utf8("model").strip()
60
228
 
61
229
  def serial(self) -> str:
230
+ """
231
+ Get the serial number of the PSU.
232
+
233
+ Returns
234
+ -------
235
+ PSU serial number
236
+ """
62
237
  return self.hwmon_sysfs.read_attr_utf8("serial").strip()
63
238
 
64
239
  def revision(self) -> str:
240
+ """
241
+ Get the revision of the PSU.
242
+
243
+ Returns
244
+ -------
245
+ PSU revision
246
+ """
65
247
  return self.hwmon_sysfs.read_attr_utf8("revision").strip()
ekfsm/devices/smbus.py ADDED
@@ -0,0 +1,24 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List
3
+
4
+
5
+ class SimSmbus(ABC):
6
+ @abstractmethod
7
+ def read_word_data(self, cmd: int) -> int:
8
+ pass
9
+
10
+ @abstractmethod
11
+ def read_block_data(self, cmd: int) -> List[int]:
12
+ pass
13
+
14
+ @abstractmethod
15
+ def write_block_data(self, cmd: int, data: List[int]):
16
+ pass
17
+
18
+ @abstractmethod
19
+ def write_byte(self, cmd: int):
20
+ pass
21
+
22
+ @abstractmethod
23
+ def write_word_data(self, cmd: int, data: int):
24
+ pass
ekfsm/simctrl.py CHANGED
@@ -1,42 +1,20 @@
1
- from abc import ABC, abstractmethod
2
1
  import socket
3
2
  import struct
4
3
  from unittest.mock import patch
5
4
  from pathlib import Path
6
5
 
6
+ from ekfsm.devices.smbus import SimSmbus
7
7
  from ekfsm.devices.gpio import EKFIdSimGpio
8
8
  from ekfsm.devices.gpio import SimGpio
9
9
  from .core.sysfs import set_sysfs_root
10
- from .core.components import SystemComponent
10
+ from .core.components import SysTree
11
11
 
12
12
  from .devices import GPIO
13
13
  from typing import List
14
14
  from smbus2 import SMBus
15
15
 
16
- GPIO_SIM_MAPPING = {}
17
- SMBUS_SIM_MAPPING = {}
18
-
19
-
20
- class SimSmbus(ABC):
21
- @abstractmethod
22
- def read_word_data(self, cmd: int) -> int:
23
- pass
24
-
25
- @abstractmethod
26
- def read_block_data(self, cmd: int) -> List[int]:
27
- pass
28
-
29
- @abstractmethod
30
- def write_block_data(self, cmd: int, data: List[int]):
31
- pass
32
-
33
- @abstractmethod
34
- def write_byte(self, cmd: int):
35
- pass
36
-
37
- @abstractmethod
38
- def write_word_data(self, cmd: int, data: int):
39
- pass
16
+ GPIO_SIM_MAPPING: dict[str, SimGpio] = {}
17
+ SMBUS_SIM_MAPPING: dict[str, SimSmbus] = {}
40
18
 
41
19
 
42
20
  def register_gpio_sim(major: int, minor: int, sim_gpio: SimGpio) -> None:
@@ -72,7 +50,7 @@ class GpioSimulator(GPIO):
72
50
  def __init__(
73
51
  self,
74
52
  name: str,
75
- parent: SystemComponent | None = None,
53
+ parent: SysTree | None = None,
76
54
  *args,
77
55
  **kwargs,
78
56
  ):
ekfsm/system.py CHANGED
@@ -2,6 +2,7 @@ from typing import Tuple, Any, Generator
2
2
  from pathlib import Path
3
3
  from munch import Munch, munchify
4
4
 
5
+ from ekfsm.core.components import SysTree
5
6
  import yaml
6
7
 
7
8
  from .core.slots import Slot, SlotType
@@ -46,7 +47,7 @@ def all_board_cfg_files() -> Generator[Path, None, None]:
46
47
  yield item
47
48
 
48
49
 
49
- class System:
50
+ class System(SysTree):
50
51
  """
51
52
  A System represents a CPCI system.
52
53
 
@@ -94,21 +95,26 @@ class System:
94
95
  >>> print(b.name + b.slot.name) # Print the name of the board and the slot it is in
95
96
  """
96
97
 
97
- def __init__(self, config: Path) -> None:
98
+ def __init__(self, config: Path, abort: bool = False) -> None:
98
99
  """
99
100
  Parameters
100
101
  ----------
101
102
  config
102
103
  Path to the config that specifies the system and how the slots are filled.
103
104
  """
105
+ self.config_path = config
106
+ self.config = load_config(str(self.config_path))
107
+ self.name = self.config.system_config.name
108
+
109
+ super().__init__(self.name, abort=abort)
110
+
104
111
  self.logger = ekfsm_logger(__name__)
105
112
  self._init_system(config)
106
113
  self._init_slot_attrs()
114
+ self._aggregate_provider_functions()
115
+ self.children = self.boards
107
116
 
108
117
  def _init_system(self, config: Path):
109
- self.config_path = config
110
- self.config = load_config(str(self.config_path))
111
- self.name = self.config.system_config.name
112
118
  self.slots: Slots = Slots()
113
119
  self.boards: list[HwModule] = []
114
120
 
@@ -136,6 +142,18 @@ class System:
136
142
  for board in self.boards:
137
143
  setattr(self, board.instance_name.lower(), board)
138
144
 
145
+ def _aggregate_provider_functions(self):
146
+ if hasattr(self.config.system_config, "aggregates"):
147
+ agg = self.config.system_config.aggregates
148
+ if agg is not None:
149
+ for key, value in agg.items():
150
+ prv = Munch()
151
+ for board in self.boards:
152
+ if hasattr(board, key):
153
+ prv.update({value: getattr(board, key)})
154
+ if value in prv.keys():
155
+ setattr(self, value, prv[value])
156
+
139
157
  def reload(self):
140
158
  """
141
159
  Reload the current system configuration.
@@ -144,7 +162,7 @@ class System:
144
162
  ---------
145
163
  This will rebuild all system objects and reinitialize the system tree.
146
164
  """
147
- self._init_system(self.config_path)
165
+ self.__init__(self.config_path)
148
166
 
149
167
  def _create_master(self) -> Tuple[HwModule | None, int]:
150
168
  for i, slot in enumerate(self.config.system_config.slots):
@@ -190,10 +208,16 @@ class System:
190
208
  hwmod = self._create_hwmodule_from_cfg_file(slot, board_name, path)
191
209
 
192
210
  except Exception as e:
193
- self.logger.error(
194
- f"failed to create desired hwmodule {board_type} (as {board_name}): {e}. Leaving slot empty!"
195
- )
196
- return None, slot
211
+ if self.abort:
212
+ self.logger.error(
213
+ f"failed to create desired hwmodule {board_type} (as {board_name}): {e}. Aborting!"
214
+ )
215
+ raise e
216
+ else:
217
+ self.logger.error(
218
+ f"failed to create desired hwmodule {board_type} (as {board_name}): {e}. Leaving slot empty!"
219
+ )
220
+ return None, slot
197
221
 
198
222
  # try to probe desired board type
199
223
  if hwmod.probe():
@@ -211,6 +235,7 @@ class System:
211
235
  hwmod = self._create_hwmodule_from_cfg_file(slot, board_name, path)
212
236
  except ConfigError:
213
237
  # slot type not matching, ignore
238
+ # ??? should we log this?
214
239
  continue
215
240
  except Exception as e:
216
241
  self.logger.debug(
@@ -274,7 +299,15 @@ class System:
274
299
  f"Slot type mismatch for slot {slot.name}: {cfg.slot_type} != {slot.slot_type}"
275
300
  )
276
301
 
277
- return HwModule(instance_name=board_name, config=yaml_data, slot=slot)
302
+ hwmod = HwModule(
303
+ instance_name=board_name,
304
+ config=yaml_data,
305
+ slot=slot,
306
+ abort=self.abort,
307
+ parent=self,
308
+ )
309
+
310
+ return hwmod
278
311
 
279
312
  def get_module_in_slot(self, idx: int) -> HwModule | None:
280
313
  return next(
@@ -316,11 +349,5 @@ class System:
316
349
  f"'{type(self).__name__}' object has no board with name '{name}'"
317
350
  )
318
351
 
319
- def __str__(self) -> str:
320
- output = ""
321
- for b in self.boards:
322
- output += b._render_tree()
323
- return output
324
-
325
- def print(self) -> None:
326
- print(self)
352
+ def __repr__(self):
353
+ return f"System (name={self.name})"