shepherd-core 2025.10.1__py3-none-any.whl → 2026.2.3__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 +4 -2
- 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 +10 -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 +4 -2
- 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 +24 -18
- shepherd_core/data_models/content/enum_datatypes.py +109 -0
- shepherd_core/data_models/content/firmware.py +50 -18
- shepherd_core/data_models/content/virtual_harvester_config.py +10 -93
- shepherd_core/data_models/content/virtual_source_config.py +21 -2
- shepherd_core/data_models/content/virtual_storage_config.py +7 -4
- shepherd_core/data_models/content/virtual_storage_fixture_creator.py +1 -1
- shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +4 -4
- 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 +55 -7
- shepherd_core/data_models/task/__init__.py +13 -2
- shepherd_core/data_models/task/emulation.py +9 -5
- shepherd_core/data_models/task/firmware_mod.py +3 -1
- shepherd_core/data_models/task/harvest.py +2 -0
- 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/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 +2 -2
- shepherd_core/logger.py +0 -1
- shepherd_core/reader.py +29 -25
- 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/virtual_converter_model.py +2 -2
- shepherd_core/vsource/virtual_harvester_model.py +2 -2
- shepherd_core/vsource/virtual_harvester_simulation.py +5 -5
- shepherd_core/vsource/virtual_source_model.py +1 -1
- shepherd_core/vsource/virtual_source_simulation.py +9 -9
- shepherd_core/vsource/virtual_storage_models_kibam.py +3 -3
- shepherd_core/writer.py +16 -9
- {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/METADATA +5 -3
- shepherd_core-2026.2.3.dist-info/RECORD +102 -0
- {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/WHEEL +1 -1
- shepherd_core-2026.2.3.dist-info/licenses/LICENSE +21 -0
- shepherd_core/data_models/content/firmware_datatype.py +0 -15
- shepherd_core-2025.10.1.dist-info/RECORD +0 -95
- {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/zip-safe +0 -0
|
@@ -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
|
|
@@ -13,6 +15,7 @@ from shepherd_core.data_models.content.firmware import Firmware
|
|
|
13
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
|
|
@@ -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
|
|
@@ -20,6 +21,7 @@ from shepherd_core.data_models.experiment.observer_features import SystemLogging
|
|
|
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)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Generalized exit handler for Shepherd."""
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from types import FrameType
|
|
7
|
+
|
|
8
|
+
from .logger import log
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def exit_gracefully(_signum: int, _frame: FrameType | None) -> None:
|
|
12
|
+
"""Usual exit handler for single-processing applications."""
|
|
13
|
+
log.warning("Exiting!")
|
|
14
|
+
sys.exit(0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def activate_exit_handler(custom: Callable = exit_gracefully) -> None:
|
|
18
|
+
"""Register the provided exit handler or use the default one."""
|
|
19
|
+
signal.signal(signal.SIGTERM, custom)
|
|
20
|
+
signal.signal(signal.SIGINT, custom)
|
|
21
|
+
if hasattr(signal, "SIGALRM"):
|
|
22
|
+
signal.signal(signal.SIGALRM, custom)
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
import zstandard as zstd
|
|
9
9
|
from pydantic import validate_call
|
|
10
10
|
|
|
11
|
-
from shepherd_core.data_models.content.
|
|
11
|
+
from shepherd_core.data_models.content.enum_datatypes import FirmwareDType
|
|
12
12
|
|
|
13
13
|
from .converter_elf import elf_to_hex
|
|
14
14
|
from .validation import is_elf
|
|
@@ -98,7 +98,7 @@ def extract_firmware(data: str | Path, data_type: FirmwareDType, file_path: Path
|
|
|
98
98
|
elif data_type == FirmwareDType.path_hex:
|
|
99
99
|
file = file_path.with_suffix(".hex")
|
|
100
100
|
else:
|
|
101
|
-
msg = "FW-Extraction failed due to unknown datatype '{data_type}'"
|
|
101
|
+
msg = f"FW-Extraction failed due to unknown datatype '{data_type}'"
|
|
102
102
|
raise ValueError(msg)
|
|
103
103
|
if not file.parent.exists():
|
|
104
104
|
file.parent.mkdir(parents=True)
|
|
@@ -13,7 +13,7 @@ from intelhex import IntelHex
|
|
|
13
13
|
from intelhex import IntelHexError
|
|
14
14
|
from pydantic import validate_call
|
|
15
15
|
|
|
16
|
-
from shepherd_core.data_models.content.
|
|
16
|
+
from shepherd_core.data_models.content.enum_datatypes import FirmwareDType
|
|
17
17
|
from shepherd_core.logger import log
|
|
18
18
|
|
|
19
19
|
from .converter_elf import elf_to_hex
|