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.
- ekfsm/__init__.py +3 -14
- ekfsm/boards/oem/ekf/shu-shuttle.yaml +43 -0
- ekfsm/boards/oem/ekf/sq3-quartet.yaml +51 -37
- ekfsm/boards/oem/ekf/z1010.yaml +102 -0
- ekfsm/boards/oem/hitron/hdrc-300s.yaml +1 -1
- ekfsm/cli.py +32 -9
- ekfsm/config.py +14 -6
- ekfsm/core/__init__.py +13 -3
- ekfsm/core/components.py +7 -8
- ekfsm/core/connections.py +19 -0
- ekfsm/core/slots.py +6 -8
- ekfsm/core/sysfs.py +215 -25
- ekfsm/core/utils.py +128 -64
- ekfsm/devices/__init__.py +27 -7
- ekfsm/devices/buttons.py +251 -0
- ekfsm/devices/colorLed.py +110 -0
- ekfsm/devices/coretemp.py +35 -13
- ekfsm/devices/eeprom.py +73 -45
- ekfsm/devices/ekf_ccu_uc.py +76 -54
- ekfsm/devices/ekf_sur_led.py +6 -2
- ekfsm/devices/generic.py +200 -59
- ekfsm/devices/gpio.py +37 -27
- ekfsm/devices/iio.py +15 -31
- ekfsm/devices/iio_thermal_humidity.py +20 -13
- ekfsm/devices/imu.py +8 -4
- ekfsm/devices/io4edge.py +185 -0
- ekfsm/devices/ledArray.py +54 -0
- ekfsm/devices/mux.py +46 -8
- ekfsm/devices/pixelDisplay.py +141 -0
- ekfsm/devices/pmbus.py +74 -101
- ekfsm/devices/smbios.py +28 -8
- ekfsm/devices/smbus.py +1 -1
- ekfsm/devices/thermal_humidity.py +80 -0
- ekfsm/devices/toggles.py +90 -0
- ekfsm/devices/utils.py +52 -8
- ekfsm/devices/watchdog.py +79 -0
- ekfsm/exceptions.py +28 -7
- ekfsm/lock.py +48 -21
- ekfsm/simctrl.py +37 -83
- ekfsm/system.py +89 -73
- ekfsm/utils.py +44 -0
- {ekfsm-0.13.0a183.dist-info → ekfsm-1.5.0.dist-info}/METADATA +12 -6
- ekfsm-1.5.0.dist-info/RECORD +57 -0
- ekfsm-0.13.0a183.dist-info/RECORD +0 -45
- {ekfsm-0.13.0a183.dist-info → ekfsm-1.5.0.dist-info}/WHEEL +0 -0
- {ekfsm-0.13.0a183.dist-info → ekfsm-1.5.0.dist-info}/entry_points.txt +0 -0
ekfsm/devices/pmbus.py
CHANGED
|
@@ -1,84 +1,48 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from enum import IntFlag
|
|
2
|
-
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
from ekfsm.core.components import SysTree
|
|
5
|
+
from ekfsm.devices.utils import retry
|
|
6
|
+
from ekfsm.exceptions import ConfigError, HWMonError
|
|
7
|
+
from ekfsm.log import ekfsm_logger
|
|
5
8
|
|
|
6
|
-
from ..core.sysfs import SysFSDevice, sysfs_root
|
|
7
|
-
|
|
8
|
-
from .generic import Device
|
|
9
9
|
from ..core.probe import ProbeableDevice
|
|
10
|
+
from ..core.sysfs import list_sysfs_attributes, sysfs_root
|
|
11
|
+
from .generic import Device
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import re
|
|
16
|
-
|
|
17
|
-
__all__ = ["PsuStatus", "PmBus", "retry"]
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PSUStatus",
|
|
15
|
+
"PMBus",
|
|
16
|
+
]
|
|
18
17
|
|
|
19
18
|
logger = ekfsm_logger(__name__)
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
Retry decorator.
|
|
25
|
-
|
|
26
|
-
Decorator that retries a function a number of times before giving up.
|
|
27
|
-
|
|
28
|
-
This is useful for functions that may fail due to transient errors.
|
|
29
|
-
|
|
30
|
-
Note
|
|
31
|
-
----
|
|
32
|
-
This is needed for certain PMBus commands that may fail due to transient errors
|
|
33
|
-
because page switching timing is not effectively handled by older kernel versions.
|
|
34
|
-
|
|
35
|
-
Important
|
|
36
|
-
---------
|
|
37
|
-
This decorator is thread-safe, meaning a read attempt is atomic and cannot
|
|
38
|
-
be interupted by scheduler.
|
|
39
|
-
|
|
40
|
-
Parameters
|
|
41
|
-
----------
|
|
42
|
-
max_attempts
|
|
43
|
-
The maximum number of attempts before giving up.
|
|
44
|
-
delay
|
|
45
|
-
The delay in seconds between attempts.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
lock = Lock()
|
|
49
|
-
|
|
50
|
-
def decorator(func):
|
|
51
|
-
@wraps(func)
|
|
52
|
-
def wrapper(*args, **kwargs):
|
|
53
|
-
attempts = 0
|
|
54
|
-
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
|
-
)
|
|
68
|
-
sleep(delay)
|
|
69
|
-
|
|
70
|
-
return wrapper
|
|
71
|
-
|
|
72
|
-
return decorator
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class PsuStatus(IntFlag):
|
|
21
|
+
class PSUStatus(IntFlag):
|
|
76
22
|
"""
|
|
77
23
|
Represents the status of a PSU according to STATUS_BYTE register.
|
|
78
24
|
|
|
79
|
-
See
|
|
25
|
+
See also
|
|
80
26
|
--------
|
|
27
|
+
External Documentation:
|
|
81
28
|
`PMBus Power System Management Protocol Specification - Part II - Revision 1.4, Fig. 60 <https://pmbus.org/>`_
|
|
29
|
+
|
|
30
|
+
Example
|
|
31
|
+
-------
|
|
32
|
+
>>> from ekfsm.devices.pmbus import PsuStatus
|
|
33
|
+
>>> status = PsuStatus(0x1F)
|
|
34
|
+
>>> status
|
|
35
|
+
<PsuStatus.OUTPUT_OVERCURRENT|INPUT_UNDERVOLTAGE|TEMP_ANORMALY|COMMUNICATION_ERROR|ERROR: 31>
|
|
36
|
+
>>> PsuStatus.OUTPUT_OVERCURRENT in status
|
|
37
|
+
True
|
|
38
|
+
>>> # OK is always present
|
|
39
|
+
>>> PsuStatus.OK in status
|
|
40
|
+
True
|
|
41
|
+
>>> # Instead, check if status is OK
|
|
42
|
+
>>> status == PsuStatus(0x00)
|
|
43
|
+
False
|
|
44
|
+
>>> PsuStatus.OUTPUT_OVERCURRENT in status
|
|
45
|
+
False
|
|
82
46
|
"""
|
|
83
47
|
|
|
84
48
|
OUTPUT_OVERVOLTAGE = 0x20
|
|
@@ -90,41 +54,50 @@ class PsuStatus(IntFlag):
|
|
|
90
54
|
OK = 0x00
|
|
91
55
|
|
|
92
56
|
|
|
93
|
-
|
|
57
|
+
logger = ekfsm_logger(__name__)
|
|
58
|
+
|
|
94
59
|
|
|
60
|
+
class PMBus(Device, ProbeableDevice):
|
|
95
61
|
def __init__(
|
|
96
62
|
self,
|
|
97
63
|
name: str,
|
|
98
64
|
parent: SysTree | None = None,
|
|
99
65
|
children: list[Device] | None = None,
|
|
66
|
+
abort: bool = False,
|
|
100
67
|
*args,
|
|
101
68
|
**kwargs,
|
|
102
69
|
):
|
|
103
|
-
super().__init__(name, parent, children, **kwargs)
|
|
70
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
104
71
|
self.addr = self.get_i2c_chip_addr()
|
|
105
|
-
self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
72
|
+
self.sysfs_device = self.get_i2c_sysfs_device(self.addr, driver_required=True)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
for entry in self.sysfs_device.path.glob("hwmon/hwmon*"):
|
|
76
|
+
if entry.is_dir():
|
|
77
|
+
attrs = list_sysfs_attributes(entry)
|
|
78
|
+
self.sysfs_device.extend_attributes(attrs)
|
|
79
|
+
|
|
80
|
+
debug_attrs_path = sysfs_root().joinpath(
|
|
81
|
+
f"kernel/debug/pmbus/{entry.name}"
|
|
82
|
+
)
|
|
83
|
+
debug_attrs = list_sysfs_attributes(debug_attrs_path)
|
|
84
|
+
self.sysfs_device.extend_attributes(debug_attrs)
|
|
85
|
+
except FileNotFoundError:
|
|
86
|
+
logger.debug("Expected sysfs attribute not found")
|
|
87
|
+
except StopIteration:
|
|
88
|
+
raise HWMonError("Device is not managed by hwmon subsystem")
|
|
117
89
|
|
|
118
90
|
def probe(self, *args, **kwargs) -> bool:
|
|
119
|
-
from ekfsm.core import
|
|
91
|
+
from ekfsm.core import HWModule
|
|
120
92
|
|
|
121
|
-
|
|
93
|
+
if not isinstance(self.hw_module, HWModule):
|
|
94
|
+
raise ConfigError(f"{self.name}: hw_module must be a HWModule instance")
|
|
122
95
|
# compare the regexp from the board yaml file with the model
|
|
123
96
|
return re.match(self.hw_module.id, self.model()) is not None
|
|
124
97
|
|
|
125
98
|
# Voltage and Current Interfaces
|
|
126
|
-
def
|
|
127
|
-
return
|
|
99
|
+
def __convert_and_scale(self, attr: str) -> float:
|
|
100
|
+
return self.sysfs.read_float(attr) / 1000.0
|
|
128
101
|
|
|
129
102
|
@retry()
|
|
130
103
|
def in1_input(self) -> float:
|
|
@@ -135,7 +108,7 @@ class PmBus(Device, ProbeableDevice):
|
|
|
135
108
|
-------
|
|
136
109
|
Input voltage in volts
|
|
137
110
|
"""
|
|
138
|
-
return self.
|
|
111
|
+
return self.__convert_and_scale("in1_input")
|
|
139
112
|
|
|
140
113
|
@retry()
|
|
141
114
|
def in2_input(self) -> float:
|
|
@@ -146,7 +119,7 @@ class PmBus(Device, ProbeableDevice):
|
|
|
146
119
|
-------
|
|
147
120
|
Input voltage in volts
|
|
148
121
|
"""
|
|
149
|
-
return self.
|
|
122
|
+
return self.__convert_and_scale("in2_input")
|
|
150
123
|
|
|
151
124
|
@retry()
|
|
152
125
|
def curr1_input(self) -> float:
|
|
@@ -157,7 +130,7 @@ class PmBus(Device, ProbeableDevice):
|
|
|
157
130
|
-------
|
|
158
131
|
Input current in amperes
|
|
159
132
|
"""
|
|
160
|
-
return self.
|
|
133
|
+
return self.__convert_and_scale("curr1_input")
|
|
161
134
|
|
|
162
135
|
@retry()
|
|
163
136
|
def curr2_input(self) -> float:
|
|
@@ -168,32 +141,32 @@ class PmBus(Device, ProbeableDevice):
|
|
|
168
141
|
-------
|
|
169
142
|
Input current in amperes
|
|
170
143
|
"""
|
|
171
|
-
return self.
|
|
144
|
+
return self.__convert_and_scale("curr2_input")
|
|
172
145
|
|
|
173
146
|
# Status Interface
|
|
174
147
|
@retry()
|
|
175
|
-
def status0_input(self) ->
|
|
148
|
+
def status0_input(self) -> PSUStatus:
|
|
176
149
|
"""
|
|
177
150
|
Get the status of PSU page 1.
|
|
178
151
|
|
|
179
152
|
Returns
|
|
180
153
|
-------
|
|
181
|
-
PSU status as defined in
|
|
154
|
+
PSU status as defined in PSUStatus
|
|
182
155
|
"""
|
|
183
|
-
status =
|
|
184
|
-
return
|
|
156
|
+
status = self.sysfs.read_hex("status0_input")
|
|
157
|
+
return PSUStatus(status)
|
|
185
158
|
|
|
186
159
|
@retry()
|
|
187
|
-
def status1_input(self) ->
|
|
160
|
+
def status1_input(self) -> PSUStatus:
|
|
188
161
|
"""
|
|
189
162
|
Get the status of PSU page 2.
|
|
190
163
|
|
|
191
164
|
Returns
|
|
192
165
|
-------
|
|
193
|
-
PSU status as defined in
|
|
166
|
+
PSU status as defined in PSUStatus
|
|
194
167
|
"""
|
|
195
|
-
status =
|
|
196
|
-
return
|
|
168
|
+
status = self.sysfs.read_hex("status1_input")
|
|
169
|
+
return PSUStatus(status)
|
|
197
170
|
|
|
198
171
|
# Temperature Interface
|
|
199
172
|
@retry()
|
|
@@ -205,7 +178,7 @@ class PmBus(Device, ProbeableDevice):
|
|
|
205
178
|
-------
|
|
206
179
|
PSU temperature in degrees celsius
|
|
207
180
|
"""
|
|
208
|
-
return self.
|
|
181
|
+
return self.__convert_and_scale("temp1_input")
|
|
209
182
|
|
|
210
183
|
# Inventory Interface
|
|
211
184
|
def vendor(self) -> str:
|
|
@@ -216,7 +189,7 @@ class PmBus(Device, ProbeableDevice):
|
|
|
216
189
|
-------
|
|
217
190
|
PSU vendor
|
|
218
191
|
"""
|
|
219
|
-
return self.
|
|
192
|
+
return self.sysfs.read_utf8("vendor")
|
|
220
193
|
|
|
221
194
|
def model(self) -> str:
|
|
222
195
|
"""
|
|
@@ -226,7 +199,7 @@ class PmBus(Device, ProbeableDevice):
|
|
|
226
199
|
-------
|
|
227
200
|
PSU model
|
|
228
201
|
"""
|
|
229
|
-
return self.
|
|
202
|
+
return self.sysfs.read_utf8("model")
|
|
230
203
|
|
|
231
204
|
def serial(self) -> str:
|
|
232
205
|
"""
|
|
@@ -236,7 +209,7 @@ class PmBus(Device, ProbeableDevice):
|
|
|
236
209
|
-------
|
|
237
210
|
PSU serial number
|
|
238
211
|
"""
|
|
239
|
-
return self.
|
|
212
|
+
return self.sysfs.read_utf8("serial")
|
|
240
213
|
|
|
241
214
|
def revision(self) -> str:
|
|
242
215
|
"""
|
|
@@ -246,4 +219,4 @@ class PmBus(Device, ProbeableDevice):
|
|
|
246
219
|
-------
|
|
247
220
|
PSU revision
|
|
248
221
|
"""
|
|
249
|
-
return self.
|
|
222
|
+
return self.sysfs.read_utf8("revision")
|
ekfsm/devices/smbios.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from ekfsm.core.
|
|
2
|
+
|
|
3
|
+
from ekfsm.core.components import HWModule
|
|
4
|
+
from ekfsm.core.sysfs import SysfsDevice, sysfs_root
|
|
5
|
+
from ekfsm.log import ekfsm_logger
|
|
6
|
+
|
|
4
7
|
from .generic import Device
|
|
5
8
|
|
|
9
|
+
logger = ekfsm_logger(__name__)
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
class SMBIOS(Device):
|
|
8
13
|
"""
|
|
@@ -18,15 +23,23 @@ class SMBIOS(Device):
|
|
|
18
23
|
def __init__(
|
|
19
24
|
self,
|
|
20
25
|
name: str,
|
|
21
|
-
parent:
|
|
26
|
+
parent: HWModule | None = None,
|
|
27
|
+
children: list["Device"] | None = None,
|
|
28
|
+
abort: bool = False,
|
|
22
29
|
*args,
|
|
23
30
|
**kwargs,
|
|
24
31
|
):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
logger.debug(f"Initializing SMBIOS device '{name}'")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
dmi_path = sysfs_root() / Path("devices/virtual/dmi/id")
|
|
36
|
+
self.sysfs_device: SysfsDevice | None = SysfsDevice(dmi_path, False)
|
|
37
|
+
logger.info(f"SMBIOS '{name}' initialized with DMI table at {dmi_path}")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"Failed to initialize SMBIOS '{name}' with DMI table: {e}")
|
|
40
|
+
raise
|
|
28
41
|
|
|
29
|
-
super().__init__(name, parent, None, *args, **kwargs)
|
|
42
|
+
super().__init__(name, parent, None, abort, *args, **kwargs)
|
|
30
43
|
|
|
31
44
|
def revision(self) -> str:
|
|
32
45
|
"""
|
|
@@ -37,4 +50,11 @@ class SMBIOS(Device):
|
|
|
37
50
|
str
|
|
38
51
|
The board revision.
|
|
39
52
|
"""
|
|
40
|
-
|
|
53
|
+
logger.debug(f"Reading board revision for SMBIOS '{self.name}'")
|
|
54
|
+
try:
|
|
55
|
+
revision = self.sysfs.read_utf8("board_version")
|
|
56
|
+
logger.debug(f"SMBIOS '{self.name}' board revision: {revision}")
|
|
57
|
+
return revision
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Failed to read board revision for SMBIOS '{self.name}': {e}")
|
|
60
|
+
raise
|
ekfsm/devices/smbus.py
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from ekfsm.devices.generic import Device
|
|
2
|
+
from ekfsm.devices.io4edge import IO4Edge
|
|
3
|
+
from ekfsm.devices.utils import retry
|
|
4
|
+
from ekfsm.log import ekfsm_logger
|
|
5
|
+
from io4edge_client.analogintypeb import Client
|
|
6
|
+
|
|
7
|
+
logger = ekfsm_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ThermalHumidity(Device):
|
|
11
|
+
"""
|
|
12
|
+
Device class for handling a thermal humidity sensor.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
parent: IO4Edge,
|
|
19
|
+
children: list[Device] | None = None,
|
|
20
|
+
abort: bool = False,
|
|
21
|
+
service_suffix: str | None = None,
|
|
22
|
+
*args,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
logger.debug(
|
|
26
|
+
f"Initializing ThermalHumidity sensor '{name}' with parent device {parent.deviceId}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
30
|
+
|
|
31
|
+
self.name = name
|
|
32
|
+
|
|
33
|
+
if service_suffix is not None:
|
|
34
|
+
self.service_suffix = service_suffix
|
|
35
|
+
logger.debug(f"Using custom service suffix: {service_suffix}")
|
|
36
|
+
else:
|
|
37
|
+
self.service_suffix = name
|
|
38
|
+
logger.debug(f"Using default service suffix: {name}")
|
|
39
|
+
|
|
40
|
+
self.service_addr = f"{parent.deviceId}-{self.service_suffix}"
|
|
41
|
+
logger.info(
|
|
42
|
+
f"ThermalHumidity '{name}' configured with service address: {self.service_addr}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
self.client = Client(self.service_addr, connect=False)
|
|
47
|
+
logger.debug(
|
|
48
|
+
f"ThermalHumidity client created for service: {self.service_addr}"
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(
|
|
52
|
+
f"Failed to create ThermalHumidity client for {self.service_addr}: {e}"
|
|
53
|
+
)
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
@retry()
|
|
57
|
+
def temperature(self) -> float:
|
|
58
|
+
"""
|
|
59
|
+
Get the temperature in Celsius.
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
RuntimeError
|
|
64
|
+
if the command fails
|
|
65
|
+
TimeoutError
|
|
66
|
+
if the command times out
|
|
67
|
+
"""
|
|
68
|
+
logger.info(f"Reading temperature from ThermalHumidity sensor '{self.name}'")
|
|
69
|
+
try:
|
|
70
|
+
temp = self.client.value()
|
|
71
|
+
logger.info(f"ThermalHumidity '{self.name}' temperature: {temp}°C")
|
|
72
|
+
return temp
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(
|
|
75
|
+
f"Failed to read temperature from ThermalHumidity '{self.name}': {e}"
|
|
76
|
+
)
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
def __repr__(self):
|
|
80
|
+
return f"{self.name}; Service Address: {self.service_addr}"
|
ekfsm/devices/toggles.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from ekfsm.devices.utils import retry
|
|
2
|
+
from io4edge_client.binaryiotypeb import Client
|
|
3
|
+
from ekfsm.devices.generic import Device
|
|
4
|
+
from ekfsm.devices.io4edge import GPIOArray
|
|
5
|
+
from ekfsm.log import ekfsm_logger
|
|
6
|
+
|
|
7
|
+
logger = ekfsm_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BinaryToggle(Device):
|
|
11
|
+
"""
|
|
12
|
+
Device class for handling a binary toggle switch.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
parent: GPIOArray,
|
|
19
|
+
children: list[Device] | None = None,
|
|
20
|
+
abort: bool = False,
|
|
21
|
+
channel_id: int = 0,
|
|
22
|
+
*args,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
26
|
+
|
|
27
|
+
self.channel_id = channel_id
|
|
28
|
+
|
|
29
|
+
logger.debug(
|
|
30
|
+
f"Initializing BinaryToggle '{name}' with parent device {parent.deviceId}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
34
|
+
|
|
35
|
+
self.name = name
|
|
36
|
+
|
|
37
|
+
self.service_addr = parent.service_addr
|
|
38
|
+
|
|
39
|
+
logger.info(
|
|
40
|
+
f"BinaryToggle '{name}' configured with service address: {self.service_addr} on channel {channel_id}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.client: Client = parent.client
|
|
44
|
+
|
|
45
|
+
@retry()
|
|
46
|
+
def set(self, state: bool):
|
|
47
|
+
"""
|
|
48
|
+
Set the state of the toggle switch.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
state
|
|
53
|
+
state to set. a "true" state turns on the toggle switch, a "false" state turns it off.
|
|
54
|
+
"""
|
|
55
|
+
logger.info(
|
|
56
|
+
f"Setting BinaryToggle '{self.name}' on channel {self.channel_id} to state {state}"
|
|
57
|
+
)
|
|
58
|
+
self.client.set_output(self.channel_id, state)
|
|
59
|
+
logger.info(
|
|
60
|
+
f"BinaryToggle '{self.name}' on channel {self.channel_id} set to state {state}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@retry()
|
|
64
|
+
def get(self) -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Get the current state of the toggle switch.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
The current state of the toggle switch.
|
|
70
|
+
"""
|
|
71
|
+
state = self.client.get_input(self.channel_id)
|
|
72
|
+
logger.info(
|
|
73
|
+
f"Retrieved state {state} for BinaryToggle '{self.name}' on channel {self.channel_id}"
|
|
74
|
+
)
|
|
75
|
+
return state
|
|
76
|
+
|
|
77
|
+
def on(self):
|
|
78
|
+
"""
|
|
79
|
+
Turn the toggle switch on.
|
|
80
|
+
"""
|
|
81
|
+
self.set(True)
|
|
82
|
+
|
|
83
|
+
def off(self):
|
|
84
|
+
"""
|
|
85
|
+
Turn the toggle switch off.
|
|
86
|
+
"""
|
|
87
|
+
self.set(False)
|
|
88
|
+
|
|
89
|
+
def __repr__(self):
|
|
90
|
+
return f"{self.name}; Channel ID: {self.channel_id}"
|
ekfsm/devices/utils.py
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from time import sleep
|
|
1
3
|
from crcmod.predefined import Crc
|
|
2
|
-
from
|
|
4
|
+
from ekfsm.log import ekfsm_logger
|
|
3
5
|
|
|
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
|
|
6
|
+
logger = ekfsm_logger(__name__)
|
|
11
7
|
|
|
12
8
|
|
|
13
9
|
def get_crc16_xmodem(data: bytes) -> int:
|
|
14
10
|
crc16_xmodem = Crc("xmodem")
|
|
15
11
|
crc16_xmodem.update(data)
|
|
16
12
|
return crc16_xmodem.crcValue
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def retry(max_attempts=5, delay=0.5):
|
|
16
|
+
"""
|
|
17
|
+
Retry decorator.
|
|
18
|
+
|
|
19
|
+
Decorator that retries a function a number of times before giving up.
|
|
20
|
+
|
|
21
|
+
This is useful for functions that may fail due to transient errors.
|
|
22
|
+
|
|
23
|
+
Note
|
|
24
|
+
----
|
|
25
|
+
This is needed for certain PMBus commands that may fail due to transient errors
|
|
26
|
+
because page switching timing is not effectively handled by older kernel versions.
|
|
27
|
+
|
|
28
|
+
Important
|
|
29
|
+
---------
|
|
30
|
+
This decorator is _not_ thread-safe across multiple ekfsm processes. Unfortunately,
|
|
31
|
+
we cannot use fcntl or flock syscalls with files on virtual filesystems like sysfs.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
max_attempts
|
|
36
|
+
The maximum number of attempts before giving up.
|
|
37
|
+
delay
|
|
38
|
+
The delay in seconds between attempts.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def decorator(func):
|
|
42
|
+
@wraps(func)
|
|
43
|
+
def wrapper(*args, **kwargs):
|
|
44
|
+
attempts = 0
|
|
45
|
+
while attempts < max_attempts:
|
|
46
|
+
try:
|
|
47
|
+
return func(*args, **kwargs)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
attempts += 1
|
|
50
|
+
if attempts == max_attempts:
|
|
51
|
+
logger.exception(
|
|
52
|
+
f"Failed to execute {func.__name__} after {max_attempts} attempts: {e}"
|
|
53
|
+
)
|
|
54
|
+
raise e
|
|
55
|
+
logger.info(f"Retrying execution of {func.__name__} in {delay}s...")
|
|
56
|
+
sleep(delay)
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
59
|
+
|
|
60
|
+
return decorator
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from ekfsm.devices.generic import Device
|
|
2
|
+
from ekfsm.devices.io4edge import IO4Edge
|
|
3
|
+
from ekfsm.devices.utils import retry
|
|
4
|
+
from ekfsm.log import ekfsm_logger
|
|
5
|
+
from io4edge_client.watchdog import Client
|
|
6
|
+
|
|
7
|
+
logger = ekfsm_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Watchdog(Device):
|
|
11
|
+
"""
|
|
12
|
+
Device class for handling an application watchdog.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
parent: IO4Edge,
|
|
19
|
+
children: list[Device] | None = None,
|
|
20
|
+
abort: bool = False,
|
|
21
|
+
service_suffix: str | None = None,
|
|
22
|
+
*args,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
logger.debug(
|
|
26
|
+
f"Initializing Watchdog '{name}' with parent device {parent.deviceId}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
30
|
+
|
|
31
|
+
self.name = name
|
|
32
|
+
|
|
33
|
+
if service_suffix is not None:
|
|
34
|
+
self.service_suffix = service_suffix
|
|
35
|
+
logger.debug(f"Using custom service suffix: {service_suffix}")
|
|
36
|
+
else:
|
|
37
|
+
self.service_suffix = name
|
|
38
|
+
logger.debug(f"Using default service suffix: {name}")
|
|
39
|
+
|
|
40
|
+
self.service_addr = f"{parent.deviceId}-{self.service_suffix}"
|
|
41
|
+
logger.info(
|
|
42
|
+
f"Watchdog '{name}' configured with service address: {self.service_addr}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
self.client = Client(self.service_addr, connect=False)
|
|
47
|
+
logger.debug(f"Watchdog client created for service: {self.service_addr}")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(
|
|
50
|
+
f"Failed to create Watchdog client for {self.service_addr}: {e}"
|
|
51
|
+
)
|
|
52
|
+
raise
|
|
53
|
+
|
|
54
|
+
@retry()
|
|
55
|
+
def describe(self):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@retry()
|
|
59
|
+
def kick(self) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Kick the watchdog.
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
RuntimeError
|
|
66
|
+
if the command fails
|
|
67
|
+
TimeoutError
|
|
68
|
+
if the command times out
|
|
69
|
+
"""
|
|
70
|
+
logger.info(f"Kicking watchdog '{self.name}' on service {self.service_addr}")
|
|
71
|
+
try:
|
|
72
|
+
self.client.kick()
|
|
73
|
+
logger.info(f"Watchdog '{self.name}' kick successful")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Failed to kick watchdog '{self.name}': {e}")
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
def __repr__(self):
|
|
79
|
+
return f"{self.name}; Service Address: {self.service_addr}"
|