shepherd-core 2025.5.3__py3-none-any.whl → 2025.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. shepherd_core/__init__.py +2 -2
  2. shepherd_core/commons.py +3 -5
  3. shepherd_core/config.py +34 -0
  4. shepherd_core/data_models/__init__.py +1 -1
  5. shepherd_core/data_models/base/calibration.py +13 -8
  6. shepherd_core/data_models/base/shepherd.py +28 -11
  7. shepherd_core/data_models/base/wrapper.py +4 -4
  8. shepherd_core/data_models/content/energy_environment.py +1 -1
  9. shepherd_core/data_models/content/firmware.py +13 -8
  10. shepherd_core/data_models/content/virtual_harvester.py +13 -13
  11. shepherd_core/data_models/content/virtual_source.py +41 -33
  12. shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -1
  13. shepherd_core/data_models/experiment/experiment.py +28 -18
  14. shepherd_core/data_models/experiment/observer_features.py +32 -13
  15. shepherd_core/data_models/experiment/target_config.py +17 -7
  16. shepherd_core/data_models/task/__init__.py +8 -4
  17. shepherd_core/data_models/task/emulation.py +52 -30
  18. shepherd_core/data_models/task/firmware_mod.py +15 -6
  19. shepherd_core/data_models/task/harvest.py +19 -13
  20. shepherd_core/data_models/task/helper_paths.py +15 -0
  21. shepherd_core/data_models/task/observer_tasks.py +20 -18
  22. shepherd_core/data_models/task/programming.py +10 -4
  23. shepherd_core/data_models/task/testbed_tasks.py +16 -7
  24. shepherd_core/data_models/testbed/cape_fixture.yaml +1 -1
  25. shepherd_core/data_models/testbed/observer.py +1 -1
  26. shepherd_core/data_models/testbed/observer_fixture.yaml +2 -2
  27. shepherd_core/data_models/testbed/target.py +1 -1
  28. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  29. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  30. shepherd_core/data_models/testbed/testbed.py +8 -9
  31. shepherd_core/decoder_waveform/uart.py +7 -7
  32. shepherd_core/fw_tools/patcher.py +13 -14
  33. shepherd_core/fw_tools/validation.py +2 -2
  34. shepherd_core/inventory/system.py +3 -5
  35. shepherd_core/logger.py +3 -3
  36. shepherd_core/reader.py +9 -2
  37. shepherd_core/testbed_client/cache_path.py +1 -1
  38. shepherd_core/testbed_client/client_web.py +2 -2
  39. shepherd_core/testbed_client/fixtures.py +5 -5
  40. shepherd_core/version.py +1 -1
  41. shepherd_core/vsource/virtual_harvester_model.py +2 -2
  42. shepherd_core/vsource/virtual_source_simulation.py +2 -2
  43. shepherd_core/writer.py +2 -2
  44. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/METADATA +12 -12
  45. shepherd_core-2025.6.2.dist-info/RECORD +83 -0
  46. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/WHEEL +1 -1
  47. shepherd_core-2025.5.3.dist-info/RECORD +0 -81
  48. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/top_level.txt +0 -0
  49. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/zip-safe +0 -0
@@ -13,9 +13,12 @@ from pydantic import model_validator
13
13
  from typing_extensions import Self
14
14
  from typing_extensions import deprecated
15
15
 
16
- from shepherd_core import logger
17
16
  from shepherd_core.data_models.base.shepherd import ShpModel
18
17
  from shepherd_core.data_models.testbed.gpio import GPIO
18
+ from shepherd_core.logger import log
19
+
20
+ # defaults (pre-init complex types)
21
+ zero_duration = timedelta(seconds=0)
19
22
 
20
23
 
21
24
  class PowerTracing(ShpModel, title="Config for Power-Tracing"):
@@ -25,19 +28,27 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
25
28
  """
26
29
 
27
30
  intermediate_voltage: bool = False
28
- # ⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
29
- # this also includes current!
30
-
31
+ """
32
+ for EMU: record storage capacitor instead of output (good for V_out = const)
33
+ this also includes current!
34
+ """
31
35
  # time
32
- delay: timedelta = timedelta(seconds=0)
36
+ delay: timedelta = zero_duration
37
+ """start recording after experiment started"""
33
38
  duration: Optional[timedelta] = None # till EOF
39
+ """duration of recording after delay starts the process.
40
+
41
+ default is None, recording till EOF"""
34
42
 
35
43
  # post-processing
36
44
  calculate_power: bool = False
37
- samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000 # down-sample
45
+ """ reduce file-size by calculating power -> not implemented ATM"""
46
+ samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000
47
+ """ ⤷ reduce file-size by down-sampling -> not implemented ATM"""
38
48
  discard_current: bool = False
49
+ """ ⤷ reduce file-size by omitting current -> not implemented ATM"""
39
50
  discard_voltage: bool = False
40
- # ⤷ reduce file-size by omitting current / voltage
51
+ """ ⤷ reduce file-size by omitting voltage -> not implemented ATM"""
41
52
 
42
53
  @model_validator(mode="after")
43
54
  def post_validation(self) -> Self:
@@ -163,11 +174,12 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
163
174
  """
164
175
 
165
176
  # time
166
- delay: timedelta = timedelta(seconds=0)
177
+ delay: timedelta = zero_duration
167
178
  duration: Optional[timedelta] = None # till EOF
168
179
 
169
180
  # post-processing,
170
181
  uart_decode: bool = False
182
+ """Automatic decoding from gpio-trace not implemented ATM."""
171
183
  uart_pin: GPIO = GPIO(name="GPIO8")
172
184
  uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
173
185
 
@@ -178,7 +190,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
178
190
  if self.duration and self.duration.total_seconds() < 0:
179
191
  raise ValueError("Duration can't be negative.")
180
192
  if self.uart_decode:
181
- logger.error(
193
+ log.error(
182
194
  "Feature GpioTracing.uart_decode reserved for future use. "
183
195
  "Use UartLogging or manually decode serial with the provided waveform decoder."
184
196
  )
@@ -205,12 +217,14 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
205
217
  """Configuration for a single GPIO-Event (Actuation)."""
206
218
 
207
219
  delay: PositiveFloat
208
- # ⤷ from start_time
209
- # ⤷ resolution 10 us (guaranteed, but finer steps are possible)
220
+ """ ⤷ from start_time
221
+
222
+ - resolution 10 us (guaranteed, but finer steps are possible)
223
+ """
210
224
  gpio: GPIO
211
225
  level: GpioLevel
212
226
  period: Annotated[float, Field(ge=10e-6)] = 1
213
- # ⤷ time base of periodicity in s
227
+ """ ⤷ time base of periodicity in s"""
214
228
  count: Annotated[int, Field(ge=1, le=4096)] = 1
215
229
 
216
230
  @model_validator(mode="after")
@@ -233,6 +247,11 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
233
247
 
234
248
  events: Annotated[list[GpioEvent], Field(min_length=1, max_length=1024)]
235
249
 
250
+ @model_validator(mode="after")
251
+ def post_validation(self) -> Self:
252
+ msg = "not implemented ATM"
253
+ raise ValueError(msg)
254
+
236
255
  def get_gpios(self) -> set:
237
256
  return {_ev.gpio for _ev in self.events}
238
257
 
@@ -245,7 +264,7 @@ class SystemLogging(ShpModel, title="Config for System-Logging"):
245
264
  sheep: bool = True
246
265
  sys_util: bool = True
247
266
 
248
- # TODO: remove lines below in 2026
267
+ # deprecated, TODO: remove lines below before public release
249
268
  dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
250
269
  ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
251
270
  shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
@@ -20,26 +20,36 @@ from .observer_features import GpioTracing
20
20
  from .observer_features import PowerTracing
21
21
  from .observer_features import UartLogging
22
22
 
23
+ # defaults (pre-init complex types)
24
+ vsrc_neutral = VirtualSourceConfig(name="neutral")
25
+
23
26
 
24
27
  class TargetConfig(ShpModel, title="Target Config"):
25
28
  """Configuration related to Target Nodes (DuT)."""
26
29
 
27
30
  target_IDs: Annotated[list[IdInt], Field(min_length=1, max_length=128)]
28
31
  custom_IDs: Optional[Annotated[list[IdInt16], Field(min_length=1, max_length=128)]] = None
29
- # ⤷ will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware
30
- # if no custom ID is provided, the original ID of target is used
32
+ """custom ID will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware.
33
+
34
+ if no custom ID is provided, the original ID of target is used
35
+ """
31
36
 
32
- energy_env: EnergyEnvironment # alias: input
33
- virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
37
+ energy_env: EnergyEnvironment
38
+ """ input for the virtual source """
39
+ virtual_source: VirtualSourceConfig = vsrc_neutral
34
40
  target_delays: Optional[
35
41
  Annotated[list[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
36
42
  ] = None
37
- # ⤷ individual starting times -> allows to use the same environment
38
- # TODO: delays not used ATM
43
+ """ ⤷ individual starting times
44
+
45
+ - allows to use the same environment
46
+ - not implemented ATM
47
+ """
39
48
 
40
49
  firmware1: Firmware
50
+ """ ⤷ omitted FW gets set to neutral deep-sleep"""
41
51
  firmware2: Optional[Firmware] = None
42
- # ⤷ omitted FW gets set to neutral deep-sleep
52
+ """ ⤷ omitted FW gets set to neutral deep-sleep"""
43
53
 
44
54
  power_tracing: Optional[PowerTracing] = None
45
55
  gpio_tracing: Optional[GpioTracing] = None
@@ -3,6 +3,7 @@
3
3
  These models import externally from all other model-modules!
4
4
  """
5
5
 
6
+ import pickle
6
7
  from pathlib import Path
7
8
  from typing import Optional
8
9
  from typing import Union
@@ -11,7 +12,7 @@ import yaml
11
12
 
12
13
  from shepherd_core.data_models.base.shepherd import ShpModel
13
14
  from shepherd_core.data_models.base.wrapper import Wrapper
14
- from shepherd_core.logger import logger
15
+ from shepherd_core.logger import log
15
16
 
16
17
  from .emulation import Compression
17
18
  from .emulation import EmulationTask
@@ -44,7 +45,10 @@ def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = N
44
45
  if isinstance(config, str):
45
46
  config = Path(config)
46
47
 
47
- if isinstance(config, Path):
48
+ if isinstance(config, Path) and config.exists() and config.suffix.lower() == ".pickle":
49
+ with config.resolve().open("rb") as shp_file:
50
+ shp_wrap = pickle.load(shp_file, fix_imports=True) # noqa: S301
51
+ elif isinstance(config, Path) and config.exists() and config.suffix.lower() == ".yaml":
48
52
  with config.resolve().open() as shp_file:
49
53
  shp_dict = yaml.safe_load(shp_file)
50
54
  shp_wrap = Wrapper(**shp_dict)
@@ -59,12 +63,12 @@ def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = N
59
63
 
60
64
  if shp_wrap.datatype == TestbedTasks.__name__:
61
65
  if observer is None:
62
- logger.debug(
66
+ log.debug(
63
67
  "Task-Set contained TestbedTasks & no observer was provided -> will return TB-Tasks"
64
68
  )
65
69
  return shp_wrap
66
70
  tbt = TestbedTasks(**shp_wrap.parameters)
67
- logger.debug("Loading Testbed-Tasks %s for %s", tbt.name, observer)
71
+ log.debug("Loading Testbed-Tasks %s for %s", tbt.name, observer)
68
72
  obt = tbt.get_observer_tasks(observer)
69
73
  if obt is None:
70
74
  msg = f"Observer '{observer}' is not in TestbedTask-Set"
@@ -1,10 +1,12 @@
1
1
  """Configuration for the Observer in Emulation-Mode."""
2
2
 
3
3
  import copy
4
+ from collections.abc import Set as AbstractSet
4
5
  from datetime import datetime
5
6
  from datetime import timedelta
6
7
  from enum import Enum
7
8
  from pathlib import Path
9
+ from pathlib import PurePosixPath
8
10
  from typing import Annotated
9
11
  from typing import Optional
10
12
  from typing import Union
@@ -25,9 +27,12 @@ from shepherd_core.data_models.experiment.observer_features import GpioTracing
25
27
  from shepherd_core.data_models.experiment.observer_features import PowerTracing
26
28
  from shepherd_core.data_models.experiment.observer_features import SystemLogging
27
29
  from shepherd_core.data_models.experiment.observer_features import UartLogging
30
+ from shepherd_core.data_models.experiment.target_config import vsrc_neutral
28
31
  from shepherd_core.data_models.testbed import Testbed
29
32
  from shepherd_core.data_models.testbed.cape import TargetPort
30
- from shepherd_core.logger import logger
33
+ from shepherd_core.logger import log
34
+
35
+ from .helper_paths import path_posix
31
36
 
32
37
 
33
38
  class Compression(str, Enum):
@@ -49,46 +54,55 @@ class EmulationTask(ShpModel):
49
54
 
50
55
  # General config
51
56
  input_path: Path
52
- # ⤷ hdf5 file containing harvesting data
57
+ """ ⤷ hdf5 file containing harvesting data"""
53
58
  output_path: Optional[Path] = None
54
- # ⤷ dir- or file-path for storing the recorded data:
55
- # - providing a directory -> file is named emu_timestamp.h5
56
- # - for a complete path the filename is not changed except it exists and
57
- # overwrite is disabled -> emu#num.h5
58
- # TODO: should the output-path be mandatory?
59
+ """ ⤷ dir- or file-path for storing the recorded data:
60
+
61
+ - providing a directory -> file is named emu_timestamp.h5
62
+ - for a complete path the filename is not changed except it exists and
63
+ overwrite is disabled -> emu#num.h5
64
+ TODO: should the output-path be mandatory?
65
+ """
59
66
  force_overwrite: bool = False
60
- # ⤷ Overwrite existing file
67
+ """ ⤷ Overwrite existing file"""
61
68
  output_compression: Optional[Compression] = Compression.default
62
- # ⤷ should be lzf, 1 (gzip level 1) or None (order of recommendation)
69
+ """ ⤷ should be lzf, 1 (gzip level 1) or None (order of recommendation)"""
63
70
  time_start: Optional[datetime] = None
64
- # timestamp or unix epoch time, None = ASAP
71
+ """ timestamp or unix epoch time, None = ASAP"""
65
72
  duration: Optional[timedelta] = None
66
- # ⤷ Duration of recording in seconds, None = till EOF
73
+ """ ⤷ Duration of recording in seconds, None = till EOF"""
67
74
  abort_on_error: Annotated[bool, deprecated("has no effect")] = False
68
75
 
69
76
  # emulation-specific
70
77
  use_cal_default: bool = False
71
- # ⤷ Use default calibration values, skip loading from EEPROM
78
+ """ ⤷ Use default calibration values, skip loading from EEPROM"""
72
79
 
73
80
  enable_io: bool = True
74
81
  # TODO: add direction of pins! also it seems error-prone when only setting _tracing
75
- # ⤷ Switch the GPIO level converter to targets on/off
76
- # pre-req for sampling gpio / uart,
82
+ """ ⤷ Switch the GPIO level converter to targets on/off
83
+
84
+ pre-req for sampling gpio / uart,
85
+ """
77
86
  io_port: TargetPort = TargetPort.A
78
- # ⤷ Either Port A or B that gets connected to IO
87
+ """ ⤷ Either Port A or B that gets connected to IO"""
79
88
  pwr_port: TargetPort = TargetPort.A
80
- #chosen port will be current-monitored (main, connected to virtual Source),
81
- # the other port is aux
82
- voltage_aux: Union[Annotated[float, Field(ge=0, le=4.5)], str] = 0
83
- # ⤷ aux_voltage options:
84
- # - 0-4.5 for specific const Voltage (0 V = disabled),
85
- # - "buffer" will output intermediate voltage (storage cap of vsource),
86
- # - "main" will mirror main target voltage
89
+ """selected port will be current-monitored
87
90
 
91
+ - main channel is nnected to virtual Source
92
+ - the other port is aux
93
+ """
94
+ voltage_aux: Union[Annotated[float, Field(ge=0, le=4.5)], str] = 0
95
+ """ ⤷ aux_voltage options
96
+ - 0-4.5 for specific const Voltage (0 V = disabled),
97
+ - "buffer" will output intermediate voltage (storage cap of vsource),
98
+ - "main" will mirror main target voltage
99
+ """
88
100
  # sub-elements, could be partly moved to emulation
89
- virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
90
- # ⤷ Use the desired setting for the virtual source,
91
- # provide parameters or name like BQ25570
101
+ virtual_source: VirtualSourceConfig = vsrc_neutral
102
+ """ ⤷ Use the desired setting for the virtual source,
103
+
104
+ provide parameters or name like BQ25570
105
+ """
92
106
 
93
107
  power_tracing: Optional[PowerTracing] = PowerTracing()
94
108
  gpio_tracing: Optional[GpioTracing] = GpioTracing()
@@ -97,7 +111,10 @@ class EmulationTask(ShpModel):
97
111
  sys_logging: Optional[SystemLogging] = SystemLogging()
98
112
 
99
113
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
100
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug, TODO: just bool now, systemwide
114
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug,
115
+
116
+ TODO: just bool now, systemwide
117
+ """
101
118
 
102
119
  @model_validator(mode="before")
103
120
  @classmethod
@@ -136,9 +153,9 @@ class EmulationTask(ShpModel):
136
153
  _io is not None for _io in (self.gpio_actuation, self.gpio_tracing, self.uart_logging)
137
154
  )
138
155
  if self.enable_io and not io_requested:
139
- logger.warning("Target IO enabled, but no feature requested IO")
156
+ log.warning("Target IO enabled, but no feature requested IO")
140
157
  if not self.enable_io and io_requested:
141
- logger.warning("Target IO not enabled, but a feature requested IO")
158
+ log.warning("Target IO not enabled, but a feature requested IO")
142
159
  return self
143
160
 
144
161
  @classmethod
@@ -152,8 +169,8 @@ class EmulationTask(ShpModel):
152
169
  )
153
170
 
154
171
  return cls(
155
- input_path=tgt_cfg.energy_env.data_path,
156
- output_path=root_path / f"emu_{obs.name}.h5",
172
+ input_path=path_posix(tgt_cfg.energy_env.data_path),
173
+ output_path=path_posix(root_path / f"emu_{obs.name}.h5"),
157
174
  time_start=copy.copy(xp.time_start),
158
175
  duration=xp.duration,
159
176
  enable_io=io_requested,
@@ -167,6 +184,11 @@ class EmulationTask(ShpModel):
167
184
  sys_logging=xp.sys_logging,
168
185
  )
169
186
 
187
+ def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
188
+ all_ok = any(self.input_path.is_relative_to(path) for path in paths)
189
+ all_ok &= any(self.output_path.is_relative_to(path) for path in paths)
190
+ return all_ok
191
+
170
192
 
171
193
  # TODO: herdConfig
172
194
  # - store if path is remote (read & write)
@@ -1,7 +1,8 @@
1
1
  """Config for Task that adds the custom ID to the firmware & stores it into a file."""
2
2
 
3
- import copy
3
+ from collections.abc import Set as AbstractSet
4
4
  from pathlib import Path
5
+ from pathlib import PurePosixPath
5
6
  from typing import Annotated
6
7
  from typing import Optional
7
8
  from typing import TypedDict
@@ -22,7 +23,9 @@ from shepherd_core.data_models.experiment.experiment import Experiment
22
23
  from shepherd_core.data_models.testbed import Testbed
23
24
  from shepherd_core.data_models.testbed.target import IdInt16
24
25
  from shepherd_core.data_models.testbed.target import MCUPort
25
- from shepherd_core.logger import logger
26
+ from shepherd_core.logger import log
27
+
28
+ from .helper_paths import path_posix
26
29
 
27
30
 
28
31
  class FirmwareModTask(ShpModel):
@@ -34,7 +37,7 @@ class FirmwareModTask(ShpModel):
34
37
  firmware_file: Path
35
38
 
36
39
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
37
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
40
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
38
41
 
39
42
  @model_validator(mode="after")
40
43
  def post_validation(self) -> Self:
@@ -42,7 +45,7 @@ class FirmwareModTask(ShpModel):
42
45
  FirmwareDType.base64_hex,
43
46
  FirmwareDType.path_hex,
44
47
  }:
45
- logger.warning("Firmware is scheduled to get custom-ID but is not in elf-format")
48
+ log.warning("Firmware is scheduled to get custom-ID but is not in elf-format")
46
49
  return self
47
50
 
48
51
  @classmethod
@@ -68,10 +71,10 @@ class FirmwareModTask(ShpModel):
68
71
  fw_id = obs.get_target(tgt_id).testbed_id
69
72
 
70
73
  return cls(
71
- data=fw.data,
74
+ data=path_posix(fw.data) if isinstance(fw.data, Path) else fw.data,
72
75
  data_type=fw.data_type,
73
76
  custom_id=fw_id,
74
- firmware_file=copy.copy(fw_path),
77
+ firmware_file=path_posix(fw_path),
75
78
  )
76
79
 
77
80
  @classmethod
@@ -90,3 +93,9 @@ class FirmwareModTask(ShpModel):
90
93
  path_new: Path = path / fw.name
91
94
  kwargs["firmware_file"] = path_new.with_suffix(".hex")
92
95
  return cls(**kwargs)
96
+
97
+ def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
98
+ all_ok = any(self.firmware_file.is_relative_to(path) for path in paths)
99
+ if isinstance(self.data, Path):
100
+ all_ok = any(self.data.is_relative_to(path) for path in paths)
101
+ return all_ok
@@ -1,8 +1,10 @@
1
1
  """Config for the Observer in Harvest-Mode to record IV data from a harvesting-source."""
2
2
 
3
+ from collections.abc import Set as AbstractSet
3
4
  from datetime import datetime
4
5
  from datetime import timedelta
5
6
  from pathlib import Path
7
+ from pathlib import PurePosixPath
6
8
  from typing import Annotated
7
9
  from typing import Optional
8
10
 
@@ -25,34 +27,35 @@ class HarvestTask(ShpModel):
25
27
 
26
28
  # General config
27
29
  output_path: Path
28
- # ⤷ dir- or file-path for storing the recorded data:
29
- # - providing a directory -> file is named hrv_timestamp.h5
30
- # - for a complete path the filename is not changed except it exists and
31
- # overwrite is disabled -> name#num.h5
30
+ """ ⤷ dir- or file-path for storing the recorded data:
31
+
32
+ - providing a directory -> file is named hrv_timestamp.h5
33
+ - for a complete path the filename is not changed except it exists and
34
+ overwrite is disabled -> name#num.h5
35
+ """
32
36
  force_overwrite: bool = False
33
- # ⤷ Overwrite existing file
37
+ """ ⤷ Overwrite existing file"""
34
38
  output_compression: Optional[Compression] = Compression.default
35
- # ⤷ should be 1 (level 1 gzip), lzf, or None (order of recommendation)
39
+ """ ⤷ should be 1 (level 1 gzip), lzf, or None (order of recommendation)"""
36
40
 
37
41
  time_start: Optional[datetime] = None
38
- # timestamp or unix epoch time, None = ASAP
42
+ """ timestamp or unix epoch time, None = ASAP"""
39
43
  duration: Optional[timedelta] = None
40
- # ⤷ Duration of recording in seconds, None = till EOFSys
44
+ """ ⤷ Duration of recording in seconds, None = till EOFSys"""
41
45
  abort_on_error: Annotated[bool, deprecated("has no effect")] = False
42
46
 
43
47
  # emulation-specific
44
48
  use_cal_default: bool = False
45
- # ⤷ Use default calibration values, skip loading from EEPROM
49
+ """ ⤷ Use default calibration values, skip loading from EEPROM"""
46
50
 
47
51
  virtual_harvester: VirtualHarvesterConfig = VirtualHarvesterConfig(name="mppt_opt")
48
- # ⤷ Choose one of the predefined virtual harvesters
49
- # or configure a new one
50
-
52
+ """ ⤷ Choose one of the predefined virtual harvesters or configure a new one
53
+ """
51
54
  power_tracing: PowerTracing = PowerTracing()
52
55
  sys_logging: Optional[SystemLogging] = SystemLogging()
53
56
 
54
57
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
55
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
58
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
56
59
 
57
60
  # TODO: there is an unused DAC-Output patched to the harvesting-port
58
61
 
@@ -83,3 +86,6 @@ class HarvestTask(ShpModel):
83
86
  if self.duration and self.duration.total_seconds() < 0:
84
87
  raise ValueError("Task-Duration can't be negative.")
85
88
  return self
89
+
90
+ def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
91
+ return any(self.output_path.is_relative_to(path) for path in paths)
@@ -0,0 +1,15 @@
1
+ r"""Helper FN to avoid unwanted behavior.
2
+
3
+ On windows Path("\xyz") gets transformed to "/xyz", but not on linux.
4
+ When sending an experiment via fastapi, this bug gets triggered.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+
10
+ def path_posix(path: Path) -> Path:
11
+ r"""Help Linux to get from "\xyz" to "/xyz".
12
+
13
+ This isn't a problem on windows and gets triggered when sending XP via fastapi.
14
+ """
15
+ return Path(path.as_posix().replace("\\", "/"))
@@ -1,8 +1,9 @@
1
1
  """Collection of tasks for selected observer included in experiment."""
2
2
 
3
+ from collections.abc import Set as AbstractSet
3
4
  from datetime import datetime
4
- from datetime import timedelta
5
5
  from pathlib import Path
6
+ from pathlib import PurePosixPath
6
7
  from typing import Annotated
7
8
  from typing import Optional
8
9
 
@@ -18,6 +19,7 @@ from shepherd_core.data_models.testbed.testbed import Testbed
18
19
 
19
20
  from .emulation import EmulationTask
20
21
  from .firmware_mod import FirmwareModTask
22
+ from .helper_paths import path_posix
21
23
  from .programming import ProgrammingTask
22
24
 
23
25
 
@@ -25,12 +27,10 @@ class ObserverTasks(ShpModel):
25
27
  """Collection of tasks for selected observer included in experiment."""
26
28
 
27
29
  observer: NameStr
28
- owner_id: Optional[IdInt] # TODO: set to optional for now, shouldn't be
29
30
 
30
31
  # PRE PROCESS
31
- time_prep: datetime # TODO: should be optional
32
+ time_prep: Optional[datetime] = None # TODO: currently not used
32
33
  root_path: Path
33
- abort_on_error: Annotated[bool, deprecated("has no effect")] = False
34
34
 
35
35
  # fw mod, store as hex-file and program
36
36
  fw1_mod: Optional[FirmwareModTask] = None
@@ -41,6 +41,10 @@ class ObserverTasks(ShpModel):
41
41
  # MAIN PROCESS
42
42
  emulation: Optional[EmulationTask] = None
43
43
 
44
+ # deprecations, TODO: remove before public release
45
+ owner_id: Annotated[Optional[IdInt], deprecated("not needed anymore")] = None
46
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
47
+
44
48
  # post_copy / cleanup, Todo: could also just intake emuTask
45
49
  # - delete firmwares
46
50
  # - decode uart
@@ -53,25 +57,14 @@ class ObserverTasks(ShpModel):
53
57
  if not tb.shared_storage:
54
58
  raise ValueError("Implementation currently relies on shared storage!")
55
59
 
56
- t_start = (
57
- xp.time_start
58
- if isinstance(xp.time_start, datetime)
59
- else datetime.now().astimezone() + timedelta(minutes=3)
60
- ) # TODO: this is messed up, just a hotfix
61
-
62
60
  obs = tb.get_observer(tgt_id)
63
- xp_dir = "experiments/" + xp.name + "_" + t_start.strftime("%Y-%m-%d_%H-%M-%S")
64
- root_path = tb.data_on_observer / xp_dir
65
- # TODO: Paths should be "friendlier"
66
- # - replace whitespace with "_" and remove non-alphanum?
67
-
61
+ root_path = tb.data_on_observer / "experiments" / xp.folder_name()
68
62
  fw_paths = [root_path / f"fw{_i}_{obs.name}.hex" for _i in [1, 2]]
69
63
 
70
64
  return cls(
71
65
  observer=obs.name,
72
- owner_id=xp.owner_id,
73
- time_prep=t_start - tb.prep_duration,
74
- root_path=root_path,
66
+ # time_prep=
67
+ root_path=path_posix(root_path),
75
68
  fw1_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
76
69
  fw2_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 2, fw_paths[1]),
77
70
  fw1_prog=ProgrammingTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
@@ -99,3 +92,12 @@ class ObserverTasks(ShpModel):
99
92
  raise ValueError("Emu-Task should have a valid output-path")
100
93
  values[self.observer] = self.emulation.output_path
101
94
  return values
95
+
96
+ def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
97
+ all_ok = any(self.root_path.is_relative_to(path) for path in paths)
98
+ all_ok &= self.fw1_mod.is_contained(paths)
99
+ all_ok &= self.fw2_mod.is_contained(paths)
100
+ all_ok &= self.fw1_prog.is_contained(paths)
101
+ all_ok &= self.fw2_prog.is_contained(paths)
102
+ all_ok &= self.emulation.is_contained(paths)
103
+ return all_ok
@@ -1,7 +1,8 @@
1
1
  """Config for a Task programming the selected target."""
2
2
 
3
- import copy
3
+ from collections.abc import Set as AbstractSet
4
4
  from pathlib import Path
5
+ from pathlib import PurePosixPath
5
6
  from typing import Annotated
6
7
  from typing import Optional
7
8
 
@@ -21,6 +22,8 @@ from shepherd_core.data_models.testbed.mcu import ProgrammerProtocol
21
22
  from shepherd_core.data_models.testbed.target import MCUPort
22
23
  from shepherd_core.data_models.testbed.testbed import Testbed
23
24
 
25
+ from .helper_paths import path_posix
26
+
24
27
 
25
28
  class ProgrammingTask(ShpModel):
26
29
  """Config for a Task programming the selected target."""
@@ -29,7 +32,7 @@ class ProgrammingTask(ShpModel):
29
32
  target_port: TargetPort = TargetPort.A
30
33
  mcu_port: MCUPort = 1
31
34
  mcu_type: SafeStr
32
- # ⤷ must be either "nrf52" or "msp430" ATM, TODO: clean xp to tasks
35
+ """ ⤷ must be either "nrf52" or "msp430" ATM, TODO: clean xp to tasks"""
33
36
  voltage: Annotated[float, Field(ge=1, lt=5)] = 3
34
37
  datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 200_000
35
38
  protocol: ProgrammerProtocol
@@ -38,7 +41,7 @@ class ProgrammingTask(ShpModel):
38
41
  simulate: bool = False
39
42
 
40
43
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
41
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
44
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
42
45
 
43
46
  @model_validator(mode="after")
44
47
  def post_validation(self) -> Self:
@@ -66,7 +69,7 @@ class ProgrammingTask(ShpModel):
66
69
  return None
67
70
 
68
71
  return cls(
69
- firmware_file=copy.copy(fw_path),
72
+ firmware_file=path_posix(fw_path),
70
73
  target_port=obs.get_target_port(tgt_id),
71
74
  mcu_port=mcu_port,
72
75
  mcu_type=fw.mcu.name,
@@ -74,3 +77,6 @@ class ProgrammingTask(ShpModel):
74
77
  datarate=fw.mcu.prog_datarate,
75
78
  protocol=fw.mcu.prog_protocol,
76
79
  )
80
+
81
+ def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
82
+ return any(self.firmware_file.is_relative_to(path) for path in paths)