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/__init__.py +13 -0
- ekfsm/boards/oem/ekf/ccu.yaml +68 -0
- ekfsm/boards/oem/ekf/sc5-festival.yaml +30 -0
- ekfsm/boards/oem/ekf/sc9-toccata.yaml +31 -0
- ekfsm/boards/oem/ekf/spv-mystic.yaml +68 -0
- ekfsm/boards/oem/ekf/sq1-track.yaml +41 -0
- ekfsm/boards/oem/ekf/srf-fan.yaml +48 -0
- ekfsm/boards/oem/ekf/sur-uart.yaml +72 -0
- ekfsm/boards/oem/hitron/hdrc-300.yaml +20 -0
- ekfsm/cli.py +111 -0
- ekfsm/config.py +37 -0
- ekfsm/core/__init__.py +4 -0
- ekfsm/core/components.py +120 -0
- ekfsm/core/probe.py +10 -0
- ekfsm/core/slots.py +201 -0
- ekfsm/core/sysfs.py +91 -0
- ekfsm/core/utils.py +77 -0
- ekfsm/devices/__init__.py +28 -0
- ekfsm/devices/eeprom.py +1054 -0
- ekfsm/devices/ekf_ccu_uc.py +390 -0
- ekfsm/devices/ekf_sur_led.py +67 -0
- ekfsm/devices/generic.py +245 -0
- ekfsm/devices/gpio.py +340 -0
- ekfsm/devices/hwmon.py +71 -0
- ekfsm/devices/iio.py +58 -0
- ekfsm/devices/iio_thermal_humidity.py +41 -0
- ekfsm/devices/mux.py +39 -0
- ekfsm/devices/pmbus.py +65 -0
- ekfsm/devices/smbios.py +38 -0
- ekfsm/devices/utils.py +16 -0
- ekfsm/exceptions.py +58 -0
- ekfsm/log.py +28 -0
- ekfsm/py.typed +2 -0
- ekfsm/simctrl.py +241 -0
- ekfsm/system.py +326 -0
- ekfsm-0.11.0b1.post3.dist-info/METADATA +86 -0
- ekfsm-0.11.0b1.post3.dist-info/RECORD +39 -0
- ekfsm-0.11.0b1.post3.dist-info/WHEEL +4 -0
- ekfsm-0.11.0b1.post3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
from .generic import Device
|
|
2
|
+
from smbus2 import SMBus
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
from ekfsm.core.components import SystemComponent
|
|
6
|
+
from ..exceptions import AcquisitionError
|
|
7
|
+
import struct
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CcuCommands(Enum):
|
|
11
|
+
NOP = 0x01
|
|
12
|
+
IMU_SAMPLES = 0x10
|
|
13
|
+
FAN_STATUS = 0x11
|
|
14
|
+
VIN_VOLTAGE = 0x12
|
|
15
|
+
CCU_TEMPERATURE = 0x13
|
|
16
|
+
CCU_HUMIDITY = 0x14
|
|
17
|
+
PUSH_TEMPERATURE = 0x15
|
|
18
|
+
SW_SHUTDOWN = 0x16
|
|
19
|
+
WD_TRIGGER = 0x17
|
|
20
|
+
IDENTIFY_FIRMWARE_TITLE = 0x80
|
|
21
|
+
IDENTIFY_FIRMWARE_VERSION = 0x81
|
|
22
|
+
LOAD_FIRMWARE_CHUNK = 0x82
|
|
23
|
+
LOAD_PARAMETERSET = 0x83
|
|
24
|
+
GET_PARAMETERSET_BEGIN = 0x84
|
|
25
|
+
GET_PARAMETERSET_FOLLOW = 0x85
|
|
26
|
+
RESTART = 0x8F
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EKFCcuUc(Device):
|
|
30
|
+
"""
|
|
31
|
+
A class to communicate with I2C microcontroller on the EKF CCU.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
name: str,
|
|
37
|
+
parent: SystemComponent | None,
|
|
38
|
+
*args,
|
|
39
|
+
**kwargs,
|
|
40
|
+
):
|
|
41
|
+
super().__init__(name, parent, None, *args, **kwargs)
|
|
42
|
+
self._i2c_addr = self.get_i2c_chip_addr()
|
|
43
|
+
self._i2c_bus = self.get_i2c_bus_number()
|
|
44
|
+
self._smbus = SMBus(self._i2c_bus)
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return (
|
|
48
|
+
f"EKFCCU - I2C Bus/Address: {self._i2c_bus}/{hex(self._i2c_addr)}; "
|
|
49
|
+
f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def temperature(self) -> float:
|
|
53
|
+
"""
|
|
54
|
+
Get the temperature from the CCU thermal/humidity sensor.
|
|
55
|
+
The temperature is read once per second.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
float
|
|
60
|
+
The temperature in degrees Celsius.
|
|
61
|
+
|
|
62
|
+
Raises
|
|
63
|
+
------
|
|
64
|
+
AcquisitionError
|
|
65
|
+
If the temperature cannot be read, for example, because the sensor is not working.
|
|
66
|
+
"""
|
|
67
|
+
return (
|
|
68
|
+
self._get_signed_word_data(CcuCommands.CCU_TEMPERATURE.value, "temperature")
|
|
69
|
+
/ 10.0
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def humidity(self) -> float:
|
|
73
|
+
"""
|
|
74
|
+
Get the relative humidity from the CCU thermal/humidity sensor.
|
|
75
|
+
The humidity is read once per second.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
float
|
|
80
|
+
The relative humidity in percent.
|
|
81
|
+
|
|
82
|
+
Raises
|
|
83
|
+
------
|
|
84
|
+
AcquisitionError
|
|
85
|
+
If the humidity cannot be read, for example, because the sensor is not working.
|
|
86
|
+
"""
|
|
87
|
+
return (
|
|
88
|
+
self._get_signed_word_data(CcuCommands.CCU_HUMIDITY.value, "humidity")
|
|
89
|
+
/ 10.0
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def vin_voltage(self) -> float:
|
|
93
|
+
"""
|
|
94
|
+
Get the system input voltage from the CCU (the pimary voltage of the PSU).
|
|
95
|
+
The voltage is read every 100ms.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
float
|
|
100
|
+
The system input voltage in volts.
|
|
101
|
+
|
|
102
|
+
Raises
|
|
103
|
+
------
|
|
104
|
+
AcquisitionError
|
|
105
|
+
If the voltage cannot be read, for example, because the ADC is not working.
|
|
106
|
+
"""
|
|
107
|
+
return (
|
|
108
|
+
self._get_signed_word_data(CcuCommands.VIN_VOLTAGE.value, "VIN voltage")
|
|
109
|
+
/ 10.0
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _get_signed_word_data(self, cmd: int, what: str) -> int:
|
|
113
|
+
v = self._smbus.read_word_data(self._i2c_addr, cmd)
|
|
114
|
+
if v == 0x8000:
|
|
115
|
+
raise AcquisitionError(f"cannot read {what}")
|
|
116
|
+
return struct.unpack("<h", struct.pack("<H", v))[0]
|
|
117
|
+
|
|
118
|
+
def fan_status(self, fan: int) -> Tuple[float, float, int]:
|
|
119
|
+
"""
|
|
120
|
+
Get the status of a fan.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
fan
|
|
125
|
+
The fan number (0-2).
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
Tuple[float, float, int]
|
|
130
|
+
The desired speed, the actual speed, and the diagnostic value.
|
|
131
|
+
The diagnostic value is a bitfield with the following meaning:
|
|
132
|
+
|
|
133
|
+
- bit 0: 0 = fan status is invalid, 1 = fan status is valid
|
|
134
|
+
- bit 1: 0 = no error detected, 1 = fan is stuck
|
|
135
|
+
"""
|
|
136
|
+
data = self._smbus.read_block_data(self._i2c_addr, CcuCommands.FAN_STATUS.value)
|
|
137
|
+
_data = bytes(data)
|
|
138
|
+
desired, actual, diag = struct.unpack("<HHB", _data[fan * 5 : fan * 5 + 5])
|
|
139
|
+
return desired, actual, diag
|
|
140
|
+
|
|
141
|
+
def push_temperature(self, fan: int, temp: float) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Tell FAN controller the external temperature, usually the CPU temperature.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
fan
|
|
148
|
+
The fan number (0-2), or -1 to set the external temperature of all fans.
|
|
149
|
+
|
|
150
|
+
temp
|
|
151
|
+
The external temperature in degrees Celsius.
|
|
152
|
+
|
|
153
|
+
Important
|
|
154
|
+
---------
|
|
155
|
+
If push_temperature is no more called for a certain time (configurable with `fan-push-tout` parameter),
|
|
156
|
+
the fan controller will fallback to it's default fan speed (configurable with the `fan-defrpm` parameter).
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
if fan == -1:
|
|
160
|
+
fan = 0xFF
|
|
161
|
+
data = struct.pack("<Bh", fan, int(temp * 10))
|
|
162
|
+
self._smbus.write_block_data(
|
|
163
|
+
self._i2c_addr, CcuCommands.PUSH_TEMPERATURE.value, list(data)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def sw_shutdown(self) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Tell CCU that the system is going to shutdown.
|
|
169
|
+
This cause the CCU's system state controller to enter shutdown state and power off the system after a certain time
|
|
170
|
+
(parameter `shutdn-delay`).
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
self._smbus.write_byte(self._i2c_addr, CcuCommands.SW_SHUTDOWN.value)
|
|
174
|
+
|
|
175
|
+
def wd_trigger(self) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Trigger the CCU's application watchdog.
|
|
178
|
+
This will reset the watchdog timer.
|
|
179
|
+
|
|
180
|
+
The CCU watchdog is only enabled when the parameter `wd-tout` is set to a value greater than 0. Triggering
|
|
181
|
+
the watchdog when the timeout is 0 will have no effect.
|
|
182
|
+
|
|
183
|
+
If the watchdog is not reset within the timeout, the CCU will power cycle the system.
|
|
184
|
+
"""
|
|
185
|
+
self._smbus.write_byte(self._i2c_addr, CcuCommands.WD_TRIGGER.value)
|
|
186
|
+
|
|
187
|
+
#
|
|
188
|
+
# Management commands
|
|
189
|
+
#
|
|
190
|
+
def identify_firmware(self) -> Tuple[str, str]:
|
|
191
|
+
"""
|
|
192
|
+
Get the firmware title and version of the CCU.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
Tuple[str, str]
|
|
197
|
+
The firmware title and version.
|
|
198
|
+
"""
|
|
199
|
+
title = bytes(
|
|
200
|
+
self._smbus.read_block_data(
|
|
201
|
+
self._i2c_addr, CcuCommands.IDENTIFY_FIRMWARE_TITLE.value
|
|
202
|
+
)
|
|
203
|
+
).decode("utf-8")
|
|
204
|
+
version = bytes(
|
|
205
|
+
self._smbus.read_block_data(
|
|
206
|
+
self._i2c_addr, CcuCommands.IDENTIFY_FIRMWARE_VERSION.value
|
|
207
|
+
)
|
|
208
|
+
).decode("utf-8")
|
|
209
|
+
return title, version
|
|
210
|
+
|
|
211
|
+
def load_firmware(self, firmware: bytes, progress_callback=None) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Load firmware into the CCU.
|
|
214
|
+
|
|
215
|
+
The firmware must be the binary firmware file containing the application partition,
|
|
216
|
+
typically named `fw-ccu-mm-default.bin`,
|
|
217
|
+
where `mm` is the major version of the CCU hardware.
|
|
218
|
+
|
|
219
|
+
The download can take several minutes, that is why a progress callback can be provided.
|
|
220
|
+
|
|
221
|
+
When the download is complete and successful, the CCU will restart. To check if the firmware was loaded successfully,
|
|
222
|
+
call :meth:`identify_firmware()` after the restart.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
firmware
|
|
227
|
+
The firmware binary data.
|
|
228
|
+
|
|
229
|
+
progress_callback
|
|
230
|
+
A callback function that is called with the current progress in bytes.
|
|
231
|
+
|
|
232
|
+
Warning
|
|
233
|
+
---------
|
|
234
|
+
Do not call this method at the same time from multiple threads or processes.
|
|
235
|
+
|
|
236
|
+
"""
|
|
237
|
+
offset = 0
|
|
238
|
+
max_chunk_len = 28
|
|
239
|
+
while len(firmware) > 0:
|
|
240
|
+
chunk, firmware = firmware[:max_chunk_len], firmware[max_chunk_len:]
|
|
241
|
+
self._load_firmware_chunk(offset, len(firmware) == 0, chunk)
|
|
242
|
+
offset += len(chunk)
|
|
243
|
+
if len(firmware) != 0:
|
|
244
|
+
self._nop()
|
|
245
|
+
if progress_callback is not None:
|
|
246
|
+
progress_callback(offset)
|
|
247
|
+
|
|
248
|
+
def _load_firmware_chunk(self, offset: int, is_last: bool, data: bytes) -> None:
|
|
249
|
+
if is_last:
|
|
250
|
+
offset |= 0x80000000
|
|
251
|
+
hdr = struct.pack("<I", offset)
|
|
252
|
+
data = hdr + data
|
|
253
|
+
self._smbus.write_block_data(
|
|
254
|
+
self._i2c_addr, CcuCommands.LOAD_FIRMWARE_CHUNK.value, list(data)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def get_parameterset(self) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Get the CCU parameterset in JSON format.
|
|
260
|
+
|
|
261
|
+
A typical parameterset looks like this:
|
|
262
|
+
|
|
263
|
+
.. code-block:: json
|
|
264
|
+
|
|
265
|
+
{
|
|
266
|
+
"version": "factory",
|
|
267
|
+
"parameters": {
|
|
268
|
+
"num-fans": "2",
|
|
269
|
+
"fan-temp2rpm": "25:2800;50:5000;100:6700",
|
|
270
|
+
"fan-rpm2duty": "2800:55;5000:88;6700:100",
|
|
271
|
+
"fan-defrpm": "5500",
|
|
272
|
+
"fan-ppr": "2",
|
|
273
|
+
"fan-push-tout": "4000",
|
|
274
|
+
"pon-min-temp": "-25",
|
|
275
|
+
"pon-max-temp": "70",
|
|
276
|
+
"shutdn-delay": "120",
|
|
277
|
+
"wd-tout": "0",
|
|
278
|
+
"pwrcycle-time": "10"
|
|
279
|
+
},
|
|
280
|
+
"unsupported_parameters": [],
|
|
281
|
+
"missing_parameters": ["num-fans", "fan-temp2rpm", "fan-rpm2duty", "fan-defrpm", "fan-ppr", \
|
|
282
|
+
"fan-push-tout", "pon-min-temp", "pon-max-temp", "shutdn-delay", "wd-tout", "pwrcycle-time"],
|
|
283
|
+
"invalid_parameters": [],
|
|
284
|
+
"reboot_required": false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
`version` is the version of the parameterset. If no parameterset has been loaded by the user, the version is `factory`,
|
|
288
|
+
otherwise it is the version of the loaded parameterset.
|
|
289
|
+
|
|
290
|
+
`parameters` contains the current values of all parameters of the parameterset.
|
|
291
|
+
|
|
292
|
+
`unsupported_parameters` contains the names of parameters that might have been downloaded, but
|
|
293
|
+
are not supported by the CCU firmware.
|
|
294
|
+
|
|
295
|
+
`missing_parameters` contains the names of parameters that have not been downloaded yet. Those parameters will
|
|
296
|
+
have their default values.
|
|
297
|
+
|
|
298
|
+
`invalid_parameters` contains the names of parameters that have been downloaded, but have invalid values.
|
|
299
|
+
Those parameters will have their default values.
|
|
300
|
+
|
|
301
|
+
`reboot_required` is a flag that indicates if a reboot is required to apply the parameterset.
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
str
|
|
307
|
+
The parameterset in JSON format.
|
|
308
|
+
|
|
309
|
+
Warning
|
|
310
|
+
---------
|
|
311
|
+
Do not call this method at the same time from multiple threads or processes.
|
|
312
|
+
"""
|
|
313
|
+
json = b""
|
|
314
|
+
begin = True
|
|
315
|
+
while True:
|
|
316
|
+
chunk = self._get_parameterset_chunk(begin)
|
|
317
|
+
if len(chunk) < 32:
|
|
318
|
+
break
|
|
319
|
+
json += chunk
|
|
320
|
+
begin = False
|
|
321
|
+
return json.decode("utf-8")
|
|
322
|
+
|
|
323
|
+
def _get_parameterset_chunk(self, begin: bool) -> bytes:
|
|
324
|
+
data = self._smbus.read_block_data(
|
|
325
|
+
self._i2c_addr,
|
|
326
|
+
(
|
|
327
|
+
CcuCommands.GET_PARAMETERSET_BEGIN.value
|
|
328
|
+
if begin
|
|
329
|
+
else CcuCommands.GET_PARAMETERSET_FOLLOW.value
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
return bytes(data)
|
|
333
|
+
|
|
334
|
+
def load_parameterset(self, _cfg: str) -> None:
|
|
335
|
+
"""
|
|
336
|
+
Load a parameterset into the CCU.
|
|
337
|
+
|
|
338
|
+
The parameterset must be a JSON string containing the parameterset, for example:
|
|
339
|
+
|
|
340
|
+
.. code-block:: json
|
|
341
|
+
|
|
342
|
+
{
|
|
343
|
+
"version": "1.0.0",
|
|
344
|
+
"parameters": {
|
|
345
|
+
"fan-defrpm": "6000"
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
This would load a parameterset with just one parameter, the default fan speed. All other parameters will
|
|
351
|
+
be set to their default values.
|
|
352
|
+
|
|
353
|
+
In order to apply the parameterset, the CCU must be restarted.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
_cfg
|
|
358
|
+
The parameterset in JSON format.
|
|
359
|
+
|
|
360
|
+
Warning
|
|
361
|
+
---------
|
|
362
|
+
Do not call this method at the same time from multiple threads or processes.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
cfg = _cfg.encode("utf-8")
|
|
366
|
+
offset = 0
|
|
367
|
+
max_chunk_len = 28
|
|
368
|
+
while len(cfg) > 0:
|
|
369
|
+
chunk, cfg = cfg[:max_chunk_len], cfg[max_chunk_len:]
|
|
370
|
+
self._load_parameterset_chunk(offset, len(cfg) == 0, chunk)
|
|
371
|
+
offset += len(chunk)
|
|
372
|
+
self._nop()
|
|
373
|
+
|
|
374
|
+
def _load_parameterset_chunk(self, offset: int, is_last: bool, data: bytes) -> None:
|
|
375
|
+
if is_last:
|
|
376
|
+
offset |= 0x80000000
|
|
377
|
+
hdr = struct.pack("<I", offset)
|
|
378
|
+
data = hdr + data
|
|
379
|
+
self._smbus.write_block_data(
|
|
380
|
+
self._i2c_addr, CcuCommands.LOAD_PARAMETERSET.value, list(data)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def restart(self) -> None:
|
|
384
|
+
"""
|
|
385
|
+
Restart the CCU.
|
|
386
|
+
"""
|
|
387
|
+
self._smbus.write_byte(self._i2c_addr, CcuCommands.RESTART.value)
|
|
388
|
+
|
|
389
|
+
def _nop(self) -> None:
|
|
390
|
+
self._smbus.read_word_data(self._i2c_addr, CcuCommands.NOP.value)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from .gpio import GPIOExpander
|
|
2
|
+
from ekfsm.core.components import SystemComponent
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EKFSurLed(GPIOExpander):
|
|
6
|
+
"""
|
|
7
|
+
A class to represent the EKF-SUR-LED devices.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
name: str,
|
|
13
|
+
parent: SystemComponent | None,
|
|
14
|
+
*args,
|
|
15
|
+
**kwargs,
|
|
16
|
+
):
|
|
17
|
+
super().__init__(name, parent, None, *args, **kwargs)
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return (
|
|
21
|
+
f"EKFSurLed - GPIO Number: {self.number}; "
|
|
22
|
+
f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def set(self, led: int, color: str):
|
|
26
|
+
"""
|
|
27
|
+
Set the color of a LED.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
led : int
|
|
32
|
+
The LED number (0 or 1).
|
|
33
|
+
color : str
|
|
34
|
+
The color of the LED.
|
|
35
|
+
Possible values: "off", "red", "blue", "green", "yellow", "purple", "cyan", "white"
|
|
36
|
+
"""
|
|
37
|
+
# 3-color LEDs,
|
|
38
|
+
# 0: Red
|
|
39
|
+
# 1: Blue
|
|
40
|
+
# 2: Green
|
|
41
|
+
|
|
42
|
+
if color == "off":
|
|
43
|
+
state = [False, False, False]
|
|
44
|
+
elif color == "red":
|
|
45
|
+
state = [True, False, False]
|
|
46
|
+
elif color == "blue":
|
|
47
|
+
state = [False, True, False]
|
|
48
|
+
elif color == "green":
|
|
49
|
+
state = [False, False, True]
|
|
50
|
+
elif color == "yellow":
|
|
51
|
+
state = [True, True, False]
|
|
52
|
+
elif color == "purple":
|
|
53
|
+
state = [True, False, True]
|
|
54
|
+
elif color == "cyan":
|
|
55
|
+
state = [False, True, True]
|
|
56
|
+
elif color == "white":
|
|
57
|
+
state = [True, True, True]
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"Invalid color: {color}")
|
|
60
|
+
|
|
61
|
+
if led < 0 or led > 1:
|
|
62
|
+
raise ValueError(f"Invalid led number: {led}")
|
|
63
|
+
|
|
64
|
+
for i in range(3):
|
|
65
|
+
self.set_direction(i + 4 * led, True)
|
|
66
|
+
# Active low
|
|
67
|
+
self.set_pin(i + 4 * led, False if state[i] else True)
|
ekfsm/devices/generic.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
from munch import Munch
|
|
3
|
+
from ekfsm.core.components import SystemComponent
|
|
4
|
+
from ekfsm.core.sysfs import SysFSDevice, sysfs_root
|
|
5
|
+
from ekfsm.exceptions import ConfigError
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ekfsm.core.components import HwModule
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Device(SystemComponent):
|
|
13
|
+
"""
|
|
14
|
+
A generic device.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
name: str,
|
|
20
|
+
parent: SystemComponent | None = None,
|
|
21
|
+
children: list["Device"] | None = None,
|
|
22
|
+
*args,
|
|
23
|
+
**kwargs
|
|
24
|
+
):
|
|
25
|
+
super().__init__(name)
|
|
26
|
+
self.parent = parent
|
|
27
|
+
self.device_args = kwargs
|
|
28
|
+
self.logger.debug(f"Device: {name} {kwargs}")
|
|
29
|
+
|
|
30
|
+
if children:
|
|
31
|
+
self.children = children
|
|
32
|
+
|
|
33
|
+
if not hasattr(self, "sysfs_device"):
|
|
34
|
+
self.sysfs_device: SysFSDevice | None = None
|
|
35
|
+
|
|
36
|
+
self._provides_attrs = kwargs.get("provides", {})
|
|
37
|
+
|
|
38
|
+
self.provides = self.__post_init__(Munch(self._provides_attrs))
|
|
39
|
+
|
|
40
|
+
def __post_init__(self, provides: Munch) -> Munch:
|
|
41
|
+
return self.__init_dynamic_attrs__(provides)
|
|
42
|
+
|
|
43
|
+
def __init_dynamic_attrs__(self, provides: Munch) -> Munch:
|
|
44
|
+
|
|
45
|
+
for key, fields in provides.items():
|
|
46
|
+
if isinstance(fields, dict):
|
|
47
|
+
provides[key] = self.__init_dynamic_attrs__(Munch(fields))
|
|
48
|
+
elif isinstance(fields, list | str):
|
|
49
|
+
provides[key] = Munch()
|
|
50
|
+
|
|
51
|
+
if isinstance(fields, str):
|
|
52
|
+
fields = [fields]
|
|
53
|
+
|
|
54
|
+
while fields:
|
|
55
|
+
iface = fields.pop()
|
|
56
|
+
if isinstance(iface, dict):
|
|
57
|
+
name = list(iface.keys())[0]
|
|
58
|
+
try:
|
|
59
|
+
func = list(iface.values())[0]
|
|
60
|
+
except IndexError:
|
|
61
|
+
raise ConfigError(
|
|
62
|
+
f"{self.name}: No function given for interface {name}."
|
|
63
|
+
)
|
|
64
|
+
if not hasattr(self, func):
|
|
65
|
+
raise NotImplementedError(
|
|
66
|
+
f"{self.name}: Function {func} for interface {name} not implemented."
|
|
67
|
+
)
|
|
68
|
+
provides[key].update({name: getattr(self, func)})
|
|
69
|
+
else:
|
|
70
|
+
if not hasattr(self, iface):
|
|
71
|
+
raise NotImplementedError(
|
|
72
|
+
f"{self.name}: Function {iface} for provider {key} not implemented."
|
|
73
|
+
)
|
|
74
|
+
provides[key].update({iface: getattr(self, iface)})
|
|
75
|
+
|
|
76
|
+
return provides
|
|
77
|
+
|
|
78
|
+
def read_sysfs_attr_bytes(self, attr: str) -> bytes | None:
|
|
79
|
+
if self.sysfs_device and len(attr) != 0:
|
|
80
|
+
return self.sysfs_device.read_attr_bytes(attr)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def read_sysfs_attr_utf8(self, attr: str) -> str | None:
|
|
84
|
+
if self.sysfs_device and len(attr) != 0:
|
|
85
|
+
return self.sysfs_device.read_attr_utf8(attr)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def write_sysfs_attr(self, attr: str, data: str | bytes) -> None:
|
|
89
|
+
if self.sysfs_device and len(attr) != 0:
|
|
90
|
+
return self.sysfs_device.write_attr(attr, data)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def hw_module(self) -> 'HwModule':
|
|
95
|
+
from ekfsm.core.components import HwModule
|
|
96
|
+
|
|
97
|
+
if isinstance(self.root, HwModule):
|
|
98
|
+
return self.root
|
|
99
|
+
else:
|
|
100
|
+
raise RuntimeError("Device is not a child of HwModule")
|
|
101
|
+
|
|
102
|
+
def get_i2c_chip_addr(self) -> int:
|
|
103
|
+
assert self.parent is not None
|
|
104
|
+
|
|
105
|
+
chip_addr = self.device_args.get("addr")
|
|
106
|
+
if chip_addr is None:
|
|
107
|
+
raise ConfigError(
|
|
108
|
+
f"{self.name}: Chip address not provided in board definition"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not hasattr(self.parent, "sysfs_device") or self.parent.sysfs_device is None:
|
|
112
|
+
# our device is the top level device of the slot
|
|
113
|
+
# compute chip address from board yaml and slot attributes
|
|
114
|
+
slot_attributes = self.hw_module.slot.attributes
|
|
115
|
+
|
|
116
|
+
if slot_attributes is None:
|
|
117
|
+
raise ConfigError(
|
|
118
|
+
f"{self.name}: Slot attributes not provided in system configuration"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if not self.hw_module.is_master:
|
|
122
|
+
# slot coding is only used for non-master devices
|
|
123
|
+
if not hasattr(slot_attributes, "slot_coding"):
|
|
124
|
+
raise ConfigError(
|
|
125
|
+
f"{self.name}: Slot coding not provided in slot attributes"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
slot_coding_mask = 0xFF
|
|
129
|
+
|
|
130
|
+
if hasattr(slot_attributes, "slot_coding_mask"):
|
|
131
|
+
slot_coding_mask = slot_attributes.slot_coding_mask
|
|
132
|
+
|
|
133
|
+
chip_addr |= slot_attributes.slot_coding & slot_coding_mask
|
|
134
|
+
|
|
135
|
+
return chip_addr
|
|
136
|
+
|
|
137
|
+
def get_i2c_sysfs_device(self, addr: int) -> SysFSDevice:
|
|
138
|
+
from ekfsm.core.components import HwModule
|
|
139
|
+
|
|
140
|
+
parent = self.parent
|
|
141
|
+
assert parent is not None
|
|
142
|
+
|
|
143
|
+
# if parent is a HwModule, we can get the i2c bus from the master device
|
|
144
|
+
if isinstance(parent, HwModule):
|
|
145
|
+
i2c_bus_path = self._master_i2c_bus()
|
|
146
|
+
else:
|
|
147
|
+
# otherwise the parent must be a MuxChannel
|
|
148
|
+
from ekfsm.devices.mux import MuxChannel
|
|
149
|
+
|
|
150
|
+
assert isinstance(parent, MuxChannel)
|
|
151
|
+
assert parent.sysfs_device is not None
|
|
152
|
+
i2c_bus_path = parent.sysfs_device.path
|
|
153
|
+
|
|
154
|
+
# search for device with addr
|
|
155
|
+
for entry in i2c_bus_path.iterdir():
|
|
156
|
+
if (
|
|
157
|
+
entry.is_dir()
|
|
158
|
+
and not (entry / "new_device").exists() # skip bus entries
|
|
159
|
+
and (entry / "name").exists()
|
|
160
|
+
):
|
|
161
|
+
# for PRP devices, address is contained in firmware_node/description
|
|
162
|
+
if (entry / "firmware_node").exists() and (
|
|
163
|
+
entry / "firmware_node" / "description"
|
|
164
|
+
).exists():
|
|
165
|
+
description = (
|
|
166
|
+
(entry / "firmware_node/description").read_text().strip()
|
|
167
|
+
)
|
|
168
|
+
got_addr = int(description.split(" - ")[0], 16)
|
|
169
|
+
if got_addr == addr:
|
|
170
|
+
return SysFSDevice(entry)
|
|
171
|
+
|
|
172
|
+
# for non-PRP devices, address is contained in the directory name (e.g. 2-0018)
|
|
173
|
+
else:
|
|
174
|
+
got_addr = int(entry.name.split("-")[1], 16)
|
|
175
|
+
if got_addr == addr:
|
|
176
|
+
return SysFSDevice(entry)
|
|
177
|
+
|
|
178
|
+
raise FileNotFoundError(
|
|
179
|
+
f"Device with address 0x{addr:x} not found in {i2c_bus_path}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def _master_i2c_get_config(master: "HwModule") -> dict:
|
|
184
|
+
if (
|
|
185
|
+
master.config.get("bus_masters") is not None
|
|
186
|
+
and master.config["bus_masters"].get("i2c") is not None
|
|
187
|
+
):
|
|
188
|
+
return master.config["bus_masters"]["i2c"]
|
|
189
|
+
else:
|
|
190
|
+
raise ConfigError("Master definition incomplete")
|
|
191
|
+
|
|
192
|
+
def _master_i2c_bus(self) -> Path:
|
|
193
|
+
if self.hw_module.is_master:
|
|
194
|
+
# we are the master
|
|
195
|
+
master = self.hw_module
|
|
196
|
+
master_key = "MASTER_LOCAL_DEFAULT"
|
|
197
|
+
override_master_key = self.device_args.get("i2c_master", None)
|
|
198
|
+
if override_master_key is not None:
|
|
199
|
+
master_key = override_master_key
|
|
200
|
+
else:
|
|
201
|
+
# another board is the master
|
|
202
|
+
if self.hw_module.slot.master is None:
|
|
203
|
+
raise ConfigError(
|
|
204
|
+
f"{self.name}: Master board not found in slot attributes"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
master = self.hw_module.slot.master
|
|
208
|
+
master_key = self.hw_module.slot.slot_type.name
|
|
209
|
+
|
|
210
|
+
i2c_masters = self._master_i2c_get_config(master)
|
|
211
|
+
|
|
212
|
+
if i2c_masters.get(master_key) is not None:
|
|
213
|
+
dir = sysfs_root() / Path(i2c_masters[master_key])
|
|
214
|
+
bus_dirs = list(dir.glob("i2c-*"))
|
|
215
|
+
if len(bus_dirs) == 1:
|
|
216
|
+
return bus_dirs[0]
|
|
217
|
+
elif len(bus_dirs) > 1:
|
|
218
|
+
raise ConfigError(f"Multiple master i2c buses found for {master_key}")
|
|
219
|
+
raise ConfigError(f"No master i2c bus found for {master_key}")
|
|
220
|
+
else:
|
|
221
|
+
raise ConfigError(f"Master i2c bus not found for {master_key}")
|
|
222
|
+
|
|
223
|
+
def get_i2c_bus_number(self) -> int:
|
|
224
|
+
"""
|
|
225
|
+
Get the I2C bus number of the device. Works for devices that do not have a sysfs_device attribute.
|
|
226
|
+
"""
|
|
227
|
+
from ekfsm.devices.mux import MuxChannel
|
|
228
|
+
|
|
229
|
+
if isinstance(self, MuxChannel):
|
|
230
|
+
raise RuntimeError(f"{self.name}: MuxChannel does not have a bus number")
|
|
231
|
+
|
|
232
|
+
if self.sysfs_device is None:
|
|
233
|
+
if self.parent is None:
|
|
234
|
+
raise RuntimeError(f"{self.name}: Must have a parent to get bus number")
|
|
235
|
+
parent_path = self.parent.sysfs_device.path
|
|
236
|
+
else:
|
|
237
|
+
parent_path = self.sysfs_device.path.parent
|
|
238
|
+
if parent_path.is_symlink():
|
|
239
|
+
parent_path = parent_path.readlink()
|
|
240
|
+
bus_number = parent_path.name.split("-")[1]
|
|
241
|
+
return int(bus_number)
|
|
242
|
+
|
|
243
|
+
def __repr__(self) -> str:
|
|
244
|
+
sysfs_path = getattr(self.sysfs_device, "path", "")
|
|
245
|
+
return f"{self.name}; sysfs_path: {sysfs_path}"
|