ekfsm 0.13.0a183__py3-none-any.whl → 1.5.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.

Files changed (46) hide show
  1. ekfsm/__init__.py +3 -14
  2. ekfsm/boards/oem/ekf/shu-shuttle.yaml +43 -0
  3. ekfsm/boards/oem/ekf/sq3-quartet.yaml +51 -37
  4. ekfsm/boards/oem/ekf/z1010.yaml +102 -0
  5. ekfsm/boards/oem/hitron/hdrc-300s.yaml +1 -1
  6. ekfsm/cli.py +32 -9
  7. ekfsm/config.py +14 -6
  8. ekfsm/core/__init__.py +13 -3
  9. ekfsm/core/components.py +7 -8
  10. ekfsm/core/connections.py +19 -0
  11. ekfsm/core/slots.py +6 -8
  12. ekfsm/core/sysfs.py +215 -25
  13. ekfsm/core/utils.py +128 -64
  14. ekfsm/devices/__init__.py +27 -7
  15. ekfsm/devices/buttons.py +251 -0
  16. ekfsm/devices/colorLed.py +110 -0
  17. ekfsm/devices/coretemp.py +35 -13
  18. ekfsm/devices/eeprom.py +73 -45
  19. ekfsm/devices/ekf_ccu_uc.py +76 -54
  20. ekfsm/devices/ekf_sur_led.py +6 -2
  21. ekfsm/devices/generic.py +200 -59
  22. ekfsm/devices/gpio.py +37 -27
  23. ekfsm/devices/iio.py +15 -31
  24. ekfsm/devices/iio_thermal_humidity.py +20 -13
  25. ekfsm/devices/imu.py +8 -4
  26. ekfsm/devices/io4edge.py +185 -0
  27. ekfsm/devices/ledArray.py +54 -0
  28. ekfsm/devices/mux.py +46 -8
  29. ekfsm/devices/pixelDisplay.py +141 -0
  30. ekfsm/devices/pmbus.py +74 -101
  31. ekfsm/devices/smbios.py +28 -8
  32. ekfsm/devices/smbus.py +1 -1
  33. ekfsm/devices/thermal_humidity.py +80 -0
  34. ekfsm/devices/toggles.py +90 -0
  35. ekfsm/devices/utils.py +52 -8
  36. ekfsm/devices/watchdog.py +79 -0
  37. ekfsm/exceptions.py +28 -7
  38. ekfsm/lock.py +48 -21
  39. ekfsm/simctrl.py +37 -83
  40. ekfsm/system.py +89 -73
  41. ekfsm/utils.py +44 -0
  42. {ekfsm-0.13.0a183.dist-info → ekfsm-1.5.0.dist-info}/METADATA +12 -6
  43. ekfsm-1.5.0.dist-info/RECORD +57 -0
  44. ekfsm-0.13.0a183.dist-info/RECORD +0 -45
  45. {ekfsm-0.13.0a183.dist-info → ekfsm-1.5.0.dist-info}/WHEEL +0 -0
  46. {ekfsm-0.13.0a183.dist-info → ekfsm-1.5.0.dist-info}/entry_points.txt +0 -0
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)
ekfsm/simctrl.py CHANGED
@@ -1,20 +1,20 @@
1
1
  import socket
2
2
  import struct
3
- from unittest.mock import patch
4
3
  from pathlib import Path
4
+ from typing import List
5
+ from unittest.mock import patch
5
6
 
6
- from ekfsm.devices.smbus import SimSmbus
7
- from ekfsm.devices.gpio import EKFIdSimGpio
8
- from ekfsm.devices.gpio import SimGpio
9
- from .core.sysfs import set_sysfs_root
10
- from .core.components import SysTree
7
+ from smbus2 import SMBus
11
8
 
9
+ from ekfsm.devices.gpio import EKFIdSimGpio, SimGpio
10
+ from ekfsm.devices.smbus import SimSMBus
11
+
12
+ from .core.components import SysTree
13
+ from .core.sysfs import set_sysfs_root
12
14
  from .devices import GPIO
13
- from typing import List
14
- from smbus2 import SMBus
15
15
 
16
16
  GPIO_SIM_MAPPING: dict[str, SimGpio] = {}
17
- SMBUS_SIM_MAPPING: dict[str, SimSmbus] = {}
17
+ SMBUS_SIM_MAPPING: dict[str, SimSMBus] = {}
18
18
 
19
19
 
20
20
  def register_gpio_sim(major: int, minor: int, sim_gpio: SimGpio) -> None:
@@ -31,14 +31,14 @@ def find_gpio_dev_with_major_minor(major: int, minor: int) -> SimGpio:
31
31
  return GPIO_SIM_MAPPING[name]
32
32
 
33
33
 
34
- def register_smbus_sim(bus_num: int, i2c_addr: int, sim_smbus: SimSmbus) -> None:
34
+ def register_smbus_sim(bus_num: int, i2c_addr: int, sim_smbus: SimSMBus) -> None:
35
35
  name = f"{bus_num}:{i2c_addr}"
36
36
  if name in SMBUS_SIM_MAPPING:
37
37
  raise ValueError(f"SMBUS_SIM_MAPPING already contains {name}")
38
38
  SMBUS_SIM_MAPPING[name] = sim_smbus
39
39
 
40
40
 
41
- def find_smbus_dev(bus_num: int, i2c_addr: int) -> SimSmbus:
41
+ def find_smbus_dev(bus_num: int, i2c_addr: int) -> SimSMBus:
42
42
  name = f"{bus_num}:{i2c_addr}"
43
43
 
44
44
  if name not in SMBUS_SIM_MAPPING:
@@ -46,7 +46,7 @@ def find_smbus_dev(bus_num: int, i2c_addr: int) -> SimSmbus:
46
46
  return SMBUS_SIM_MAPPING[name]
47
47
 
48
48
 
49
- class GpioSimulator(GPIO):
49
+ class GPIOSimulator(GPIO):
50
50
  def __init__(
51
51
  self,
52
52
  name: str,
@@ -80,7 +80,7 @@ class GpioSimulator(GPIO):
80
80
  return f"GPIO_SIM({self.name})"
81
81
 
82
82
 
83
- class SmbusSimulator:
83
+ class SMBusSimulator:
84
84
  def __init__(self, bus_num: int):
85
85
  self.bus_num = bus_num
86
86
 
@@ -100,73 +100,12 @@ class SmbusSimulator:
100
100
  find_smbus_dev(self.bus_num, i2c_addr).write_word_data(cmd, data)
101
101
 
102
102
 
103
- def enable_gpio_simulation():
104
- patched_methods = []
105
-
106
- patched_methods.append(
107
- patch.object(GPIO, "__init__", new_callable=lambda: GpioSimulator.__init__)
108
- )
109
- patched_methods.append(
110
- patch.object(GPIO, "num_lines", new_callable=lambda: GpioSimulator.num_lines)
111
- )
112
- patched_methods.append(
113
- patch.object(GPIO, "set_pin", new_callable=lambda: GpioSimulator.set_pin)
114
- )
115
- patched_methods.append(
116
- patch.object(GPIO, "get_pin", new_callable=lambda: GpioSimulator.get_pin)
117
- )
118
- patched_methods.append(
119
- patch.object(
120
- GPIO, "set_direction", new_callable=lambda: GpioSimulator.set_direction
121
- )
122
- )
123
- patched_methods.append(
124
- patch.object(GPIO, "__str__", new_callable=lambda: GpioSimulator.__str__)
125
- )
126
- for pm in patched_methods:
127
- pm.start()
128
-
129
-
130
- def enable_smbus_simulation():
131
- patched_methods = []
132
-
133
- patched_methods.append(
134
- patch.object(SMBus, "__init__", new_callable=lambda: SmbusSimulator.__init__)
135
- )
136
- patched_methods.append(
137
- patch.object(
138
- SMBus, "read_word_data", new_callable=lambda: SmbusSimulator.read_word_data
139
- )
140
- )
141
- patched_methods.append(
142
- patch.object(
143
- SMBus,
144
- "read_block_data",
145
- new_callable=lambda: SmbusSimulator.read_block_data,
146
- )
147
- )
148
- patched_methods.append(
149
- patch.object(
150
- SMBus,
151
- "write_block_data",
152
- new_callable=lambda: SmbusSimulator.write_block_data,
153
- )
154
- )
155
- patched_methods.append(
156
- patch.object(
157
- SMBus, "write_byte", new_callable=lambda: SmbusSimulator.write_byte
158
- )
159
- )
160
- patched_methods.append(
161
- patch.object(
162
- SMBus,
163
- "write_word_data",
164
- new_callable=lambda: SmbusSimulator.write_word_data,
165
- )
166
- )
167
-
168
- for pm in patched_methods:
169
- pm.start()
103
+ def patch_methods(cls, simulator, methods):
104
+ patched = []
105
+ for i, method in enumerate(methods):
106
+ if hasattr(cls, method) and hasattr(simulator, method):
107
+ patched.append(patch.object(cls, method, new_callable=lambda: getattr(simulator, method)))
108
+ patched[i].start()
170
109
 
171
110
 
172
111
  def enable_simulation(sysfs_path: Path | str) -> None:
@@ -180,8 +119,23 @@ def enable_simulation(sysfs_path: Path | str) -> None:
180
119
  sysfs_path = Path(sysfs_path)
181
120
 
182
121
  set_sysfs_root(sysfs_path)
183
- enable_gpio_simulation()
184
- enable_smbus_simulation()
122
+ patch_methods(
123
+ GPIO,
124
+ GPIOSimulator,
125
+ ["__init__", "num_lines", "set_pin", "get_pin", "set_direction", "__str__"],
126
+ )
127
+ patch_methods(
128
+ SMBus,
129
+ SMBusSimulator,
130
+ [
131
+ "__init__",
132
+ "read_word_data",
133
+ "read_block_data",
134
+ "write_block_data",
135
+ "write_byte",
136
+ "write_word_data",
137
+ ],
138
+ )
185
139
 
186
140
 
187
141
  def register_gpio_simulations():
@@ -189,7 +143,7 @@ def register_gpio_simulations():
189
143
  register_gpio_sim(233, 2, EKFIdSimGpio(0x34, 0xA, 0x0, 0x1)) # CCU Rev 0
190
144
 
191
145
 
192
- class SocketSmbus(SimSmbus):
146
+ class SocketSmbus(SimSMBus):
193
147
  def __init__(self, host: str, port: int) -> None:
194
148
  self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
195
149
  self.sock.connect((host, port))
ekfsm/system.py CHANGED
@@ -1,52 +1,19 @@
1
- from typing import Tuple, Any, Generator
2
1
  from pathlib import Path
3
- from munch import Munch, munchify
2
+ from typing import Any, Tuple
4
3
 
5
- from ekfsm.core.components import SysTree
6
4
  import yaml
5
+ from munch import Munch, munchify
7
6
 
8
- from .core.slots import Slot, SlotType
7
+ from ekfsm.core.components import SysTree
8
+ from ekfsm.utils import all_board_cfg_files, find_board_config
9
9
 
10
10
  from .config import load_config
11
- from .core import HwModule
12
- from .core.slots import Slots
11
+ from .core import HWModule
12
+ from .core.slots import Slot, Slots, SlotType
13
13
  from .exceptions import ConfigError
14
14
  from .log import ekfsm_logger
15
15
 
16
16
 
17
- _CFG_DIR = Path(__file__).parent / "boards"
18
-
19
-
20
- def find_board_config(module_type: str) -> Path | None:
21
- """
22
- Find a matching board config in `boards/oem/` given the module type specified in
23
- the system configuration file.
24
-
25
- Parameters
26
- ----------
27
- module_type
28
- Board type specified in the system configuration for a slot.
29
- It must consist of an OEM and the board type, separated by whitespace. Neither
30
- part may contain any other whitespace.
31
- """
32
- oem, board = module_type.split(maxsplit=1)
33
- if (
34
- path := _CFG_DIR / "oem" / oem.strip().lower() / f"{board.strip().lower()}.yaml"
35
- ).exists():
36
- return path
37
- return None
38
-
39
-
40
- def all_board_cfg_files() -> Generator[Path, None, None]:
41
- """
42
- Generator that recursively yields all *.yaml files in a directory
43
- """
44
- path = Path(_CFG_DIR)
45
- for item in path.rglob("*.yaml"):
46
- if item.is_file():
47
- yield item
48
-
49
-
50
17
  class System(SysTree):
51
18
  """
52
19
  A System represents a CPCI system.
@@ -57,15 +24,6 @@ class System(SysTree):
57
24
 
58
25
  Visual representation of the system is shown as trees of HW Modules and attached devices.
59
26
 
60
- Iterating over the system will iterate over all boards in the system.
61
-
62
- Accessing boards
63
- ----------------
64
- <board_name>
65
- The board object can be accessed by its name.
66
- <slot_number>
67
- The board object can be accessed by its slot number.
68
-
69
27
  Attributes
70
28
  ----------
71
29
  name
@@ -81,11 +39,23 @@ class System(SysTree):
81
39
  config
82
40
  The system configuration.
83
41
 
42
+
43
+ Accessing boards
44
+ ----------------
45
+
46
+ Iterating over the system will iterate over all boards in the system.
47
+
48
+ <board_name>
49
+ The board object can be accessed by its name.
50
+ <slot_number>
51
+ The board object can be accessed by its slot number.
52
+
53
+
84
54
  Example
85
55
  -------
86
56
  >>> from ekfsm.system import System
87
57
  >>> system = System("path/to/config.yaml")
88
- >>> print(system) # Print the system configuration as trees of HwModules
58
+ >>> print(system) # Print the system configuration as trees of HWModules
89
59
  >>> system.print() # same as above
90
60
  >>> cpu = system.cpu # Access the CPU board by its name
91
61
  >>> cpu = system[0] # Access the CPU board by its slot index (index as in configuration file)
@@ -101,6 +71,9 @@ class System(SysTree):
101
71
  ----------
102
72
  config
103
73
  Path to the config that specifies the system and how the slots are filled.
74
+ abort
75
+ If True, abort the program if a board cannot be created. If False, leave the slot empty.
76
+ Default is False.
104
77
  """
105
78
  self.config_path = config
106
79
  self.config = load_config(str(self.config_path))
@@ -116,7 +89,7 @@ class System(SysTree):
116
89
 
117
90
  def _init_system(self, config: Path):
118
91
  self.slots: Slots = Slots()
119
- self.boards: list[HwModule] = []
92
+ self.boards: list[HWModule] = []
120
93
 
121
94
  self.master, self.master_slot_number = self._create_master()
122
95
  if self.master is None:
@@ -125,7 +98,7 @@ class System(SysTree):
125
98
  self.logger.info(f"Master board found in slot {self.master_slot_number}")
126
99
 
127
100
  for i, slot_cfg in enumerate(self.config.system_config.slots):
128
- hwmod: HwModule | Slot | None
101
+ hwmod: HWModule | Slot | None
129
102
  if i == self.master_slot_number:
130
103
  hwmod = self.master
131
104
  else:
@@ -164,35 +137,44 @@ class System(SysTree):
164
137
  """
165
138
  self.__init__(self.config_path)
166
139
 
167
- def _create_master(self) -> Tuple[HwModule | None, int]:
140
+ def _create_master(self) -> Tuple[HWModule | None, int]:
168
141
  for i, slot in enumerate(self.config.system_config.slots):
169
142
  if "attributes" in slot:
170
143
  if "is_master" in slot.attributes:
171
144
  if slot.attributes.is_master:
172
145
  master, _ = self.create_hwmodule(slot, i, None)
173
146
  if master is not None:
174
- master.master = master
147
+ master.master = master # ???
175
148
  return master, i
176
149
  else:
177
150
  return None, -1
178
151
  return None, -1 # ???
179
152
 
180
153
  def create_hwmodule(
181
- self, slot_entry: Munch, slot_number: int, master: HwModule | None
182
- ) -> Tuple[HwModule | None, Slot]:
154
+ self, slot_entry: Munch, slot_number: int, master: HWModule | None
155
+ ) -> Tuple[HWModule | None, Slot]:
183
156
  """
184
- Create HwModule object for the slot.
157
+ Create HWModule object for the slot.
158
+
159
+ Parameters
160
+ ----------
161
+ slot_entry
162
+ The slot entry config (usually part of the system configuration).
163
+ slot_number
164
+ The slot number of the slot.
165
+ master
166
+ The master board of the system.
185
167
 
186
168
  Returns
187
169
  -------
188
- HwModule and Slot. HwModule is None if it cannot be created.
170
+ HWModule and Slot. HWodule is None if it cannot be created.
189
171
  """
190
172
  slot = self._create_slot(slot_entry, slot_number, master)
191
173
  board_type = slot_entry.desired_hwmodule_type
192
174
  board_name = slot_entry.desired_hwmodule_name
193
175
 
194
176
  self.logger.debug(
195
- f"Creating hwmodule {board_type} (desired name: {board_name}) in slot {slot.name}"
177
+ f"Creating HWModule {board_type} (desired name: {board_name}) in slot {slot.name}"
196
178
  )
197
179
 
198
180
  if board_type != "":
@@ -210,12 +192,12 @@ class System(SysTree):
210
192
  except Exception as e:
211
193
  if self.abort:
212
194
  self.logger.error(
213
- f"failed to create desired hwmodule {board_type} (as {board_name}): {e}. Aborting!"
195
+ f"failed to create desired HWModule {board_type} (as {board_name}): {e}. Aborting!"
214
196
  )
215
197
  raise e
216
198
  else:
217
199
  self.logger.error(
218
- f"failed to create desired hwmodule {board_type} (as {board_name}): {e}. Leaving slot empty!"
200
+ f"failed to create desired HWModule {board_type} (as {board_name}): {e}. Leaving slot empty!"
219
201
  )
220
202
  return None, slot
221
203
 
@@ -239,7 +221,7 @@ class System(SysTree):
239
221
  continue
240
222
  except Exception as e:
241
223
  self.logger.debug(
242
- f"failed to create hwmodule {path} for slot {slot.name}: {e}"
224
+ f"failed to create HWmodule {path} for slot {slot.name}: {e}"
243
225
  )
244
226
  continue
245
227
 
@@ -252,7 +234,7 @@ class System(SysTree):
252
234
  return None, slot
253
235
 
254
236
  def _create_slot(
255
- self, slot_entry: Munch, slot_number: int, master: HwModule | None
237
+ self, slot_entry: Munch, slot_number: int, master: HWModule | None
256
238
  ) -> Slot:
257
239
  attributes = None
258
240
  if "attributes" in slot_entry:
@@ -271,14 +253,14 @@ class System(SysTree):
271
253
 
272
254
  def _create_hwmodule_from_cfg_file(
273
255
  self, slot: Slot, board_name: str, path: Path
274
- ) -> HwModule:
256
+ ) -> HWModule:
275
257
  """
276
- Try to create a HwModule object from a board config file.
258
+ Try to create a HWModule object from a board config file.
277
259
  It does not probe the hardware.
278
260
 
279
261
  Returns
280
262
  -------
281
- HwModule object.
263
+ HWModule object.
282
264
 
283
265
  Raises
284
266
  ------
@@ -299,7 +281,7 @@ class System(SysTree):
299
281
  f"Slot type mismatch for slot {slot.name}: {cfg.slot_type} != {slot.slot_type}"
300
282
  )
301
283
 
302
- hwmod = HwModule(
284
+ hwmod = HWModule(
303
285
  instance_name=board_name,
304
286
  config=yaml_data,
305
287
  slot=slot,
@@ -309,7 +291,21 @@ class System(SysTree):
309
291
 
310
292
  return hwmod
311
293
 
312
- def get_module_in_slot(self, idx: int) -> HwModule | None:
294
+ def get_module_in_slot(self, idx: int) -> HWModule | None:
295
+ """
296
+ Get the HWModule in the given slot.
297
+
298
+ Parameters
299
+ ----------
300
+ idx
301
+ The slot index.
302
+ Returns
303
+ -------
304
+ HWModule
305
+ The HWModule in the given slot.
306
+ None
307
+ If no HWModule is present in the given slot.
308
+ """
313
309
  return next(
314
310
  (
315
311
  v.hwmodule
@@ -319,16 +315,36 @@ class System(SysTree):
319
315
  None,
320
316
  )
321
317
 
322
- def get_module_by_name(self, name: str) -> HwModule | None:
318
+ def get_module_by_name(self, name: str) -> HWModule | None:
319
+ """
320
+ Get the HWModule by its name.
321
+
322
+ Parameters
323
+ ----------
324
+ name
325
+ The name of the HWModule.
326
+
327
+ Returns
328
+ -------
329
+ HWModule
330
+ The HWModule with the given name.
331
+ None
332
+ If no HWModule is present with the given name.
333
+ """
323
334
  return next(
324
- (b for b in self.boards if getattr(b, "instance_name", None) == name),
335
+ (
336
+ b
337
+ for b in self.boards
338
+ if getattr(b, "instance_name", None) is not None
339
+ and getattr(b, "instance_name").lower() == name.lower()
340
+ ),
325
341
  None,
326
342
  )
327
343
 
328
344
  def __iter__(self):
329
345
  return iter(self.boards)
330
346
 
331
- def __getitem__(self, key) -> HwModule:
347
+ def __getitem__(self, key) -> HWModule:
332
348
  if isinstance(key, int):
333
349
  value = self.get_module_in_slot(key)
334
350
  else:
@@ -342,9 +358,9 @@ class System(SysTree):
342
358
  def __getattr__(self, name: str) -> Any:
343
359
  """Access board by attribute using dot notation"""
344
360
  # This fixes mypy error: "... has no object ..."
345
- hwModule = self.get_module_by_name(name)
346
- if hwModule is not None:
347
- return hwModule
361
+ if (hw_module := self.get_module_by_name(name)) is not None:
362
+ return hw_module
363
+
348
364
  raise AttributeError(
349
365
  f"'{type(self).__name__}' object has no board with name '{name}'"
350
366
  )
ekfsm/utils.py ADDED
@@ -0,0 +1,44 @@
1
+ from pathlib import Path
2
+ from typing import Generator
3
+
4
+ _CFG_DIR = Path(__file__).parent / "boards"
5
+
6
+
7
+ def find_board_config(module_type: str) -> Path | None:
8
+ """
9
+ Find a matching board config in `boards/oem/` given the module type specified in
10
+ the system configuration file.
11
+
12
+ Parameters
13
+ ----------
14
+ module_type
15
+ Board type specified in the system configuration for a slot.
16
+ It must consist of an OEM and the board type, separated by whitespace. Neither
17
+ part may contain any other whitespace.
18
+ """
19
+ oem, board = module_type.split(maxsplit=1)
20
+ if (path := _CFG_DIR / "oem" / oem.strip().lower() / f"{board.strip().lower()}.yaml").exists():
21
+ return path
22
+ return None
23
+
24
+
25
+ def all_board_cfg_files() -> Generator[Path, None, None]:
26
+ """
27
+ Generator that recursively yields all *.yaml files in the config directory.
28
+
29
+ Yields
30
+ ------
31
+ item: Path
32
+ Path to a config file.
33
+ """
34
+ path = Path(_CFG_DIR)
35
+ for item in path.rglob("*.yaml"):
36
+ if item.is_file():
37
+ yield item
38
+
39
+
40
+ def next_or_raise(it, exc):
41
+ value = next(it, None)
42
+ if value is None:
43
+ raise exc
44
+ return value