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/devices/gpio.py ADDED
@@ -0,0 +1,340 @@
1
+ from abc import ABC, abstractmethod
2
+ import os
3
+ import re
4
+ from pathlib import Path
5
+
6
+ import gpiod
7
+ from gpiod.chip import LineSettings
8
+ from gpiod.line import Direction, Value
9
+ from more_itertools import first_true
10
+
11
+ from ekfsm.core.components import SystemComponent
12
+ from ekfsm.exceptions import GPIOError
13
+ from ekfsm.log import ekfsm_logger
14
+
15
+ from .generic import Device
16
+ from ..core.probe import ProbeableDevice
17
+
18
+ gpio_pat = re.compile(r"gpiochip\d+")
19
+
20
+
21
+ def get_gpio_major_minor(path: Path) -> tuple[int, int]:
22
+ for d in path.iterdir():
23
+ if gpio_pat.match(d.name):
24
+ dev = d / "dev"
25
+ if dev.exists():
26
+ content = dev.read_text().strip()
27
+ major, minor = map(int, content.split(":"))
28
+ return major, minor
29
+
30
+ raise GPIOError(
31
+ GPIOError.ErrorType.NO_MAJOR_MINOR,
32
+ f"No minor/major number found for GPIO device at {path}",
33
+ )
34
+
35
+
36
+ def find_gpio_dev_with_major_minor(major: int, minor: int) -> Path | None:
37
+ for dev in Path("/dev").iterdir():
38
+ if gpio_pat.match(dev.name):
39
+ stat_info = dev.stat()
40
+
41
+ cmaj = os.major(stat_info.st_rdev)
42
+ cmin = os.minor(stat_info.st_rdev)
43
+
44
+ if cmaj == major and cmin == minor:
45
+ return dev
46
+
47
+ raise GPIOError(
48
+ GPIOError.ErrorType.NO_MATCHING_DEVICE,
49
+ f"Failed to find GPIO device with major {major} and minor {minor}",
50
+ )
51
+
52
+
53
+ class GPIO(Device):
54
+
55
+ def __init__(
56
+ self,
57
+ name: str,
58
+ parent: SystemComponent | None = None,
59
+ *args,
60
+ **kwargs,
61
+ ):
62
+ super().__init__(
63
+ name,
64
+ parent,
65
+ None,
66
+ *args,
67
+ **kwargs,
68
+ )
69
+ self.logger = ekfsm_logger("GPIODevice" + name)
70
+ major, minor = self._find_gpio_dev(parent, *args, **kwargs)
71
+ self.gpio = find_gpio_dev_with_major_minor(major, minor)
72
+
73
+ assert self.gpio is not None
74
+ match = re.search(r"\d+", self.gpio.name)
75
+ if match:
76
+ self.number: int = int(match.group().strip())
77
+ else:
78
+ raise GPIOError(
79
+ GPIOError.ErrorType.NO_MATCHING_DEVICE, "Failed to find matching device"
80
+ )
81
+
82
+ self.init_dev()
83
+
84
+ def _find_gpio_dev(
85
+ self,
86
+ parent: SystemComponent | None = None,
87
+ *args,
88
+ **kwargs,
89
+ ) -> tuple[int, int]:
90
+ self.addr = self.get_i2c_chip_addr()
91
+ self.logger.debug(f"GPIO: {self.addr}")
92
+ self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
93
+ return get_gpio_major_minor(self.sysfs_device.path)
94
+
95
+ def init_dev(self):
96
+ if self.gpio:
97
+ try:
98
+ self.dev = gpiod.Chip(str(self.gpio))
99
+ self.initialized = True
100
+ except FileNotFoundError:
101
+ raise FileNotFoundError(f"{self.gpio} does not exist")
102
+
103
+ def num_lines(self) -> int:
104
+ """
105
+ Get number of GPIO lines available on the device.
106
+ """
107
+ return self.dev.get_info().num_lines
108
+
109
+ def set_pin(self, pin: int, value: bool) -> None:
110
+ """
111
+ Set the value of a GPIO pin.
112
+
113
+ Parameters
114
+ ----------
115
+ pin : int
116
+ The pin number.
117
+ value : bool
118
+ The value to set.
119
+ """
120
+ v = Value.ACTIVE if value else Value.INACTIVE
121
+ with self.dev.request_lines(
122
+ consumer="set-pin",
123
+ config={pin: gpiod.LineSettings()},
124
+ ) as request:
125
+ request.set_value(pin, v)
126
+
127
+ def get_pin(self, pin: int) -> bool:
128
+ """
129
+ Get the value of a GPIO pin.
130
+
131
+ Parameters
132
+ ----------
133
+ pin : int
134
+ The pin number.
135
+
136
+ Returns
137
+ -------
138
+ bool
139
+ The value of the pin.
140
+ """
141
+ with self.dev.request_lines(
142
+ consumer="get-pin",
143
+ config={pin: gpiod.LineSettings()},
144
+ ) as req:
145
+ value = req.get_value(pin)
146
+ return value == Value.ACTIVE
147
+
148
+ def get_lines(self, lines: list[int]):
149
+ if (
150
+ invalid_pin := first_true(
151
+ lines,
152
+ pred=lambda line: line < 0 or line >= self.num_lines(),
153
+ default=None,
154
+ )
155
+ ) is not None:
156
+ raise GPIOError(
157
+ GPIOError.ErrorType.INVALID_PIN, f"GPIO {invalid_pin} is invalid."
158
+ )
159
+
160
+ def set_direction(self, pin: int, direction: bool) -> None:
161
+ """
162
+ Set the direction of a GPIO pin.
163
+
164
+ Parameters
165
+ ----------
166
+ pin : int
167
+ The pin number.
168
+ direction : bool
169
+ The direction to set. True for output, False for input.
170
+ """
171
+ dir = Direction.OUTPUT if direction else Direction.INPUT
172
+ self.dev.request_lines(
173
+ consumer="set-direction",
174
+ config={pin: LineSettings(direction=dir)},
175
+ )
176
+
177
+ def __str__(self) -> str:
178
+ return (
179
+ f"GPIO - Number: {self.number}; "
180
+ f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''} "
181
+ f"(dev: {self.gpio if self.gpio else 'No matching device found'})"
182
+ )
183
+
184
+
185
+ class GPIOExpander(GPIO):
186
+ def __init__(
187
+ self,
188
+ name: str,
189
+ parent: SystemComponent | None,
190
+ *args,
191
+ **kwargs,
192
+ ):
193
+ super().__init__(name, parent, None, *args, **kwargs)
194
+
195
+ def __str__(self) -> str:
196
+ return (
197
+ f"GPIOExpander - Number: {self.number}; "
198
+ f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''}"
199
+ )
200
+
201
+
202
+ class EKFIdentificationIOExpander(GPIOExpander, ProbeableDevice):
203
+ def __init__(
204
+ self,
205
+ name: str,
206
+ parent: SystemComponent | None,
207
+ *args,
208
+ **kwargs,
209
+ ):
210
+ super().__init__(name, parent, None, *args, **kwargs)
211
+
212
+ def probe(self, *args, **kwargs) -> bool:
213
+ from ekfsm.core import HwModule
214
+
215
+ assert isinstance(self.root, HwModule)
216
+ id, _ = self.read_board_id_rev()
217
+
218
+ return self.root.id == id
219
+
220
+ def read_board_id_rev(self) -> tuple[int, int]:
221
+ for pin in range(6, 8):
222
+ self.set_direction(pin, True)
223
+ self.set_pin(pin, False)
224
+ for pin in range(0, 6):
225
+ self.set_direction(pin, False)
226
+
227
+ v_gnd = self.read_id_gpio_inputs()
228
+ self.set_pin(7, True)
229
+ v_7h = self.read_id_gpio_inputs()
230
+ self.set_pin(6, True)
231
+ v_6h = self.read_id_gpio_inputs()
232
+
233
+ readings = [v_gnd, v_7h, v_6h]
234
+ bit_sums = []
235
+ for bit in range(5, -1, -1):
236
+ s = 0
237
+ for reading in readings:
238
+ s += (reading >> bit) & 1
239
+ bit_sums.append(s)
240
+
241
+ return (
242
+ sum(val * (4**i) for i, val in enumerate(reversed(bit_sums[2:]))),
243
+ self._get_board_rev(bit_sums[:2]), # board_rev
244
+ )
245
+
246
+ def revision(self) -> str:
247
+ id, rev = self.read_board_id_rev()
248
+ return str(rev)
249
+
250
+ def read_id_gpio_inputs(self) -> int:
251
+ value = 0
252
+ for pin in range(6):
253
+ if self.get_pin(pin):
254
+ value |= 1 << pin
255
+
256
+ return value
257
+
258
+ @staticmethod
259
+ def _get_board_rev(bits: list[int]) -> int:
260
+ """Convert 2-bit sum values to board revision number."""
261
+ rev_map = {
262
+ (0, 0): 0,
263
+ (0, 1): 1,
264
+ (0, 2): 2,
265
+ (1, 0): 3,
266
+ (1, 1): 4,
267
+ (1, 2): 5,
268
+ (2, 0): 6,
269
+ (2, 1): 7,
270
+ (2, 2): 8,
271
+ }
272
+ return rev_map.get((bits[0], bits[1]), -1)
273
+
274
+ def __str__(self) -> str:
275
+ return (
276
+ f"EKFIdentificationIOExpander - Number: {self.number}; "
277
+ f'sysfs_path: {self.sysfs_device.path if self.sysfs_device else ""}'
278
+ )
279
+
280
+
281
+ class SimGpio(ABC):
282
+ @abstractmethod
283
+ def num_lines(self) -> int:
284
+ pass
285
+
286
+ @abstractmethod
287
+ def set_pin(self, pin: int, value: bool) -> None:
288
+ pass
289
+
290
+ @abstractmethod
291
+ def get_pin(self, pin: int) -> bool:
292
+ pass
293
+
294
+ @abstractmethod
295
+ def set_direction(self, pin: int, direction: bool) -> None:
296
+ pass
297
+
298
+
299
+ class EKFIdSimGpio(SimGpio):
300
+ def __init__(self, coding_gnd, coding_vcc, coding_6, coding_7) -> None:
301
+ self._coding_gnd = coding_gnd
302
+ self._coding_vcc = coding_vcc
303
+ self._coding_6 = coding_6
304
+ self._coding_7 = coding_7
305
+ self._dir = 0
306
+ self._out = 0
307
+ self._in = 0
308
+
309
+ def num_lines(self) -> int:
310
+ return 8
311
+
312
+ def set_pin(self, pin: int, value: bool) -> None:
313
+ mask = 1 << pin
314
+ if value not in [0, 1]:
315
+ raise RuntimeError("value must be 0 or 1")
316
+ if self._dir & mask:
317
+ self._out = (self._out & ~mask) | (value << pin)
318
+ else:
319
+ raise RuntimeError("pin not set as output")
320
+
321
+ def get_pin(self, pin: int) -> bool:
322
+ mask = 1 << pin
323
+ if self._coding_gnd & mask:
324
+ return False
325
+ if self._coding_vcc & mask:
326
+ return True
327
+ if self._coding_6 & mask:
328
+ return True if self._out & (1 << 6) else False
329
+ if self._coding_7 & mask:
330
+ return True if self._out & (1 << 7) else False
331
+ return False
332
+
333
+ def set_direction(self, pin: int, direction: bool) -> None:
334
+ if direction == 1 and (pin != 6 and pin != 7):
335
+ raise RuntimeError("only pins 6 and 7 supported as output")
336
+ mask = 1 << pin
337
+ if direction:
338
+ self._dir |= mask
339
+ else:
340
+ self._dir &= ~mask
ekfsm/devices/hwmon.py ADDED
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+ import os
3
+ import glob
4
+ from pathlib import Path
5
+
6
+ from ekfsm.core.sysfs import SYSFS_ROOT
7
+ from ekfsm.devices.generic import Device
8
+
9
+ # Path to the root of the HWMON sysfs filesystem
10
+ HWMON_ROOT = SYSFS_ROOT / Path('class/hwmon')
11
+
12
+
13
+ def find_core_temp_dir(hwmon_dir) -> Path:
14
+ """
15
+ Find the directory containing the coretemp hwmon device.
16
+
17
+ Args:
18
+ hwmon_dir: Path to the hwmon directory
19
+
20
+ Returns:
21
+ Path to the directory containing the coretemp hwmon device
22
+
23
+ Raises:
24
+ FileNotFoundError: If no coretemp directory is found
25
+ """
26
+ # List all 'name' files in each subdirectory of hwmon_dir
27
+ name_files = glob.glob(os.path.join(hwmon_dir, '*', 'name'))
28
+
29
+ # Search for the file containing "coretemp"
30
+ for name_file in name_files:
31
+ with open(name_file, 'r') as file:
32
+ if file.readline().strip() == 'coretemp':
33
+ # Return the directory containing this file
34
+ return Path(os.path.dirname(name_file))
35
+
36
+ raise FileNotFoundError("No coretemp directory found")
37
+
38
+
39
+ class HWMON(Device):
40
+ """
41
+ A class to represent the HWMON device.
42
+
43
+ A HWMON device is a virtual device that is used to read hardware monitoring values from the sysfs filesystem.
44
+
45
+ Note:
46
+ Currently, only the CPU temperature is read from the HWMON device.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ name: str,
52
+ parent: Device,
53
+ *args,
54
+ **kwargs,
55
+ ):
56
+ from ekfsm.core.sysfs import SysFSDevice, SYSFS_ROOT
57
+
58
+ self.sysfs_device: SysFSDevice = SysFSDevice(find_core_temp_dir(SYSFS_ROOT / Path('class/hwmon')))
59
+
60
+ super().__init__(name, parent, None, *args, **kwargs)
61
+
62
+ def cputemp(self):
63
+ """
64
+ Get the CPU temperature from the HWMON device.
65
+
66
+ Returns
67
+ -------
68
+ int
69
+ The CPU temperature in degrees Celsius.
70
+ """
71
+ return int(self.sysfs_device.read_attr_utf8("temp1_input").strip()) / 1000
ekfsm/devices/iio.py ADDED
@@ -0,0 +1,58 @@
1
+ from .generic import SysFSDevice
2
+
3
+
4
+ def iio_get_in_value(dev: SysFSDevice, attrset: str) -> float:
5
+ """
6
+ Calculate a value from an IIO in_* attribute set, using the _raw, _scale and _offset attributes (if present).
7
+ Typical name of attrset are "in_temp" or "in_voltage0".
8
+
9
+ Formula according to https://wiki.st.com/stm32mpu/wiki/How_to_use_the_IIO_user_space_interface
10
+
11
+ Parameters
12
+ ----------
13
+ dev
14
+ sysfs device object pointing to the iio directory
15
+ attrset
16
+ name of the attribute set to read from (e.g. "in_temp")
17
+
18
+ Returns
19
+ -------
20
+ float
21
+ calculated value from the attribute set (no unit conversion)
22
+
23
+ Raises
24
+ ------
25
+ FileNotFoundError
26
+ if the neiter _input nor _raw attribute is found
27
+
28
+ """
29
+ # check if _input exists
30
+ try:
31
+ content = dev.read_attr_utf8(f"{attrset}_input")
32
+ return float(content)
33
+ except StopIteration:
34
+ pass
35
+
36
+ # check if _raw exists
37
+ try:
38
+ content = dev.read_attr_utf8(f"{attrset}_raw")
39
+ except StopIteration:
40
+ raise FileNotFoundError(f"{attrset}_raw not found")
41
+
42
+ raw = float(content)
43
+
44
+ # use offset if present
45
+ try:
46
+ content = dev.read_attr_utf8(f"{attrset}_offset")
47
+ raw += float(content)
48
+ except StopIteration:
49
+ pass
50
+
51
+ # use scale if present
52
+ try:
53
+ content = dev.read_attr_utf8(f"{attrset}_scale")
54
+ raw *= float(content)
55
+ except StopIteration:
56
+ pass
57
+
58
+ return raw
@@ -0,0 +1,41 @@
1
+ from pathlib import Path
2
+
3
+ from ekfsm.core.components import SystemComponent
4
+ from ekfsm.log import ekfsm_logger
5
+
6
+ from ..core.sysfs import SysFSDevice
7
+
8
+ from .generic import Device
9
+ from .iio import iio_get_in_value
10
+
11
+
12
+ class IIOThermalHumidity(Device):
13
+ """
14
+ Device for IIO thermal and/or humidity sensors.
15
+
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ name: str,
21
+ parent: SystemComponent | None = None,
22
+ children: list[Device] | None = None,
23
+ *args,
24
+ **kwargs,
25
+ ):
26
+ self.logger = ekfsm_logger("IIOThermalHumidity:" + name)
27
+ super().__init__(name, parent, children, **kwargs)
28
+ self.addr = self.get_i2c_chip_addr()
29
+ self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
30
+
31
+ dir = list(Path(self.sysfs_device.path).glob("iio:device*"))
32
+ if len(dir) == 0:
33
+ raise FileNotFoundError("iio entry not found")
34
+ self.iio_sysfs = SysFSDevice(dir[0])
35
+ self.logger.debug(f"iio: {self.iio_sysfs.path}")
36
+
37
+ def temperature(self) -> float:
38
+ return iio_get_in_value(self.iio_sysfs, "in_temp") / 1000.0
39
+
40
+ def humidity(self) -> float:
41
+ return iio_get_in_value(self.iio_sysfs, "in_humidityrelative") / 1000.0
ekfsm/devices/mux.py ADDED
@@ -0,0 +1,39 @@
1
+ from ekfsm.core.components import SystemComponent
2
+
3
+ from ..core.sysfs import SysFSDevice
4
+ from .generic import Device
5
+
6
+
7
+ class MuxChannel(Device):
8
+ def __init__(
9
+ self,
10
+ name: str,
11
+ channel_id: int,
12
+ parent: 'I2CMux',
13
+ children: list[Device] | None = None,
14
+ *args,
15
+ **kwargs,
16
+ ) -> None:
17
+ super().__init__(name=name, parent=parent, children=children)
18
+ self.channel_id = channel_id
19
+
20
+ assert parent.sysfs_device is not None
21
+ assert isinstance(self.parent, I2CMux)
22
+
23
+ path = parent.sysfs_device.path / f"channel-{self.channel_id}"
24
+ self.sysfs_device = SysFSDevice(path)
25
+
26
+
27
+ class I2CMux(Device):
28
+ def __init__(
29
+ self,
30
+ name: str,
31
+ parent: SystemComponent | None = None,
32
+ children: list[MuxChannel] | None = None,
33
+ *args,
34
+ **kwargs,
35
+ ):
36
+ super().__init__(name, parent, children, **kwargs)
37
+
38
+ self.addr = self.get_i2c_chip_addr()
39
+ self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
ekfsm/devices/pmbus.py ADDED
@@ -0,0 +1,65 @@
1
+ from pathlib import Path
2
+
3
+ from ekfsm.core.components import SystemComponent
4
+
5
+ from ..core.sysfs import SysFSDevice
6
+
7
+ from .generic import Device
8
+ from ..core.probe import ProbeableDevice
9
+
10
+
11
+ class PmBus(Device, ProbeableDevice):
12
+ def __init__(
13
+ self,
14
+ name: str,
15
+ parent: SystemComponent | None = None,
16
+ children: list[Device] | None = None,
17
+ *args,
18
+ **kwargs,
19
+ ):
20
+ super().__init__(name, parent, children, **kwargs)
21
+ self.addr = self.get_i2c_chip_addr()
22
+ self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
23
+
24
+ files = list(Path(self.sysfs_device.path).rglob("hwmon/*/in1_input"))
25
+ if len(files) == 0:
26
+ raise FileNotFoundError("No HWMON entries found")
27
+ self.hwmon_sysfs = SysFSDevice(files[0].parent)
28
+
29
+ def probe(self, *args, **kwargs) -> bool:
30
+ from ekfsm.core import HwModule
31
+
32
+ assert isinstance(self.root, HwModule)
33
+ return self.root.id == self.model()
34
+
35
+ # 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:
40
+ return float(self.hwmon_sysfs.read_attr_utf8(in_file)) / 1000.0
41
+
42
+ def in1_input(self) -> float:
43
+ return self._in_conversion("in1_input")
44
+
45
+ def in2_input(self) -> float:
46
+ return self._in_conversion("in2_input")
47
+
48
+ def curr1_input(self) -> float:
49
+ return self._current_conversion("curr1_input")
50
+
51
+ def curr2_input(self) -> float:
52
+ return self._current_conversion("curr2_input")
53
+
54
+ # Inventory Interface
55
+ def vendor(self) -> str:
56
+ return self.hwmon_sysfs.read_attr_utf8("vendor").strip()
57
+
58
+ def model(self) -> str:
59
+ return self.hwmon_sysfs.read_attr_utf8("model").strip()
60
+
61
+ def serial(self) -> str:
62
+ return self.hwmon_sysfs.read_attr_utf8("serial").strip()
63
+
64
+ def revision(self) -> str:
65
+ return self.hwmon_sysfs.read_attr_utf8("revision").strip()
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+ from ekfsm.core.components import HwModule
3
+ from ekfsm.core.sysfs import SysFSDevice, SYSFS_ROOT
4
+ from .generic import Device
5
+
6
+
7
+ class SMBIOS(Device):
8
+ """
9
+ A class to represent the SMBIOS device.
10
+
11
+ A SMBIOS device is a virtual device that is used to read system
12
+ configuration values from the DMI table.
13
+
14
+ Note:
15
+ Currently, only the board version / revision is read from the DMI table.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ name: str,
21
+ parent: HwModule | None = None,
22
+ *args,
23
+ **kwargs,
24
+ ):
25
+ self.sysfs_device: SysFSDevice = SysFSDevice(SYSFS_ROOT / Path("devices/virtual/dmi/id"))
26
+
27
+ super().__init__(name, parent, None, *args, **kwargs)
28
+
29
+ def revision(self) -> str:
30
+ """
31
+ Get the board revision from the DMI table.
32
+
33
+ Returns
34
+ -------
35
+ str
36
+ The board revision.
37
+ """
38
+ return self.sysfs_device.read_attr_utf8("board_version").strip()
ekfsm/devices/utils.py ADDED
@@ -0,0 +1,16 @@
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
+
12
+
13
+ def get_crc16_xmodem(data: bytes) -> int:
14
+ crc16_xmodem = Crc("xmodem")
15
+ crc16_xmodem.update(data)
16
+ return crc16_xmodem.crcValue