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/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
- from time import sleep
12
- from functools import wraps
13
- from ekfsm.log import ekfsm_logger
14
- from threading import Lock
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
- def retry(max_attempts=5, delay=0.5):
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 Also
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
- class PmBus(Device, ProbeableDevice):
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
- files = list(Path(self.sysfs_device.path).rglob("hwmon/*/in1_input"))
108
- if len(files) == 0:
109
- raise FileNotFoundError("No HWMON entries found in sysfs")
110
- self.hwmon_sysfs = SysFSDevice(files[0].parent)
111
-
112
- self.debugfs_root = sysfs_root() / "kernel/debug/pmbus"
113
- files = list(self.debugfs_root.rglob("hwmon*/status*_input"))
114
- if len(files) == 0:
115
- raise FileNotFoundError("No HWMON entries found in debugfs")
116
- self.hwmon_debugfs = SysFSDevice(files[0].parent)
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 HwModule
91
+ from ekfsm.core import HWModule
120
92
 
121
- assert isinstance(self.hw_module, HwModule)
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 _conversion(self, in_file: str) -> float:
127
- return float(self.hwmon_sysfs.read_attr_utf8(in_file)) / 1000.0
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._conversion("in1_input")
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._conversion("in2_input")
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._conversion("curr1_input")
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._conversion("curr2_input")
144
+ return self.__convert_and_scale("curr2_input")
172
145
 
173
146
  # Status Interface
174
147
  @retry()
175
- def status0_input(self) -> PsuStatus:
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 PsuStatus
154
+ PSU status as defined in PSUStatus
182
155
  """
183
- status = int(self.hwmon_debugfs.read_attr_utf8("status0_input").strip(), 16)
184
- return PsuStatus(status)
156
+ status = self.sysfs.read_hex("status0_input")
157
+ return PSUStatus(status)
185
158
 
186
159
  @retry()
187
- def status1_input(self) -> PsuStatus:
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 PsuStatus
166
+ PSU status as defined in PSUStatus
194
167
  """
195
- status = int(self.hwmon_debugfs.read_attr_utf8("status1_input").strip(), 16)
196
- return PsuStatus(status)
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._conversion("temp1_input")
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.hwmon_sysfs.read_attr_utf8("vendor").strip()
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.hwmon_sysfs.read_attr_utf8("model").strip()
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.hwmon_sysfs.read_attr_utf8("serial").strip()
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.hwmon_sysfs.read_attr_utf8("revision").strip()
222
+ return self.sysfs.read_utf8("revision")
ekfsm/devices/smbios.py CHANGED
@@ -1,8 +1,13 @@
1
1
  from pathlib import Path
2
- from ekfsm.core.components import HwModule
3
- from ekfsm.core.sysfs import SysFSDevice, sysfs_root
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: HwModule | None = None,
26
+ parent: HWModule | None = None,
27
+ children: list["Device"] | None = None,
28
+ abort: bool = False,
22
29
  *args,
23
30
  **kwargs,
24
31
  ):
25
- self.sysfs_device: SysFSDevice = SysFSDevice(
26
- sysfs_root() / Path("devices/virtual/dmi/id")
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
- return self.sysfs_device.read_attr_utf8("board_version").strip()
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
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from typing import List
3
3
 
4
4
 
5
- class SimSmbus(ABC):
5
+ class SimSMBus(ABC):
6
6
  @abstractmethod
7
7
  def read_word_data(self, cmd: int) -> int:
8
8
  pass
@@ -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}"
@@ -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 typing import Sequence
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}"