shepherd-core 2025.5.2__py3-none-any.whl → 2025.6.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 (44) hide show
  1. shepherd_core/commons.py +3 -5
  2. shepherd_core/config.py +34 -0
  3. shepherd_core/data_models/__init__.py +2 -2
  4. shepherd_core/data_models/base/calibration.py +13 -8
  5. shepherd_core/data_models/base/content.py +4 -13
  6. shepherd_core/data_models/base/wrapper.py +4 -4
  7. shepherd_core/data_models/content/_external_fixtures.yaml +11 -11
  8. shepherd_core/data_models/content/energy_environment.py +1 -1
  9. shepherd_core/data_models/content/firmware.py +10 -5
  10. shepherd_core/data_models/content/virtual_harvester.py +256 -27
  11. shepherd_core/data_models/content/virtual_source.py +37 -28
  12. shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -1
  13. shepherd_core/data_models/experiment/experiment.py +29 -19
  14. shepherd_core/data_models/experiment/observer_features.py +64 -28
  15. shepherd_core/data_models/experiment/target_config.py +19 -9
  16. shepherd_core/data_models/task/emulation.py +45 -32
  17. shepherd_core/data_models/task/firmware_mod.py +1 -1
  18. shepherd_core/data_models/task/harvest.py +16 -14
  19. shepherd_core/data_models/task/observer_tasks.py +8 -6
  20. shepherd_core/data_models/task/programming.py +3 -2
  21. shepherd_core/data_models/task/testbed_tasks.py +7 -9
  22. shepherd_core/data_models/testbed/cape_fixture.yaml +9 -1
  23. shepherd_core/data_models/testbed/gpio.py +7 -0
  24. shepherd_core/data_models/testbed/observer.py +1 -1
  25. shepherd_core/data_models/testbed/observer_fixture.yaml +19 -2
  26. shepherd_core/data_models/testbed/target.py +1 -1
  27. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  28. shepherd_core/data_models/testbed/target_fixture.yaml +14 -1
  29. shepherd_core/data_models/testbed/testbed.py +8 -9
  30. shepherd_core/data_models/testbed/testbed_fixture.yaml +11 -0
  31. shepherd_core/fw_tools/patcher.py +7 -8
  32. shepherd_core/inventory/system.py +1 -3
  33. shepherd_core/reader.py +15 -7
  34. shepherd_core/testbed_client/cache_path.py +1 -1
  35. shepherd_core/testbed_client/client_web.py +2 -2
  36. shepherd_core/testbed_client/fixtures.py +13 -11
  37. shepherd_core/testbed_client/user_model.py +3 -6
  38. shepherd_core/version.py +1 -1
  39. shepherd_core/writer.py +2 -2
  40. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/METADATA +12 -12
  41. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/RECORD +44 -43
  42. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/WHEEL +1 -1
  43. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/top_level.txt +0 -0
  44. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/zip-safe +0 -0
@@ -7,7 +7,7 @@ from pydantic import Field
7
7
  from pydantic import model_validator
8
8
  from typing_extensions import Self
9
9
 
10
- from shepherd_core.commons import SAMPLERATE_SPS_DEFAULT
10
+ from shepherd_core.config import config
11
11
  from shepherd_core.data_models.base.content import ContentModel
12
12
  from shepherd_core.data_models.base.shepherd import ShpModel
13
13
  from shepherd_core.logger import logger
@@ -23,6 +23,9 @@ NormedNum = Annotated[float, Field(ge=0.0, le=1.0)]
23
23
  LUT1D = Annotated[list[NormedNum], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
24
24
  LUT2D = Annotated[list[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
25
25
 
26
+ # defaults (pre-init complex types for improved perf) TODO: is documentation still fine?
27
+ vhrv_mppt_opt = VirtualHarvesterConfig(name="mppt_opt")
28
+
26
29
 
27
30
  class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
28
31
  """The vSrc uses the energy environment (file) for supplying the Target Node.
@@ -41,45 +44,48 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
41
44
  # General Metadata & Ownership -> ContentModel
42
45
 
43
46
  enable_boost: bool = False
44
- # ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
47
+ """ ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable"""
45
48
  enable_buck: bool = False
46
- # ⤷ if false -> v_output = v_intermediate
49
+ """ ⤷ if false -> v_output = v_intermediate"""
47
50
  enable_feedback_to_hrv: bool = False
48
- # src can control a cv-harvester for ivcurve
51
+ """ src can control a cv-harvester for ivcurve"""
49
52
 
50
53
  interval_startup_delay_drain_ms: Annotated[float, Field(ge=0, le=10_000)] = 0
51
54
 
52
- harvester: VirtualHarvesterConfig = VirtualHarvesterConfig(name="mppt_opt")
55
+ harvester: VirtualHarvesterConfig = vhrv_mppt_opt
53
56
 
54
57
  V_input_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
55
58
  I_input_max_mA: Annotated[float, Field(ge=0, le=4.29e3)] = 4_200
56
59
  V_input_drop_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 0
57
- # ⤷ simulate input-diode
60
+ """ ⤷ simulate input-diode"""
58
61
  R_input_mOhm: Annotated[float, Field(ge=0, le=4.29e6)] = 0
59
- # ⤷ resistance only active with disabled boost, range [1 mOhm; 1MOhm]
62
+ """ ⤷ resistance only active with disabled boost, range [1 mOhm; 1MOhm]"""
60
63
 
61
64
  # primary storage-Cap
62
65
  C_intermediate_uF: Annotated[float, Field(ge=0, le=100_000)] = 0
63
66
  V_intermediate_init_mV: Annotated[float, Field(ge=0, le=10_000)] = 3_000
64
- # ⤷ allow a proper / fast startup
67
+ """ ⤷ allow a proper / fast startup"""
65
68
  I_intermediate_leak_nA: Annotated[float, Field(ge=0, le=4.29e9)] = 0
66
69
 
67
70
  V_intermediate_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 1
68
- # ⤷ target gets connected (hysteresis-combo with next value)
71
+ """ ⤷ target gets connected (hysteresis-combo with next value)"""
69
72
  V_intermediate_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
70
- # ⤷ target gets disconnected
73
+ """ ⤷ target gets disconnected"""
71
74
  interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
72
- # ⤷ some ICs (BQ) check every 64 ms if output should be disconnected
75
+ """ ⤷ some ICs (BQ) check every 64 ms if output should be disconnected"""
76
+ # TODO: add intervals for input-disable, output-disable & power-good-signal
73
77
 
74
78
  # pwr-good: target is informed on output-pin (hysteresis) -> for intermediate voltage
75
79
  V_pwr_good_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2_800
76
80
  V_pwr_good_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2200
77
81
  immediate_pwr_good_signal: bool = True
78
- # ⤷ 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds
82
+ """ ⤷ 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds"""
79
83
 
80
- # final (always last) stage to compensate undetectable current spikes
81
- # when enabling power for target
82
84
  C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
85
+ """
86
+ final (always last) stage to compensate undetectable current spikes when
87
+ enabling power for target
88
+ """
83
89
  # TODO: C_output is handled internally as delta-V, but should be a I_transient
84
90
  # that makes it visible in simulation as additional i_out_drain
85
91
  # TODO: potential weakness, ACD lowpass is capturing transient,
@@ -87,32 +93,35 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
87
93
 
88
94
  # Extra
89
95
  V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
90
- # ⤷ min voltage needed to enable recording changes in gpio-bank
96
+ """ ⤷ min voltage needed to enable recording changes in gpio-bank"""
91
97
 
92
98
  # Boost Converter
93
99
  V_input_boost_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
94
- # ⤷ min input-voltage for the boost converter to work
100
+ """ ⤷ min input-voltage for the boost converter to work"""
95
101
  V_intermediate_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
96
- # ⤷ boost converter shuts off
102
+ """ ⤷ boost converter shuts off"""
97
103
 
98
104
  LUT_input_efficiency: LUT2D = 12 * [12 * [1.00]]
99
- # ⤷ rows are current -> first row a[V=0][:]
100
- # input-LUT[12][12] depending on array[inp_voltage][log(inp_current)],
101
- # influence of cap-voltage is not implemented
105
+ """ ⤷ rows are current -> first row a[V=0][:]
106
+
107
+ input-LUT[12][12] depending on array[inp_voltage][log(inp_current)],
108
+ influence of cap-voltage is not implemented
109
+ """
110
+
102
111
  LUT_input_V_min_log2_uV: Annotated[int, Field(ge=0, le=20)] = 0
103
- # ⤷ 2^7 = 128 uV -> LUT[0][:] is for inputs < 128 uV
112
+ """i.e. 2^7 = 128 uV -> LUT[0][:] is for inputs < 128 uV"""
104
113
  LUT_input_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
105
- # ⤷ 2^8 = 256 nA -> LUT[:][0] is for inputs < 256 nA
114
+ """i.e. 2^8 = 256 nA -> LUT[:][0] is for inputs < 256 nA"""
106
115
 
107
116
  # Buck Converter
108
117
  V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
109
118
  V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
110
- # ⤷ simulate LDO / diode min voltage differential or output-diode
119
+ """ ⤷ simulate LDO / diode min voltage differential or output-diode"""
111
120
 
112
121
  LUT_output_efficiency: LUT1D = 12 * [1.00]
113
- # ⤷ array[12] depending on output_current
122
+ """ ⤷ array[12] depending on output_current"""
114
123
  LUT_output_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
115
- # ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation
124
+ """ ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation"""
116
125
 
117
126
  @model_validator(mode="before")
118
127
  @classmethod
@@ -241,7 +250,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
241
250
  dV[uV] = constant[us/nF] * current[nA] = constant[us*V/nAs] * current[nA]
242
251
  """
243
252
  C_cap_uF = max(self.C_intermediate_uF, 0.001)
244
- return int((10**3 * (2**28)) // (C_cap_uF * SAMPLERATE_SPS_DEFAULT))
253
+ return int((10**3 * (2**28)) // (C_cap_uF * config.SAMPLERATE_SPS))
245
254
 
246
255
 
247
256
  u32 = Annotated[int, Field(ge=0, lt=2**32)]
@@ -316,7 +325,7 @@ class ConverterPRUConfig(ShpModel):
316
325
  dtype_in, log_intermediate_node=log_intermediate_node
317
326
  ),
318
327
  interval_startup_delay_drain_n=round(
319
- data.interval_startup_delay_drain_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
328
+ data.interval_startup_delay_drain_ms * config.SAMPLERATE_SPS * 1e-3
320
329
  ),
321
330
  V_input_max_uV=round(data.V_input_max_mV * 1e3),
322
331
  I_input_max_nA=round(data.I_input_max_mA * 1e6),
@@ -329,7 +338,7 @@ class ConverterPRUConfig(ShpModel):
329
338
  V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
330
339
  dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
331
340
  interval_check_thresholds_n=round(
332
- data.interval_check_thresholds_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
341
+ data.interval_check_thresholds_ms * config.SAMPLERATE_SPS * 1e-3
333
342
  ),
334
343
  V_pwr_good_enable_threshold_uV=round(data.V_pwr_good_enable_threshold_mV * 1e3),
335
344
  V_pwr_good_disable_threshold_uV=round(data.V_pwr_good_disable_threshold_mV * 1e3),
@@ -1,6 +1,6 @@
1
1
  # info:
2
2
  # - compendium of all parameters & description
3
- # - base for neutral fallback values if provided yml is sparse
3
+ # - base for neutral fallback values if provided yaml is sparse
4
4
  # - -> it is encouraged to omit redundant parameters
5
5
  ---
6
6
  - datatype: VirtualSourceConfig
@@ -5,18 +5,18 @@ from datetime import datetime
5
5
  from datetime import timedelta
6
6
  from typing import Annotated
7
7
  from typing import Optional
8
- from typing import Union
9
- from uuid import uuid4
10
8
 
11
- from pydantic import UUID4
12
9
  from pydantic import Field
13
10
  from pydantic import model_validator
14
11
  from typing_extensions import Self
12
+ from typing_extensions import deprecated
15
13
 
14
+ from shepherd_core.config import config
16
15
  from shepherd_core.data_models.base.content import IdInt
17
16
  from shepherd_core.data_models.base.content import NameStr
18
17
  from shepherd_core.data_models.base.content import SafeStr
19
18
  from shepherd_core.data_models.base.shepherd import ShpModel
19
+ from shepherd_core.data_models.base.timezone import local_now
20
20
  from shepherd_core.data_models.testbed.target import Target
21
21
  from shepherd_core.data_models.testbed.testbed import Testbed
22
22
  from shepherd_core.version import version
@@ -24,34 +24,28 @@ from shepherd_core.version import version
24
24
  from .observer_features import SystemLogging
25
25
  from .target_config import TargetConfig
26
26
 
27
+ # defaults (pre-init complex types)
28
+ sys_log_all = SystemLogging() # = all active
29
+
27
30
 
28
31
  class Experiment(ShpModel, title="Config of an Experiment"):
29
32
  """Config for experiments on the testbed emulating energy environments for target nodes."""
30
33
 
31
34
  # General Properties
32
- # id: UUID4 ... # TODO: db-migration - temp fix for documentation
33
- id: Union[UUID4, int] = Field(default_factory=uuid4)
34
- # ⤷ TODO: automatic ID is problematic for identification by hash
35
-
36
35
  name: NameStr
37
36
  description: Annotated[
38
37
  Optional[SafeStr], Field(description="Required for public instances")
39
38
  ] = None
40
39
  comment: Optional[SafeStr] = None
41
- created: datetime = Field(default_factory=datetime.now)
42
-
43
- # Ownership & Access
44
- owner_id: Optional[IdInt] = None
45
40
 
46
41
  # feedback
47
- email_results: bool = False
42
+ email_results: bool = True
48
43
 
49
- sys_logging: SystemLogging = SystemLogging() # = all active
44
+ sys_logging: SystemLogging = sys_log_all
50
45
 
51
46
  # schedule
52
47
  time_start: Optional[datetime] = None # = ASAP
53
48
  duration: Optional[timedelta] = None # = till EOF
54
- abort_on_error: bool = False
55
49
 
56
50
  # targets
57
51
  target_configs: Annotated[list[TargetConfig], Field(min_length=1, max_length=128)]
@@ -59,11 +53,16 @@ class Experiment(ShpModel, title="Config of an Experiment"):
59
53
  # debug
60
54
  lib_ver: Optional[str] = version
61
55
 
56
+ # deprecated fields, TODO: remove before public release
57
+ id: Annotated[Optional[int], deprecated("not needed")] = None
58
+ created: Annotated[Optional[datetime], deprecated("not needed")] = None
59
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
60
+ owner_id: Annotated[Optional[IdInt], deprecated("not needed")] = None
61
+
62
62
  @model_validator(mode="after")
63
63
  def post_validation(self) -> Self:
64
- testbed = Testbed() # this will query the first (and only) entry of client
64
+ self._validate_observers(self.target_configs)
65
65
  self._validate_targets(self.target_configs)
66
- self._validate_observers(self.target_configs, testbed)
67
66
  if self.duration and self.duration.total_seconds() < 0:
68
67
  raise ValueError("Duration of experiment can't be negative.")
69
68
  return self
@@ -75,8 +74,9 @@ class Experiment(ShpModel, title="Config of an Experiment"):
75
74
  for _config in configs:
76
75
  for _id in _config.target_IDs:
77
76
  target_ids.append(_id)
78
- Target(id=_id)
79
- # ⤷ this can raise exception for non-existing targets
77
+ if config.VALIDATE_INFRA:
78
+ Target(id=_id)
79
+ # ⤷ this can raise exception for non-existing targets
80
80
  if _config.custom_IDs is not None:
81
81
  custom_ids = custom_ids + _config.custom_IDs[: len(_config.target_IDs)]
82
82
  else:
@@ -87,7 +87,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
87
87
  raise ValueError("Custom Target-ID are faulty (some form of id-collisions)!")
88
88
 
89
89
  @staticmethod
90
- def _validate_observers(configs: Iterable[TargetConfig], testbed: Testbed) -> None:
90
+ def _validate_observers(configs: Iterable[TargetConfig]) -> None:
91
+ if not config.VALIDATE_INFRA:
92
+ return
93
+ testbed = Testbed()
91
94
  target_ids = [_id for _config in configs for _id in _config.target_IDs]
92
95
  obs_ids = [testbed.get_observer(_id).id for _id in target_ids]
93
96
  if len(target_ids) > len(set(obs_ids)):
@@ -105,3 +108,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
105
108
  # gets already caught in target_config - but keep:
106
109
  msg = f"Target-ID {target_id} was not found in Experiment '{self.name}'"
107
110
  raise ValueError(msg)
111
+
112
+ def folder_name(self, custom_date: Optional[datetime] = None) -> str:
113
+ date = custom_date if custom_date is not None else self.time_start
114
+ timestamp = local_now() if date is None else date
115
+ timestrng = timestamp.strftime("%Y-%m-%d_%H-%M-%S")
116
+ # ⤷ closest to ISO 8601, avoids ":"
117
+ return f"{timestrng}_{self.name.replace(' ', '_')}"
@@ -6,15 +6,20 @@ from typing import Annotated
6
6
  from typing import Optional
7
7
 
8
8
  import numpy as np
9
+ from annotated_types import Interval
9
10
  from pydantic import Field
10
11
  from pydantic import PositiveFloat
11
12
  from pydantic import model_validator
12
13
  from typing_extensions import Self
13
14
  from typing_extensions import deprecated
14
15
 
16
+ from shepherd_core import logger
15
17
  from shepherd_core.data_models.base.shepherd import ShpModel
16
18
  from shepherd_core.data_models.testbed.gpio import GPIO
17
19
 
20
+ # defaults (pre-init complex types)
21
+ zero_duration = timedelta(seconds=0)
22
+
18
23
 
19
24
  class PowerTracing(ShpModel, title="Config for Power-Tracing"):
20
25
  """Configuration for recording the Power-Consumption of the Target Nodes.
@@ -23,19 +28,27 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
23
28
  """
24
29
 
25
30
  intermediate_voltage: bool = False
26
- # ⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
27
- # this also includes current!
28
-
31
+ """
32
+ for EMU: record storage capacitor instead of output (good for V_out = const)
33
+ this also includes current!
34
+ """
29
35
  # time
30
- delay: timedelta = timedelta(seconds=0)
36
+ delay: timedelta = zero_duration
37
+ """start recording after experiment started"""
31
38
  duration: Optional[timedelta] = None # till EOF
39
+ """duration of recording after delay starts the process.
40
+
41
+ default is None, recording till EOF"""
32
42
 
33
43
  # post-processing
34
44
  calculate_power: bool = False
35
- 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"""
36
48
  discard_current: bool = False
49
+ """ ⤷ reduce file-size by omitting current -> not implemented ATM"""
37
50
  discard_voltage: bool = False
38
- # ⤷ reduce file-size by omitting current / voltage
51
+ """ ⤷ reduce file-size by omitting voltage -> not implemented ATM"""
39
52
 
40
53
  @model_validator(mode="after")
41
54
  def post_validation(self) -> Self:
@@ -105,7 +118,7 @@ STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
105
118
  STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
106
119
 
107
120
 
108
- class UartTracing(ShpModel, title="Config for UART Tracing"):
121
+ class UartLogging(ShpModel, title="Config for UART Logging"):
109
122
  """Configuration for recording UART-Output of the Target Nodes.
110
123
 
111
124
  Note that the Communication has to be on a specific port that
@@ -132,49 +145,65 @@ class UartTracing(ShpModel, title="Config for UART Tracing"):
132
145
  return self
133
146
 
134
147
 
148
+ GpioInt = Annotated[int, Interval(ge=0, le=17)]
149
+ GpioList = Annotated[list[GpioInt], Field(min_length=1, max_length=18)]
150
+ all_gpio = list(range(18))
151
+
152
+
135
153
  class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
136
154
  """Configuration for recording the GPIO-Output of the Target Nodes.
137
155
 
138
156
  TODO: postprocessing not implemented ATM
139
157
  """
140
158
 
141
- # initial recording
142
- mask: Annotated[int, Field(ge=0, lt=2**10)] = 0b11_1111_1111 # all
143
- # ⤷ TODO: custom mask not implemented in PRU, ATM
144
- gpios: Optional[Annotated[list[GPIO], Field(min_length=1, max_length=10)]] = None # = all
145
- # TODO: list of GPIO to build mask, one of both should be internal / computed field
159
+ gpios: GpioList = all_gpio
160
+ """List of GPIO to record.
161
+
162
+ This feature allows to remove unwanted pins from recording,
163
+ i.e. for chatty pins with separate UART Logging enabled.
164
+ Numbering is based on the Target-Port and its 16x GPIO and two PwrGood-Signals.
165
+ See doc for nRF_FRAM_Target_v1.3+ to see mapping of target port.
166
+
167
+ Example for skipping UART (pin 0 & 1):
168
+ .gpio = range(2,18)
169
+
170
+ Note:
171
+ - Cape 2.4 (2023) and lower only has 9x GPIO + 1x PwrGood
172
+ - Cape 2.5 (2025) has first 12 GPIO & both PwrGood
173
+ - this will be mapped accordingly by the observer
174
+ """
146
175
 
147
176
  # time
148
- delay: timedelta = timedelta(seconds=0)
177
+ delay: timedelta = zero_duration
149
178
  duration: Optional[timedelta] = None # till EOF
150
179
 
151
180
  # post-processing,
152
181
  uart_decode: bool = False
153
- # TODO: quickfix - uart-log currently done online in userspace
154
- # NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
182
+ """Automatic decoding from gpio-trace not implemented ATM."""
155
183
  uart_pin: GPIO = GPIO(name="GPIO8")
156
184
  uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
157
- # TODO: add a "discard_gpio" (if only uart is wanted)
158
185
 
159
186
  @model_validator(mode="after")
160
187
  def post_validation(self) -> Self:
161
- if self.mask == 0:
162
- raise ValueError("Error in config -> tracing enabled but mask is 0")
163
188
  if self.delay and self.delay.total_seconds() < 0:
164
189
  raise ValueError("Delay can't be negative.")
165
190
  if self.duration and self.duration.total_seconds() < 0:
166
191
  raise ValueError("Duration can't be negative.")
167
- if self.mask != 0b11_1111_1111: # GpioTracing.mask
168
- raise NotImplementedError("Feature GpioTracing.mask reserved for future use.")
169
- if self.gpios is not None:
170
- raise NotImplementedError("Feature GpioTracing.gpios reserved for future use.")
171
192
  if self.uart_decode:
172
- raise NotImplementedError(
193
+ logger.error(
173
194
  "Feature GpioTracing.uart_decode reserved for future use. "
174
- "Use UartTracing or manually decode serial with the provided waveform decoder."
195
+ "Use UartLogging or manually decode serial with the provided waveform decoder."
175
196
  )
176
197
  return self
177
198
 
199
+ @property
200
+ def gpio_mask(self) -> int:
201
+ # valid for cape v2.5
202
+ mask = 0
203
+ for gpio in set(self.gpios):
204
+ mask |= 2**gpio
205
+ return mask
206
+
178
207
 
179
208
  class GpioLevel(str, Enum):
180
209
  """Options for setting the gpio-level or state."""
@@ -188,12 +217,14 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
188
217
  """Configuration for a single GPIO-Event (Actuation)."""
189
218
 
190
219
  delay: PositiveFloat
191
- # ⤷ from start_time
192
- # ⤷ 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
+ """
193
224
  gpio: GPIO
194
225
  level: GpioLevel
195
226
  period: Annotated[float, Field(ge=10e-6)] = 1
196
- # ⤷ time base of periodicity in s
227
+ """ ⤷ time base of periodicity in s"""
197
228
  count: Annotated[int, Field(ge=1, le=4096)] = 1
198
229
 
199
230
  @model_validator(mode="after")
@@ -216,6 +247,11 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
216
247
 
217
248
  events: Annotated[list[GpioEvent], Field(min_length=1, max_length=1024)]
218
249
 
250
+ @model_validator(mode="after")
251
+ def post_validation(self) -> Self:
252
+ msg = "not implemented ATM"
253
+ raise ValueError(msg)
254
+
219
255
  def get_gpios(self) -> set:
220
256
  return {_ev.gpio for _ev in self.events}
221
257
 
@@ -228,7 +264,7 @@ class SystemLogging(ShpModel, title="Config for System-Logging"):
228
264
  sheep: bool = True
229
265
  sys_util: bool = True
230
266
 
231
- # TODO: remove lines below in 2026
267
+ # deprecated, TODO: remove lines below before public release
232
268
  dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
233
269
  ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
234
270
  shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
@@ -18,7 +18,10 @@ from shepherd_core.data_models.testbed.target import Target
18
18
  from .observer_features import GpioActuation
19
19
  from .observer_features import GpioTracing
20
20
  from .observer_features import PowerTracing
21
- from .observer_features import UartTracing
21
+ from .observer_features import UartLogging
22
+
23
+ # defaults (pre-init complex types)
24
+ vsrc_neutral = VirtualSourceConfig(name="neutral")
22
25
 
23
26
 
24
27
  class TargetConfig(ShpModel, title="Target Config"):
@@ -26,25 +29,32 @@ class TargetConfig(ShpModel, title="Target Config"):
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
46
- uart_tracing: Optional[UartTracing] = None
47
56
  gpio_actuation: Optional[GpioActuation] = None
57
+ uart_logging: Optional[UartLogging] = None
48
58
 
49
59
  @model_validator(mode="after")
50
60
  def post_validation(self) -> Self:
@@ -13,6 +13,7 @@ from pydantic import Field
13
13
  from pydantic import model_validator
14
14
  from pydantic import validate_call
15
15
  from typing_extensions import Self
16
+ from typing_extensions import deprecated
16
17
 
17
18
  from shepherd_core.data_models.base.content import IdInt
18
19
  from shepherd_core.data_models.base.shepherd import ShpModel
@@ -23,7 +24,8 @@ from shepherd_core.data_models.experiment.observer_features import GpioActuation
23
24
  from shepherd_core.data_models.experiment.observer_features import GpioTracing
24
25
  from shepherd_core.data_models.experiment.observer_features import PowerTracing
25
26
  from shepherd_core.data_models.experiment.observer_features import SystemLogging
26
- from shepherd_core.data_models.experiment.observer_features import UartTracing
27
+ from shepherd_core.data_models.experiment.observer_features import UartLogging
28
+ from shepherd_core.data_models.experiment.target_config import vsrc_neutral
27
29
  from shepherd_core.data_models.testbed import Testbed
28
30
  from shepherd_core.data_models.testbed.cape import TargetPort
29
31
  from shepherd_core.logger import logger
@@ -48,55 +50,67 @@ class EmulationTask(ShpModel):
48
50
 
49
51
  # General config
50
52
  input_path: Path
51
- # ⤷ hdf5 file containing harvesting data
53
+ """ ⤷ hdf5 file containing harvesting data"""
52
54
  output_path: Optional[Path] = None
53
- # ⤷ dir- or file-path for storing the recorded data:
54
- # - providing a directory -> file is named emu_timestamp.h5
55
- # - for a complete path the filename is not changed except it exists and
56
- # overwrite is disabled -> emu#num.h5
57
- # TODO: should the output-path be mandatory?
55
+ """ ⤷ dir- or file-path for storing the recorded data:
56
+
57
+ - providing a directory -> file is named emu_timestamp.h5
58
+ - for a complete path the filename is not changed except it exists and
59
+ overwrite is disabled -> emu#num.h5
60
+ TODO: should the output-path be mandatory?
61
+ """
58
62
  force_overwrite: bool = False
59
- # ⤷ Overwrite existing file
63
+ """ ⤷ Overwrite existing file"""
60
64
  output_compression: Optional[Compression] = Compression.default
61
- # ⤷ should be lzf, 1 (gzip level 1) or None (order of recommendation)
65
+ """ ⤷ should be lzf, 1 (gzip level 1) or None (order of recommendation)"""
62
66
  time_start: Optional[datetime] = None
63
- # timestamp or unix epoch time, None = ASAP
67
+ """ timestamp or unix epoch time, None = ASAP"""
64
68
  duration: Optional[timedelta] = None
65
- # ⤷ Duration of recording in seconds, None = till EOF
66
- abort_on_error: bool = False # TODO: remove, should not exist
69
+ """ ⤷ Duration of recording in seconds, None = till EOF"""
70
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
67
71
 
68
72
  # emulation-specific
69
73
  use_cal_default: bool = False
70
- # ⤷ Use default calibration values, skip loading from EEPROM
74
+ """ ⤷ Use default calibration values, skip loading from EEPROM"""
71
75
 
72
76
  enable_io: bool = True
73
77
  # TODO: add direction of pins! also it seems error-prone when only setting _tracing
74
- # ⤷ Switch the GPIO level converter to targets on/off
75
- # pre-req for sampling gpio / uart,
78
+ """ ⤷ Switch the GPIO level converter to targets on/off
79
+
80
+ pre-req for sampling gpio / uart,
81
+ """
76
82
  io_port: TargetPort = TargetPort.A
77
- # ⤷ Either Port A or B that gets connected to IO
83
+ """ ⤷ Either Port A or B that gets connected to IO"""
78
84
  pwr_port: TargetPort = TargetPort.A
79
- #chosen port will be current-monitored (main, connected to virtual Source),
80
- # the other port is aux
81
- voltage_aux: Union[Annotated[float, Field(ge=0, le=4.5)], str] = 0
82
- # ⤷ aux_voltage options:
83
- # - 0-4.5 for specific const Voltage (0 V = disabled),
84
- # - "buffer" will output intermediate voltage (storage cap of vsource),
85
- # - "main" will mirror main target voltage
85
+ """selected port will be current-monitored
86
86
 
87
+ - main channel is nnected to virtual Source
88
+ - the other port is aux
89
+ """
90
+ voltage_aux: Union[Annotated[float, Field(ge=0, le=4.5)], str] = 0
91
+ """ ⤷ aux_voltage options
92
+ - 0-4.5 for specific const Voltage (0 V = disabled),
93
+ - "buffer" will output intermediate voltage (storage cap of vsource),
94
+ - "main" will mirror main target voltage
95
+ """
87
96
  # sub-elements, could be partly moved to emulation
88
- virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
89
- # ⤷ Use the desired setting for the virtual source,
90
- # provide parameters or name like BQ25570
97
+ virtual_source: VirtualSourceConfig = vsrc_neutral
98
+ """ ⤷ Use the desired setting for the virtual source,
99
+
100
+ provide parameters or name like BQ25570
101
+ """
91
102
 
92
103
  power_tracing: Optional[PowerTracing] = PowerTracing()
93
104
  gpio_tracing: Optional[GpioTracing] = GpioTracing()
94
- uart_tracing: Optional[UartTracing] = UartTracing()
105
+ uart_logging: Optional[UartLogging] = UartLogging()
95
106
  gpio_actuation: Optional[GpioActuation] = None
96
107
  sys_logging: Optional[SystemLogging] = SystemLogging()
97
108
 
98
109
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
99
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug, TODO: just bool now, systemwide
110
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug,
111
+
112
+ TODO: just bool now, systemwide
113
+ """
100
114
 
101
115
  @model_validator(mode="before")
102
116
  @classmethod
@@ -132,7 +146,7 @@ class EmulationTask(ShpModel):
132
146
  raise ValueError("GPIO Actuation not yet implemented!")
133
147
 
134
148
  io_requested = any(
135
- _io is not None for _io in (self.gpio_actuation, self.gpio_tracing, self.uart_tracing)
149
+ _io is not None for _io in (self.gpio_actuation, self.gpio_tracing, self.uart_logging)
136
150
  )
137
151
  if self.enable_io and not io_requested:
138
152
  logger.warning("Target IO enabled, but no feature requested IO")
@@ -147,7 +161,7 @@ class EmulationTask(ShpModel):
147
161
  tgt_cfg = xp.get_target_config(tgt_id)
148
162
  io_requested = any(
149
163
  _io is not None
150
- for _io in (tgt_cfg.gpio_actuation, tgt_cfg.gpio_tracing, tgt_cfg.uart_tracing)
164
+ for _io in (tgt_cfg.gpio_actuation, tgt_cfg.gpio_tracing, tgt_cfg.uart_logging)
151
165
  )
152
166
 
153
167
  return cls(
@@ -155,14 +169,13 @@ class EmulationTask(ShpModel):
155
169
  output_path=root_path / f"emu_{obs.name}.h5",
156
170
  time_start=copy.copy(xp.time_start),
157
171
  duration=xp.duration,
158
- abort_on_error=xp.abort_on_error,
159
172
  enable_io=io_requested,
160
173
  io_port=obs.get_target_port(tgt_id),
161
174
  pwr_port=obs.get_target_port(tgt_id),
162
175
  virtual_source=tgt_cfg.virtual_source,
163
176
  power_tracing=tgt_cfg.power_tracing,
164
177
  gpio_tracing=tgt_cfg.gpio_tracing,
165
- uart_tracing=tgt_cfg.uart_tracing,
178
+ uart_logging=tgt_cfg.uart_logging,
166
179
  gpio_actuation=tgt_cfg.gpio_actuation,
167
180
  sys_logging=xp.sys_logging,
168
181
  )