ekfsm 0.11.0b1.post3__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/exceptions.py ADDED
@@ -0,0 +1,58 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EkfSmException(Exception):
5
+ """Base class for all exceptions in the EKFSM Library"""
6
+ pass
7
+
8
+
9
+ class ConfigError(EkfSmException):
10
+ """Error in configuration"""
11
+
12
+ pass
13
+
14
+
15
+ class SYSFSError(EkfSmException):
16
+ """Error while handling sysfs pseudo file system"""
17
+
18
+ pass
19
+
20
+
21
+ class GPIOError(EkfSmException):
22
+ """Error while handling GPIO"""
23
+
24
+ class ErrorType(Enum):
25
+ INVALID_PIN = "Pin not found"
26
+ NO_MATCHING_DEVICE = "No matching device found"
27
+ NO_MAJOR_MINOR = "No major/minor number found"
28
+
29
+ pass
30
+
31
+ def __init__(self, error_type: ErrorType, details: str | None = None):
32
+ self.error_type = error_type
33
+ self.details = details
34
+ super().__init__(
35
+ f"{error_type.value}: {details}" if details else error_type.value
36
+ )
37
+
38
+
39
+ class FirmwareNodeError(EkfSmException):
40
+ """Error while handlig firmware node"""
41
+
42
+ pass
43
+
44
+
45
+ class DataCorruptionError(EkfSmException):
46
+ """Error while handling data corruption"""
47
+
48
+ def __init__(self, details: str | None = None):
49
+ self.details = details
50
+ super().__init__(
51
+ f"Data corruption: {details}" if details else "Data corruption"
52
+ )
53
+
54
+
55
+ class AcquisitionError(EkfSmException):
56
+ """Error while handling data acquisition"""
57
+
58
+ pass
ekfsm/log.py ADDED
@@ -0,0 +1,28 @@
1
+ import logging
2
+
3
+ #
4
+ # We follow the recommendations from https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library
5
+ #
6
+ # By default, if the application does not configure logging, the logging module will log
7
+ # only messages with level WARNING or above and is using the default formatting, i.e.
8
+ # only the message is printed.
9
+ #
10
+ # To get a more verbose output, the application should call, for example
11
+ # logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
12
+
13
+
14
+ def ekfsm_logger(name: str) -> logging.Logger:
15
+ """
16
+ Create a logger with the name 'ekfsm:name'
17
+
18
+ Returns
19
+ -------
20
+ logging.Logger
21
+ The logger object.
22
+
23
+ Parameters
24
+ ----------
25
+ name
26
+ The name of the module, class or object that is using the logger.
27
+ """
28
+ return logging.getLogger("ekfsm:" + name)
ekfsm/py.typed ADDED
@@ -0,0 +1,2 @@
1
+ # This file is intentionally left empty.
2
+ # It indicates that the package supports PEP 561 type hints.
ekfsm/simctrl.py ADDED
@@ -0,0 +1,241 @@
1
+ from abc import ABC, abstractmethod
2
+ import socket
3
+ import struct
4
+ from unittest.mock import patch
5
+ from pathlib import Path
6
+
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 SystemComponent
11
+
12
+ from .devices import GPIO
13
+ from typing import List
14
+ from smbus2 import SMBus
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
40
+
41
+
42
+ def register_gpio_sim(major: int, minor: int, sim_gpio: SimGpio) -> None:
43
+ name = f"{major}:{minor}"
44
+ if name in GPIO_SIM_MAPPING:
45
+ raise ValueError(f"GPIO_SIM_MAPPING already contains {name}")
46
+ GPIO_SIM_MAPPING[name] = sim_gpio
47
+
48
+
49
+ def find_gpio_dev_with_major_minor(major: int, minor: int) -> SimGpio:
50
+ name = f"{major}:{minor}"
51
+ if name not in GPIO_SIM_MAPPING:
52
+ raise ValueError(f"GPIO_SIM_MAPPING does not contain {name}")
53
+ return GPIO_SIM_MAPPING[name]
54
+
55
+
56
+ def register_smbus_sim(bus_num: int, i2c_addr: int, sim_smbus: SimSmbus) -> None:
57
+ name = f"{bus_num}:{i2c_addr}"
58
+ if name in SMBUS_SIM_MAPPING:
59
+ raise ValueError(f"SMBUS_SIM_MAPPING already contains {name}")
60
+ SMBUS_SIM_MAPPING[name] = sim_smbus
61
+
62
+
63
+ def find_smbus_dev(bus_num: int, i2c_addr: int) -> SimSmbus:
64
+ name = f"{bus_num}:{i2c_addr}"
65
+
66
+ if name not in SMBUS_SIM_MAPPING:
67
+ raise ValueError(f"SMBUS_SIM_MAPPING does not contain {name}")
68
+ return SMBUS_SIM_MAPPING[name]
69
+
70
+
71
+ class GpioSimulator(GPIO):
72
+ def __init__(
73
+ self,
74
+ name: str,
75
+ parent: SystemComponent | None = None,
76
+ *args,
77
+ **kwargs,
78
+ ):
79
+ super(GPIO, self).__init__(
80
+ name,
81
+ parent,
82
+ *args,
83
+ **kwargs,
84
+ )
85
+ major, minor = self._find_gpio_dev(parent, *args, **kwargs)
86
+ self._sim_gpio = find_gpio_dev_with_major_minor(major, minor)
87
+ self.number = minor
88
+
89
+ def num_lines(self) -> int:
90
+ return self._sim_gpio.num_lines()
91
+
92
+ def set_pin(self, pin: int, value: bool) -> None:
93
+ self._sim_gpio.set_pin(pin, value)
94
+
95
+ def get_pin(self, pin: int) -> bool:
96
+ return self._sim_gpio.get_pin(pin)
97
+
98
+ def set_direction(self, pin: int, direction: bool) -> None:
99
+ self._sim_gpio.set_direction(pin, direction)
100
+
101
+ def __str__(self) -> str:
102
+ return f"GPIO_SIM({self.name})"
103
+
104
+
105
+ class SmbusSimulator:
106
+ def __init__(self, bus_num: int):
107
+ self.bus_num = bus_num
108
+
109
+ def read_word_data(self, i2c_addr: int, cmd: int) -> int:
110
+ return find_smbus_dev(self.bus_num, i2c_addr).read_word_data(cmd)
111
+
112
+ def read_block_data(self, i2c_addr: int, cmd: int) -> List[int]:
113
+ return find_smbus_dev(self.bus_num, i2c_addr).read_block_data(cmd)
114
+
115
+ def write_block_data(self, i2c_addr: int, cmd: int, data: List[int]):
116
+ find_smbus_dev(self.bus_num, i2c_addr).write_block_data(cmd, data)
117
+
118
+ def write_byte(self, i2c_addr: int, cmd: int):
119
+ find_smbus_dev(self.bus_num, i2c_addr).write_byte(cmd)
120
+
121
+ def write_word_data(self, i2c_addr: int, cmd: int, data: int):
122
+ find_smbus_dev(self.bus_num, i2c_addr).write_word_data(cmd, data)
123
+
124
+
125
+ def enable_gpio_simulation():
126
+ patched_methods = []
127
+
128
+ patched_methods.append(
129
+ patch.object(GPIO, "__init__", new_callable=lambda: GpioSimulator.__init__)
130
+ )
131
+ patched_methods.append(
132
+ patch.object(GPIO, "num_lines", new_callable=lambda: GpioSimulator.num_lines)
133
+ )
134
+ patched_methods.append(
135
+ patch.object(GPIO, "set_pin", new_callable=lambda: GpioSimulator.set_pin)
136
+ )
137
+ patched_methods.append(
138
+ patch.object(GPIO, "get_pin", new_callable=lambda: GpioSimulator.get_pin)
139
+ )
140
+ patched_methods.append(
141
+ patch.object(
142
+ GPIO, "set_direction", new_callable=lambda: GpioSimulator.set_direction
143
+ )
144
+ )
145
+ patched_methods.append(
146
+ patch.object(GPIO, "__str__", new_callable=lambda: GpioSimulator.__str__)
147
+ )
148
+ for pm in patched_methods:
149
+ pm.start()
150
+
151
+
152
+ def enable_smbus_simulation():
153
+ patched_methods = []
154
+
155
+ patched_methods.append(
156
+ patch.object(SMBus, "__init__", new_callable=lambda: SmbusSimulator.__init__)
157
+ )
158
+ patched_methods.append(
159
+ patch.object(
160
+ SMBus, "read_word_data", new_callable=lambda: SmbusSimulator.read_word_data
161
+ )
162
+ )
163
+ patched_methods.append(
164
+ patch.object(
165
+ SMBus,
166
+ "read_block_data",
167
+ new_callable=lambda: SmbusSimulator.read_block_data,
168
+ )
169
+ )
170
+ patched_methods.append(
171
+ patch.object(
172
+ SMBus,
173
+ "write_block_data",
174
+ new_callable=lambda: SmbusSimulator.write_block_data,
175
+ )
176
+ )
177
+ patched_methods.append(
178
+ patch.object(
179
+ SMBus, "write_byte", new_callable=lambda: SmbusSimulator.write_byte
180
+ )
181
+ )
182
+ patched_methods.append(
183
+ patch.object(
184
+ SMBus,
185
+ "write_word_data",
186
+ new_callable=lambda: SmbusSimulator.write_word_data,
187
+ )
188
+ )
189
+
190
+ for pm in patched_methods:
191
+ pm.start()
192
+
193
+
194
+ def enable_simulation(sysfs_path: Path):
195
+ global GPIO_SIM_MAPPING
196
+ GPIO_SIM_MAPPING = {}
197
+
198
+ global SMBUS_SIM_MAPPING
199
+ SMBUS_SIM_MAPPING = {}
200
+
201
+ set_sysfs_root(sysfs_path)
202
+ enable_gpio_simulation()
203
+ enable_smbus_simulation()
204
+
205
+
206
+ def register_gpio_simulations():
207
+ register_gpio_sim(233, 1, EKFIdSimGpio(0x38, 0x1, 0x0, 0x6)) # SRF Rev 0
208
+ register_gpio_sim(233, 2, EKFIdSimGpio(0x34, 0xA, 0x0, 0x1)) # CCU Rev 0
209
+
210
+
211
+ class SocketSmbus(SimSmbus):
212
+ def __init__(self, host: str, port: int) -> None:
213
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
214
+ self.sock.connect((host, port))
215
+
216
+ def read_word_data(self, cmd: int) -> int:
217
+ req = struct.pack("BB", 0x4, cmd)
218
+ self.sock.send(req)
219
+ data = self.sock.recv(2)
220
+ return struct.unpack("<H", data)[0]
221
+
222
+ def read_block_data(self, cmd: int) -> List[int]:
223
+ req = struct.pack("BB", 0x1, cmd)
224
+ self.sock.send(req)
225
+ _count = self.sock.recv(1)
226
+ count = struct.unpack("B", _count)[0]
227
+ data = self.sock.recv(count)
228
+ return [int(data[i]) for i in range(0, len(data), 1)]
229
+
230
+ def write_block_data(self, cmd: int, data: List[int]):
231
+ _data = bytes(data)
232
+ hdr = struct.pack("BBB", 0x2, cmd, len(_data))
233
+ self.sock.send(hdr + _data)
234
+
235
+ def write_byte(self, cmd: int):
236
+ hdr = struct.pack("BB", 0x3, cmd)
237
+ self.sock.send(hdr)
238
+
239
+ def write_word_data(self, cmd: int, data: int):
240
+ hdr = struct.pack("BBH", 0x5, cmd, data)
241
+ self.sock.send(hdr)
ekfsm/system.py ADDED
@@ -0,0 +1,326 @@
1
+ from typing import Tuple, Any, Generator
2
+ from pathlib import Path
3
+ from munch import Munch, munchify
4
+
5
+ import yaml
6
+
7
+ from .core.slots import Slot, SlotType
8
+
9
+ from .config import load_config
10
+ from .core import HwModule
11
+ from .core.slots import Slots
12
+ from .exceptions import ConfigError
13
+ from .log import ekfsm_logger
14
+
15
+
16
+ _CFG_DIR = Path(__file__).parent / "boards"
17
+
18
+
19
+ def find_board_config(module_type: str) -> Path | None:
20
+ """
21
+ Find a matching board config in `boards/oem/` given the module type specified in
22
+ the system configuration file.
23
+
24
+ Parameters
25
+ ----------
26
+ module_type
27
+ Board type specified in the system configuration for a slot.
28
+ It must consist of an OEM and the board type, separated by whitespace. Neither
29
+ part may contain any other whitespace.
30
+ """
31
+ oem, board = module_type.split(maxsplit=1)
32
+ if (
33
+ path := _CFG_DIR / "oem" / oem.strip().lower() / f"{board.strip().lower()}.yaml"
34
+ ).exists():
35
+ return path
36
+ return None
37
+
38
+
39
+ def all_board_cfg_files() -> Generator[Path, None, None]:
40
+ """
41
+ Generator that recursively yields all *.yaml files in a directory
42
+ """
43
+ path = Path(_CFG_DIR)
44
+ for item in path.rglob("*.yaml"):
45
+ if item.is_file():
46
+ yield item
47
+
48
+
49
+ class System:
50
+ """
51
+ A System represents a CPCI system.
52
+
53
+ Once initialised, it will create:
54
+ - a list of boards that are present in the system which can be accessed either by name or by slot number.
55
+ - a list of slots that are present in the system which can be accessed under the slots attribute.
56
+
57
+ Visual representation of the system is shown as trees of HW Modules and attached devices.
58
+
59
+ Iterating over the system will iterate over all boards in the system.
60
+
61
+ Accessing boards
62
+ ----------------
63
+ <board_name>
64
+ The board object can be accessed by its name.
65
+ <slot_number>
66
+ The board object can be accessed by its slot number.
67
+
68
+ Attributes
69
+ ----------
70
+ name
71
+ The name of the system.
72
+ slots
73
+ A dictionary-like object that contains all slots in the system.
74
+ boards
75
+ A list of all boards in the system.
76
+ master
77
+ The master board of the system.
78
+ master_slot_number
79
+ The slot number of the master board.
80
+ config
81
+ The system configuration.
82
+
83
+ Example
84
+ -------
85
+ >>> from ekfsm.system import System
86
+ >>> system = System("path/to/config.yaml")
87
+ >>> print(system) # Print the system configuration as trees of HwModules
88
+ >>> system.print() # same as above
89
+ >>> cpu = system.cpu # Access the CPU board by its name
90
+ >>> cpu = system[0] # Access the CPU board by its slot index (index as in configuration file)
91
+ >>> print(system.slots) # Print all slots in the system
92
+ >>> print(system.boards) # Print all boards in the system
93
+ >>> for b in system: # Iterate over all boards in the system
94
+ >>> print(b.name + b.slot.name) # Print the name of the board and the slot it is in
95
+ """
96
+
97
+ def __init__(self, config: Path) -> None:
98
+ """
99
+ Parameters
100
+ ----------
101
+ config
102
+ Path to the config that specifies the system and how the slots are filled.
103
+ """
104
+ self.logger = ekfsm_logger(__name__)
105
+ self._init_system(config)
106
+ self._init_slot_attrs()
107
+
108
+ 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
+ self.slots: Slots = Slots()
113
+ self.boards: list[HwModule] = []
114
+
115
+ self.master, self.master_slot_number = self._create_master()
116
+ if self.master is None:
117
+ raise ConfigError("No master board found in system configuration!")
118
+
119
+ self.logger.info(f"Master board found in slot {self.master_slot_number}")
120
+
121
+ for i, slot_cfg in enumerate(self.config.system_config.slots):
122
+ hwmod: HwModule | Slot | None
123
+ if i == self.master_slot_number:
124
+ hwmod = self.master
125
+ else:
126
+ hwmod, slot = self.create_hwmodule(slot_cfg, i, self.master)
127
+
128
+ if hwmod is not None:
129
+ hwmod.slot.hwmodule = hwmod
130
+ self.boards.append(hwmod)
131
+ self.slots.add(hwmod.slot)
132
+ else:
133
+ self.slots.add(slot)
134
+
135
+ def _init_slot_attrs(self):
136
+ for board in self.boards:
137
+ setattr(self, board.instance_name.lower(), board)
138
+
139
+ def reload(self):
140
+ """
141
+ Reload the current system configuration.
142
+
143
+ Important
144
+ ---------
145
+ This will rebuild all system objects and reinitialize the system tree.
146
+ """
147
+ self._init_system(self.config_path)
148
+
149
+ def _create_master(self) -> Tuple[HwModule | None, int]:
150
+ for i, slot in enumerate(self.config.system_config.slots):
151
+ if "attributes" in slot:
152
+ if "is_master" in slot.attributes:
153
+ if slot.attributes.is_master:
154
+ master, _ = self.create_hwmodule(slot, i, None)
155
+ if master is not None:
156
+ master.master = master
157
+ return master, i
158
+ else:
159
+ return None, -1
160
+ return None, -1 # ???
161
+
162
+ def create_hwmodule(
163
+ self, slot_entry: Munch, slot_number: int, master: HwModule | None
164
+ ) -> Tuple[HwModule | None, Slot]:
165
+ """
166
+ Create HwModule object for the slot.
167
+
168
+ Returns
169
+ -------
170
+ HwModule and Slot. HwModule is None if it cannot be created.
171
+ """
172
+ slot = self._create_slot(slot_entry, slot_number, master)
173
+ board_type = slot_entry.desired_hwmodule_type
174
+ board_name = slot_entry.desired_hwmodule_name
175
+
176
+ self.logger.debug(
177
+ f"Creating hwmodule {board_type} (desired name: {board_name}) in slot {slot.name}"
178
+ )
179
+
180
+ if board_type != "":
181
+ # try to create first the desired board
182
+ path = find_board_config(board_type)
183
+ if path is None:
184
+ self.logger.error(
185
+ f"No board config found for {board_type} (desired name: {board_name})"
186
+ )
187
+ return None, slot
188
+
189
+ try:
190
+ hwmod = self._create_hwmodule_from_cfg_file(slot, board_name, path)
191
+
192
+ 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
197
+
198
+ # try to probe desired board type
199
+ if hwmod.probe():
200
+ self.logger.info(
201
+ f"Found desired board type {hwmod.board_type} for slot {slot.name}"
202
+ )
203
+ return hwmod, slot
204
+
205
+ # try all other boards types. Maybe someone inserted the wrong board
206
+ self.logger.info(
207
+ f"Probing failed. Trying all other board types for slot {slot.name}"
208
+ )
209
+ for path in all_board_cfg_files():
210
+ try:
211
+ hwmod = self._create_hwmodule_from_cfg_file(slot, board_name, path)
212
+ except ConfigError:
213
+ # slot type not matching, ignore
214
+ continue
215
+ except Exception as e:
216
+ self.logger.debug(
217
+ f"failed to create hwmodule {path} for slot {slot.name}: {e}"
218
+ )
219
+ continue
220
+
221
+ if hwmod.probe():
222
+ self.logger.info(
223
+ f"Found other board type {hwmod.board_type} for slot {slot.name}"
224
+ )
225
+ return hwmod, slot
226
+
227
+ return None, slot
228
+
229
+ def _create_slot(
230
+ self, slot_entry: Munch, slot_number: int, master: HwModule | None
231
+ ) -> Slot:
232
+ attributes = None
233
+ if "attributes" in slot_entry:
234
+ attributes = slot_entry.attributes
235
+
236
+ return Slot(
237
+ slot_entry.name,
238
+ SlotType.from_string(slot_entry.slot_type),
239
+ slot_entry.desired_hwmodule_type,
240
+ slot_entry.desired_hwmodule_name,
241
+ slot_number,
242
+ None,
243
+ master,
244
+ attributes,
245
+ )
246
+
247
+ def _create_hwmodule_from_cfg_file(
248
+ self, slot: Slot, board_name: str, path: Path
249
+ ) -> HwModule:
250
+ """
251
+ Try to create a HwModule object from a board config file.
252
+ It does not probe the hardware.
253
+
254
+ Returns
255
+ -------
256
+ HwModule object.
257
+
258
+ Raises
259
+ ------
260
+ FileNotFoundError
261
+ If the board config file does not exist.
262
+ ConfigError
263
+ If the slot type in the config file does not match the slot type.
264
+ Exception
265
+ If something else went wrong.
266
+ """
267
+
268
+ with open(path) as file:
269
+ yaml_data = yaml.safe_load(file)
270
+ cfg = munchify(yaml_data)
271
+ # only instantiate if slot type matches
272
+ if cfg.slot_type != slot.slot_type.to_string():
273
+ raise ConfigError(
274
+ f"Slot type mismatch for slot {slot.name}: {cfg.slot_type} != {slot.slot_type}"
275
+ )
276
+
277
+ return HwModule(instance_name=board_name, config=yaml_data, slot=slot)
278
+
279
+ def get_module_in_slot(self, idx: int) -> HwModule | None:
280
+ return next(
281
+ (
282
+ v.hwmodule
283
+ for k, v in self.slots.items()
284
+ if getattr(v, "number", None) == idx
285
+ ),
286
+ None,
287
+ )
288
+
289
+ def get_module_by_name(self, name: str) -> HwModule | None:
290
+ return next(
291
+ (b for b in self.boards if getattr(b, "instance_name", None) == name),
292
+ None,
293
+ )
294
+
295
+ def __iter__(self):
296
+ return iter(self.boards)
297
+
298
+ def __getitem__(self, key) -> HwModule:
299
+ if isinstance(key, int):
300
+ value = self.get_module_in_slot(key)
301
+ else:
302
+ value = self.get_module_by_name(key)
303
+
304
+ if value is None:
305
+ raise KeyError(f"Board {key} not found in system!")
306
+
307
+ return value
308
+
309
+ def __getattr__(self, name: str) -> Any:
310
+ """Access board by attribute using dot notation"""
311
+ # This fixes mypy error: "... has no object ..."
312
+ hwModule = self.get_module_by_name(name)
313
+ if hwModule is not None:
314
+ return hwModule
315
+ raise AttributeError(
316
+ f"'{type(self).__name__}' object has no board with name '{name}'"
317
+ )
318
+
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)