shepherd-core 2025.4.1__py3-none-any.whl → 2025.5.2__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.
- shepherd_core/calibration_hw_def.py +11 -11
- shepherd_core/commons.py +4 -4
- shepherd_core/data_models/__init__.py +2 -0
- shepherd_core/data_models/base/cal_measurement.py +10 -11
- shepherd_core/data_models/base/calibration.py +7 -6
- shepherd_core/data_models/base/content.py +1 -1
- shepherd_core/data_models/base/shepherd.py +6 -7
- shepherd_core/data_models/base/wrapper.py +2 -2
- shepherd_core/data_models/content/_external_fixtures.yaml +32 -32
- shepherd_core/data_models/content/energy_environment.py +6 -5
- shepherd_core/data_models/content/firmware.py +9 -7
- shepherd_core/data_models/content/virtual_harvester.py +34 -26
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml +2 -2
- shepherd_core/data_models/content/virtual_source.py +20 -17
- shepherd_core/data_models/content/virtual_source_fixture.yaml +3 -3
- shepherd_core/data_models/experiment/experiment.py +15 -15
- shepherd_core/data_models/experiment/observer_features.py +109 -16
- shepherd_core/data_models/experiment/target_config.py +17 -12
- shepherd_core/data_models/task/__init__.py +11 -8
- shepherd_core/data_models/task/emulation.py +32 -17
- shepherd_core/data_models/task/firmware_mod.py +11 -11
- shepherd_core/data_models/task/harvest.py +7 -6
- shepherd_core/data_models/task/observer_tasks.py +7 -7
- shepherd_core/data_models/task/programming.py +13 -12
- shepherd_core/data_models/task/testbed_tasks.py +8 -8
- shepherd_core/data_models/testbed/cape.py +7 -6
- shepherd_core/data_models/testbed/gpio.py +8 -7
- shepherd_core/data_models/testbed/mcu.py +8 -7
- shepherd_core/data_models/testbed/mcu_fixture.yaml +4 -4
- shepherd_core/data_models/testbed/observer.py +9 -7
- shepherd_core/data_models/testbed/target.py +9 -7
- shepherd_core/data_models/testbed/testbed.py +11 -10
- shepherd_core/data_models/virtual_source_doc.txt +3 -3
- shepherd_core/decoder_waveform/uart.py +5 -5
- shepherd_core/fw_tools/converter.py +10 -6
- shepherd_core/fw_tools/patcher.py +14 -15
- shepherd_core/fw_tools/validation.py +11 -6
- shepherd_core/inventory/__init__.py +6 -6
- shepherd_core/inventory/python.py +1 -1
- shepherd_core/inventory/system.py +11 -8
- shepherd_core/inventory/target.py +3 -3
- shepherd_core/logger.py +2 -2
- shepherd_core/reader.py +105 -78
- shepherd_core/testbed_client/client_abc_fix.py +22 -16
- shepherd_core/testbed_client/client_web.py +18 -11
- shepherd_core/testbed_client/fixtures.py +21 -22
- shepherd_core/testbed_client/user_model.py +6 -5
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/target_model.py +3 -3
- shepherd_core/vsource/virtual_converter_model.py +3 -3
- shepherd_core/vsource/virtual_harvester_model.py +7 -9
- shepherd_core/vsource/virtual_harvester_simulation.py +7 -6
- shepherd_core/vsource/virtual_source_model.py +6 -5
- shepherd_core/vsource/virtual_source_simulation.py +8 -7
- shepherd_core/writer.py +37 -39
- {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/METADATA +2 -3
- shepherd_core-2025.5.2.dist-info/RECORD +81 -0
- {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/WHEEL +1 -1
- shepherd_core-2025.4.1.dist-info/RECORD +0 -81
- {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/zip-safe +0 -0
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
"""Generalized energy harvester data models."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Mapping
|
|
3
4
|
from enum import Enum
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from typing import Any
|
|
4
7
|
from typing import Optional
|
|
5
|
-
from typing import Tuple
|
|
6
8
|
|
|
7
9
|
from pydantic import Field
|
|
8
10
|
from pydantic import model_validator
|
|
9
|
-
from typing_extensions import Annotated
|
|
10
11
|
from typing_extensions import Self
|
|
11
12
|
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from
|
|
13
|
+
from shepherd_core.commons import SAMPLERATE_SPS_DEFAULT
|
|
14
|
+
from shepherd_core.data_models.base.calibration import CalibrationHarvester
|
|
15
|
+
from shepherd_core.data_models.base.content import ContentModel
|
|
16
|
+
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
17
|
+
from shepherd_core.logger import logger
|
|
18
|
+
from shepherd_core.testbed_client import tb_client
|
|
19
|
+
|
|
18
20
|
from .energy_environment import EnergyDType
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class AlgorithmDType(str, Enum):
|
|
22
24
|
"""Options for choosing a harvesting algorithm."""
|
|
23
25
|
|
|
24
|
-
direct = disable = neutral = "neutral"
|
|
25
|
-
isc_voc = "isc_voc"
|
|
26
|
-
ivcurve = ivcurves =
|
|
26
|
+
direct = disable = neutral = "neutral" # for just using IVTrace / samples
|
|
27
|
+
isc_voc = "isc_voc" # only recordable ATM
|
|
28
|
+
ivcurve = ivcurves = ivsurface = "ivcurve"
|
|
27
29
|
constant = cv = "cv"
|
|
28
30
|
# ci .. constant current -> is this desired?
|
|
29
31
|
mppt_voc = "mppt_voc"
|
|
@@ -35,7 +37,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
35
37
|
"""A vHrv makes a source-characterization (i.e. ivcurve) usable for the vSrc.
|
|
36
38
|
|
|
37
39
|
Mostly used when the file-based energy environment of the virtual source
|
|
38
|
-
is not already supplied as pre-harvested
|
|
40
|
+
is not already supplied as pre-harvested ivtrace.
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
43
|
# General Metadata & Ownership -> ContentModel
|
|
@@ -73,7 +75,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
73
75
|
|
|
74
76
|
@model_validator(mode="before")
|
|
75
77
|
@classmethod
|
|
76
|
-
def query_database(cls, values: dict) -> dict:
|
|
78
|
+
def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
77
79
|
values, chain = tb_client.try_completing_model(cls.__name__, values)
|
|
78
80
|
values = tb_client.fill_in_user_data(values)
|
|
79
81
|
if values["name"] == "neutral":
|
|
@@ -114,14 +116,14 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
114
116
|
return 1 * int(for_emu) + 2 * self.rising + 4 * self.enable_linear_extrapolation
|
|
115
117
|
|
|
116
118
|
def calc_algorithm_num(self, *, for_emu: bool) -> int:
|
|
117
|
-
num =
|
|
119
|
+
num: int = ALGO_TO_NUM.get(self.algorithm, ALGO_TO_NUM["neutral"])
|
|
118
120
|
if for_emu and self.get_datatype() != EnergyDType.ivsample:
|
|
119
121
|
msg = (
|
|
120
122
|
f"[{self.name}] Select valid harvest-algorithm for emulator, "
|
|
121
123
|
f"current usage = {self.algorithm}"
|
|
122
124
|
)
|
|
123
125
|
raise ValueError(msg)
|
|
124
|
-
if not for_emu and num <
|
|
126
|
+
if not for_emu and num < ALGO_TO_NUM["isc_voc"]:
|
|
125
127
|
msg = (
|
|
126
128
|
f"[{self.name}] Select valid harvest-algorithm for harvester, "
|
|
127
129
|
f"current usage = {self.algorithm}"
|
|
@@ -129,12 +131,12 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
129
131
|
raise ValueError(msg)
|
|
130
132
|
return num
|
|
131
133
|
|
|
132
|
-
def calc_timings_ms(self, *, for_emu: bool) ->
|
|
134
|
+
def calc_timings_ms(self, *, for_emu: bool) -> tuple[float, float]:
|
|
133
135
|
"""factor-in model-internal timing-constraints."""
|
|
134
136
|
window_length = self.samples_n * (1 + self.wait_cycles)
|
|
135
|
-
time_min_ms = (1 + self.wait_cycles) * 1_000 /
|
|
137
|
+
time_min_ms = (1 + self.wait_cycles) * 1_000 / SAMPLERATE_SPS_DEFAULT
|
|
136
138
|
if for_emu:
|
|
137
|
-
window_ms = window_length * 1_000 /
|
|
139
|
+
window_ms = window_length * 1_000 / SAMPLERATE_SPS_DEFAULT
|
|
138
140
|
time_min_ms = max(time_min_ms, window_ms)
|
|
139
141
|
|
|
140
142
|
interval_ms = min(max(self.interval_ms, time_min_ms), 1_000_000)
|
|
@@ -149,7 +151,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
149
151
|
return interval_ms, duration_ms
|
|
150
152
|
|
|
151
153
|
def get_datatype(self) -> EnergyDType:
|
|
152
|
-
return
|
|
154
|
+
return ALGO_TO_DTYPE[self.algorithm]
|
|
153
155
|
|
|
154
156
|
def calc_window_size(
|
|
155
157
|
self,
|
|
@@ -184,7 +186,7 @@ u32 = Annotated[int, Field(ge=0, lt=2**32)]
|
|
|
184
186
|
# - harvesting on "neutral" is not possible - direct pass-through
|
|
185
187
|
# - emulation with "ivcurve" or lower is also resulting in Error
|
|
186
188
|
# - "_opt" has its own algo for emulation, but is only a fast mppt_po for harvesting
|
|
187
|
-
|
|
189
|
+
ALGO_TO_NUM: Mapping[str, int] = {
|
|
188
190
|
"neutral": 2**0,
|
|
189
191
|
"isc_voc": 2**3,
|
|
190
192
|
"ivcurve": 2**4,
|
|
@@ -195,7 +197,7 @@ algo_to_num = {
|
|
|
195
197
|
"mppt_opt": 2**14,
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
ALGO_TO_DTYPE: Mapping[str, EnergyDType] = {
|
|
199
201
|
"neutral": EnergyDType.ivsample,
|
|
200
202
|
"isc_voc": EnergyDType.isc_voc,
|
|
201
203
|
"ivcurve": EnergyDType.ivcurve,
|
|
@@ -266,9 +268,15 @@ class HarvesterPRUConfig(ShpModel):
|
|
|
266
268
|
if window_size is not None
|
|
267
269
|
else data.calc_window_size(dtype_in, for_emu=for_emu)
|
|
268
270
|
)
|
|
269
|
-
|
|
270
|
-
1e3 * voltage_step_V
|
|
271
|
-
|
|
271
|
+
if voltage_step_V is not None:
|
|
272
|
+
voltage_step_mV = 1e3 * voltage_step_V
|
|
273
|
+
elif data.voltage_step_mV is not None:
|
|
274
|
+
voltage_step_mV = data.voltage_step_mV
|
|
275
|
+
else:
|
|
276
|
+
raise ValueError(
|
|
277
|
+
"For correct emulation specify voltage_step used by harvester "
|
|
278
|
+
"e.g. via file_src.get_voltage_step()"
|
|
279
|
+
)
|
|
272
280
|
|
|
273
281
|
return cls(
|
|
274
282
|
algorithm=data.calc_algorithm_num(for_emu=for_emu),
|
|
@@ -280,7 +288,7 @@ class HarvesterPRUConfig(ShpModel):
|
|
|
280
288
|
voltage_step_uV=round(voltage_step_mV * 10**3),
|
|
281
289
|
current_limit_nA=round(data.current_limit_uA * 10**3),
|
|
282
290
|
setpoint_n8=round(min(255, data.setpoint_n * 2**8)),
|
|
283
|
-
interval_n=round(interval_ms *
|
|
284
|
-
duration_n=round(duration_ms *
|
|
291
|
+
interval_n=round(interval_ms * SAMPLERATE_SPS_DEFAULT * 1e-3),
|
|
292
|
+
duration_n=round(duration_ms * SAMPLERATE_SPS_DEFAULT * 1e-3),
|
|
285
293
|
wait_cycles_n=data.wait_cycles,
|
|
286
294
|
)
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
parameters:
|
|
22
22
|
id: 1100
|
|
23
23
|
name: ivcurve
|
|
24
|
-
description: Postpone harvesting by sampling
|
|
24
|
+
description: Postpone harvesting by sampling ivsurface / curves (voltage stepped as sawtooth-wave)
|
|
25
25
|
comment: ~110 Hz, Between 50 & 60 Hz line-frequency to avoid standing waves
|
|
26
26
|
inherit_from: neutral
|
|
27
27
|
algorithm: ivcurve
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
- datatype: VirtualHarvesterConfig
|
|
37
37
|
parameters:
|
|
38
38
|
id: 1101
|
|
39
|
-
name:
|
|
39
|
+
name: ivsurface # synonym
|
|
40
40
|
inherit_from: ivcurve
|
|
41
41
|
|
|
42
42
|
- datatype: VirtualHarvesterConfig
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
"""Generalized virtual source data models."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from pydantic import Field
|
|
6
7
|
from pydantic import model_validator
|
|
7
|
-
from typing_extensions import Annotated
|
|
8
8
|
from typing_extensions import Self
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
10
|
+
from shepherd_core.commons import SAMPLERATE_SPS_DEFAULT
|
|
11
|
+
from shepherd_core.data_models.base.content import ContentModel
|
|
12
|
+
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
13
|
+
from shepherd_core.logger import logger
|
|
14
|
+
from shepherd_core.testbed_client import tb_client
|
|
15
|
+
|
|
15
16
|
from .energy_environment import EnergyDType
|
|
16
17
|
from .virtual_harvester import HarvesterPRUConfig
|
|
17
18
|
from .virtual_harvester import VirtualHarvesterConfig
|
|
@@ -19,8 +20,8 @@ from .virtual_harvester import VirtualHarvesterConfig
|
|
|
19
20
|
# Custom Types
|
|
20
21
|
LUT_SIZE: int = 12
|
|
21
22
|
NormedNum = Annotated[float, Field(ge=0.0, le=1.0)]
|
|
22
|
-
LUT1D = Annotated[
|
|
23
|
-
LUT2D = Annotated[
|
|
23
|
+
LUT1D = Annotated[list[NormedNum], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
|
|
24
|
+
LUT2D = Annotated[list[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
@@ -32,7 +33,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
32
33
|
The converter-stage is software defined and offers:
|
|
33
34
|
- buck-boost-combinations,
|
|
34
35
|
- a simple diode + resistor and
|
|
35
|
-
- an intermediate
|
|
36
|
+
- an intermediate storage capacitor.
|
|
36
37
|
"""
|
|
37
38
|
|
|
38
39
|
# TODO: I,V,R should be in regular unit (V, A, Ohm)
|
|
@@ -81,6 +82,8 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
81
82
|
C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
|
|
82
83
|
# TODO: C_output is handled internally as delta-V, but should be a I_transient
|
|
83
84
|
# that makes it visible in simulation as additional i_out_drain
|
|
85
|
+
# TODO: potential weakness, ACD lowpass is capturing transient,
|
|
86
|
+
# but energy is LOST with this model
|
|
84
87
|
|
|
85
88
|
# Extra
|
|
86
89
|
V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
|
|
@@ -104,7 +107,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
104
107
|
# Buck Converter
|
|
105
108
|
V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
|
|
106
109
|
V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
|
|
107
|
-
# ⤷ simulate LDO min voltage differential or output-diode
|
|
110
|
+
# ⤷ simulate LDO / diode min voltage differential or output-diode
|
|
108
111
|
|
|
109
112
|
LUT_output_efficiency: LUT1D = 12 * [1.00]
|
|
110
113
|
# ⤷ array[12] depending on output_current
|
|
@@ -113,7 +116,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
113
116
|
|
|
114
117
|
@model_validator(mode="before")
|
|
115
118
|
@classmethod
|
|
116
|
-
def query_database(cls, values: dict) -> dict:
|
|
119
|
+
def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
117
120
|
values, chain = tb_client.try_completing_model(cls.__name__, values)
|
|
118
121
|
values = tb_client.fill_in_user_data(values)
|
|
119
122
|
logger.debug("VSrc-Inheritances: %s", chain)
|
|
@@ -238,19 +241,19 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
238
241
|
dV[uV] = constant[us/nF] * current[nA] = constant[us*V/nAs] * current[nA]
|
|
239
242
|
"""
|
|
240
243
|
C_cap_uF = max(self.C_intermediate_uF, 0.001)
|
|
241
|
-
return int((10**3 * (2**28)) // (C_cap_uF *
|
|
244
|
+
return int((10**3 * (2**28)) // (C_cap_uF * SAMPLERATE_SPS_DEFAULT))
|
|
242
245
|
|
|
243
246
|
|
|
244
247
|
u32 = Annotated[int, Field(ge=0, lt=2**32)]
|
|
245
248
|
u8 = Annotated[int, Field(ge=0, lt=2**8)]
|
|
246
249
|
lut_i = Annotated[
|
|
247
|
-
|
|
250
|
+
list[Annotated[list[u8], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]],
|
|
248
251
|
Field(
|
|
249
252
|
min_length=LUT_SIZE,
|
|
250
253
|
max_length=LUT_SIZE,
|
|
251
254
|
),
|
|
252
255
|
]
|
|
253
|
-
lut_o = Annotated[
|
|
256
|
+
lut_o = Annotated[list[u32], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
|
|
254
257
|
|
|
255
258
|
|
|
256
259
|
class ConverterPRUConfig(ShpModel):
|
|
@@ -313,7 +316,7 @@ class ConverterPRUConfig(ShpModel):
|
|
|
313
316
|
dtype_in, log_intermediate_node=log_intermediate_node
|
|
314
317
|
),
|
|
315
318
|
interval_startup_delay_drain_n=round(
|
|
316
|
-
data.interval_startup_delay_drain_ms *
|
|
319
|
+
data.interval_startup_delay_drain_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
|
|
317
320
|
),
|
|
318
321
|
V_input_max_uV=round(data.V_input_max_mV * 1e3),
|
|
319
322
|
I_input_max_nA=round(data.I_input_max_mA * 1e6),
|
|
@@ -326,7 +329,7 @@ class ConverterPRUConfig(ShpModel):
|
|
|
326
329
|
V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
|
|
327
330
|
dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
|
|
328
331
|
interval_check_thresholds_n=round(
|
|
329
|
-
data.interval_check_thresholds_ms *
|
|
332
|
+
data.interval_check_thresholds_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
|
|
330
333
|
),
|
|
331
334
|
V_pwr_good_enable_threshold_uV=round(data.V_pwr_good_enable_threshold_mV * 1e3),
|
|
332
335
|
V_pwr_good_disable_threshold_uV=round(data.V_pwr_good_disable_threshold_mV * 1e3),
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
parameters:
|
|
94
94
|
id: 1011
|
|
95
95
|
name: diode+capacitor
|
|
96
|
-
description: Simple Converter based on diode and
|
|
96
|
+
description: Simple Converter based on diode and storage capacitor
|
|
97
97
|
inherit_from: neutral
|
|
98
98
|
V_input_drop_mV: 300 # simulate input-diode
|
|
99
99
|
C_intermediate_uF: 47 # primary storage-Cap
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
parameters:
|
|
115
115
|
id: 1013
|
|
116
116
|
name: diode+resistor+capacitor
|
|
117
|
-
description: Simple Converter based on diode, current limiting resistor and
|
|
117
|
+
description: Simple Converter based on diode, current limiting resistor and storage capacitor
|
|
118
118
|
inherit_from: diode+capacitor
|
|
119
119
|
R_input_mOhm: 10000
|
|
120
120
|
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
enable_boost: true # if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
|
|
134
134
|
|
|
135
135
|
harvester:
|
|
136
|
-
name: mppt_bq_solar # harvester only active if input is
|
|
136
|
+
name: mppt_bq_solar # harvester only active if input is ivsurface / curves
|
|
137
137
|
|
|
138
138
|
V_input_max_mV: 3000
|
|
139
139
|
I_input_max_mA: 100
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Config for testbed experiments."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from datetime import timedelta
|
|
5
|
-
from typing import
|
|
6
|
-
from typing import List
|
|
6
|
+
from typing import Annotated
|
|
7
7
|
from typing import Optional
|
|
8
8
|
from typing import Union
|
|
9
9
|
from uuid import uuid4
|
|
@@ -11,16 +11,16 @@ from uuid import uuid4
|
|
|
11
11
|
from pydantic import UUID4
|
|
12
12
|
from pydantic import Field
|
|
13
13
|
from pydantic import model_validator
|
|
14
|
-
from typing_extensions import Annotated
|
|
15
14
|
from typing_extensions import Self
|
|
16
15
|
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
22
|
-
from
|
|
23
|
-
from
|
|
16
|
+
from shepherd_core.data_models.base.content import IdInt
|
|
17
|
+
from shepherd_core.data_models.base.content import NameStr
|
|
18
|
+
from shepherd_core.data_models.base.content import SafeStr
|
|
19
|
+
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
20
|
+
from shepherd_core.data_models.testbed.target import Target
|
|
21
|
+
from shepherd_core.data_models.testbed.testbed import Testbed
|
|
22
|
+
from shepherd_core.version import version
|
|
23
|
+
|
|
24
24
|
from .observer_features import SystemLogging
|
|
25
25
|
from .target_config import TargetConfig
|
|
26
26
|
|
|
@@ -46,7 +46,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
46
46
|
# feedback
|
|
47
47
|
email_results: bool = False
|
|
48
48
|
|
|
49
|
-
sys_logging: SystemLogging = SystemLogging(
|
|
49
|
+
sys_logging: SystemLogging = SystemLogging() # = all active
|
|
50
50
|
|
|
51
51
|
# schedule
|
|
52
52
|
time_start: Optional[datetime] = None # = ASAP
|
|
@@ -54,9 +54,9 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
54
54
|
abort_on_error: bool = False
|
|
55
55
|
|
|
56
56
|
# targets
|
|
57
|
-
target_configs: Annotated[
|
|
57
|
+
target_configs: Annotated[list[TargetConfig], Field(min_length=1, max_length=128)]
|
|
58
58
|
|
|
59
|
-
#
|
|
59
|
+
# debug
|
|
60
60
|
lib_ver: Optional[str] = version
|
|
61
61
|
|
|
62
62
|
@model_validator(mode="after")
|
|
@@ -70,8 +70,8 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
70
70
|
|
|
71
71
|
@staticmethod
|
|
72
72
|
def _validate_targets(configs: Iterable[TargetConfig]) -> None:
|
|
73
|
-
target_ids = []
|
|
74
|
-
custom_ids = []
|
|
73
|
+
target_ids: list[int] = []
|
|
74
|
+
custom_ids: list[int] = []
|
|
75
75
|
for _config in configs:
|
|
76
76
|
for _id in _config.target_IDs:
|
|
77
77
|
target_ids.append(_id)
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import timedelta
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Annotated
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
from pydantic import PositiveFloat
|
|
11
11
|
from pydantic import model_validator
|
|
12
|
-
from typing_extensions import Annotated
|
|
13
12
|
from typing_extensions import Self
|
|
13
|
+
from typing_extensions import deprecated
|
|
14
14
|
|
|
15
|
-
from
|
|
16
|
-
from
|
|
15
|
+
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
16
|
+
from shepherd_core.data_models.testbed.gpio import GPIO
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
@@ -23,11 +23,11 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
intermediate_voltage: bool = False
|
|
26
|
-
# ⤷ for EMU: record
|
|
26
|
+
# ⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
|
|
27
27
|
# this also includes current!
|
|
28
28
|
|
|
29
29
|
# time
|
|
30
|
-
delay: timedelta = 0
|
|
30
|
+
delay: timedelta = timedelta(seconds=0)
|
|
31
31
|
duration: Optional[timedelta] = None # till EOF
|
|
32
32
|
|
|
33
33
|
# post-processing
|
|
@@ -48,7 +48,87 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
48
48
|
if not self.calculate_power and discard_all:
|
|
49
49
|
raise ValueError("Error in config -> tracing enabled, but output gets discarded")
|
|
50
50
|
if self.calculate_power:
|
|
51
|
-
raise NotImplementedError(
|
|
51
|
+
raise NotImplementedError(
|
|
52
|
+
"Feature PowerTracing.calculate_power reserved for future use."
|
|
53
|
+
)
|
|
54
|
+
if self.samplerate != 100_000:
|
|
55
|
+
raise NotImplementedError("Feature PowerTracing.samplerate reserved for future use.")
|
|
56
|
+
if self.discard_current:
|
|
57
|
+
raise NotImplementedError(
|
|
58
|
+
"Feature PowerTracing.discard_current reserved for future use."
|
|
59
|
+
)
|
|
60
|
+
if self.discard_voltage:
|
|
61
|
+
raise NotImplementedError(
|
|
62
|
+
"Feature PowerTracing.discard_voltage reserved for future use."
|
|
63
|
+
)
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# NOTE: this was taken from pyserial (removes one dependency)
|
|
68
|
+
BAUDRATES = (
|
|
69
|
+
50,
|
|
70
|
+
75,
|
|
71
|
+
110,
|
|
72
|
+
134,
|
|
73
|
+
150,
|
|
74
|
+
200,
|
|
75
|
+
300,
|
|
76
|
+
600,
|
|
77
|
+
1200,
|
|
78
|
+
1800,
|
|
79
|
+
2400,
|
|
80
|
+
4800,
|
|
81
|
+
9600,
|
|
82
|
+
19200,
|
|
83
|
+
38400,
|
|
84
|
+
57600,
|
|
85
|
+
115200,
|
|
86
|
+
230400,
|
|
87
|
+
460800,
|
|
88
|
+
500000,
|
|
89
|
+
576000,
|
|
90
|
+
921600,
|
|
91
|
+
1000000,
|
|
92
|
+
1152000,
|
|
93
|
+
1500000,
|
|
94
|
+
2000000,
|
|
95
|
+
2500000,
|
|
96
|
+
3000000,
|
|
97
|
+
3500000,
|
|
98
|
+
4000000,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = "N", "E", "O", "M", "S"
|
|
102
|
+
PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
|
|
103
|
+
|
|
104
|
+
STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
|
|
105
|
+
STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class UartTracing(ShpModel, title="Config for UART Tracing"):
|
|
109
|
+
"""Configuration for recording UART-Output of the Target Nodes.
|
|
110
|
+
|
|
111
|
+
Note that the Communication has to be on a specific port that
|
|
112
|
+
reaches the hardware-module of the SBC.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
baudrate: Annotated[int, Field(ge=2_400, le=460_800)] = 115_200
|
|
116
|
+
# ⤷ TODO: find maximum that the system can handle
|
|
117
|
+
bytesize: Annotated[int, Field(ge=5, le=8)] = 8
|
|
118
|
+
stopbits: Annotated[float, Field(ge=1, le=2)] = 1
|
|
119
|
+
parity: str = PARITY_NONE
|
|
120
|
+
|
|
121
|
+
@model_validator(mode="after")
|
|
122
|
+
def post_validation(self) -> Self:
|
|
123
|
+
if self.baudrate not in BAUDRATES:
|
|
124
|
+
msg = f"Error in config -> baud-rate must be one of: {BAUDRATES}"
|
|
125
|
+
raise ValueError(msg)
|
|
126
|
+
if self.stopbits not in STOPBITS:
|
|
127
|
+
msg = f"Error in config -> stop-bits must be one of: {STOPBITS}"
|
|
128
|
+
raise ValueError(msg)
|
|
129
|
+
if self.parity not in PARITIES:
|
|
130
|
+
msg = f"Error in config -> parity must be one of: {PARITIES}"
|
|
131
|
+
raise ValueError(msg)
|
|
52
132
|
return self
|
|
53
133
|
|
|
54
134
|
|
|
@@ -61,11 +141,11 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
61
141
|
# initial recording
|
|
62
142
|
mask: Annotated[int, Field(ge=0, lt=2**10)] = 0b11_1111_1111 # all
|
|
63
143
|
# ⤷ TODO: custom mask not implemented in PRU, ATM
|
|
64
|
-
gpios: Optional[Annotated[
|
|
144
|
+
gpios: Optional[Annotated[list[GPIO], Field(min_length=1, max_length=10)]] = None # = all
|
|
65
145
|
# ⤷ TODO: list of GPIO to build mask, one of both should be internal / computed field
|
|
66
146
|
|
|
67
147
|
# time
|
|
68
|
-
delay: timedelta = 0
|
|
148
|
+
delay: timedelta = timedelta(seconds=0)
|
|
69
149
|
duration: Optional[timedelta] = None # till EOF
|
|
70
150
|
|
|
71
151
|
# post-processing,
|
|
@@ -73,7 +153,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
73
153
|
# TODO: quickfix - uart-log currently done online in userspace
|
|
74
154
|
# NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
|
|
75
155
|
uart_pin: GPIO = GPIO(name="GPIO8")
|
|
76
|
-
uart_baudrate: Annotated[int, Field(ge=2_400, le=
|
|
156
|
+
uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
|
|
77
157
|
# TODO: add a "discard_gpio" (if only uart is wanted)
|
|
78
158
|
|
|
79
159
|
@model_validator(mode="after")
|
|
@@ -84,6 +164,15 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
84
164
|
raise ValueError("Delay can't be negative.")
|
|
85
165
|
if self.duration and self.duration.total_seconds() < 0:
|
|
86
166
|
raise ValueError("Duration can't be negative.")
|
|
167
|
+
if self.mask != 0b11_1111_1111: # GpioTracing.mask
|
|
168
|
+
raise NotImplementedError("Feature GpioTracing.mask reserved for future use.")
|
|
169
|
+
if self.gpios is not None:
|
|
170
|
+
raise NotImplementedError("Feature GpioTracing.gpios reserved for future use.")
|
|
171
|
+
if self.uart_decode:
|
|
172
|
+
raise NotImplementedError(
|
|
173
|
+
"Feature GpioTracing.uart_decode reserved for future use. "
|
|
174
|
+
"Use UartTracing or manually decode serial with the provided waveform decoder."
|
|
175
|
+
)
|
|
87
176
|
return self
|
|
88
177
|
|
|
89
178
|
|
|
@@ -125,7 +214,7 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
|
125
214
|
# TODO: not implemented ATM - decide if pru control sys-gpio or
|
|
126
215
|
# TODO: not implemented ATM - reverses pru-gpio (preferred if possible)
|
|
127
216
|
|
|
128
|
-
events: Annotated[
|
|
217
|
+
events: Annotated[list[GpioEvent], Field(min_length=1, max_length=1024)]
|
|
129
218
|
|
|
130
219
|
def get_gpios(self) -> set:
|
|
131
220
|
return {_ev.gpio for _ev in self.events}
|
|
@@ -134,11 +223,15 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
|
134
223
|
class SystemLogging(ShpModel, title="Config for System-Logging"):
|
|
135
224
|
"""Configuration for recording Debug-Output of the Observers System-Services."""
|
|
136
225
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
226
|
+
kernel: bool = True
|
|
227
|
+
time_sync: bool = True
|
|
228
|
+
sheep: bool = True
|
|
229
|
+
sys_util: bool = True
|
|
230
|
+
|
|
231
|
+
# TODO: remove lines below in 2026
|
|
232
|
+
dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
|
|
233
|
+
ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
|
|
234
|
+
shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
|
|
142
235
|
|
|
143
236
|
|
|
144
237
|
# TODO: some more interaction would be good
|
|
@@ -1,46 +1,49 @@
|
|
|
1
1
|
"""Configuration related to Target Nodes (DuT)."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Annotated
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from pydantic import Field
|
|
7
7
|
from pydantic import model_validator
|
|
8
|
-
from typing_extensions import Annotated
|
|
9
8
|
from typing_extensions import Self
|
|
10
9
|
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from
|
|
10
|
+
from shepherd_core.data_models.base.content import IdInt
|
|
11
|
+
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
12
|
+
from shepherd_core.data_models.content.energy_environment import EnergyEnvironment
|
|
13
|
+
from shepherd_core.data_models.content.firmware import Firmware
|
|
14
|
+
from shepherd_core.data_models.content.virtual_source import VirtualSourceConfig
|
|
15
|
+
from shepherd_core.data_models.testbed.target import IdInt16
|
|
16
|
+
from shepherd_core.data_models.testbed.target import Target
|
|
17
|
+
|
|
18
18
|
from .observer_features import GpioActuation
|
|
19
19
|
from .observer_features import GpioTracing
|
|
20
20
|
from .observer_features import PowerTracing
|
|
21
|
+
from .observer_features import UartTracing
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class TargetConfig(ShpModel, title="Target Config"):
|
|
24
25
|
"""Configuration related to Target Nodes (DuT)."""
|
|
25
26
|
|
|
26
|
-
target_IDs: Annotated[
|
|
27
|
-
custom_IDs: Optional[Annotated[
|
|
27
|
+
target_IDs: Annotated[list[IdInt], Field(min_length=1, max_length=128)]
|
|
28
|
+
custom_IDs: Optional[Annotated[list[IdInt16], Field(min_length=1, max_length=128)]] = None
|
|
28
29
|
# ⤷ will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware
|
|
29
30
|
# if no custom ID is provided, the original ID of target is used
|
|
30
31
|
|
|
31
32
|
energy_env: EnergyEnvironment # alias: input
|
|
32
33
|
virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
|
|
33
34
|
target_delays: Optional[
|
|
34
|
-
Annotated[
|
|
35
|
+
Annotated[list[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
|
|
35
36
|
] = None
|
|
36
37
|
# ⤷ individual starting times -> allows to use the same environment
|
|
37
38
|
# TODO: delays not used ATM
|
|
38
39
|
|
|
39
40
|
firmware1: Firmware
|
|
40
41
|
firmware2: Optional[Firmware] = None
|
|
42
|
+
# ⤷ omitted FW gets set to neutral deep-sleep
|
|
41
43
|
|
|
42
44
|
power_tracing: Optional[PowerTracing] = None
|
|
43
45
|
gpio_tracing: Optional[GpioTracing] = None
|
|
46
|
+
uart_tracing: Optional[UartTracing] = None
|
|
44
47
|
gpio_actuation: Optional[GpioActuation] = None
|
|
45
48
|
|
|
46
49
|
@model_validator(mode="after")
|
|
@@ -79,6 +82,8 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
79
82
|
msg = f"Provided custom IDs {c_ids} not enough to cover target range {t_ids}"
|
|
80
83
|
raise ValueError(msg)
|
|
81
84
|
# TODO: if custom ids present, firmware must be ELF
|
|
85
|
+
if self.gpio_actuation is not None:
|
|
86
|
+
raise NotImplementedError("Feature GpioActuation reserved for future use.")
|
|
82
87
|
return self
|
|
83
88
|
|
|
84
89
|
def get_custom_id(self, target_id: int) -> Optional[int]:
|