shepherd-core 2025.8.1__py3-none-any.whl → 2026.2.1__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/config.py +1 -1
- shepherd_core/data_models/__init__.py +8 -4
- shepherd_core/data_models/base/cal_measurement.py +7 -2
- shepherd_core/data_models/base/calibration.py +23 -12
- shepherd_core/data_models/base/content.py +12 -2
- shepherd_core/data_models/base/shepherd.py +13 -4
- shepherd_core/data_models/base/wrapper.py +2 -0
- shepherd_core/data_models/content/__init__.py +8 -4
- shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
- shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
- shepherd_core/data_models/content/energy_environment.py +341 -23
- shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
- shepherd_core/data_models/content/enum_datatypes.py +109 -0
- shepherd_core/data_models/content/firmware.py +44 -16
- shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
- shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
- shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
- shepherd_core/data_models/content/virtual_storage_config.py +429 -0
- shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
- shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
- shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
- shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
- shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
- shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
- shepherd_core/data_models/experiment/experiment.py +38 -13
- shepherd_core/data_models/experiment/observer_features.py +17 -4
- shepherd_core/data_models/experiment/target_config.py +56 -8
- shepherd_core/data_models/task/__init__.py +13 -2
- shepherd_core/data_models/task/emulation.py +10 -6
- shepherd_core/data_models/task/firmware_mod.py +3 -1
- shepherd_core/data_models/task/harvest.py +3 -1
- shepherd_core/data_models/task/helper_paths.py +2 -2
- shepherd_core/data_models/task/observer_tasks.py +8 -6
- shepherd_core/data_models/task/programming.py +4 -2
- shepherd_core/data_models/task/testbed_tasks.py +8 -2
- shepherd_core/data_models/testbed/cape.py +2 -0
- shepherd_core/data_models/testbed/gpio.py +2 -0
- shepherd_core/data_models/testbed/mcu.py +2 -0
- shepherd_core/data_models/testbed/observer.py +2 -0
- shepherd_core/data_models/testbed/target.py +7 -5
- shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
- shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/testbed.py +17 -15
- shepherd_core/decoder_waveform/uart.py +1 -1
- shepherd_core/exit_handler.py +22 -0
- shepherd_core/fw_tools/converter.py +2 -2
- shepherd_core/fw_tools/validation.py +1 -1
- shepherd_core/inventory/__init__.py +23 -21
- shepherd_core/inventory/system.py +3 -3
- shepherd_core/logger.py +0 -1
- shepherd_core/reader.py +32 -27
- shepherd_core/testbed_client/cache_path.py +3 -3
- shepherd_core/testbed_client/client_abc_fix.py +14 -3
- shepherd_core/testbed_client/client_web.py +7 -5
- shepherd_core/testbed_client/fixtures.py +7 -7
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/__init__.py +4 -0
- shepherd_core/vsource/virtual_converter_model.py +29 -28
- shepherd_core/vsource/virtual_harvester_model.py +29 -21
- shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
- shepherd_core/vsource/virtual_source_model.py +18 -14
- shepherd_core/vsource/virtual_source_simulation.py +71 -73
- shepherd_core/vsource/virtual_storage_model.py +164 -0
- shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
- shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
- shepherd_core/vsource/virtual_storage_simulator.py +104 -0
- shepherd_core/writer.py +16 -9
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
- shepherd_core-2026.2.1.dist-info/RECORD +102 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
- shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
- shepherd_core/data_models/content/firmware_datatype.py +0 -15
- shepherd_core/data_models/virtual_source_doc.txt +0 -207
- shepherd_core-2025.8.1.dist-info/RECORD +0 -83
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/zip-safe +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from datetime import timedelta
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from typing import Annotated
|
|
6
|
+
from typing import final
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
from annotated_types import Interval
|
|
@@ -19,12 +20,17 @@ from shepherd_core.logger import log
|
|
|
19
20
|
zero_duration = timedelta(seconds=0)
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
@final
|
|
22
24
|
class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
23
|
-
"""Configuration for recording the Power-Consumption of the Target Nodes.
|
|
25
|
+
"""Configuration for recording the Power-Consumption of the Target Nodes.
|
|
26
|
+
|
|
27
|
+
With the default configuration voltage and current are sampled with 100 kHz.
|
|
28
|
+
"""
|
|
24
29
|
|
|
25
30
|
intermediate_voltage: bool = False
|
|
26
31
|
"""
|
|
27
|
-
⤷ for EMU: record
|
|
32
|
+
⤷ for EMU: record output-path of intermediate energy storage (capacitor, battery)
|
|
33
|
+
instead of direct target voltage-output (good for V_out = const)
|
|
28
34
|
this also includes current!
|
|
29
35
|
"""
|
|
30
36
|
# time
|
|
@@ -38,7 +44,8 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
38
44
|
# further processing of IV-Samples
|
|
39
45
|
only_power: bool = False
|
|
40
46
|
""" ⤷ reduce file-size by calculating power and automatically discard I&V
|
|
41
|
-
Caution: increases cpu-utilization on observer
|
|
47
|
+
Caution: increases cpu-utilization on observer
|
|
48
|
+
sampling power @ 100 kHz is not recommended
|
|
42
49
|
"""
|
|
43
50
|
samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000
|
|
44
51
|
""" ⤷ reduce file-size by re-sampling (mean over x samples)
|
|
@@ -107,6 +114,7 @@ STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
|
|
|
107
114
|
STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
|
|
108
115
|
|
|
109
116
|
|
|
117
|
+
@final
|
|
110
118
|
class UartLogging(ShpModel, title="Config for UART Logging"):
|
|
111
119
|
"""Configuration for recording UART-Output of the Target Nodes.
|
|
112
120
|
|
|
@@ -139,6 +147,7 @@ GpioList = Annotated[list[GpioInt], Field(min_length=1, max_length=18)]
|
|
|
139
147
|
all_gpio = list(range(18))
|
|
140
148
|
|
|
141
149
|
|
|
150
|
+
@final
|
|
142
151
|
class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
143
152
|
"""Configuration for recording the GPIO-Output of the Target Nodes.
|
|
144
153
|
|
|
@@ -194,6 +203,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
194
203
|
return mask
|
|
195
204
|
|
|
196
205
|
|
|
206
|
+
@final
|
|
197
207
|
class GpioLevel(str, Enum):
|
|
198
208
|
"""Options for setting the gpio-level or state."""
|
|
199
209
|
|
|
@@ -202,6 +212,7 @@ class GpioLevel(str, Enum):
|
|
|
202
212
|
toggle = "X" # TODO: not the smartest decision for writing a converter
|
|
203
213
|
|
|
204
214
|
|
|
215
|
+
@final
|
|
205
216
|
class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
|
|
206
217
|
"""Configuration for a single GPIO-Event (Actuation)."""
|
|
207
218
|
|
|
@@ -228,6 +239,7 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
|
|
|
228
239
|
return np.arange(self.delay, stop, self.period)
|
|
229
240
|
|
|
230
241
|
|
|
242
|
+
@final
|
|
231
243
|
class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
232
244
|
"""Configuration for a GPIO-Actuation-Sequence."""
|
|
233
245
|
|
|
@@ -242,9 +254,10 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
|
242
254
|
raise ValueError(msg)
|
|
243
255
|
|
|
244
256
|
def get_gpios(self) -> set:
|
|
245
|
-
return {
|
|
257
|
+
return {ev_.gpio for ev_ in self.events}
|
|
246
258
|
|
|
247
259
|
|
|
260
|
+
@final
|
|
248
261
|
class SystemLogging(ShpModel, title="Config for System-Logging"):
|
|
249
262
|
"""Configuration for recording Debug-Output of the Observers System-Services."""
|
|
250
263
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Configuration related to Target Nodes (DuT)."""
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import Annotated
|
|
5
|
+
from typing import final
|
|
4
6
|
|
|
5
7
|
from pydantic import Field
|
|
6
8
|
from pydantic import model_validator
|
|
@@ -10,9 +12,10 @@ from shepherd_core.data_models.base.content import IdInt
|
|
|
10
12
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
11
13
|
from shepherd_core.data_models.content.energy_environment import EnergyEnvironment
|
|
12
14
|
from shepherd_core.data_models.content.firmware import Firmware
|
|
13
|
-
from shepherd_core.data_models.content.
|
|
15
|
+
from shepherd_core.data_models.content.virtual_source_config import VirtualSourceConfig
|
|
14
16
|
from shepherd_core.data_models.testbed.target import IdInt16
|
|
15
17
|
from shepherd_core.data_models.testbed.target import Target
|
|
18
|
+
from shepherd_core.logger import log
|
|
16
19
|
|
|
17
20
|
from .observer_features import GpioActuation
|
|
18
21
|
from .observer_features import GpioTracing
|
|
@@ -23,7 +26,8 @@ from .observer_features import UartLogging
|
|
|
23
26
|
vsrc_neutral = VirtualSourceConfig(name="neutral")
|
|
24
27
|
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
@final
|
|
30
|
+
class TargetConfig(ShpModel):
|
|
27
31
|
"""Configuration related to Target Nodes (DuT)."""
|
|
28
32
|
|
|
29
33
|
target_IDs: Annotated[list[IdInt], Field(min_length=1, max_length=128)]
|
|
@@ -55,13 +59,40 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
55
59
|
gpio_actuation: GpioActuation | None = None
|
|
56
60
|
uart_logging: UartLogging | None = None
|
|
57
61
|
|
|
62
|
+
@model_validator(mode="after")
|
|
63
|
+
def validate_eenv_mapping(self) -> Self:
|
|
64
|
+
"""Validate that a mapping between targets and EEnvs can be found."""
|
|
65
|
+
if self.energy_env.repetitions_ok:
|
|
66
|
+
return self
|
|
67
|
+
n_env = len(self.energy_env)
|
|
68
|
+
n_tgt = len(self.target_IDs)
|
|
69
|
+
if n_env == n_tgt:
|
|
70
|
+
return self
|
|
71
|
+
if n_env > n_tgt:
|
|
72
|
+
log.debug(
|
|
73
|
+
f"TargetConfig for {self.target_IDs} has remaining "
|
|
74
|
+
f"{n_env - n_tgt} EEnv-profiles -> will not be used there"
|
|
75
|
+
)
|
|
76
|
+
return self
|
|
77
|
+
msg = (
|
|
78
|
+
f"Energy-Environment of TargetConfig for tgt{self.target_IDs} was too small "
|
|
79
|
+
f"({n_tgt - n_env} missing). Please use a larger environment."
|
|
80
|
+
)
|
|
81
|
+
raise ValueError(msg)
|
|
82
|
+
|
|
58
83
|
@model_validator(mode="after")
|
|
59
84
|
def post_validation(self) -> Self:
|
|
60
|
-
if
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
# trigger a reuse warning if needed
|
|
86
|
+
self.get_critical_paths(warn_reuse=True)
|
|
87
|
+
try:
|
|
88
|
+
self.energy_env.enforce_validity()
|
|
89
|
+
except ValueError as xpt:
|
|
90
|
+
msg = f"EnergyEnv '{self.energy_env.name}' for TargetConfig must be valid.\n{xpt}"
|
|
91
|
+
# note: added xpt in text because pydantic refuses to show "from xpt" part below
|
|
92
|
+
raise ValueError(msg) from xpt
|
|
93
|
+
# check IDs
|
|
94
|
+
for id_ in self.target_IDs:
|
|
95
|
+
target = Target(id=id_)
|
|
65
96
|
for mcu_num in [1, 2]:
|
|
66
97
|
val_fw = getattr(self, f"firmware{mcu_num}")
|
|
67
98
|
has_fw = val_fw is not None
|
|
@@ -91,11 +122,28 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
91
122
|
msg = f"Provided custom IDs {c_ids} not enough to cover target range {t_ids}"
|
|
92
123
|
raise ValueError(msg)
|
|
93
124
|
# TODO: if custom ids present, firmware must be ELF
|
|
125
|
+
if self.target_delays is not None:
|
|
126
|
+
log.warning("Feature TargetDelays is reserved for future use (not implemented).")
|
|
94
127
|
if self.gpio_actuation is not None:
|
|
95
|
-
|
|
128
|
+
log.warning("Feature GpioActuation is reserved for future use (not implemented).")
|
|
96
129
|
return self
|
|
97
130
|
|
|
98
131
|
def get_custom_id(self, target_id: int) -> int | None:
|
|
99
132
|
if self.custom_IDs is not None and target_id in self.target_IDs:
|
|
100
133
|
return self.custom_IDs[self.target_IDs.index(target_id)]
|
|
101
134
|
return None
|
|
135
|
+
|
|
136
|
+
def get_critical_paths(self, *, warn_reuse: bool = True) -> set[Path]:
|
|
137
|
+
"""Return all paths of non-repeatable energy profiles to warn about re-usage."""
|
|
138
|
+
paths: list[Path] = [
|
|
139
|
+
profile.data_path
|
|
140
|
+
for profile in self.energy_env.energy_profiles
|
|
141
|
+
if not profile.repetitions_ok
|
|
142
|
+
]
|
|
143
|
+
path_set = set(paths)
|
|
144
|
+
if warn_reuse and len(paths) != len(path_set):
|
|
145
|
+
log.warning(
|
|
146
|
+
f"Detected re-usage of non-repeatable EnergyProfiles "
|
|
147
|
+
f"in EnergyEnv '{self.energy_env.name}' (TargetConfig-Level)"
|
|
148
|
+
)
|
|
149
|
+
return path_set
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
These models import externally from all other model-modules!
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import pathlib
|
|
6
7
|
import pickle
|
|
8
|
+
import sys
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
|
|
9
11
|
import yaml
|
|
@@ -44,8 +46,17 @@ def prepare_task(config: ShpModel | Path | str, observer: str | None = None) ->
|
|
|
44
46
|
config = Path(config)
|
|
45
47
|
|
|
46
48
|
if isinstance(config, Path) and config.exists() and config.suffix.lower() == ".pickle":
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
try:
|
|
50
|
+
with config.resolve().open("rb") as shp_file:
|
|
51
|
+
shp_dict = pickle.load(shp_file) # noqa: S301
|
|
52
|
+
except ModuleNotFoundError as e:
|
|
53
|
+
# NOTE: workaround for interop-problem
|
|
54
|
+
# "No module named 'pathlib._local'; 'pathlib' is not a package"
|
|
55
|
+
log.warning("Had trouble loading pickled task -> activate pathlib-workaround")
|
|
56
|
+
log.warning(" -> Caught Exception: %s", e.msg)
|
|
57
|
+
sys.modules["pathlib._local"] = pathlib
|
|
58
|
+
with config.resolve().open("rb") as shp_file:
|
|
59
|
+
shp_dict = pickle.load(shp_file) # noqa: S301
|
|
49
60
|
shp_wrap = Wrapper(**shp_dict)
|
|
50
61
|
elif isinstance(config, Path) and config.exists() and config.suffix.lower() == ".yaml":
|
|
51
62
|
with config.resolve().open() as shp_file:
|
|
@@ -8,6 +8,7 @@ from enum import Enum
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from pathlib import PurePosixPath
|
|
10
10
|
from typing import Annotated
|
|
11
|
+
from typing import final
|
|
11
12
|
|
|
12
13
|
from pydantic import Field
|
|
13
14
|
from pydantic import model_validator
|
|
@@ -17,7 +18,7 @@ from typing_extensions import Self
|
|
|
17
18
|
from shepherd_core.data_models.base.content import IdInt
|
|
18
19
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
19
20
|
from shepherd_core.data_models.base.timezone import local_tz
|
|
20
|
-
from shepherd_core.data_models.content.
|
|
21
|
+
from shepherd_core.data_models.content.virtual_source_config import VirtualSourceConfig
|
|
21
22
|
from shepherd_core.data_models.experiment.experiment import Experiment
|
|
22
23
|
from shepherd_core.data_models.experiment.observer_features import GpioActuation
|
|
23
24
|
from shepherd_core.data_models.experiment.observer_features import GpioTracing
|
|
@@ -46,6 +47,7 @@ compressions_allowed: list = [None, "lzf", 1]
|
|
|
46
47
|
c_translate = {"lzf": "lzf", "1": 1, "None": None, None: None}
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
@final
|
|
49
51
|
class EmulationTask(ShpModel):
|
|
50
52
|
"""Configuration for the Observer in Emulation-Mode."""
|
|
51
53
|
|
|
@@ -145,7 +147,7 @@ class EmulationTask(ShpModel):
|
|
|
145
147
|
raise ValueError("GPIO Actuation not yet implemented!")
|
|
146
148
|
|
|
147
149
|
io_requested = any(
|
|
148
|
-
|
|
150
|
+
io_ is not None for io_ in (self.gpio_actuation, self.gpio_tracing, self.uart_logging)
|
|
149
151
|
)
|
|
150
152
|
if self.enable_io and not io_requested:
|
|
151
153
|
log.warning("Target IO enabled, but no feature requested IO")
|
|
@@ -158,13 +160,14 @@ class EmulationTask(ShpModel):
|
|
|
158
160
|
def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path) -> Self:
|
|
159
161
|
obs = tb.get_observer(tgt_id)
|
|
160
162
|
tgt_cfg = xp.get_target_config(tgt_id)
|
|
163
|
+
tgt_idx = tgt_cfg.target_IDs.index(tgt_id)
|
|
161
164
|
io_requested = any(
|
|
162
|
-
|
|
163
|
-
for
|
|
165
|
+
io_ is not None
|
|
166
|
+
for io_ in (tgt_cfg.gpio_actuation, tgt_cfg.gpio_tracing, tgt_cfg.uart_logging)
|
|
164
167
|
)
|
|
165
168
|
|
|
166
169
|
return cls(
|
|
167
|
-
input_path=path_posix(tgt_cfg.energy_env.data_path),
|
|
170
|
+
input_path=path_posix(tgt_cfg.energy_env[tgt_idx].data_path),
|
|
168
171
|
output_path=path_posix(root_path / f"emu_{obs.name}.h5"),
|
|
169
172
|
time_start=copy.copy(xp.time_start),
|
|
170
173
|
duration=xp.duration,
|
|
@@ -185,7 +188,8 @@ class EmulationTask(ShpModel):
|
|
|
185
188
|
TODO: could be added to validator (with a switch)
|
|
186
189
|
"""
|
|
187
190
|
all_ok = any(self.input_path.is_relative_to(path) for path in paths)
|
|
188
|
-
|
|
191
|
+
if self.output_path is not None:
|
|
192
|
+
all_ok &= any(self.output_path.is_relative_to(path) for path in paths)
|
|
189
193
|
return all_ok
|
|
190
194
|
|
|
191
195
|
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from pathlib import PurePosixPath
|
|
6
6
|
from typing import Annotated
|
|
7
7
|
from typing import TypedDict
|
|
8
|
+
from typing import final
|
|
8
9
|
|
|
9
10
|
from pydantic import Field
|
|
10
11
|
from pydantic import model_validator
|
|
@@ -14,8 +15,8 @@ from typing_extensions import Unpack
|
|
|
14
15
|
|
|
15
16
|
from shepherd_core.data_models.base.content import IdInt
|
|
16
17
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
18
|
+
from shepherd_core.data_models.content.enum_datatypes import FirmwareDType
|
|
17
19
|
from shepherd_core.data_models.content.firmware import Firmware
|
|
18
|
-
from shepherd_core.data_models.content.firmware import FirmwareDType
|
|
19
20
|
from shepherd_core.data_models.content.firmware import FirmwareStr
|
|
20
21
|
from shepherd_core.data_models.experiment.experiment import Experiment
|
|
21
22
|
from shepherd_core.data_models.testbed import Testbed
|
|
@@ -26,6 +27,7 @@ from shepherd_core.logger import log
|
|
|
26
27
|
from .helper_paths import path_posix
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
@final
|
|
29
31
|
class FirmwareModTask(ShpModel):
|
|
30
32
|
"""Config for Task that adds the custom ID to the firmware & stores it into a file."""
|
|
31
33
|
|
|
@@ -6,6 +6,7 @@ from datetime import timedelta
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from pathlib import PurePosixPath
|
|
8
8
|
from typing import Annotated
|
|
9
|
+
from typing import final
|
|
9
10
|
|
|
10
11
|
from pydantic import Field
|
|
11
12
|
from pydantic import model_validator
|
|
@@ -13,13 +14,14 @@ from typing_extensions import Self
|
|
|
13
14
|
|
|
14
15
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
15
16
|
from shepherd_core.data_models.base.timezone import local_tz
|
|
16
|
-
from shepherd_core.data_models.content.
|
|
17
|
+
from shepherd_core.data_models.content.virtual_harvester_config import VirtualHarvesterConfig
|
|
17
18
|
from shepherd_core.data_models.experiment.observer_features import PowerTracing
|
|
18
19
|
from shepherd_core.data_models.experiment.observer_features import SystemLogging
|
|
19
20
|
|
|
20
21
|
from .emulation import Compression
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
@final
|
|
23
25
|
class HarvestTask(ShpModel):
|
|
24
26
|
"""Config for the Observer in Harvest-Mode to record IV data from a harvesting-source."""
|
|
25
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
r"""Helper FN to avoid unwanted behavior.
|
|
2
2
|
|
|
3
|
-
On
|
|
3
|
+
On WinOS Path("\xyz") gets transformed to "/xyz", but not on linux.
|
|
4
4
|
When sending an experiment via fastapi, this bug gets triggered.
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -10,6 +10,6 @@ from pathlib import Path
|
|
|
10
10
|
def path_posix(path: Path) -> Path:
|
|
11
11
|
r"""Help Linux to get from "\xyz" to "/xyz".
|
|
12
12
|
|
|
13
|
-
This isn't a problem on
|
|
13
|
+
This isn't a problem on WinOS and gets triggered when sending experiments via fastapi.
|
|
14
14
|
"""
|
|
15
15
|
return Path(path.as_posix().replace("\\", "/"))
|
|
@@ -5,6 +5,7 @@ from datetime import datetime
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from pathlib import PurePosixPath
|
|
7
7
|
from typing import Annotated
|
|
8
|
+
from typing import final
|
|
8
9
|
|
|
9
10
|
from pydantic import validate_call
|
|
10
11
|
from typing_extensions import Self
|
|
@@ -22,6 +23,7 @@ from .helper_paths import path_posix
|
|
|
22
23
|
from .programming import ProgrammingTask
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
@final
|
|
25
27
|
class ObserverTasks(ShpModel):
|
|
26
28
|
"""Collection of tasks for selected observer included in experiment."""
|
|
27
29
|
|
|
@@ -60,7 +62,7 @@ class ObserverTasks(ShpModel):
|
|
|
60
62
|
if xp_folder is None:
|
|
61
63
|
xp_folder = xp.folder_name() # moved a layer up for consistent naming
|
|
62
64
|
root_path = tb.data_on_observer / "experiments" / xp_folder
|
|
63
|
-
fw_paths = [root_path / f"fw{
|
|
65
|
+
fw_paths = [root_path / f"fw{i_}_{obs.name}.hex" for i_ in [1, 2]]
|
|
64
66
|
|
|
65
67
|
return cls(
|
|
66
68
|
observer=obs.name,
|
|
@@ -97,9 +99,9 @@ class ObserverTasks(ShpModel):
|
|
|
97
99
|
def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
|
|
98
100
|
"""Limit paths to allowed directories."""
|
|
99
101
|
all_ok = any(self.root_path.is_relative_to(path) for path in paths)
|
|
100
|
-
all_ok &= self.fw1_mod.is_contained(paths)
|
|
101
|
-
all_ok &= self.fw2_mod.is_contained(paths)
|
|
102
|
-
all_ok &= self.fw1_prog.is_contained(paths)
|
|
103
|
-
all_ok &= self.fw2_prog.is_contained(paths)
|
|
104
|
-
all_ok &= self.emulation.is_contained(paths)
|
|
102
|
+
all_ok &= self.fw1_mod is None or self.fw1_mod.is_contained(paths)
|
|
103
|
+
all_ok &= self.fw2_mod is None or self.fw2_mod.is_contained(paths)
|
|
104
|
+
all_ok &= self.fw1_prog is None or self.fw1_prog.is_contained(paths)
|
|
105
|
+
all_ok &= self.fw2_prog is None or self.fw2_prog.is_contained(paths)
|
|
106
|
+
all_ok &= self.emulation is None or self.emulation.is_contained(paths)
|
|
105
107
|
return all_ok
|
|
@@ -4,6 +4,7 @@ from collections.abc import Set as AbstractSet
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from pathlib import PurePosixPath
|
|
6
6
|
from typing import Annotated
|
|
7
|
+
from typing import final
|
|
7
8
|
|
|
8
9
|
from pydantic import Field
|
|
9
10
|
from pydantic import model_validator
|
|
@@ -13,8 +14,8 @@ from typing_extensions import Self
|
|
|
13
14
|
from shepherd_core.data_models.base.content import IdInt
|
|
14
15
|
from shepherd_core.data_models.base.content import SafeStr
|
|
15
16
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
17
|
+
from shepherd_core.data_models.content.enum_datatypes import FirmwareDType
|
|
16
18
|
from shepherd_core.data_models.content.firmware import suffix_to_DType
|
|
17
|
-
from shepherd_core.data_models.content.firmware_datatype import FirmwareDType
|
|
18
19
|
from shepherd_core.data_models.experiment.experiment import Experiment
|
|
19
20
|
from shepherd_core.data_models.testbed.cape import TargetPort
|
|
20
21
|
from shepherd_core.data_models.testbed.mcu import ProgrammerProtocol
|
|
@@ -24,6 +25,7 @@ from shepherd_core.data_models.testbed.testbed import Testbed
|
|
|
24
25
|
from .helper_paths import path_posix
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
@final
|
|
27
29
|
class ProgrammingTask(ShpModel):
|
|
28
30
|
"""Config for a Task programming the selected target."""
|
|
29
31
|
|
|
@@ -31,7 +33,7 @@ class ProgrammingTask(ShpModel):
|
|
|
31
33
|
target_port: TargetPort = TargetPort.A
|
|
32
34
|
mcu_port: MCUPort = 1
|
|
33
35
|
mcu_type: SafeStr
|
|
34
|
-
""" ⤷ must be either "nrf52" or "msp430" ATM, TODO: clean
|
|
36
|
+
""" ⤷ must be either "nrf52" or "msp430" ATM, TODO: clean Experiment to tasks"""
|
|
35
37
|
voltage: Annotated[float, Field(ge=1, lt=5)] = 3
|
|
36
38
|
datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 200_000
|
|
37
39
|
protocol: ProgrammerProtocol
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
from typing import Annotated
|
|
7
|
+
from typing import final
|
|
7
8
|
|
|
8
9
|
from pydantic import Field
|
|
9
10
|
from pydantic import validate_call
|
|
@@ -20,6 +21,7 @@ if TYPE_CHECKING:
|
|
|
20
21
|
from collections.abc import Set as AbstractSet
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
@final
|
|
23
25
|
class TestbedTasks(ShpModel):
|
|
24
26
|
"""Collection of tasks for all observers included in experiment."""
|
|
25
27
|
|
|
@@ -35,7 +37,7 @@ class TestbedTasks(ShpModel):
|
|
|
35
37
|
|
|
36
38
|
tgt_ids = xp.get_target_ids()
|
|
37
39
|
xp_folder = xp.folder_name()
|
|
38
|
-
obs_tasks = [ObserverTasks.from_xp(xp, xp_folder, tb,
|
|
40
|
+
obs_tasks = [ObserverTasks.from_xp(xp, xp_folder, tb, id_) for id_ in tgt_ids]
|
|
39
41
|
return cls(
|
|
40
42
|
name=xp.name,
|
|
41
43
|
observer_tasks=obs_tasks,
|
|
@@ -60,7 +62,11 @@ class TestbedTasks(ShpModel):
|
|
|
60
62
|
return values
|
|
61
63
|
|
|
62
64
|
def is_contained(self) -> bool:
|
|
63
|
-
"""Limit paths to allowed directories.
|
|
65
|
+
"""Limit paths to allowed directories.
|
|
66
|
+
|
|
67
|
+
This is the central checking point for the webserver.
|
|
68
|
+
"""
|
|
69
|
+
# TODO: load paths from config
|
|
64
70
|
paths_allowed: AbstractSet[PurePosixPath] = {
|
|
65
71
|
PurePosixPath("/var/shepherd/"),
|
|
66
72
|
PurePosixPath("/tmp/"), # noqa: S108
|
|
@@ -4,6 +4,7 @@ from datetime import date
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import Any
|
|
7
|
+
from typing import final
|
|
7
8
|
|
|
8
9
|
from pydantic import Field
|
|
9
10
|
from pydantic import model_validator
|
|
@@ -22,6 +23,7 @@ class TargetPort(str, Enum):
|
|
|
22
23
|
B = b = "B"
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
@final
|
|
25
27
|
class Cape(ShpModel, title="Shepherd-Cape"):
|
|
26
28
|
"""meta-data representation of a testbed-component (physical object)."""
|
|
27
29
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
+
from typing import final
|
|
6
7
|
|
|
7
8
|
from pydantic import Field
|
|
8
9
|
from pydantic import StringConstraints
|
|
@@ -24,6 +25,7 @@ class Direction(str, Enum):
|
|
|
24
25
|
Bidirectional = IO = "IO"
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
@final
|
|
27
29
|
class GPIO(ShpModel, title="GPIO of Observer Node"):
|
|
28
30
|
"""meta-data representation of a testbed-component."""
|
|
29
31
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
+
from typing import final
|
|
6
7
|
|
|
7
8
|
from pydantic import Field
|
|
8
9
|
from pydantic import model_validator
|
|
@@ -23,6 +24,7 @@ class ProgrammerProtocol(str, Enum):
|
|
|
23
24
|
UART = uart = "UART"
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
@final
|
|
26
28
|
class MCU(ShpModel, title="Microcontroller of the Target Node"):
|
|
27
29
|
"""meta-data representation of a testbed-component (physical object)."""
|
|
28
30
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
+
from typing import final
|
|
6
7
|
|
|
7
8
|
from pydantic import Field
|
|
8
9
|
from pydantic import IPvAnyAddress
|
|
@@ -26,6 +27,7 @@ MACStr = Annotated[
|
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
@final
|
|
29
31
|
class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
30
32
|
"""meta-data representation of a testbed-component (physical object)."""
|
|
31
33
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
+
from typing import final
|
|
6
7
|
|
|
7
8
|
from pydantic import Field
|
|
8
9
|
from pydantic import model_validator
|
|
@@ -20,6 +21,7 @@ IdInt16 = Annotated[int, Field(ge=0, lt=2**16)]
|
|
|
20
21
|
MCUPort = Annotated[int, Field(ge=1, le=2)]
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
@final
|
|
23
25
|
class Target(ShpModel, title="Target Node (DuT)"):
|
|
24
26
|
"""meta-data representation of a testbed-component (physical object)."""
|
|
25
27
|
|
|
@@ -49,12 +51,12 @@ class Target(ShpModel, title="Target Node (DuT)"):
|
|
|
49
51
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
50
52
|
|
|
51
53
|
# post correction
|
|
52
|
-
for
|
|
53
|
-
if isinstance(values.get(
|
|
54
|
-
values[
|
|
54
|
+
for mcu in ["mcu1", "mcu2"]:
|
|
55
|
+
if isinstance(values.get(mcu), str):
|
|
56
|
+
values[mcu] = MCU(name=values[mcu])
|
|
55
57
|
# ⤷ this will raise if default is faulty
|
|
56
|
-
elif isinstance(values.get(
|
|
57
|
-
values[
|
|
58
|
+
elif isinstance(values.get(mcu), dict):
|
|
59
|
+
values[mcu] = MCU(**values[mcu])
|
|
58
60
|
if values.get("testbed_id") is None:
|
|
59
61
|
values["testbed_id"] = values.get("id") % 2**16
|
|
60
62
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# https://github.com/orgua/shepherd-v2-planning/blob/main/doc_testbed/Target_pre-deployment-tests.xlsx
|
|
4
4
|
- datatype: target
|
|
5
5
|
parameters:
|
|
6
|
-
id: 6 # Outer ID - selected by user for
|
|
6
|
+
id: 6 # Outer ID - selected by user for Experiment - can be rearranged
|
|
7
7
|
name: nRF52_FRAM_001 # inner ID - used to link all parts together
|
|
8
8
|
version: v1.0
|
|
9
9
|
description: nRF52 as MCU + Radio, MSP430FR as SPI-FRAM or additional MCU
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# https://github.com/orgua/shepherd-v2-planning/blob/main/doc_testbed/Target_pre-deployment-tests.xlsx
|
|
4
4
|
- datatype: target
|
|
5
5
|
parameters:
|
|
6
|
-
id: 2 # Outer ID - selected by user for
|
|
6
|
+
id: 2 # Outer ID - selected by user for experiment - can be rearranged
|
|
7
7
|
name: nRF52_FRAM_1392_377 # inner ID - used to link all parts together
|
|
8
8
|
version: v1.3
|
|
9
9
|
description: nRF52 as MCU + Radio, MSP430FR as SPI-FRAM or additional MCU
|
|
@@ -4,6 +4,7 @@ from datetime import timedelta
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Annotated
|
|
6
6
|
from typing import Any
|
|
7
|
+
from typing import final
|
|
7
8
|
|
|
8
9
|
from pydantic import Field
|
|
9
10
|
from pydantic import HttpUrl
|
|
@@ -22,6 +23,7 @@ from .observer import Observer
|
|
|
22
23
|
duration_5min = timedelta(minutes=5)
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
@final
|
|
25
27
|
class Testbed(ShpModel):
|
|
26
28
|
"""meta-data representation of a testbed-component (physical object)."""
|
|
27
29
|
|
|
@@ -61,17 +63,17 @@ class Testbed(ShpModel):
|
|
|
61
63
|
capes = []
|
|
62
64
|
targets = []
|
|
63
65
|
eth_ports = []
|
|
64
|
-
for
|
|
65
|
-
observers.append(
|
|
66
|
-
ips.append(
|
|
67
|
-
macs.append(
|
|
68
|
-
if
|
|
69
|
-
capes.append(
|
|
70
|
-
if
|
|
71
|
-
targets.append(
|
|
72
|
-
if
|
|
73
|
-
targets.append(
|
|
74
|
-
eth_ports.append(
|
|
66
|
+
for obs in self.observers:
|
|
67
|
+
observers.append(obs.id)
|
|
68
|
+
ips.append(obs.ip)
|
|
69
|
+
macs.append(obs.mac)
|
|
70
|
+
if obs.cape is not None:
|
|
71
|
+
capes.append(obs.cape)
|
|
72
|
+
if obs.target_a is not None:
|
|
73
|
+
targets.append(obs.target_a)
|
|
74
|
+
if obs.target_b is not None:
|
|
75
|
+
targets.append(obs.target_b)
|
|
76
|
+
eth_ports.append(obs.eth_port)
|
|
75
77
|
if len(observers) > len(set(observers)):
|
|
76
78
|
raise ValueError("Observers used more than once in Testbed")
|
|
77
79
|
if len(ips) > len(set(ips)):
|
|
@@ -91,11 +93,11 @@ class Testbed(ShpModel):
|
|
|
91
93
|
return self
|
|
92
94
|
|
|
93
95
|
def get_observer(self, target_id: int) -> Observer:
|
|
94
|
-
for
|
|
95
|
-
if not
|
|
96
|
+
for obs in self.observers:
|
|
97
|
+
if not obs.active or not obs.cape.active:
|
|
96
98
|
# skip decommissioned setups
|
|
97
99
|
continue
|
|
98
|
-
if
|
|
99
|
-
return
|
|
100
|
+
if obs.has_target(target_id):
|
|
101
|
+
return obs
|
|
100
102
|
msg = f"Target-ID {target_id} was not found in Testbed '{self.name}'"
|
|
101
103
|
raise ValueError(msg)
|
|
@@ -156,7 +156,7 @@ class Uart:
|
|
|
156
156
|
"""Analyze bit-state during long pauses (unchanged states).
|
|
157
157
|
|
|
158
158
|
- pause should be HIGH for non-inverted mode (default)
|
|
159
|
-
- assumes
|
|
159
|
+
- assumes maximum frame size of 64 bit + x for safety
|
|
160
160
|
"""
|
|
161
161
|
events = self.events_sig[:1000, :] # speedup for large datasets
|
|
162
162
|
pauses = events[:, 2] > 80
|