shepherd-core 2025.4.1__py3-none-any.whl → 2025.5.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 (61) hide show
  1. shepherd_core/calibration_hw_def.py +11 -11
  2. shepherd_core/commons.py +4 -4
  3. shepherd_core/data_models/__init__.py +2 -0
  4. shepherd_core/data_models/base/cal_measurement.py +10 -11
  5. shepherd_core/data_models/base/calibration.py +7 -6
  6. shepherd_core/data_models/base/content.py +1 -1
  7. shepherd_core/data_models/base/shepherd.py +6 -7
  8. shepherd_core/data_models/base/wrapper.py +2 -2
  9. shepherd_core/data_models/content/_external_fixtures.yaml +32 -32
  10. shepherd_core/data_models/content/energy_environment.py +6 -5
  11. shepherd_core/data_models/content/firmware.py +9 -7
  12. shepherd_core/data_models/content/virtual_harvester.py +34 -26
  13. shepherd_core/data_models/content/virtual_harvester_fixture.yaml +2 -2
  14. shepherd_core/data_models/content/virtual_source.py +20 -17
  15. shepherd_core/data_models/content/virtual_source_fixture.yaml +3 -3
  16. shepherd_core/data_models/experiment/experiment.py +15 -15
  17. shepherd_core/data_models/experiment/observer_features.py +109 -16
  18. shepherd_core/data_models/experiment/target_config.py +17 -12
  19. shepherd_core/data_models/task/__init__.py +11 -8
  20. shepherd_core/data_models/task/emulation.py +32 -17
  21. shepherd_core/data_models/task/firmware_mod.py +11 -11
  22. shepherd_core/data_models/task/harvest.py +7 -6
  23. shepherd_core/data_models/task/observer_tasks.py +7 -7
  24. shepherd_core/data_models/task/programming.py +13 -12
  25. shepherd_core/data_models/task/testbed_tasks.py +8 -8
  26. shepherd_core/data_models/testbed/cape.py +7 -6
  27. shepherd_core/data_models/testbed/gpio.py +8 -7
  28. shepherd_core/data_models/testbed/mcu.py +8 -7
  29. shepherd_core/data_models/testbed/mcu_fixture.yaml +4 -4
  30. shepherd_core/data_models/testbed/observer.py +9 -7
  31. shepherd_core/data_models/testbed/target.py +9 -7
  32. shepherd_core/data_models/testbed/testbed.py +11 -10
  33. shepherd_core/data_models/virtual_source_doc.txt +3 -3
  34. shepherd_core/decoder_waveform/uart.py +5 -5
  35. shepherd_core/fw_tools/converter.py +10 -6
  36. shepherd_core/fw_tools/patcher.py +14 -15
  37. shepherd_core/fw_tools/validation.py +11 -6
  38. shepherd_core/inventory/__init__.py +6 -6
  39. shepherd_core/inventory/python.py +1 -1
  40. shepherd_core/inventory/system.py +11 -8
  41. shepherd_core/inventory/target.py +3 -3
  42. shepherd_core/logger.py +2 -2
  43. shepherd_core/reader.py +105 -78
  44. shepherd_core/testbed_client/client_abc_fix.py +22 -16
  45. shepherd_core/testbed_client/client_web.py +18 -11
  46. shepherd_core/testbed_client/fixtures.py +21 -22
  47. shepherd_core/testbed_client/user_model.py +6 -5
  48. shepherd_core/version.py +1 -1
  49. shepherd_core/vsource/target_model.py +3 -3
  50. shepherd_core/vsource/virtual_converter_model.py +3 -3
  51. shepherd_core/vsource/virtual_harvester_model.py +7 -9
  52. shepherd_core/vsource/virtual_harvester_simulation.py +7 -6
  53. shepherd_core/vsource/virtual_source_model.py +6 -5
  54. shepherd_core/vsource/virtual_source_simulation.py +8 -7
  55. shepherd_core/writer.py +37 -39
  56. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/METADATA +2 -3
  57. shepherd_core-2025.5.2.dist-info/RECORD +81 -0
  58. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/WHEEL +1 -1
  59. shepherd_core-2025.4.1.dist-info/RECORD +0 -81
  60. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/top_level.txt +0 -0
  61. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.5.2.dist-info}/zip-safe +0 -0
@@ -1,29 +1,31 @@
1
1
  """Generalized energy harvester data models."""
2
2
 
3
+ from collections.abc import Mapping
3
4
  from enum import Enum
5
+ from typing import Annotated
6
+ from typing import Any
4
7
  from typing import Optional
5
- from typing import Tuple
6
8
 
7
9
  from pydantic import Field
8
10
  from pydantic import model_validator
9
- from typing_extensions import Annotated
10
11
  from typing_extensions import Self
11
12
 
12
- from ...commons import samplerate_sps_default
13
- from ...logger import logger
14
- from ...testbed_client import tb_client
15
- from ..base.calibration import CalibrationHarvester
16
- from ..base.content import ContentModel
17
- from ..base.shepherd import ShpModel
13
+ from shepherd_core.commons import SAMPLERATE_SPS_DEFAULT
14
+ from shepherd_core.data_models.base.calibration import CalibrationHarvester
15
+ from shepherd_core.data_models.base.content import ContentModel
16
+ from shepherd_core.data_models.base.shepherd import ShpModel
17
+ from shepherd_core.logger import logger
18
+ from shepherd_core.testbed_client import tb_client
19
+
18
20
  from .energy_environment import EnergyDType
19
21
 
20
22
 
21
23
  class AlgorithmDType(str, Enum):
22
24
  """Options for choosing a harvesting algorithm."""
23
25
 
24
- direct = disable = neutral = "neutral"
25
- isc_voc = "isc_voc"
26
- ivcurve = ivcurves = ("ivcurve",)
26
+ direct = disable = neutral = "neutral" # for just using IVTrace / samples
27
+ isc_voc = "isc_voc" # only recordable ATM
28
+ ivcurve = ivcurves = ivsurface = "ivcurve"
27
29
  constant = cv = "cv"
28
30
  # ci .. constant current -> is this desired?
29
31
  mppt_voc = "mppt_voc"
@@ -35,7 +37,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
35
37
  """A vHrv makes a source-characterization (i.e. ivcurve) usable for the vSrc.
36
38
 
37
39
  Mostly used when the file-based energy environment of the virtual source
38
- is not already supplied as pre-harvested ivsample-stream.
40
+ is not already supplied as pre-harvested ivtrace.
39
41
  """
40
42
 
41
43
  # General Metadata & Ownership -> ContentModel
@@ -73,7 +75,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
73
75
 
74
76
  @model_validator(mode="before")
75
77
  @classmethod
76
- def query_database(cls, values: dict) -> dict:
78
+ def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
77
79
  values, chain = tb_client.try_completing_model(cls.__name__, values)
78
80
  values = tb_client.fill_in_user_data(values)
79
81
  if values["name"] == "neutral":
@@ -114,14 +116,14 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
114
116
  return 1 * int(for_emu) + 2 * self.rising + 4 * self.enable_linear_extrapolation
115
117
 
116
118
  def calc_algorithm_num(self, *, for_emu: bool) -> int:
117
- num = algo_to_num.get(self.algorithm)
119
+ num: int = ALGO_TO_NUM.get(self.algorithm, ALGO_TO_NUM["neutral"])
118
120
  if for_emu and self.get_datatype() != EnergyDType.ivsample:
119
121
  msg = (
120
122
  f"[{self.name}] Select valid harvest-algorithm for emulator, "
121
123
  f"current usage = {self.algorithm}"
122
124
  )
123
125
  raise ValueError(msg)
124
- if not for_emu and num < algo_to_num["isc_voc"]:
126
+ if not for_emu and num < ALGO_TO_NUM["isc_voc"]:
125
127
  msg = (
126
128
  f"[{self.name}] Select valid harvest-algorithm for harvester, "
127
129
  f"current usage = {self.algorithm}"
@@ -129,12 +131,12 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
129
131
  raise ValueError(msg)
130
132
  return num
131
133
 
132
- def calc_timings_ms(self, *, for_emu: bool) -> Tuple[float, float]:
134
+ def calc_timings_ms(self, *, for_emu: bool) -> tuple[float, float]:
133
135
  """factor-in model-internal timing-constraints."""
134
136
  window_length = self.samples_n * (1 + self.wait_cycles)
135
- time_min_ms = (1 + self.wait_cycles) * 1_000 / samplerate_sps_default
137
+ time_min_ms = (1 + self.wait_cycles) * 1_000 / SAMPLERATE_SPS_DEFAULT
136
138
  if for_emu:
137
- window_ms = window_length * 1_000 / samplerate_sps_default
139
+ window_ms = window_length * 1_000 / SAMPLERATE_SPS_DEFAULT
138
140
  time_min_ms = max(time_min_ms, window_ms)
139
141
 
140
142
  interval_ms = min(max(self.interval_ms, time_min_ms), 1_000_000)
@@ -149,7 +151,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
149
151
  return interval_ms, duration_ms
150
152
 
151
153
  def get_datatype(self) -> EnergyDType:
152
- return algo_to_dtype[self.algorithm]
154
+ return ALGO_TO_DTYPE[self.algorithm]
153
155
 
154
156
  def calc_window_size(
155
157
  self,
@@ -184,7 +186,7 @@ u32 = Annotated[int, Field(ge=0, lt=2**32)]
184
186
  # - harvesting on "neutral" is not possible - direct pass-through
185
187
  # - emulation with "ivcurve" or lower is also resulting in Error
186
188
  # - "_opt" has its own algo for emulation, but is only a fast mppt_po for harvesting
187
- algo_to_num = {
189
+ ALGO_TO_NUM: Mapping[str, int] = {
188
190
  "neutral": 2**0,
189
191
  "isc_voc": 2**3,
190
192
  "ivcurve": 2**4,
@@ -195,7 +197,7 @@ algo_to_num = {
195
197
  "mppt_opt": 2**14,
196
198
  }
197
199
 
198
- algo_to_dtype = {
200
+ ALGO_TO_DTYPE: Mapping[str, EnergyDType] = {
199
201
  "neutral": EnergyDType.ivsample,
200
202
  "isc_voc": EnergyDType.isc_voc,
201
203
  "ivcurve": EnergyDType.ivcurve,
@@ -266,9 +268,15 @@ class HarvesterPRUConfig(ShpModel):
266
268
  if window_size is not None
267
269
  else data.calc_window_size(dtype_in, for_emu=for_emu)
268
270
  )
269
- voltage_step_mV = (
270
- 1e3 * voltage_step_V if voltage_step_V is not None else data.voltage_step_mV
271
- )
271
+ if voltage_step_V is not None:
272
+ voltage_step_mV = 1e3 * voltage_step_V
273
+ elif data.voltage_step_mV is not None:
274
+ voltage_step_mV = data.voltage_step_mV
275
+ else:
276
+ raise ValueError(
277
+ "For correct emulation specify voltage_step used by harvester "
278
+ "e.g. via file_src.get_voltage_step()"
279
+ )
272
280
 
273
281
  return cls(
274
282
  algorithm=data.calc_algorithm_num(for_emu=for_emu),
@@ -280,7 +288,7 @@ class HarvesterPRUConfig(ShpModel):
280
288
  voltage_step_uV=round(voltage_step_mV * 10**3),
281
289
  current_limit_nA=round(data.current_limit_uA * 10**3),
282
290
  setpoint_n8=round(min(255, data.setpoint_n * 2**8)),
283
- interval_n=round(interval_ms * samplerate_sps_default * 1e-3),
284
- duration_n=round(duration_ms * samplerate_sps_default * 1e-3),
291
+ interval_n=round(interval_ms * SAMPLERATE_SPS_DEFAULT * 1e-3),
292
+ duration_n=round(duration_ms * SAMPLERATE_SPS_DEFAULT * 1e-3),
285
293
  wait_cycles_n=data.wait_cycles,
286
294
  )
@@ -21,7 +21,7 @@
21
21
  parameters:
22
22
  id: 1100
23
23
  name: ivcurve
24
- description: Postpone harvesting by sampling ivcurves (voltage stepped as sawtooth-wave)
24
+ description: Postpone harvesting by sampling ivsurface / curves (voltage stepped as sawtooth-wave)
25
25
  comment: ~110 Hz, Between 50 & 60 Hz line-frequency to avoid standing waves
26
26
  inherit_from: neutral
27
27
  algorithm: ivcurve
@@ -36,7 +36,7 @@
36
36
  - datatype: VirtualHarvesterConfig
37
37
  parameters:
38
38
  id: 1101
39
- name: ivcurves # synonym
39
+ name: ivsurface # synonym
40
40
  inherit_from: ivcurve
41
41
 
42
42
  - datatype: VirtualHarvesterConfig
@@ -1,17 +1,18 @@
1
1
  """Generalized virtual source data models."""
2
2
 
3
- from typing import List
3
+ from typing import Annotated
4
+ from typing import Any
4
5
 
5
6
  from pydantic import Field
6
7
  from pydantic import model_validator
7
- from typing_extensions import Annotated
8
8
  from typing_extensions import Self
9
9
 
10
- from ...commons import samplerate_sps_default
11
- from ...logger import logger
12
- from ...testbed_client import tb_client
13
- from ..base.content import ContentModel
14
- from ..base.shepherd import ShpModel
10
+ from shepherd_core.commons import SAMPLERATE_SPS_DEFAULT
11
+ from shepherd_core.data_models.base.content import ContentModel
12
+ from shepherd_core.data_models.base.shepherd import ShpModel
13
+ from shepherd_core.logger import logger
14
+ from shepherd_core.testbed_client import tb_client
15
+
15
16
  from .energy_environment import EnergyDType
16
17
  from .virtual_harvester import HarvesterPRUConfig
17
18
  from .virtual_harvester import VirtualHarvesterConfig
@@ -19,8 +20,8 @@ from .virtual_harvester import VirtualHarvesterConfig
19
20
  # Custom Types
20
21
  LUT_SIZE: int = 12
21
22
  NormedNum = Annotated[float, Field(ge=0.0, le=1.0)]
22
- LUT1D = Annotated[List[NormedNum], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
23
- LUT2D = Annotated[List[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
23
+ LUT1D = Annotated[list[NormedNum], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
24
+ LUT2D = Annotated[list[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
24
25
 
25
26
 
26
27
  class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
@@ -32,7 +33,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
32
33
  The converter-stage is software defined and offers:
33
34
  - buck-boost-combinations,
34
35
  - a simple diode + resistor and
35
- - an intermediate buffer capacitor.
36
+ - an intermediate storage capacitor.
36
37
  """
37
38
 
38
39
  # TODO: I,V,R should be in regular unit (V, A, Ohm)
@@ -81,6 +82,8 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
81
82
  C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
82
83
  # TODO: C_output is handled internally as delta-V, but should be a I_transient
83
84
  # that makes it visible in simulation as additional i_out_drain
85
+ # TODO: potential weakness, ACD lowpass is capturing transient,
86
+ # but energy is LOST with this model
84
87
 
85
88
  # Extra
86
89
  V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
@@ -104,7 +107,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
104
107
  # Buck Converter
105
108
  V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
106
109
  V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
107
- # ⤷ simulate LDO min voltage differential or output-diode
110
+ # ⤷ simulate LDO / diode min voltage differential or output-diode
108
111
 
109
112
  LUT_output_efficiency: LUT1D = 12 * [1.00]
110
113
  # ⤷ array[12] depending on output_current
@@ -113,7 +116,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
113
116
 
114
117
  @model_validator(mode="before")
115
118
  @classmethod
116
- def query_database(cls, values: dict) -> dict:
119
+ def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
117
120
  values, chain = tb_client.try_completing_model(cls.__name__, values)
118
121
  values = tb_client.fill_in_user_data(values)
119
122
  logger.debug("VSrc-Inheritances: %s", chain)
@@ -238,19 +241,19 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
238
241
  dV[uV] = constant[us/nF] * current[nA] = constant[us*V/nAs] * current[nA]
239
242
  """
240
243
  C_cap_uF = max(self.C_intermediate_uF, 0.001)
241
- return int((10**3 * (2**28)) // (C_cap_uF * samplerate_sps_default))
244
+ return int((10**3 * (2**28)) // (C_cap_uF * SAMPLERATE_SPS_DEFAULT))
242
245
 
243
246
 
244
247
  u32 = Annotated[int, Field(ge=0, lt=2**32)]
245
248
  u8 = Annotated[int, Field(ge=0, lt=2**8)]
246
249
  lut_i = Annotated[
247
- List[Annotated[List[u8], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]],
250
+ list[Annotated[list[u8], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]],
248
251
  Field(
249
252
  min_length=LUT_SIZE,
250
253
  max_length=LUT_SIZE,
251
254
  ),
252
255
  ]
253
- lut_o = Annotated[List[u32], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
256
+ lut_o = Annotated[list[u32], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
254
257
 
255
258
 
256
259
  class ConverterPRUConfig(ShpModel):
@@ -313,7 +316,7 @@ class ConverterPRUConfig(ShpModel):
313
316
  dtype_in, log_intermediate_node=log_intermediate_node
314
317
  ),
315
318
  interval_startup_delay_drain_n=round(
316
- data.interval_startup_delay_drain_ms * samplerate_sps_default * 1e-3
319
+ data.interval_startup_delay_drain_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
317
320
  ),
318
321
  V_input_max_uV=round(data.V_input_max_mV * 1e3),
319
322
  I_input_max_nA=round(data.I_input_max_mA * 1e6),
@@ -326,7 +329,7 @@ class ConverterPRUConfig(ShpModel):
326
329
  V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
327
330
  dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
328
331
  interval_check_thresholds_n=round(
329
- data.interval_check_thresholds_ms * samplerate_sps_default * 1e-3
332
+ data.interval_check_thresholds_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
330
333
  ),
331
334
  V_pwr_good_enable_threshold_uV=round(data.V_pwr_good_enable_threshold_mV * 1e3),
332
335
  V_pwr_good_disable_threshold_uV=round(data.V_pwr_good_disable_threshold_mV * 1e3),
@@ -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
@@ -1,9 +1,9 @@
1
1
  """Config for testbed experiments."""
2
2
 
3
+ from collections.abc import Iterable
3
4
  from datetime import datetime
4
5
  from datetime import timedelta
5
- from typing import Iterable
6
- from typing import List
6
+ from typing import Annotated
7
7
  from typing import Optional
8
8
  from typing import Union
9
9
  from uuid import uuid4
@@ -11,16 +11,16 @@ from uuid import uuid4
11
11
  from pydantic import UUID4
12
12
  from pydantic import Field
13
13
  from pydantic import model_validator
14
- from typing_extensions import Annotated
15
14
  from typing_extensions import Self
16
15
 
17
- from ...version import version
18
- from ..base.content import IdInt
19
- from ..base.content import NameStr
20
- from ..base.content import SafeStr
21
- from ..base.shepherd import ShpModel
22
- from ..testbed.target import Target
23
- from ..testbed.testbed import Testbed
16
+ from shepherd_core.data_models.base.content import IdInt
17
+ from shepherd_core.data_models.base.content import NameStr
18
+ from shepherd_core.data_models.base.content import SafeStr
19
+ from shepherd_core.data_models.base.shepherd import ShpModel
20
+ from shepherd_core.data_models.testbed.target import Target
21
+ from shepherd_core.data_models.testbed.testbed import Testbed
22
+ from shepherd_core.version import version
23
+
24
24
  from .observer_features import SystemLogging
25
25
  from .target_config import TargetConfig
26
26
 
@@ -46,7 +46,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
46
46
  # feedback
47
47
  email_results: bool = False
48
48
 
49
- sys_logging: SystemLogging = SystemLogging(dmesg=True, ptp=True, shepherd=True)
49
+ sys_logging: SystemLogging = SystemLogging() # = all active
50
50
 
51
51
  # schedule
52
52
  time_start: Optional[datetime] = None # = ASAP
@@ -54,9 +54,9 @@ class Experiment(ShpModel, title="Config of an Experiment"):
54
54
  abort_on_error: bool = False
55
55
 
56
56
  # targets
57
- target_configs: Annotated[List[TargetConfig], Field(min_length=1, max_length=128)]
57
+ target_configs: Annotated[list[TargetConfig], Field(min_length=1, max_length=128)]
58
58
 
59
- # for debug-purposes and later comp-checks
59
+ # debug
60
60
  lib_ver: Optional[str] = version
61
61
 
62
62
  @model_validator(mode="after")
@@ -70,8 +70,8 @@ class Experiment(ShpModel, title="Config of an Experiment"):
70
70
 
71
71
  @staticmethod
72
72
  def _validate_targets(configs: Iterable[TargetConfig]) -> None:
73
- target_ids = []
74
- custom_ids = []
73
+ target_ids: list[int] = []
74
+ custom_ids: list[int] = []
75
75
  for _config in configs:
76
76
  for _id in _config.target_IDs:
77
77
  target_ids.append(_id)
@@ -2,18 +2,18 @@
2
2
 
3
3
  from datetime import timedelta
4
4
  from enum import Enum
5
- from typing import List
5
+ from typing import Annotated
6
6
  from typing import Optional
7
7
 
8
8
  import numpy as np
9
9
  from pydantic import Field
10
10
  from pydantic import PositiveFloat
11
11
  from pydantic import model_validator
12
- from typing_extensions import Annotated
13
12
  from typing_extensions import Self
13
+ from typing_extensions import deprecated
14
14
 
15
- from ..base.shepherd import ShpModel
16
- from ..testbed.gpio import GPIO
15
+ from shepherd_core.data_models.base.shepherd import ShpModel
16
+ from shepherd_core.data_models.testbed.gpio import GPIO
17
17
 
18
18
 
19
19
  class PowerTracing(ShpModel, title="Config for Power-Tracing"):
@@ -23,11 +23,11 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
23
23
  """
24
24
 
25
25
  intermediate_voltage: bool = False
26
- # ⤷ for EMU: record buffer capacitor instead of output (good for V_out = const)
26
+ # ⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
27
27
  # this also includes current!
28
28
 
29
29
  # time
30
- delay: timedelta = 0 # seconds
30
+ delay: timedelta = timedelta(seconds=0)
31
31
  duration: Optional[timedelta] = None # till EOF
32
32
 
33
33
  # post-processing
@@ -48,7 +48,87 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
48
48
  if not self.calculate_power and discard_all:
49
49
  raise ValueError("Error in config -> tracing enabled, but output gets discarded")
50
50
  if self.calculate_power:
51
- raise NotImplementedError("postprocessing not implemented ATM")
51
+ raise NotImplementedError(
52
+ "Feature PowerTracing.calculate_power reserved for future use."
53
+ )
54
+ if self.samplerate != 100_000:
55
+ raise NotImplementedError("Feature PowerTracing.samplerate reserved for future use.")
56
+ if self.discard_current:
57
+ raise NotImplementedError(
58
+ "Feature PowerTracing.discard_current reserved for future use."
59
+ )
60
+ if self.discard_voltage:
61
+ raise NotImplementedError(
62
+ "Feature PowerTracing.discard_voltage reserved for future use."
63
+ )
64
+ return self
65
+
66
+
67
+ # NOTE: this was taken from pyserial (removes one dependency)
68
+ BAUDRATES = (
69
+ 50,
70
+ 75,
71
+ 110,
72
+ 134,
73
+ 150,
74
+ 200,
75
+ 300,
76
+ 600,
77
+ 1200,
78
+ 1800,
79
+ 2400,
80
+ 4800,
81
+ 9600,
82
+ 19200,
83
+ 38400,
84
+ 57600,
85
+ 115200,
86
+ 230400,
87
+ 460800,
88
+ 500000,
89
+ 576000,
90
+ 921600,
91
+ 1000000,
92
+ 1152000,
93
+ 1500000,
94
+ 2000000,
95
+ 2500000,
96
+ 3000000,
97
+ 3500000,
98
+ 4000000,
99
+ )
100
+
101
+ PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = "N", "E", "O", "M", "S"
102
+ PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
103
+
104
+ STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
105
+ STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
106
+
107
+
108
+ class UartTracing(ShpModel, title="Config for UART Tracing"):
109
+ """Configuration for recording UART-Output of the Target Nodes.
110
+
111
+ Note that the Communication has to be on a specific port that
112
+ reaches the hardware-module of the SBC.
113
+ """
114
+
115
+ baudrate: Annotated[int, Field(ge=2_400, le=460_800)] = 115_200
116
+ # ⤷ TODO: find maximum that the system can handle
117
+ bytesize: Annotated[int, Field(ge=5, le=8)] = 8
118
+ stopbits: Annotated[float, Field(ge=1, le=2)] = 1
119
+ parity: str = PARITY_NONE
120
+
121
+ @model_validator(mode="after")
122
+ def post_validation(self) -> Self:
123
+ if self.baudrate not in BAUDRATES:
124
+ msg = f"Error in config -> baud-rate must be one of: {BAUDRATES}"
125
+ raise ValueError(msg)
126
+ if self.stopbits not in STOPBITS:
127
+ msg = f"Error in config -> stop-bits must be one of: {STOPBITS}"
128
+ raise ValueError(msg)
129
+ if self.parity not in PARITIES:
130
+ msg = f"Error in config -> parity must be one of: {PARITIES}"
131
+ raise ValueError(msg)
52
132
  return self
53
133
 
54
134
 
@@ -61,11 +141,11 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
61
141
  # initial recording
62
142
  mask: Annotated[int, Field(ge=0, lt=2**10)] = 0b11_1111_1111 # all
63
143
  # ⤷ TODO: custom mask not implemented in PRU, ATM
64
- gpios: Optional[Annotated[List[GPIO], Field(min_length=1, max_length=10)]] = None # = all
144
+ gpios: Optional[Annotated[list[GPIO], Field(min_length=1, max_length=10)]] = None # = all
65
145
  # ⤷ TODO: list of GPIO to build mask, one of both should be internal / computed field
66
146
 
67
147
  # time
68
- delay: timedelta = 0 # seconds
148
+ delay: timedelta = timedelta(seconds=0)
69
149
  duration: Optional[timedelta] = None # till EOF
70
150
 
71
151
  # post-processing,
@@ -73,7 +153,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
73
153
  # TODO: quickfix - uart-log currently done online in userspace
74
154
  # NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
75
155
  uart_pin: GPIO = GPIO(name="GPIO8")
76
- uart_baudrate: Annotated[int, Field(ge=2_400, le=921_600)] = 115_200
156
+ uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
77
157
  # TODO: add a "discard_gpio" (if only uart is wanted)
78
158
 
79
159
  @model_validator(mode="after")
@@ -84,6 +164,15 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
84
164
  raise ValueError("Delay can't be negative.")
85
165
  if self.duration and self.duration.total_seconds() < 0:
86
166
  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
+ if self.uart_decode:
172
+ raise NotImplementedError(
173
+ "Feature GpioTracing.uart_decode reserved for future use. "
174
+ "Use UartTracing or manually decode serial with the provided waveform decoder."
175
+ )
87
176
  return self
88
177
 
89
178
 
@@ -125,7 +214,7 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
125
214
  # TODO: not implemented ATM - decide if pru control sys-gpio or
126
215
  # TODO: not implemented ATM - reverses pru-gpio (preferred if possible)
127
216
 
128
- events: Annotated[List[GpioEvent], Field(min_length=1, max_length=1024)]
217
+ events: Annotated[list[GpioEvent], Field(min_length=1, max_length=1024)]
129
218
 
130
219
  def get_gpios(self) -> set:
131
220
  return {_ev.gpio for _ev in self.events}
@@ -134,11 +223,15 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
134
223
  class SystemLogging(ShpModel, title="Config for System-Logging"):
135
224
  """Configuration for recording Debug-Output of the Observers System-Services."""
136
225
 
137
- dmesg: bool = True
138
- ptp: bool = True
139
- shepherd: bool = True
140
- # TODO: rename to kernel, timesync, sheep
141
- # TODO: add utilization as option
226
+ kernel: bool = True
227
+ time_sync: bool = True
228
+ sheep: bool = True
229
+ sys_util: bool = True
230
+
231
+ # TODO: remove lines below in 2026
232
+ dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
233
+ ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
234
+ shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
142
235
 
143
236
 
144
237
  # TODO: some more interaction would be good
@@ -1,46 +1,49 @@
1
1
  """Configuration related to Target Nodes (DuT)."""
2
2
 
3
- from typing import List
3
+ from typing import Annotated
4
4
  from typing import Optional
5
5
 
6
6
  from pydantic import Field
7
7
  from pydantic import model_validator
8
- from typing_extensions import Annotated
9
8
  from typing_extensions import Self
10
9
 
11
- from ..base.content import IdInt
12
- from ..base.shepherd import ShpModel
13
- from ..content.energy_environment import EnergyEnvironment
14
- from ..content.firmware import Firmware
15
- from ..content.virtual_source import VirtualSourceConfig
16
- from ..testbed.target import IdInt16
17
- from ..testbed.target import Target
10
+ from shepherd_core.data_models.base.content import IdInt
11
+ from shepherd_core.data_models.base.shepherd import ShpModel
12
+ from shepherd_core.data_models.content.energy_environment import EnergyEnvironment
13
+ from shepherd_core.data_models.content.firmware import Firmware
14
+ from shepherd_core.data_models.content.virtual_source import VirtualSourceConfig
15
+ from shepherd_core.data_models.testbed.target import IdInt16
16
+ from shepherd_core.data_models.testbed.target import Target
17
+
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
22
 
22
23
 
23
24
  class TargetConfig(ShpModel, title="Target Config"):
24
25
  """Configuration related to Target Nodes (DuT)."""
25
26
 
26
- target_IDs: Annotated[List[IdInt], Field(min_length=1, max_length=128)]
27
- custom_IDs: Optional[Annotated[List[IdInt16], Field(min_length=1, max_length=128)]] = None
27
+ target_IDs: Annotated[list[IdInt], Field(min_length=1, max_length=128)]
28
+ custom_IDs: Optional[Annotated[list[IdInt16], Field(min_length=1, max_length=128)]] = None
28
29
  # ⤷ will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware
29
30
  # if no custom ID is provided, the original ID of target is used
30
31
 
31
32
  energy_env: EnergyEnvironment # alias: input
32
33
  virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
33
34
  target_delays: Optional[
34
- Annotated[List[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
35
+ Annotated[list[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
35
36
  ] = None
36
37
  # ⤷ individual starting times -> allows to use the same environment
37
38
  # TODO: delays not used ATM
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
46
+ uart_tracing: Optional[UartTracing] = None
44
47
  gpio_actuation: Optional[GpioActuation] = None
45
48
 
46
49
  @model_validator(mode="after")
@@ -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]: