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/buttons.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Callable
|
|
3
|
+
from ekfsm.devices.utils import retry
|
|
4
|
+
from io4edge_client.binaryiotypeb import Pb
|
|
5
|
+
from ekfsm.devices.generic import Device
|
|
6
|
+
from ekfsm.devices.io4edge import GPIOArray
|
|
7
|
+
from ekfsm.log import ekfsm_logger
|
|
8
|
+
import io4edge_client.functionblock as fb
|
|
9
|
+
|
|
10
|
+
logger = ekfsm_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Button(Device):
|
|
14
|
+
"""
|
|
15
|
+
Device class for handling a single button as part on array.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
name: str,
|
|
21
|
+
parent: Device,
|
|
22
|
+
children: list[Device] | None = None,
|
|
23
|
+
abort: bool = False,
|
|
24
|
+
channel_id: int = 0,
|
|
25
|
+
*args,
|
|
26
|
+
**kwargs,
|
|
27
|
+
):
|
|
28
|
+
logger.debug(f"Initializing Button '{name}' on channel {channel_id}")
|
|
29
|
+
|
|
30
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
31
|
+
|
|
32
|
+
self.channel_id = channel_id
|
|
33
|
+
logger.debug(f"Button '{name}' assigned to channel {channel_id}")
|
|
34
|
+
|
|
35
|
+
self._handler: Callable | None = None
|
|
36
|
+
logger.info(f"Button '{name}' initialized on channel {channel_id}")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def handler(self):
|
|
40
|
+
"""
|
|
41
|
+
Handle button events with a callback function.
|
|
42
|
+
"""
|
|
43
|
+
return self._handler
|
|
44
|
+
|
|
45
|
+
@handler.setter
|
|
46
|
+
def handler(self, func: Callable | None, *args, **kwargs):
|
|
47
|
+
"""
|
|
48
|
+
Handle button events with a callback function.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
func : Callable | None
|
|
53
|
+
The function to call on button events. If None, no function is called.
|
|
54
|
+
"""
|
|
55
|
+
if callable(func):
|
|
56
|
+
self._handler = func
|
|
57
|
+
logger.info(
|
|
58
|
+
f"Handler set for button '{self.name}' on channel {self.channel_id}"
|
|
59
|
+
)
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"Handler function: {func.__name__ if hasattr(func, '__name__') else str(func)}"
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
self._handler = None
|
|
65
|
+
logger.debug(
|
|
66
|
+
f"Handler cleared for button '{self.name}' on channel {self.channel_id}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __repr__(self):
|
|
70
|
+
return f"{self.name}; Channel ID: {self.channel_id}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ButtonArray(Device):
|
|
74
|
+
"""
|
|
75
|
+
Device class for handling an io4edge gpio based button array.
|
|
76
|
+
|
|
77
|
+
To read button events, call the `read` method in a separate thread.
|
|
78
|
+
|
|
79
|
+
Note
|
|
80
|
+
----
|
|
81
|
+
Button handlers are called in the context of the `read` method's thread and need to be set in the Button instances.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
name: str,
|
|
87
|
+
parent: GPIOArray,
|
|
88
|
+
children: list[Device] | None = None,
|
|
89
|
+
abort: bool = False,
|
|
90
|
+
keepaliveInterval: int = 10000,
|
|
91
|
+
*args,
|
|
92
|
+
**kwargs,
|
|
93
|
+
):
|
|
94
|
+
logger.debug(
|
|
95
|
+
f"Initializing ButtonArray '{name}' with parent device {parent.deviceId}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
99
|
+
|
|
100
|
+
self.name = name
|
|
101
|
+
|
|
102
|
+
self.service_addr = parent.service_addr
|
|
103
|
+
self.client = parent.client
|
|
104
|
+
|
|
105
|
+
logger.info(
|
|
106
|
+
f"ButtonArray '{name}' configured with service address: {self.service_addr}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.subscriptionType = Pb.SubscriptionType.BINARYIOTYPEB_ON_RISING_EDGE
|
|
110
|
+
self.stream_cfg = fb.Pb.StreamControlStart(
|
|
111
|
+
bucketSamples=1, # 1 sample per bucket, also ein event pro bucket
|
|
112
|
+
keepaliveInterval=keepaliveInterval,
|
|
113
|
+
bufferedSamples=2, # 2 samples werden gepuffert
|
|
114
|
+
low_latency_mode=True, # schickt soweit moeglich sofort die Events
|
|
115
|
+
)
|
|
116
|
+
logger.debug(
|
|
117
|
+
"Stream configuration initialized with rising edge subscription and low latency mode"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Log button children count
|
|
121
|
+
button_count = sum(1 for child in (children or []) if isinstance(child, Button))
|
|
122
|
+
logger.info(f"ButtonArray '{name}' initialized with {button_count} button(s)")
|
|
123
|
+
|
|
124
|
+
def read(self, stop_event: threading.Event | None = None, timeout: float = 1):
|
|
125
|
+
"""
|
|
126
|
+
Read all button events and dispatch to handlers.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
stop_event : threading.Event, optional
|
|
131
|
+
Event to signal stopping the reading loop. If None, the loop will run indefinitely.
|
|
132
|
+
timeout : float, optional
|
|
133
|
+
Timeout for reading from the stream in seconds. Default is 0.1 seconds.
|
|
134
|
+
|
|
135
|
+
Note
|
|
136
|
+
----
|
|
137
|
+
This method blocks and should be run in a separate thread.
|
|
138
|
+
"""
|
|
139
|
+
button_channels = [
|
|
140
|
+
button for button in self.children if isinstance(button, Button)
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
if not button_channels:
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"No button children found in ButtonArray '{self.name}', read operation will have no effect"
|
|
146
|
+
)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
logger.info(
|
|
150
|
+
f"Starting button event reading for {len(button_channels)} buttons on '{self.name}'"
|
|
151
|
+
)
|
|
152
|
+
logger.debug(
|
|
153
|
+
f"Read timeout: {timeout}s, stop_event provided: {stop_event is not None}"
|
|
154
|
+
)
|
|
155
|
+
# Prepare subscription channels
|
|
156
|
+
subscribe_channels = tuple(
|
|
157
|
+
Pb.SubscribeChannel(
|
|
158
|
+
channel=button.channel_id,
|
|
159
|
+
subscriptionType=self.subscriptionType,
|
|
160
|
+
)
|
|
161
|
+
for button in button_channels
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
channel_ids = [button.channel_id for button in button_channels]
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
self._button_event_handling(
|
|
168
|
+
stop_event,
|
|
169
|
+
timeout,
|
|
170
|
+
subscribe_channels,
|
|
171
|
+
button_channels,
|
|
172
|
+
channel_ids,
|
|
173
|
+
)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(
|
|
176
|
+
f"Failed to establish connection or start stream for ButtonArray '{self.name}': {e}"
|
|
177
|
+
)
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
@retry()
|
|
181
|
+
def _button_event_handling(
|
|
182
|
+
self,
|
|
183
|
+
stop_event: threading.Event | None,
|
|
184
|
+
timeout: float,
|
|
185
|
+
subscribe_channels: tuple,
|
|
186
|
+
button_channels: list,
|
|
187
|
+
channel_ids: list,
|
|
188
|
+
):
|
|
189
|
+
with self.client as client:
|
|
190
|
+
logger.debug(f"IO4Edge client connected to service: {self.service_addr}")
|
|
191
|
+
|
|
192
|
+
logger.debug(
|
|
193
|
+
f"Subscribing to {len(subscribe_channels)} button channels: {channel_ids}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
client.start_stream(
|
|
197
|
+
Pb.StreamControlStart(subscribeChannel=subscribe_channels),
|
|
198
|
+
self.stream_cfg,
|
|
199
|
+
)
|
|
200
|
+
logger.info(f"Button event stream started for ButtonArray '{self.name}'")
|
|
201
|
+
|
|
202
|
+
event_count = 0
|
|
203
|
+
try:
|
|
204
|
+
while not (stop_event and stop_event.is_set()):
|
|
205
|
+
try:
|
|
206
|
+
_, samples = client.read_stream(timeout=timeout)
|
|
207
|
+
|
|
208
|
+
for sample in samples.samples:
|
|
209
|
+
for button in button_channels:
|
|
210
|
+
pressed = bool(sample.inputs & (1 << button.channel_id))
|
|
211
|
+
if pressed:
|
|
212
|
+
event_count += 1
|
|
213
|
+
button_name = getattr(button, "name", "unnamed")
|
|
214
|
+
logger.debug(
|
|
215
|
+
f"Button press on channel {button.channel_id} ({button_name})"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if button.handler:
|
|
219
|
+
try:
|
|
220
|
+
logger.debug(
|
|
221
|
+
f"Calling handler for button on channel {button.channel_id}"
|
|
222
|
+
)
|
|
223
|
+
button.handler()
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(
|
|
226
|
+
f"Error in button handler for channel {button.channel_id}: {e}"
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
logger.debug(
|
|
230
|
+
f"No handler set for button on channel {button.channel_id}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
except TimeoutError:
|
|
234
|
+
# Timeout is expected during normal operation
|
|
235
|
+
continue
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"Error reading button events from stream: {e}")
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
except KeyboardInterrupt:
|
|
241
|
+
logger.info(f"Button reading interrupted for ButtonArray '{self.name}'")
|
|
242
|
+
finally:
|
|
243
|
+
logger.info(
|
|
244
|
+
f"Button event reading stopped for '{self.name}' after processing {event_count} events"
|
|
245
|
+
)
|
|
246
|
+
if stop_event:
|
|
247
|
+
stop_event.clear()
|
|
248
|
+
logger.debug("Stop event cleared")
|
|
249
|
+
|
|
250
|
+
def __repr__(self):
|
|
251
|
+
return f"{self.name}; Service Address: {self.service_addr}"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from ekfsm.devices.generic import Device
|
|
2
|
+
from ekfsm.devices.ledArray import LEDArray
|
|
3
|
+
from ekfsm.devices.utils import retry
|
|
4
|
+
from ekfsm.log import ekfsm_logger
|
|
5
|
+
from io4edge_client.api.colorLED.python.colorLED.v1alpha1.colorLED_pb2 import Color
|
|
6
|
+
|
|
7
|
+
logger = ekfsm_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ColorLED(Device):
|
|
11
|
+
"""
|
|
12
|
+
Device class for handling a color LED.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
parent: LEDArray,
|
|
19
|
+
children: list[Device] | None = None,
|
|
20
|
+
abort: bool = False,
|
|
21
|
+
channel_id: int = 0,
|
|
22
|
+
*args,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
logger.debug(f"Initializing ColorLED '{name}' on channel {channel_id}")
|
|
26
|
+
|
|
27
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
28
|
+
|
|
29
|
+
self.name = name
|
|
30
|
+
self.channel_id = channel_id
|
|
31
|
+
|
|
32
|
+
self.client = parent.client
|
|
33
|
+
logger.info(
|
|
34
|
+
f"ColorLED '{name}' initialized on channel {channel_id} with parent LEDArray"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@retry()
|
|
38
|
+
def describe(self):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@retry()
|
|
42
|
+
def get(self) -> tuple[Color, bool]:
|
|
43
|
+
"""
|
|
44
|
+
Get color LED state.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
Current color and blink state.
|
|
49
|
+
|
|
50
|
+
Raises
|
|
51
|
+
------
|
|
52
|
+
RuntimeError
|
|
53
|
+
if the command fails
|
|
54
|
+
TimeoutError
|
|
55
|
+
if the command times out
|
|
56
|
+
"""
|
|
57
|
+
logger.info(
|
|
58
|
+
"Getting color LED state for '%s' on channel %s", self.name, self.channel_id
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
result = self.client.get(self.channel_id)
|
|
62
|
+
color, blink = result
|
|
63
|
+
logger.info(
|
|
64
|
+
"ColorLED '%s' state: color=%s, blink=%s", self.name, color, blink
|
|
65
|
+
)
|
|
66
|
+
return result
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(
|
|
69
|
+
"Failed to get ColorLED '%s' state on channel %s: %s",
|
|
70
|
+
self.name,
|
|
71
|
+
self.channel_id,
|
|
72
|
+
e,
|
|
73
|
+
)
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
@retry()
|
|
77
|
+
def set(self, color: Color, blink: bool) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Set the color of the color LED.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
color : Color
|
|
84
|
+
The color to set the LED to.
|
|
85
|
+
blink : bool
|
|
86
|
+
Whether to blink the LED.
|
|
87
|
+
|
|
88
|
+
Raises
|
|
89
|
+
------
|
|
90
|
+
RuntimeError
|
|
91
|
+
if the command fails
|
|
92
|
+
TimeoutError
|
|
93
|
+
if the command times out
|
|
94
|
+
"""
|
|
95
|
+
logger.info(
|
|
96
|
+
f"Setting ColorLED '{self.name}' on channel {self.channel_id}: color={color}, blink={blink}"
|
|
97
|
+
)
|
|
98
|
+
try:
|
|
99
|
+
self.client.set(self.channel_id, color, blink)
|
|
100
|
+
logger.debug(
|
|
101
|
+
f"ColorLED '{self.name}' successfully set to color={color}, blink={blink}"
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(
|
|
105
|
+
f"Failed to set ColorLED '{self.name}' on channel {self.channel_id}: {e}"
|
|
106
|
+
)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
def __repr__(self):
|
|
110
|
+
return f"{self.name}; Channel ID: {self.channel_id}"
|
ekfsm/devices/coretemp.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import glob
|
|
4
|
+
import os
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
import ekfsm.core
|
|
8
|
+
from ekfsm.core.sysfs import sysfs_root
|
|
7
9
|
from ekfsm.devices.generic import Device
|
|
10
|
+
from ekfsm.log import ekfsm_logger
|
|
11
|
+
|
|
12
|
+
logger = ekfsm_logger(__name__)
|
|
8
13
|
|
|
9
14
|
# Path to the root of the HWMON sysfs filesystem
|
|
10
|
-
HWMON_ROOT =
|
|
15
|
+
HWMON_ROOT = sysfs_root() / Path("class/hwmon")
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
def find_core_temp_dir(hwmon_dir) -> Path:
|
|
@@ -38,28 +43,33 @@ def find_core_temp_dir(hwmon_dir) -> Path:
|
|
|
38
43
|
|
|
39
44
|
class CoreTemp(Device):
|
|
40
45
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
A HWMON device is a virtual device that is used to read hardware monitoring values from the sysfs filesystem.
|
|
46
|
+
This class provides an interface to read the CPU core temperature from the HWMON device.
|
|
44
47
|
|
|
45
48
|
Note:
|
|
46
|
-
Currently, only the
|
|
49
|
+
Currently, only the average temperature over all cores is read from the HWMON device.
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
52
|
def __init__(
|
|
50
53
|
self,
|
|
51
54
|
name: str,
|
|
52
55
|
parent: Device,
|
|
56
|
+
children: list["Device"] | None = None,
|
|
57
|
+
abort: bool = False,
|
|
53
58
|
*args,
|
|
54
59
|
**kwargs,
|
|
55
60
|
):
|
|
56
|
-
|
|
61
|
+
logger.debug(f"Initializing CoreTemp device '{name}'")
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
find_core_temp_dir(
|
|
60
|
-
|
|
63
|
+
try:
|
|
64
|
+
dir = find_core_temp_dir(sysfs_root() / Path("class/hwmon"))
|
|
65
|
+
logger.debug(f"Found coretemp directory: {dir}")
|
|
66
|
+
self.sysfs_device = ekfsm.core.sysfs.SysfsDevice(dir, False)
|
|
67
|
+
logger.info(f"CoreTemp '{name}' initialized with sysfs device at {dir}")
|
|
68
|
+
except FileNotFoundError as e:
|
|
69
|
+
logger.error(f"Failed to initialize CoreTemp '{name}': {e}")
|
|
70
|
+
raise
|
|
61
71
|
|
|
62
|
-
super().__init__(name, parent, None, *args, **kwargs)
|
|
72
|
+
super().__init__(name, parent, None, abort, *args, **kwargs)
|
|
63
73
|
|
|
64
74
|
def cputemp(self):
|
|
65
75
|
"""
|
|
@@ -70,4 +80,16 @@ class CoreTemp(Device):
|
|
|
70
80
|
int
|
|
71
81
|
The CPU temperature in degrees Celsius.
|
|
72
82
|
"""
|
|
73
|
-
|
|
83
|
+
logger.debug(f"Reading CPU temperature for CoreTemp '{self.name}'")
|
|
84
|
+
try:
|
|
85
|
+
temp_raw = self.sysfs.read_float("temp1_input")
|
|
86
|
+
temp_celsius = temp_raw / 1000
|
|
87
|
+
logger.debug(
|
|
88
|
+
f"CoreTemp '{self.name}' raw reading: {temp_raw}, temperature: {temp_celsius}°C"
|
|
89
|
+
)
|
|
90
|
+
return temp_celsius
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(
|
|
93
|
+
f"Failed to read CPU temperature for CoreTemp '{self.name}': {e}"
|
|
94
|
+
)
|
|
95
|
+
raise
|
ekfsm/devices/eeprom.py
CHANGED
|
@@ -11,20 +11,18 @@ Routine Listings
|
|
|
11
11
|
|
|
12
12
|
from abc import ABC, abstractmethod
|
|
13
13
|
from datetime import date
|
|
14
|
-
from typing import Any, Callable, Literal, Sequence
|
|
15
14
|
from functools import wraps
|
|
15
|
+
from typing import Any, Callable, Literal, Sequence
|
|
16
16
|
|
|
17
|
-
from ekfsm.core.components import SysTree
|
|
18
|
-
from ekfsm.core.probe import ProbeableDevice
|
|
19
|
-
|
|
20
|
-
from .generic import Device
|
|
21
|
-
from .utils import compute_int_from_bytes, get_crc16_xmodem
|
|
22
|
-
from ekfsm.exceptions import DataCorruptionError
|
|
23
17
|
from hexdump import hexdump
|
|
24
18
|
|
|
19
|
+
from ekfsm.core.components import SysTree
|
|
20
|
+
from ekfsm.core.probe import ProbeableDevice
|
|
21
|
+
from ekfsm.exceptions import DataCorruptionError, DriverError, SysFSError
|
|
25
22
|
from ekfsm.log import ekfsm_logger
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
from .generic import Device
|
|
25
|
+
from .utils import get_crc16_xmodem
|
|
28
26
|
|
|
29
27
|
logger = ekfsm_logger(__name__)
|
|
30
28
|
|
|
@@ -82,13 +80,19 @@ class EEPROM(Device):
|
|
|
82
80
|
self,
|
|
83
81
|
name: str,
|
|
84
82
|
parent: SysTree | None = None,
|
|
83
|
+
children: list[Device] | None = None,
|
|
84
|
+
abort: bool = False,
|
|
85
85
|
*args,
|
|
86
86
|
**kwargs,
|
|
87
87
|
):
|
|
88
|
-
super().__init__(name, parent, None, *args, **kwargs)
|
|
88
|
+
super().__init__(name, parent, None, abort, *args, **kwargs)
|
|
89
89
|
|
|
90
90
|
self.addr = self.get_i2c_chip_addr()
|
|
91
91
|
self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
|
|
92
|
+
|
|
93
|
+
if not self.sysfs_device.get_driver():
|
|
94
|
+
raise DriverError("No driver attached to device {self.name}")
|
|
95
|
+
|
|
92
96
|
self._update_content()
|
|
93
97
|
|
|
94
98
|
def _update_content(self) -> None:
|
|
@@ -119,20 +123,10 @@ class EEPROM(Device):
|
|
|
119
123
|
|
|
120
124
|
Raises
|
|
121
125
|
------
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
RuntimeError
|
|
125
|
-
If the sysfs device is not found.
|
|
126
|
+
SysFSError
|
|
127
|
+
No sysfs device found for EEPROM or `eeprom` attribute does not exist
|
|
126
128
|
"""
|
|
127
|
-
|
|
128
|
-
if self.sysfs_device:
|
|
129
|
-
cnt = self.sysfs_device.read_attr_bytes("eeprom")
|
|
130
|
-
else:
|
|
131
|
-
raise RuntimeError("No sysfs device for EEPROM")
|
|
132
|
-
except FileNotFoundError:
|
|
133
|
-
raise FileNotFoundError("EEPROM not found")
|
|
134
|
-
|
|
135
|
-
return cnt
|
|
129
|
+
return self.read_sysfs_bytes("eeprom")
|
|
136
130
|
|
|
137
131
|
def write(self, data: bytes, offset: int = 0) -> None:
|
|
138
132
|
"""
|
|
@@ -157,15 +151,27 @@ class EEPROM(Device):
|
|
|
157
151
|
Note
|
|
158
152
|
----
|
|
159
153
|
Operation is checked for data corruption by reading back the written data.
|
|
154
|
+
|
|
155
|
+
Important
|
|
156
|
+
---------
|
|
157
|
+
The offset parameter is only supported if the EEPROM driver is bin_attribute enabled.
|
|
158
|
+
|
|
159
|
+
For almost any other native sysfs attribute, this is NOT the case!
|
|
160
160
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
|
|
162
|
+
if self.sysfs_device:
|
|
163
|
+
attr = next(x for x in self.sysfs_device.attributes if x.name == "eeprom")
|
|
164
|
+
logger.info(f"Writing {len(data)} bytes to EEPROM at offset {offset}")
|
|
165
|
+
if attr.is_sysfs_attr() and data is not None:
|
|
166
|
+
mode = "r+" if isinstance(data, str) else "rb+"
|
|
167
|
+
try:
|
|
168
|
+
with open(attr.path, mode) as f:
|
|
169
|
+
f.seek(offset)
|
|
170
|
+
f.write(data)
|
|
171
|
+
except OSError as e:
|
|
172
|
+
raise SysFSError("Error accessing SysFS attribute") from e
|
|
173
|
+
else:
|
|
174
|
+
raise RuntimeError("No sysfs device for EEPROM")
|
|
169
175
|
|
|
170
176
|
self._update_content()
|
|
171
177
|
written = self._content[offset : offset + len(data)]
|
|
@@ -204,6 +210,10 @@ class Validatable_EEPROM(EEPROM, ABC):
|
|
|
204
210
|
|
|
205
211
|
def __init__(
|
|
206
212
|
self,
|
|
213
|
+
name: str,
|
|
214
|
+
parent: SysTree | None = None,
|
|
215
|
+
children: list[Device] | None = None,
|
|
216
|
+
abort: bool = False,
|
|
207
217
|
crc_pos: Literal["start", "end"] = "end",
|
|
208
218
|
crc_length: int = 2,
|
|
209
219
|
*args,
|
|
@@ -212,7 +222,7 @@ class Validatable_EEPROM(EEPROM, ABC):
|
|
|
212
222
|
self._crc_length: int = crc_length
|
|
213
223
|
self._crc_pos: str = crc_pos
|
|
214
224
|
|
|
215
|
-
super().__init__(*args, **kwargs)
|
|
225
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
216
226
|
|
|
217
227
|
self._crc_pos_start = len(self._data) if self._crc_pos == "end" else 0
|
|
218
228
|
self._crc_pos_end = self._crc_pos_start + self._crc_length
|
|
@@ -390,13 +400,16 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
|
|
|
390
400
|
|
|
391
401
|
def __init__(
|
|
392
402
|
self,
|
|
403
|
+
name: str,
|
|
404
|
+
parent: SysTree | None = None,
|
|
405
|
+
children: list[Device] | None = None,
|
|
406
|
+
abort: bool = False,
|
|
393
407
|
*args,
|
|
394
408
|
**kwargs,
|
|
395
409
|
) -> None:
|
|
396
|
-
super().__init__(*args, **kwargs)
|
|
410
|
+
super().__init__(name, parent, children, abort, *args, **kwargs)
|
|
397
411
|
|
|
398
412
|
def _update_content(self) -> None:
|
|
399
|
-
|
|
400
413
|
super()._update_content()
|
|
401
414
|
|
|
402
415
|
# EKF EEPROM content is restricted to 128 bytes, so strip the rest!
|
|
@@ -426,7 +439,7 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
|
|
|
426
439
|
The serial number of the root device.
|
|
427
440
|
"""
|
|
428
441
|
area = self._content[self._sernum_index_start : self._sernum_index_end]
|
|
429
|
-
sernum =
|
|
442
|
+
sernum = int.from_bytes(area, byteorder="little")
|
|
430
443
|
return str(sernum)
|
|
431
444
|
|
|
432
445
|
def write_serial(self, serial: int) -> None:
|
|
@@ -463,7 +476,7 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
|
|
|
463
476
|
area = self._content[
|
|
464
477
|
self._customer_serial_index_start : self._customer_serial_index_end
|
|
465
478
|
]
|
|
466
|
-
sernum =
|
|
479
|
+
sernum = int.from_bytes(area, byteorder="little")
|
|
467
480
|
return str(sernum)
|
|
468
481
|
|
|
469
482
|
def write_custom_serial(self, serial: int) -> None:
|
|
@@ -507,8 +520,8 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
|
|
|
507
520
|
The date the device was manufactured.
|
|
508
521
|
"""
|
|
509
522
|
area = self._content[self._date_mft_index_start : self._date_mft_index_end]
|
|
510
|
-
encoded_mft_date = area[::-1]
|
|
511
|
-
return self._decode_date(
|
|
523
|
+
# encoded_mft_date = area[::-1]
|
|
524
|
+
return self._decode_date(area)
|
|
512
525
|
|
|
513
526
|
@validated
|
|
514
527
|
def repaired_at(self) -> date:
|
|
@@ -520,8 +533,8 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
|
|
|
520
533
|
The most recent date the device was repaired.
|
|
521
534
|
"""
|
|
522
535
|
area = self._content[self._date_rep_index_start : self._date_rep_index_end]
|
|
523
|
-
encoded_rep_date = area[::-1]
|
|
524
|
-
return self._decode_date(
|
|
536
|
+
# encoded_rep_date = area[::-1]
|
|
537
|
+
return self._decode_date(area)
|
|
525
538
|
|
|
526
539
|
@validated
|
|
527
540
|
def write_repaired_at(self, date: date) -> None:
|
|
@@ -692,8 +705,7 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
|
|
|
692
705
|
date
|
|
693
706
|
The decoded date.
|
|
694
707
|
"""
|
|
695
|
-
|
|
696
|
-
bdate = compute_int_from_bytes(encoded_date)
|
|
708
|
+
bdate = int.from_bytes(encoded_date, byteorder="little")
|
|
697
709
|
|
|
698
710
|
# Extract the day (bit 0-4)
|
|
699
711
|
day = bdate & 0x1F # 0x1F is 00011111 in binary (5 bits)
|
|
@@ -715,7 +727,7 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
|
|
|
715
727
|
) # Handle invalid dates, e.g., 30th Feb
|
|
716
728
|
|
|
717
729
|
@classmethod
|
|
718
|
-
def _encode_date(
|
|
730
|
+
def _encode_date(cls, date: date) -> bytes:
|
|
719
731
|
"""
|
|
720
732
|
Encode a date into a proprietary 2-byte format.
|
|
721
733
|
|
|
@@ -774,7 +786,6 @@ class EKF_CCU_EEPROM(EKF_EEPROM):
|
|
|
774
786
|
super().__init__(*args, **kwargs)
|
|
775
787
|
|
|
776
788
|
def _update_content(self) -> None:
|
|
777
|
-
|
|
778
789
|
super()._update_content()
|
|
779
790
|
|
|
780
791
|
# CCU content is the raw content area of the EEPROM
|
|
@@ -935,7 +946,7 @@ class EKF_CCU_EEPROM(EKF_EEPROM):
|
|
|
935
946
|
area = self._content[
|
|
936
947
|
self._cserial_index_start : self._cserial_index_start + self._cserial_length
|
|
937
948
|
]
|
|
938
|
-
cserial =
|
|
949
|
+
cserial = int.from_bytes(area, byteorder="little")
|
|
939
950
|
return cserial
|
|
940
951
|
|
|
941
952
|
def write_cserial(self, serial: int) -> None:
|
|
@@ -1000,7 +1011,7 @@ class EKF_CCU_EEPROM(EKF_EEPROM):
|
|
|
1000
1011
|
The unit number of the subsystem.
|
|
1001
1012
|
"""
|
|
1002
1013
|
area = self._content[self._unit_index_start]
|
|
1003
|
-
unit =
|
|
1014
|
+
unit = int.from_bytes([area], byteorder="little")
|
|
1004
1015
|
return unit
|
|
1005
1016
|
|
|
1006
1017
|
@validated
|
|
@@ -1069,6 +1080,23 @@ class EKF_CCU_EEPROM(EKF_EEPROM):
|
|
|
1069
1080
|
def write_customer_area(self, data: bytes) -> None:
|
|
1070
1081
|
"""
|
|
1071
1082
|
Write data to CCU EEPROM customer area.
|
|
1083
|
+
|
|
1084
|
+
Parameters
|
|
1085
|
+
----------
|
|
1086
|
+
data
|
|
1087
|
+
The data to write to the customer area of the CCU EEPROM.
|
|
1088
|
+
|
|
1089
|
+
Raises
|
|
1090
|
+
------
|
|
1091
|
+
ValueError
|
|
1092
|
+
If the data exceeds the customer area length.
|
|
1093
|
+
|
|
1094
|
+
Example
|
|
1095
|
+
-------
|
|
1096
|
+
>>> eeprom = EKF_CCU_EEPROM()
|
|
1097
|
+
>>> eeprom.write_customer_area(b"Hello, World!")
|
|
1098
|
+
>>> eeprom.customer_area()
|
|
1099
|
+
b'Hello, World!'
|
|
1072
1100
|
"""
|
|
1073
1101
|
if len(data) > self._customer_area_length:
|
|
1074
1102
|
raise ValueError("Data exceeds customer area length")
|