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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. shepherd_core/data_models/__init__.py +2 -0
  2. shepherd_core/data_models/base/content.py +4 -13
  3. shepherd_core/data_models/content/_external_fixtures.yaml +43 -43
  4. shepherd_core/data_models/content/energy_environment.py +2 -2
  5. shepherd_core/data_models/content/virtual_harvester.py +245 -16
  6. shepherd_core/data_models/content/virtual_harvester_fixture.yaml +2 -2
  7. shepherd_core/data_models/content/virtual_source.py +5 -2
  8. shepherd_core/data_models/content/virtual_source_fixture.yaml +3 -3
  9. shepherd_core/data_models/experiment/experiment.py +8 -8
  10. shepherd_core/data_models/experiment/observer_features.py +129 -18
  11. shepherd_core/data_models/experiment/target_config.py +5 -0
  12. shepherd_core/data_models/task/__init__.py +6 -3
  13. shepherd_core/data_models/task/emulation.py +21 -5
  14. shepherd_core/data_models/task/harvest.py +3 -2
  15. shepherd_core/data_models/task/observer_tasks.py +5 -4
  16. shepherd_core/data_models/task/programming.py +3 -1
  17. shepherd_core/data_models/task/testbed_tasks.py +3 -2
  18. shepherd_core/data_models/testbed/cape_fixture.yaml +8 -0
  19. shepherd_core/data_models/testbed/gpio.py +7 -0
  20. shepherd_core/data_models/testbed/mcu_fixture.yaml +4 -4
  21. shepherd_core/data_models/testbed/observer_fixture.yaml +17 -0
  22. shepherd_core/data_models/testbed/target_fixture.yaml +13 -0
  23. shepherd_core/data_models/testbed/testbed_fixture.yaml +11 -0
  24. shepherd_core/data_models/virtual_source_doc.txt +3 -3
  25. shepherd_core/fw_tools/converter.py +6 -3
  26. shepherd_core/fw_tools/validation.py +8 -4
  27. shepherd_core/reader.py +77 -47
  28. shepherd_core/testbed_client/client_abc_fix.py +2 -3
  29. shepherd_core/testbed_client/fixtures.py +15 -17
  30. shepherd_core/testbed_client/user_model.py +3 -6
  31. shepherd_core/version.py +1 -1
  32. shepherd_core/vsource/virtual_harvester_simulation.py +1 -1
  33. shepherd_core/vsource/virtual_source_simulation.py +1 -1
  34. shepherd_core/writer.py +8 -8
  35. {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/METADATA +1 -1
  36. {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/RECORD +39 -39
  37. {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/WHEEL +1 -1
  38. {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/top_level.txt +0 -0
  39. {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/zip-safe +0 -0
@@ -33,7 +33,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
33
33
  The converter-stage is software defined and offers:
34
34
  - buck-boost-combinations,
35
35
  - a simple diode + resistor and
36
- - an intermediate buffer capacitor.
36
+ - an intermediate storage capacitor.
37
37
  """
38
38
 
39
39
  # TODO: I,V,R should be in regular unit (V, A, Ohm)
@@ -70,6 +70,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
70
70
  # ⤷ target gets disconnected
71
71
  interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
72
72
  # ⤷ some ICs (BQ) check every 64 ms if output should be disconnected
73
+ # TODO: add intervals for input-disable, output-disable & power-good-signal
73
74
 
74
75
  # pwr-good: target is informed on output-pin (hysteresis) -> for intermediate voltage
75
76
  V_pwr_good_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2_800
@@ -82,6 +83,8 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
82
83
  C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
83
84
  # TODO: C_output is handled internally as delta-V, but should be a I_transient
84
85
  # that makes it visible in simulation as additional i_out_drain
86
+ # TODO: potential weakness, ACD lowpass is capturing transient,
87
+ # but energy is LOST with this model
85
88
 
86
89
  # Extra
87
90
  V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
@@ -105,7 +108,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
105
108
  # Buck Converter
106
109
  V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
107
110
  V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
108
- # ⤷ simulate LDO min voltage differential or output-diode
111
+ # ⤷ simulate LDO / diode min voltage differential or output-diode
109
112
 
110
113
  LUT_output_efficiency: LUT1D = 12 * [1.00]
111
114
  # ⤷ array[12] depending on output_current
@@ -93,7 +93,7 @@
93
93
  parameters:
94
94
  id: 1011
95
95
  name: diode+capacitor
96
- description: Simple Converter based on diode and buffer capacitor
96
+ description: Simple Converter based on diode and storage capacitor
97
97
  inherit_from: neutral
98
98
  V_input_drop_mV: 300 # simulate input-diode
99
99
  C_intermediate_uF: 47 # primary storage-Cap
@@ -114,7 +114,7 @@
114
114
  parameters:
115
115
  id: 1013
116
116
  name: diode+resistor+capacitor
117
- description: Simple Converter based on diode, current limiting resistor and buffer capacitor
117
+ description: Simple Converter based on diode, current limiting resistor and storage capacitor
118
118
  inherit_from: diode+capacitor
119
119
  R_input_mOhm: 10000
120
120
 
@@ -133,7 +133,7 @@
133
133
  enable_boost: true # if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
134
134
 
135
135
  harvester:
136
- name: mppt_bq_solar # harvester only active if input is "ivcurves"
136
+ name: mppt_bq_solar # harvester only active if input is ivsurface / curves
137
137
 
138
138
  V_input_max_mV: 3000
139
139
  I_input_max_mA: 100
@@ -5,17 +5,16 @@ 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
 
16
14
  from shepherd_core.data_models.base.content import IdInt
17
15
  from shepherd_core.data_models.base.content import NameStr
18
16
  from shepherd_core.data_models.base.content import SafeStr
17
+ from shepherd_core.data_models.base.content import id_default
19
18
  from shepherd_core.data_models.base.shepherd import ShpModel
20
19
  from shepherd_core.data_models.testbed.target import Target
21
20
  from shepherd_core.data_models.testbed.testbed import Testbed
@@ -29,8 +28,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
29
28
  """Config for experiments on the testbed emulating energy environments for target nodes."""
30
29
 
31
30
  # General Properties
32
- # id: UUID4 ... # TODO: db-migration - temp fix for documentation
33
- id: Union[UUID4, int] = Field(default_factory=uuid4)
31
+ id: int = Field(description="Unique ID", default_factory=id_default)
34
32
  # ⤷ TODO: automatic ID is problematic for identification by hash
35
33
 
36
34
  name: NameStr
@@ -46,21 +44,23 @@ class Experiment(ShpModel, title="Config of an Experiment"):
46
44
  # feedback
47
45
  email_results: bool = False
48
46
 
49
- sys_logging: SystemLogging = SystemLogging(dmesg=True, ptp=True, shepherd=True)
47
+ sys_logging: SystemLogging = SystemLogging() # = all active
50
48
 
51
49
  # schedule
52
50
  time_start: Optional[datetime] = None # = ASAP
53
51
  duration: Optional[timedelta] = None # = till EOF
54
- abort_on_error: bool = False
52
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
55
53
 
56
54
  # targets
57
55
  target_configs: Annotated[list[TargetConfig], Field(min_length=1, max_length=128)]
58
56
 
59
- # for debug-purposes and later comp-checks
57
+ # debug
60
58
  lib_ver: Optional[str] = version
61
59
 
62
60
  @model_validator(mode="after")
63
61
  def post_validation(self) -> Self:
62
+ # TODO: only do deep validation with active connection to TB-client
63
+ # or with cached fixtures
64
64
  testbed = Testbed() # this will query the first (and only) entry of client
65
65
  self._validate_targets(self.target_configs)
66
66
  self._validate_observers(self.target_configs, testbed)
@@ -6,11 +6,14 @@ 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
14
+ from typing_extensions import deprecated
13
15
 
16
+ from shepherd_core import logger
14
17
  from shepherd_core.data_models.base.shepherd import ShpModel
15
18
  from shepherd_core.data_models.testbed.gpio import GPIO
16
19
 
@@ -22,7 +25,7 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
22
25
  """
23
26
 
24
27
  intermediate_voltage: bool = False
25
- # ⤷ for EMU: record buffer capacitor instead of output (good for V_out = const)
28
+ # ⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
26
29
  # this also includes current!
27
30
 
28
31
  # time
@@ -47,21 +50,117 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
47
50
  if not self.calculate_power and discard_all:
48
51
  raise ValueError("Error in config -> tracing enabled, but output gets discarded")
49
52
  if self.calculate_power:
50
- raise NotImplementedError("postprocessing not implemented ATM")
53
+ raise NotImplementedError(
54
+ "Feature PowerTracing.calculate_power reserved for future use."
55
+ )
56
+ if self.samplerate != 100_000:
57
+ raise NotImplementedError("Feature PowerTracing.samplerate reserved for future use.")
58
+ if self.discard_current:
59
+ raise NotImplementedError(
60
+ "Feature PowerTracing.discard_current reserved for future use."
61
+ )
62
+ if self.discard_voltage:
63
+ raise NotImplementedError(
64
+ "Feature PowerTracing.discard_voltage reserved for future use."
65
+ )
51
66
  return self
52
67
 
53
68
 
69
+ # NOTE: this was taken from pyserial (removes one dependency)
70
+ BAUDRATES = (
71
+ 50,
72
+ 75,
73
+ 110,
74
+ 134,
75
+ 150,
76
+ 200,
77
+ 300,
78
+ 600,
79
+ 1200,
80
+ 1800,
81
+ 2400,
82
+ 4800,
83
+ 9600,
84
+ 19200,
85
+ 38400,
86
+ 57600,
87
+ 115200,
88
+ 230400,
89
+ 460800,
90
+ 500000,
91
+ 576000,
92
+ 921600,
93
+ 1000000,
94
+ 1152000,
95
+ 1500000,
96
+ 2000000,
97
+ 2500000,
98
+ 3000000,
99
+ 3500000,
100
+ 4000000,
101
+ )
102
+
103
+ PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = "N", "E", "O", "M", "S"
104
+ PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
105
+
106
+ STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
107
+ STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
108
+
109
+
110
+ class UartLogging(ShpModel, title="Config for UART Logging"):
111
+ """Configuration for recording UART-Output of the Target Nodes.
112
+
113
+ Note that the Communication has to be on a specific port that
114
+ reaches the hardware-module of the SBC.
115
+ """
116
+
117
+ baudrate: Annotated[int, Field(ge=2_400, le=460_800)] = 115_200
118
+ # ⤷ TODO: find maximum that the system can handle
119
+ bytesize: Annotated[int, Field(ge=5, le=8)] = 8
120
+ stopbits: Annotated[float, Field(ge=1, le=2)] = 1
121
+ parity: str = PARITY_NONE
122
+
123
+ @model_validator(mode="after")
124
+ def post_validation(self) -> Self:
125
+ if self.baudrate not in BAUDRATES:
126
+ msg = f"Error in config -> baud-rate must be one of: {BAUDRATES}"
127
+ raise ValueError(msg)
128
+ if self.stopbits not in STOPBITS:
129
+ msg = f"Error in config -> stop-bits must be one of: {STOPBITS}"
130
+ raise ValueError(msg)
131
+ if self.parity not in PARITIES:
132
+ msg = f"Error in config -> parity must be one of: {PARITIES}"
133
+ raise ValueError(msg)
134
+ return self
135
+
136
+
137
+ GpioInt = Annotated[int, Interval(ge=0, le=17)]
138
+ GpioList = Annotated[list[GpioInt], Field(min_length=1, max_length=18)]
139
+ all_gpio = list(range(18))
140
+
141
+
54
142
  class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
55
143
  """Configuration for recording the GPIO-Output of the Target Nodes.
56
144
 
57
145
  TODO: postprocessing not implemented ATM
58
146
  """
59
147
 
60
- # initial recording
61
- mask: Annotated[int, Field(ge=0, lt=2**10)] = 0b11_1111_1111 # all
62
- # ⤷ TODO: custom mask not implemented in PRU, ATM
63
- gpios: Optional[Annotated[list[GPIO], Field(min_length=1, max_length=10)]] = None # = all
64
- # TODO: list of GPIO to build mask, one of both should be internal / computed field
148
+ gpios: GpioList = all_gpio
149
+ """List of GPIO to record.
150
+
151
+ This feature allows to remove unwanted pins from recording,
152
+ i.e. for chatty pins with separate UART Logging enabled.
153
+ Numbering is based on the Target-Port and its 16x GPIO and two PwrGood-Signals.
154
+ See doc for nRF_FRAM_Target_v1.3+ to see mapping of target port.
155
+
156
+ Example for skipping UART (pin 0 & 1):
157
+ .gpio = range(2,18)
158
+
159
+ Note:
160
+ - Cape 2.4 (2023) and lower only has 9x GPIO + 1x PwrGood
161
+ - Cape 2.5 (2025) has first 12 GPIO & both PwrGood
162
+ - this will be mapped accordingly by the observer
163
+ """
65
164
 
66
165
  # time
67
166
  delay: timedelta = timedelta(seconds=0)
@@ -69,22 +168,30 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
69
168
 
70
169
  # post-processing,
71
170
  uart_decode: bool = False
72
- # TODO: quickfix - uart-log currently done online in userspace
73
- # NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
74
171
  uart_pin: GPIO = GPIO(name="GPIO8")
75
- uart_baudrate: Annotated[int, Field(ge=2_400, le=921_600)] = 115_200
76
- # TODO: add a "discard_gpio" (if only uart is wanted)
172
+ uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
77
173
 
78
174
  @model_validator(mode="after")
79
175
  def post_validation(self) -> Self:
80
- if self.mask == 0:
81
- raise ValueError("Error in config -> tracing enabled but mask is 0")
82
176
  if self.delay and self.delay.total_seconds() < 0:
83
177
  raise ValueError("Delay can't be negative.")
84
178
  if self.duration and self.duration.total_seconds() < 0:
85
179
  raise ValueError("Duration can't be negative.")
180
+ if self.uart_decode:
181
+ logger.error(
182
+ "Feature GpioTracing.uart_decode reserved for future use. "
183
+ "Use UartLogging or manually decode serial with the provided waveform decoder."
184
+ )
86
185
  return self
87
186
 
187
+ @property
188
+ def gpio_mask(self) -> int:
189
+ # valid for cape v2.5
190
+ mask = 0
191
+ for gpio in set(self.gpios):
192
+ mask |= 2**gpio
193
+ return mask
194
+
88
195
 
89
196
  class GpioLevel(str, Enum):
90
197
  """Options for setting the gpio-level or state."""
@@ -133,11 +240,15 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
133
240
  class SystemLogging(ShpModel, title="Config for System-Logging"):
134
241
  """Configuration for recording Debug-Output of the Observers System-Services."""
135
242
 
136
- dmesg: bool = True
137
- ptp: bool = True
138
- shepherd: bool = True
139
- # TODO: rename to kernel, timesync, sheep
140
- # TODO: add utilization as option
243
+ kernel: bool = True
244
+ time_sync: bool = True
245
+ sheep: bool = True
246
+ sys_util: bool = True
247
+
248
+ # TODO: remove lines below in 2026
249
+ dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
250
+ ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
251
+ shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
141
252
 
142
253
 
143
254
  # TODO: some more interaction would be good
@@ -18,6 +18,7 @@ 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 UartLogging
21
22
 
22
23
 
23
24
  class TargetConfig(ShpModel, title="Target Config"):
@@ -38,10 +39,12 @@ class TargetConfig(ShpModel, title="Target Config"):
38
39
 
39
40
  firmware1: Firmware
40
41
  firmware2: Optional[Firmware] = None
42
+ # ⤷ omitted FW gets set to neutral deep-sleep
41
43
 
42
44
  power_tracing: Optional[PowerTracing] = None
43
45
  gpio_tracing: Optional[GpioTracing] = None
44
46
  gpio_actuation: Optional[GpioActuation] = None
47
+ uart_logging: Optional[UartLogging] = None
45
48
 
46
49
  @model_validator(mode="after")
47
50
  def post_validation(self) -> Self:
@@ -79,6 +82,8 @@ class TargetConfig(ShpModel, title="Target Config"):
79
82
  msg = f"Provided custom IDs {c_ids} not enough to cover target range {t_ids}"
80
83
  raise ValueError(msg)
81
84
  # TODO: if custom ids present, firmware must be ELF
85
+ if self.gpio_actuation is not None:
86
+ raise NotImplementedError("Feature GpioActuation reserved for future use.")
82
87
  return self
83
88
 
84
89
  def get_custom_id(self, target_id: int) -> Optional[int]:
@@ -54,7 +54,8 @@ def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = N
54
54
  parameters=config.model_dump(),
55
55
  )
56
56
  else:
57
- raise TypeError("had unknown input: %s", type(config))
57
+ msg = f"had unknown input: {type(config)}"
58
+ raise TypeError(msg)
58
59
 
59
60
  if shp_wrap.datatype == TestbedTasks.__name__:
60
61
  if observer is None:
@@ -66,7 +67,8 @@ def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = N
66
67
  logger.debug("Loading Testbed-Tasks %s for %s", tbt.name, observer)
67
68
  obt = tbt.get_observer_tasks(observer)
68
69
  if obt is None:
69
- raise ValueError("Observer '%s' is not in TestbedTask-Set", observer)
70
+ msg = f"Observer '{observer}' is not in TestbedTask-Set"
71
+ raise ValueError(msg)
70
72
  shp_wrap = Wrapper(
71
73
  datatype=type(obt).__name__,
72
74
  parameters=obt.model_dump(),
@@ -92,6 +94,7 @@ def extract_tasks(shp_wrap: Wrapper, *, no_task_sets: bool = True) -> list[ShpMo
92
94
  raise ValueError("Model in Wrapper was TestbedTasks -> Task-Sets not allowed!")
93
95
  content = [TestbedTasks(**shp_wrap.parameters)]
94
96
  else:
95
- raise ValueError("Extractor had unknown task: %s", shp_wrap.datatype)
97
+ msg = f"Extractor had unknown task: {shp_wrap.datatype}"
98
+ raise ValueError(msg)
96
99
 
97
100
  return content
@@ -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,8 +24,10 @@ 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
27
+ from shepherd_core.data_models.experiment.observer_features import UartLogging
26
28
  from shepherd_core.data_models.testbed import Testbed
27
29
  from shepherd_core.data_models.testbed.cape import TargetPort
30
+ from shepherd_core.logger import logger
28
31
 
29
32
 
30
33
  class Compression(str, Enum):
@@ -61,16 +64,16 @@ class EmulationTask(ShpModel):
61
64
  # timestamp or unix epoch time, None = ASAP
62
65
  duration: Optional[timedelta] = None
63
66
  # ⤷ Duration of recording in seconds, None = till EOF
64
- abort_on_error: bool = False # TODO: remove, should not exist
67
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
65
68
 
66
69
  # emulation-specific
67
70
  use_cal_default: bool = False
68
71
  # ⤷ Use default calibration values, skip loading from EEPROM
69
72
 
70
- enable_io: bool = False
73
+ enable_io: bool = True
71
74
  # TODO: add direction of pins! also it seems error-prone when only setting _tracing
72
75
  # ⤷ Switch the GPIO level converter to targets on/off
73
- # pre-req for sampling gpio,
76
+ # pre-req for sampling gpio / uart,
74
77
  io_port: TargetPort = TargetPort.A
75
78
  # ⤷ Either Port A or B that gets connected to IO
76
79
  pwr_port: TargetPort = TargetPort.A
@@ -89,6 +92,7 @@ class EmulationTask(ShpModel):
89
92
 
90
93
  power_tracing: Optional[PowerTracing] = PowerTracing()
91
94
  gpio_tracing: Optional[GpioTracing] = GpioTracing()
95
+ uart_logging: Optional[UartLogging] = UartLogging()
92
96
  gpio_actuation: Optional[GpioActuation] = None
93
97
  sys_logging: Optional[SystemLogging] = SystemLogging()
94
98
 
@@ -127,6 +131,14 @@ class EmulationTask(ShpModel):
127
131
  raise ValueError("Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'.")
128
132
  if self.gpio_actuation is not None:
129
133
  raise ValueError("GPIO Actuation not yet implemented!")
134
+
135
+ io_requested = any(
136
+ _io is not None for _io in (self.gpio_actuation, self.gpio_tracing, self.uart_logging)
137
+ )
138
+ if self.enable_io and not io_requested:
139
+ logger.warning("Target IO enabled, but no feature requested IO")
140
+ if not self.enable_io and io_requested:
141
+ logger.warning("Target IO not enabled, but a feature requested IO")
130
142
  return self
131
143
 
132
144
  @classmethod
@@ -134,19 +146,23 @@ class EmulationTask(ShpModel):
134
146
  def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path) -> Self:
135
147
  obs = tb.get_observer(tgt_id)
136
148
  tgt_cfg = xp.get_target_config(tgt_id)
149
+ io_requested = any(
150
+ _io is not None
151
+ for _io in (tgt_cfg.gpio_actuation, tgt_cfg.gpio_tracing, tgt_cfg.uart_logging)
152
+ )
137
153
 
138
154
  return cls(
139
155
  input_path=tgt_cfg.energy_env.data_path,
140
156
  output_path=root_path / f"emu_{obs.name}.h5",
141
157
  time_start=copy.copy(xp.time_start),
142
158
  duration=xp.duration,
143
- abort_on_error=xp.abort_on_error,
144
- enable_io=(tgt_cfg.gpio_tracing is not None) or (tgt_cfg.gpio_actuation is not None),
159
+ enable_io=io_requested,
145
160
  io_port=obs.get_target_port(tgt_id),
146
161
  pwr_port=obs.get_target_port(tgt_id),
147
162
  virtual_source=tgt_cfg.virtual_source,
148
163
  power_tracing=tgt_cfg.power_tracing,
149
164
  gpio_tracing=tgt_cfg.gpio_tracing,
165
+ uart_logging=tgt_cfg.uart_logging,
150
166
  gpio_actuation=tgt_cfg.gpio_actuation,
151
167
  sys_logging=xp.sys_logging,
152
168
  )
@@ -9,6 +9,7 @@ from typing import Optional
9
9
  from pydantic import Field
10
10
  from pydantic import model_validator
11
11
  from typing_extensions import Self
12
+ from typing_extensions import deprecated
12
13
 
13
14
  from shepherd_core.data_models.base.shepherd import ShpModel
14
15
  from shepherd_core.data_models.base.timezone import local_tz
@@ -36,8 +37,8 @@ class HarvestTask(ShpModel):
36
37
  time_start: Optional[datetime] = None
37
38
  # timestamp or unix epoch time, None = ASAP
38
39
  duration: Optional[timedelta] = None
39
- # ⤷ Duration of recording in seconds, None = till EOF
40
- abort_on_error: bool = False
40
+ # ⤷ Duration of recording in seconds, None = till EOFSys
41
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
41
42
 
42
43
  # emulation-specific
43
44
  use_cal_default: bool = False
@@ -3,10 +3,12 @@
3
3
  from datetime import datetime
4
4
  from datetime import timedelta
5
5
  from pathlib import Path
6
+ from typing import Annotated
6
7
  from typing import Optional
7
8
 
8
9
  from pydantic import validate_call
9
10
  from typing_extensions import Self
11
+ from typing_extensions import deprecated
10
12
 
11
13
  from shepherd_core.data_models.base.content import IdInt
12
14
  from shepherd_core.data_models.base.content import NameStr
@@ -28,7 +30,7 @@ class ObserverTasks(ShpModel):
28
30
  # PRE PROCESS
29
31
  time_prep: datetime # TODO: should be optional
30
32
  root_path: Path
31
- abort_on_error: bool
33
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
32
34
 
33
35
  # fw mod, store as hex-file and program
34
36
  fw1_mod: Optional[FirmwareModTask] = None
@@ -70,7 +72,6 @@ class ObserverTasks(ShpModel):
70
72
  owner_id=xp.owner_id,
71
73
  time_prep=t_start - tb.prep_duration,
72
74
  root_path=root_path,
73
- abort_on_error=xp.abort_on_error,
74
75
  fw1_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
75
76
  fw2_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 2, fw_paths[1]),
76
77
  fw1_prog=ProgrammingTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
@@ -91,8 +92,8 @@ class ObserverTasks(ShpModel):
91
92
  tasks.append(task)
92
93
  return tasks
93
94
 
94
- def get_output_paths(self) -> dict:
95
- values = {}
95
+ def get_output_paths(self) -> dict[str, Path]:
96
+ values: dict[str, Path] = {}
96
97
  if isinstance(self.emulation, EmulationTask):
97
98
  if self.emulation.output_path is None:
98
99
  raise ValueError("Emu-Task should have a valid output-path")
@@ -33,6 +33,7 @@ class ProgrammingTask(ShpModel):
33
33
  voltage: Annotated[float, Field(ge=1, lt=5)] = 3
34
34
  datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 200_000
35
35
  protocol: ProgrammerProtocol
36
+ # TODO: eradicate - should not exist. derive protocol from mcu_type
36
37
 
37
38
  simulate: bool = False
38
39
 
@@ -43,7 +44,8 @@ class ProgrammingTask(ShpModel):
43
44
  def post_validation(self) -> Self:
44
45
  d_type = suffix_to_DType.get(self.firmware_file.suffix.lower())
45
46
  if d_type != FirmwareDType.base64_hex:
46
- raise ValueError("Firmware is not intel-.hex ('%s')", self.firmware_file.as_posix())
47
+ msg = (f"Firmware is not intel-hex ({self.firmware_file.as_posix()})",)
48
+ raise ValueError(msg)
47
49
  return self
48
50
 
49
51
  @classmethod
@@ -1,5 +1,6 @@
1
1
  """Collection of tasks for all observers included in experiment."""
2
2
 
3
+ from pathlib import Path
3
4
  from typing import Annotated
4
5
  from typing import Optional
5
6
 
@@ -50,11 +51,11 @@ class TestbedTasks(ShpModel):
50
51
  return tasks
51
52
  return None
52
53
 
53
- def get_output_paths(self) -> dict:
54
+ def get_output_paths(self) -> dict[str, Path]:
54
55
  # TODO: computed field preferred, but they don't work here, as
55
56
  # - they are always stored in yaml despite "repr=False"
56
57
  # - solution will shift to some kind of "result"-datatype that is combinable
57
- values = {}
58
+ values: dict[str, Path] = {}
58
59
  for obt in self.observer_tasks:
59
60
  values = {**values, **obt.get_output_paths()}
60
61
  return values
@@ -92,3 +92,11 @@
92
92
  id: 1270065
93
93
  name: cape65
94
94
  comment: only 220u/440uF on 5V
95
+ - datatype: cape
96
+ parameters:
97
+ id: 42000
98
+ name: unit_testing_cape
99
+ version: none
100
+ description: for unit testing
101
+ created: 2025-04-29
102
+ calibrated: 2025-04-29
@@ -65,3 +65,10 @@ class GPIO(ShpModel, title="GPIO of Observer Node"):
65
65
 
66
66
  def user_controllable(self) -> bool:
67
67
  return ("gpio" in self.name.lower()) and (self.direction in {"IO", "OUT"})
68
+
69
+ def user_recordable(self) -> bool:
70
+ return (
71
+ ("gpio" in self.name.lower())
72
+ and (self.direction in {"IO", "IN"})
73
+ and (self.pin_pru is not None)
74
+ )
@@ -3,17 +3,17 @@
3
3
  parameters:
4
4
  id: 1001
5
5
  name: nRF52
6
- description: Panasonic PAN1780, ENW-89854A1KF, Bluetooth 5 Low Energy Module
6
+ description: MCU with RF, 802.15.4, Bluetooth v5.0, 2.4GHz
7
7
  platform: nRF52
8
8
  core: nRF52840
9
9
  prog_protocol: SWD
10
- fw_name_default: nrf52_demo_rf
10
+ fw_name_default: nrf52_deep_sleep
11
11
  - datatype: mcu
12
12
  parameters:
13
13
  id: 1002
14
14
  name: MSP430FR
15
- description: 16MHz Ultra-Low-Pwr MCU with 128 KB FRAM
15
+ description: 16MHz Ultra-Low-Pwr MCU with 256 KB FRAM
16
16
  platform: MSP430
17
- core: MSP430FR5962
17
+ core: MSP430FR5994
18
18
  prog_protocol: SBW
19
19
  fw_name_default: msp430_deep_sleep
@@ -221,3 +221,20 @@
221
221
  target_a:
222
222
  name: nRF52_FRAM_1392_390
223
223
  created: 2023-09-22 12:12:12
224
+ - datatype: observer
225
+ parameters:
226
+ id: 42
227
+ name: unit_testing_sheep
228
+ ip: 0.0.0.0
229
+ mac: 00:00:00:00:00:00
230
+ room: none
231
+ eth_port: none
232
+ description: unit testing
233
+ longitude: 0
234
+ latitude: 0
235
+ cape:
236
+ name: unit_testing_cape
237
+ target_a:
238
+ name: unit_testing_target
239
+ created: 2025-04-29 13:07:00
240
+ active: true
@@ -161,3 +161,16 @@
161
161
  name: nRF52
162
162
  mcu2: null
163
163
  created: 2022-12-12 12:12:12
164
+
165
+ - datatype: target
166
+ parameters:
167
+ id: 42
168
+ name: unit_testing_target
169
+ version: v0
170
+ description: for unit testing
171
+ comment: no comment
172
+ created: 2025-04-29
173
+ mcu1:
174
+ name: nRF52
175
+ mcu2:
176
+ name: MSP430FR
@@ -23,3 +23,14 @@
23
23
  - name: sheep12
24
24
  - name: sheep13
25
25
  - name: sheep14
26
+
27
+ - datatype: testbed
28
+ parameters:
29
+ id: 42
30
+ name: unit_testing_testbed
31
+ description: "Artificial Testbed used in unit testing"
32
+ shared_storage: true
33
+ data_on_server: /tmp/
34
+ data_on_observer: /tmp/
35
+ observers:
36
+ - name: unit_testing_sheep