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.
Files changed (82) hide show
  1. shepherd_core/config.py +1 -1
  2. shepherd_core/data_models/__init__.py +8 -4
  3. shepherd_core/data_models/base/cal_measurement.py +7 -2
  4. shepherd_core/data_models/base/calibration.py +23 -12
  5. shepherd_core/data_models/base/content.py +12 -2
  6. shepherd_core/data_models/base/shepherd.py +13 -4
  7. shepherd_core/data_models/base/wrapper.py +2 -0
  8. shepherd_core/data_models/content/__init__.py +8 -4
  9. shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
  10. shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
  11. shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
  12. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
  13. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
  14. shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
  15. shepherd_core/data_models/content/energy_environment.py +341 -23
  16. shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
  17. shepherd_core/data_models/content/enum_datatypes.py +109 -0
  18. shepherd_core/data_models/content/firmware.py +44 -16
  19. shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
  20. shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
  21. shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
  22. shepherd_core/data_models/content/virtual_storage_config.py +429 -0
  23. shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
  24. shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
  25. shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
  26. shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
  27. shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
  28. shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
  29. shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
  30. shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
  31. shepherd_core/data_models/experiment/experiment.py +38 -13
  32. shepherd_core/data_models/experiment/observer_features.py +17 -4
  33. shepherd_core/data_models/experiment/target_config.py +56 -8
  34. shepherd_core/data_models/task/__init__.py +13 -2
  35. shepherd_core/data_models/task/emulation.py +10 -6
  36. shepherd_core/data_models/task/firmware_mod.py +3 -1
  37. shepherd_core/data_models/task/harvest.py +3 -1
  38. shepherd_core/data_models/task/helper_paths.py +2 -2
  39. shepherd_core/data_models/task/observer_tasks.py +8 -6
  40. shepherd_core/data_models/task/programming.py +4 -2
  41. shepherd_core/data_models/task/testbed_tasks.py +8 -2
  42. shepherd_core/data_models/testbed/cape.py +2 -0
  43. shepherd_core/data_models/testbed/gpio.py +2 -0
  44. shepherd_core/data_models/testbed/mcu.py +2 -0
  45. shepherd_core/data_models/testbed/observer.py +2 -0
  46. shepherd_core/data_models/testbed/target.py +7 -5
  47. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  48. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  49. shepherd_core/data_models/testbed/testbed.py +17 -15
  50. shepherd_core/decoder_waveform/uart.py +1 -1
  51. shepherd_core/exit_handler.py +22 -0
  52. shepherd_core/fw_tools/converter.py +2 -2
  53. shepherd_core/fw_tools/validation.py +1 -1
  54. shepherd_core/inventory/__init__.py +23 -21
  55. shepherd_core/inventory/system.py +3 -3
  56. shepherd_core/logger.py +0 -1
  57. shepherd_core/reader.py +32 -27
  58. shepherd_core/testbed_client/cache_path.py +3 -3
  59. shepherd_core/testbed_client/client_abc_fix.py +14 -3
  60. shepherd_core/testbed_client/client_web.py +7 -5
  61. shepherd_core/testbed_client/fixtures.py +7 -7
  62. shepherd_core/version.py +1 -1
  63. shepherd_core/vsource/__init__.py +4 -0
  64. shepherd_core/vsource/virtual_converter_model.py +29 -28
  65. shepherd_core/vsource/virtual_harvester_model.py +29 -21
  66. shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
  67. shepherd_core/vsource/virtual_source_model.py +18 -14
  68. shepherd_core/vsource/virtual_source_simulation.py +71 -73
  69. shepherd_core/vsource/virtual_storage_model.py +164 -0
  70. shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
  71. shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
  72. shepherd_core/vsource/virtual_storage_simulator.py +104 -0
  73. shepherd_core/writer.py +16 -9
  74. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
  75. shepherd_core-2026.2.1.dist-info/RECORD +102 -0
  76. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
  77. shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
  78. shepherd_core/data_models/content/firmware_datatype.py +0 -15
  79. shepherd_core/data_models/virtual_source_doc.txt +0 -207
  80. shepherd_core-2025.8.1.dist-info/RECORD +0 -83
  81. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
  82. {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 storage capacitor instead of output (good for V_out = const)
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 - power @ 100 kHz is not recommended
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 {_ev.gpio for _ev in self.events}
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.virtual_source import VirtualSourceConfig
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
- class TargetConfig(ShpModel, title="Target Config"):
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 not self.energy_env.valid:
61
- msg = f"EnergyEnv '{self.energy_env.name}' for target must be valid"
62
- raise ValueError(msg)
63
- for _id in self.target_IDs:
64
- target = Target(id=_id)
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
- raise NotImplementedError("Feature GpioActuation reserved for future use.")
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
- with config.resolve().open("rb") as shp_file:
48
- shp_dict = pickle.load(shp_file) # noqa: S301
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.virtual_source import VirtualSourceConfig
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
- _io is not None for _io in (self.gpio_actuation, self.gpio_tracing, self.uart_logging)
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
- _io is not None
163
- for _io in (tgt_cfg.gpio_actuation, tgt_cfg.gpio_tracing, tgt_cfg.uart_logging)
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
- all_ok &= any(self.output_path.is_relative_to(path) for path in paths)
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.virtual_harvester import VirtualHarvesterConfig
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 windows Path("\xyz") gets transformed to "/xyz", but not on linux.
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 windows and gets triggered when sending XP via fastapi.
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{_i}_{obs.name}.hex" for _i in [1, 2]]
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 xp to tasks"""
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, _id) for _id in tgt_ids]
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 _mcu in ["mcu1", "mcu2"]:
53
- if isinstance(values.get(_mcu), str):
54
- values[_mcu] = MCU(name=values[_mcu])
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(_mcu), dict):
57
- values[_mcu] = MCU(**values[_mcu])
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 XP - can be rearranged
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 XP - can be rearranged
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 _obs in self.observers:
65
- observers.append(_obs.id)
66
- ips.append(_obs.ip)
67
- macs.append(_obs.mac)
68
- if _obs.cape is not None:
69
- capes.append(_obs.cape)
70
- if _obs.target_a is not None:
71
- targets.append(_obs.target_a)
72
- if _obs.target_b is not None:
73
- targets.append(_obs.target_b)
74
- eth_ports.append(_obs.eth_port)
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 _observer in self.observers:
95
- if not _observer.active or not _observer.cape.active:
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 _observer.has_target(target_id):
99
- return _observer
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 max frame size of 64 bit + x for safety
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