shepherd-core 2023.8.6__py3-none-any.whl → 2023.8.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. shepherd_core/__init__.py +1 -1
  2. shepherd_core/data_models/__init__.py +3 -1
  3. shepherd_core/data_models/base/cal_measurement.py +17 -14
  4. shepherd_core/data_models/base/calibration.py +41 -8
  5. shepherd_core/data_models/base/content.py +17 -13
  6. shepherd_core/data_models/base/shepherd.py +29 -22
  7. shepherd_core/data_models/base/wrapper.py +5 -4
  8. shepherd_core/data_models/content/energy_environment.py +3 -2
  9. shepherd_core/data_models/content/firmware.py +10 -6
  10. shepherd_core/data_models/content/virtual_harvester.py +42 -39
  11. shepherd_core/data_models/content/virtual_source.py +83 -72
  12. shepherd_core/data_models/doc_virtual_source.py +7 -14
  13. shepherd_core/data_models/experiment/experiment.py +20 -15
  14. shepherd_core/data_models/experiment/observer_features.py +33 -31
  15. shepherd_core/data_models/experiment/target_config.py +24 -18
  16. shepherd_core/data_models/task/__init__.py +13 -5
  17. shepherd_core/data_models/task/emulation.py +35 -23
  18. shepherd_core/data_models/task/firmware_mod.py +14 -13
  19. shepherd_core/data_models/task/harvest.py +28 -13
  20. shepherd_core/data_models/task/observer_tasks.py +17 -7
  21. shepherd_core/data_models/task/programming.py +13 -13
  22. shepherd_core/data_models/task/testbed_tasks.py +16 -6
  23. shepherd_core/data_models/testbed/cape.py +3 -2
  24. shepherd_core/data_models/testbed/gpio.py +18 -15
  25. shepherd_core/data_models/testbed/mcu.py +7 -6
  26. shepherd_core/data_models/testbed/observer.py +23 -19
  27. shepherd_core/data_models/testbed/target.py +15 -14
  28. shepherd_core/data_models/testbed/testbed.py +14 -11
  29. shepherd_core/fw_tools/converter.py +7 -7
  30. shepherd_core/fw_tools/converter_elf.py +2 -2
  31. shepherd_core/fw_tools/patcher.py +7 -6
  32. shepherd_core/fw_tools/validation.py +3 -3
  33. shepherd_core/inventory/__init__.py +16 -8
  34. shepherd_core/inventory/python.py +4 -3
  35. shepherd_core/inventory/system.py +5 -5
  36. shepherd_core/inventory/target.py +4 -4
  37. shepherd_core/reader.py +3 -3
  38. shepherd_core/testbed_client/client.py +6 -4
  39. shepherd_core/testbed_client/user_model.py +14 -10
  40. shepherd_core/writer.py +2 -2
  41. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/METADATA +9 -2
  42. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/RECORD +49 -49
  43. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/WHEEL +1 -1
  44. tests/data_models/example_cal_data.yaml +2 -1
  45. tests/data_models/example_cal_meas.yaml +2 -1
  46. tests/data_models/test_base_models.py +19 -2
  47. tests/inventory/test_inventory.py +1 -1
  48. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/top_level.txt +0 -0
  49. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/zip-safe +0 -0
@@ -1,7 +1,8 @@
1
- from pydantic import confloat
2
- from pydantic import conint
3
- from pydantic import conlist
4
- from pydantic import root_validator
1
+ from typing import List
2
+
3
+ from pydantic import Field
4
+ from pydantic import model_validator
5
+ from typing_extensions import Annotated
5
6
 
6
7
  from ...commons import samplerate_sps_default
7
8
  from ...logger import logger
@@ -11,6 +12,12 @@ from ..base.content import ContentModel
11
12
  from .virtual_harvester import HarvesterPRUConfig
12
13
  from .virtual_harvester import VirtualHarvesterConfig
13
14
 
15
+ # Custom Types
16
+ LUT_SIZE: int = 12
17
+ NormedNum = Annotated[float, Field(ge=0.0, le=1.0)]
18
+ LUT1D = Annotated[List[NormedNum], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
19
+ LUT2D = Annotated[List[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
20
+
14
21
 
15
22
  class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
16
23
  """The virtual Source uses the energy environment (file)
@@ -30,89 +37,82 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
30
37
  enable_buck: bool = False
31
38
  # ⤷ if false -> v_output = v_intermediate
32
39
 
33
- interval_startup_delay_drain_ms: confloat(ge=0, le=10_000) = 0
40
+ interval_startup_delay_drain_ms: Annotated[float, Field(ge=0, le=10_000)] = 0
34
41
 
35
42
  harvester: VirtualHarvesterConfig = VirtualHarvesterConfig(name="mppt_opt")
36
43
 
37
- V_input_max_mV: confloat(ge=0, le=10_000) = 10_000
38
- I_input_max_mA: confloat(ge=0, le=4.29e3) = 4_200
39
- V_input_drop_mV: confloat(ge=0, le=4.29e6) = 0
44
+ V_input_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
45
+ I_input_max_mA: Annotated[float, Field(ge=0, le=4.29e3)] = 4_200
46
+ V_input_drop_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 0
40
47
  # ⤷ simulate input-diode
41
- R_input_mOhm: confloat(ge=0, le=4.29e6) = 0
48
+ R_input_mOhm: Annotated[float, Field(ge=0, le=4.29e6)] = 0
42
49
  # ⤷ resistance only active with disabled boost, range [1 mOhm; 1MOhm]
43
50
 
44
51
  # primary storage-Cap
45
- C_intermediate_uF: confloat(ge=0, le=100_000) = 0
46
- V_intermediate_init_mV: confloat(ge=0, le=10_000) = 3_000
52
+ C_intermediate_uF: Annotated[float, Field(ge=0, le=100_000)] = 0
53
+ V_intermediate_init_mV: Annotated[float, Field(ge=0, le=10_000)] = 3_000
47
54
  # ⤷ allow a proper / fast startup
48
- I_intermediate_leak_nA: confloat(ge=0, le=4.29e9) = 0
55
+ I_intermediate_leak_nA: Annotated[float, Field(ge=0, le=4.29e9)] = 0
49
56
 
50
- V_intermediate_enable_threshold_mV: confloat(ge=0, le=10_000) = 1
57
+ V_intermediate_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 1
51
58
  # ⤷ target gets connected (hysteresis-combo with next value)
52
- V_intermediate_disable_threshold_mV: confloat(ge=0, le=10_000) = 0
59
+ V_intermediate_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
53
60
  # ⤷ target gets disconnected
54
- interval_check_thresholds_ms: confloat(ge=0, le=4.29e3) = 0
61
+ interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
55
62
  # ⤷ some ICs (BQ) check every 64 ms if output should be disconnected
56
63
 
57
64
  # pwr-good: target is informed on output-pin (hysteresis) -> for intermediate voltage
58
- V_pwr_good_enable_threshold_mV: confloat(ge=0, le=10_000) = 2_800
59
- V_pwr_good_disable_threshold_mV: confloat(ge=0, le=10_000) = 2200
65
+ V_pwr_good_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2_800
66
+ V_pwr_good_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2200
60
67
  immediate_pwr_good_signal: bool = True
61
68
  # ⤷ 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds
62
69
 
63
70
  # final (always last) stage to compensate undetectable current spikes
64
71
  # when enabling power for target
65
- C_output_uF: confloat(ge=0, le=4.29e6) = 1.0
72
+ C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
66
73
 
67
74
  # Extra
68
- V_output_log_gpio_threshold_mV: confloat(ge=0, le=4.29e6) = 1_400
75
+ V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
69
76
  # ⤷ min voltage needed to enable recording changes in gpio-bank
70
77
 
71
78
  # Boost Converter
72
- V_input_boost_threshold_mV: confloat(ge=0, le=10_000) = 0
79
+ V_input_boost_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
73
80
  # ⤷ min input-voltage for the boost converter to work
74
- V_intermediate_max_mV: confloat(ge=0, le=10_000) = 10_000
81
+ V_intermediate_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
75
82
  # ⤷ boost converter shuts off
76
83
 
77
- LUT_input_efficiency: conlist(
78
- item_type=conlist(confloat(ge=0.0, le=1.0), min_items=12, max_items=12),
79
- min_items=12,
80
- max_items=12,
81
- ) = 12 * [12 * [1.00]]
84
+ LUT_input_efficiency: LUT2D = 12 * [12 * [1.00]]
82
85
  # ⤷ rows are current -> first row a[V=0][:]
83
86
  # input-LUT[12][12] depending on array[inp_voltage][log(inp_current)],
84
87
  # influence of cap-voltage is not implemented
85
- LUT_input_V_min_log2_uV: conint(ge=0, le=20) = 0
88
+ LUT_input_V_min_log2_uV: Annotated[int, Field(ge=0, le=20)] = 0
86
89
  # ⤷ 2^7 = 128 uV -> LUT[0][:] is for inputs < 128 uV
87
- LUT_input_I_min_log2_nA: conint(ge=0, le=20) = 0
90
+ LUT_input_I_min_log2_nA: Annotated[int, Field(ge=0, le=20)] = 0
88
91
  # ⤷ 2^8 = 256 nA -> LUT[:][0] is for inputs < 256 nA
89
92
 
90
93
  # Buck Converter
91
- V_output_mV: confloat(ge=0, le=5_000) = 2_400
92
- V_buck_drop_mV: confloat(ge=0, le=5_000) = 0
94
+ V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
95
+ V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
93
96
  # ⤷ simulate LDO min voltage differential or output-diode
94
97
 
95
- LUT_output_efficiency: conlist(
96
- item_type=confloat(ge=0.0, le=1.0),
97
- min_items=12,
98
- max_items=12,
99
- ) = 12 * [1.00]
98
+ LUT_output_efficiency: LUT1D = 12 * [1.00]
100
99
  # ⤷ array[12] depending on output_current
101
- LUT_output_I_min_log2_nA: conint(ge=0, le=20) = 0
100
+ LUT_output_I_min_log2_nA: Annotated[int, Field(ge=0, le=20)] = 0
102
101
  # ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation
103
102
 
104
- @root_validator(pre=True)
103
+ @model_validator(mode="before")
104
+ @classmethod
105
105
  def query_database(cls, values: dict) -> dict:
106
106
  values, chain = tb_client.try_completing_model(cls.__name__, values)
107
107
  values = tb_client.fill_in_user_data(values)
108
108
  logger.debug("VSrc-Inheritances: %s", chain)
109
109
  return values
110
110
 
111
- @root_validator(pre=False)
112
- def post_validation(cls, values: dict) -> dict:
111
+ @model_validator(mode="after")
112
+ def post_validation(self):
113
113
  # trigger stricter test of harv-parameters
114
- HarvesterPRUConfig.from_vhrv(values.get("harvester"), for_emu=True)
115
- return values
114
+ HarvesterPRUConfig.from_vhrv(self.harvester, for_emu=True)
115
+ return self
116
116
 
117
117
  def calc_internal_states(self) -> dict:
118
118
  """
@@ -220,15 +220,16 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
220
220
  return int((10**3 * (2**28)) // (C_cap_uF * samplerate_sps_default))
221
221
 
222
222
 
223
- u32 = conint(ge=0, lt=2**32)
224
- u8 = conint(ge=0, lt=2**8)
225
- LUT_SIZE: int = 12
226
- lut_i = conlist(
227
- item_type=conlist(u8, min_items=LUT_SIZE, max_items=LUT_SIZE),
228
- min_items=LUT_SIZE,
229
- max_items=LUT_SIZE,
230
- )
231
- lut_o = conlist(u32, min_items=LUT_SIZE, max_items=LUT_SIZE)
223
+ u32 = Annotated[int, Field(ge=0, lt=2**32)]
224
+ u8 = Annotated[int, Field(ge=0, lt=2**8)]
225
+ lut_i = Annotated[
226
+ List[Annotated[List[u8], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]],
227
+ Field(
228
+ min_length=LUT_SIZE,
229
+ max_length=LUT_SIZE,
230
+ ),
231
+ ]
232
+ lut_o = Annotated[List[u32], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
232
233
 
233
234
 
234
235
  class ConverterPRUConfig(ShpModel):
@@ -282,32 +283,42 @@ class ConverterPRUConfig(ShpModel):
282
283
  return cls(
283
284
  # General
284
285
  converter_mode=data.calc_converter_mode(log_intermediate_node),
285
- interval_startup_delay_drain_n=data.interval_startup_delay_drain_ms
286
- * samplerate_sps_default
287
- * 1e-3,
288
- V_input_max_uV=data.V_input_max_mV * 1e3,
289
- I_input_max_nA=data.I_input_max_mA * 1e6,
290
- V_input_drop_uV=data.V_input_drop_mV * 1e3,
291
- R_input_kOhm_n22=data.R_input_mOhm * (1e-6 * 2**22),
286
+ interval_startup_delay_drain_n=round(
287
+ data.interval_startup_delay_drain_ms * samplerate_sps_default * 1e-3
288
+ ),
289
+ V_input_max_uV=round(data.V_input_max_mV * 1e3),
290
+ I_input_max_nA=round(data.I_input_max_mA * 1e6),
291
+ V_input_drop_uV=round(data.V_input_drop_mV * 1e3),
292
+ R_input_kOhm_n22=round(data.R_input_mOhm * (1e-6 * 2**22)),
292
293
  Constant_us_per_nF_n28=data.calc_cap_constant_us_per_nF_n28(),
293
- V_intermediate_init_uV=data.V_intermediate_init_mV * 1e3,
294
- I_intermediate_leak_nA=data.I_intermediate_leak_nA,
295
- V_enable_output_threshold_uV=states["V_enable_output_threshold_mV"] * 1e3,
296
- V_disable_output_threshold_uV=states["V_disable_output_threshold_mV"] * 1e3,
297
- dV_enable_output_uV=states["dV_enable_output_mV"] * 1e3,
298
- interval_check_thresholds_n=data.interval_check_thresholds_ms
299
- * samplerate_sps_default
300
- * 1e-3,
301
- V_pwr_good_enable_threshold_uV=data.V_pwr_good_enable_threshold_mV * 1e3,
302
- V_pwr_good_disable_threshold_uV=data.V_pwr_good_disable_threshold_mV * 1e3,
294
+ V_intermediate_init_uV=round(data.V_intermediate_init_mV * 1e3),
295
+ I_intermediate_leak_nA=round(data.I_intermediate_leak_nA),
296
+ V_enable_output_threshold_uV=round(
297
+ states["V_enable_output_threshold_mV"] * 1e3
298
+ ),
299
+ V_disable_output_threshold_uV=round(
300
+ states["V_disable_output_threshold_mV"] * 1e3
301
+ ),
302
+ dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
303
+ interval_check_thresholds_n=round(
304
+ data.interval_check_thresholds_ms * samplerate_sps_default * 1e-3
305
+ ),
306
+ V_pwr_good_enable_threshold_uV=round(
307
+ data.V_pwr_good_enable_threshold_mV * 1e3
308
+ ),
309
+ V_pwr_good_disable_threshold_uV=round(
310
+ data.V_pwr_good_disable_threshold_mV * 1e3
311
+ ),
303
312
  immediate_pwr_good_signal=data.immediate_pwr_good_signal,
304
- V_output_log_gpio_threshold_uV=data.V_output_log_gpio_threshold_mV * 1e3,
313
+ V_output_log_gpio_threshold_uV=round(
314
+ data.V_output_log_gpio_threshold_mV * 1e3
315
+ ),
305
316
  # Boost-Converter
306
- V_input_boost_threshold_uV=data.V_input_boost_threshold_mV * 1e3,
307
- V_intermediate_max_uV=data.V_intermediate_max_mV * 1e3,
317
+ V_input_boost_threshold_uV=round(data.V_input_boost_threshold_mV * 1e3),
318
+ V_intermediate_max_uV=round(data.V_intermediate_max_mV * 1e3),
308
319
  # Buck-Converter
309
- V_output_uV=data.V_output_mV * 1e3,
310
- V_buck_drop_uV=data.V_buck_drop_mV * 1e3,
320
+ V_output_uV=round(data.V_output_mV * 1e3),
321
+ V_buck_drop_uV=round(data.V_buck_drop_mV * 1e3),
311
322
  # LUTs
312
323
  LUT_input_V_min_log2_uV=data.LUT_input_V_min_log2_uV,
313
324
  LUT_input_I_min_log2_nA=data.LUT_input_I_min_log2_nA,
@@ -1,14 +1,14 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from pydantic import Field
4
- from pydantic import confloat
5
- from pydantic import conlist
6
- from pydantic import root_validator
4
+ from pydantic import model_validator
7
5
 
8
6
  from .. import logger
9
7
  from ..data_models import Fixture
10
8
  from ..data_models import ShpModel
11
9
  from .content import VirtualHarvesterConfig
10
+ from .content.virtual_source import LUT1D
11
+ from .content.virtual_source import LUT2D
12
12
 
13
13
  fixture_path = Path(__file__).resolve().with_name("content/virtual_source_fixture.yaml")
14
14
  fixtures = Fixture(fixture_path, "content.VirtualSource")
@@ -168,11 +168,7 @@ class VirtualSourceDoc(ShpModel, title="Virtual Source (Documented, Testversion)
168
168
  le=10_000,
169
169
  )
170
170
 
171
- LUT_input_efficiency: conlist(
172
- item_type=conlist(confloat(ge=0.0, le=1.0), min_items=12, max_items=12),
173
- min_items=12,
174
- max_items=12,
175
- ) = Field(
171
+ LUT_input_efficiency: LUT2D = Field(
176
172
  description="# input-array[12][12] depending on "
177
173
  "array[inp_voltage][log(inp_current)], "
178
174
  "influence of cap-voltage is not implemented",
@@ -206,11 +202,7 @@ class VirtualSourceDoc(ShpModel, title="Virtual Source (Documented, Testversion)
206
202
  le=5_000,
207
203
  )
208
204
 
209
- LUT_output_efficiency: conlist(
210
- item_type=confloat(ge=0.0, le=1.0),
211
- min_items=12,
212
- max_items=12,
213
- ) = Field(
205
+ LUT_output_efficiency: LUT1D = Field(
214
206
  description="Output-Array[12] depending on output_current. In & Output is linear",
215
207
  default=12 * [1.00],
216
208
  )
@@ -222,7 +214,8 @@ class VirtualSourceDoc(ShpModel, title="Virtual Source (Documented, Testversion)
222
214
  le=20,
223
215
  )
224
216
 
225
- @root_validator(pre=True)
217
+ @model_validator(mode="before")
218
+ @classmethod
226
219
  def query_database(cls, values: dict) -> dict:
227
220
  values = fixtures.try_completing_model(values)
228
221
  values, chain = fixtures.try_inheritance(values)
@@ -1,11 +1,12 @@
1
1
  from datetime import datetime
2
2
  from datetime import timedelta
3
+ from typing import List
3
4
  from typing import Optional
4
5
 
5
6
  from pydantic import EmailStr
6
7
  from pydantic import Field
7
- from pydantic import conlist
8
- from pydantic import root_validator
8
+ from pydantic import model_validator
9
+ from typing_extensions import Annotated
9
10
 
10
11
  from ..base.content import IdInt
11
12
  from ..base.content import NameStr
@@ -27,8 +28,11 @@ class Experiment(ShpModel, title="Config of an Experiment"):
27
28
  description="Unique ID",
28
29
  default_factory=id_default,
29
30
  )
31
+ # ⤷ TODO: automatic ID is problematic for identification by hash
30
32
  name: NameStr
31
- description: Optional[SafeStr] = Field(description="Required for public instances")
33
+ description: Annotated[
34
+ Optional[SafeStr], Field(description="Required for public instances")
35
+ ] = None
32
36
  comment: Optional[SafeStr] = None
33
37
  created: datetime = Field(default_factory=datetime.now)
34
38
 
@@ -36,7 +40,8 @@ class Experiment(ShpModel, title="Config of an Experiment"):
36
40
  owner_id: Optional[IdInt] = 5472 # UUID?
37
41
 
38
42
  # feedback
39
- email_results: Optional[EmailStr] # TODO: can be bool, as its linked to account
43
+ email_results: Optional[EmailStr] = None
44
+ # ⤷ TODO: can be bool, as its linked to account
40
45
  sys_logging: SystemLogging = SystemLogging(dmesg=True, ptp=False, shepherd=True)
41
46
 
42
47
  # schedule
@@ -45,21 +50,21 @@ class Experiment(ShpModel, title="Config of an Experiment"):
45
50
  abort_on_error: bool = False
46
51
 
47
52
  # targets
48
- target_configs: conlist(item_type=TargetConfig, min_items=1, max_items=64)
53
+ target_configs: Annotated[List[TargetConfig], Field(min_length=1, max_length=64)]
49
54
 
50
- @root_validator(pre=False)
51
- def post_validation(cls, values: dict) -> dict:
52
- cls.validate_targets(values)
53
- cls.validate_observers(values)
54
- if values.get("duration") and values["duration"].total_seconds() < 0:
55
+ @model_validator(mode="after")
56
+ def post_validation(self):
57
+ self.validate_targets(self.target_configs)
58
+ self.validate_observers(self.target_configs)
59
+ if self.duration and self.duration.total_seconds() < 0:
55
60
  raise ValueError("Duration of experiment can't be negative.")
56
- return values
61
+ return self
57
62
 
58
63
  @staticmethod
59
- def validate_targets(values: dict) -> None:
64
+ def validate_targets(configs: List[TargetConfig]) -> None:
60
65
  target_ids = []
61
66
  custom_ids = []
62
- for _config in values.get("target_configs"):
67
+ for _config in configs:
63
68
  for _id in _config.target_IDs:
64
69
  target_ids.append(_id)
65
70
  Target(id=_id)
@@ -76,9 +81,9 @@ class Experiment(ShpModel, title="Config of an Experiment"):
76
81
  )
77
82
 
78
83
  @staticmethod
79
- def validate_observers(values: dict) -> None:
84
+ def validate_observers(configs: List[TargetConfig]) -> None:
80
85
  target_ids = []
81
- for _config in values.get("target_configs"):
86
+ for _config in configs:
82
87
  for _id in _config.target_IDs:
83
88
  target_ids.append(_id)
84
89
 
@@ -1,13 +1,13 @@
1
1
  from datetime import timedelta
2
2
  from enum import Enum
3
+ from typing import List
3
4
  from typing import Optional
4
5
 
5
6
  import numpy as np
7
+ from pydantic import Field
6
8
  from pydantic import PositiveFloat
7
- from pydantic import confloat
8
- from pydantic import conint
9
- from pydantic import conlist
10
- from pydantic import root_validator
9
+ from pydantic import model_validator
10
+ from typing_extensions import Annotated
11
11
 
12
12
  from ..base.shepherd import ShpModel
13
13
  from ..testbed.gpio import GPIO
@@ -28,26 +28,26 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
28
28
 
29
29
  # post-processing
30
30
  calculate_power: bool = False
31
- samplerate: conint(ge=10, le=100_000) = 100_000 # down-sample
31
+ samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000 # down-sample
32
32
  discard_current: bool = False
33
33
  discard_voltage: bool = False
34
34
  # ⤷ reduce file-size by omitting current / voltage
35
35
 
36
- @root_validator(pre=False)
37
- def post_validation(cls, values: dict) -> dict:
38
- if values.get("delay") and values["delay"].total_seconds() < 0:
36
+ @model_validator(mode="after")
37
+ def post_validation(self):
38
+ if self.delay and self.delay.total_seconds() < 0:
39
39
  raise ValueError("Delay can't be negative.")
40
- if values.get("duration") and values["duration"].total_seconds() < 0:
40
+ if self.duration and self.duration.total_seconds() < 0:
41
41
  raise ValueError("Duration can't be negative.")
42
42
 
43
- discard_all = values.get("discard_current") and values.get("discard_voltage")
44
- if not values.get("calculate_power") and discard_all:
43
+ discard_all = self.discard_current and self.discard_voltage
44
+ if not self.calculate_power and discard_all:
45
45
  raise ValueError(
46
46
  "Error in config -> tracing enabled, but output gets discarded"
47
47
  )
48
- if values.get("calculate_power"):
48
+ if self.calculate_power:
49
49
  raise ValueError("postprocessing not implemented ATM")
50
- return values
50
+ return self
51
51
 
52
52
 
53
53
  class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
@@ -56,10 +56,12 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
56
56
  """
57
57
 
58
58
  # initial recording
59
- mask: conint(ge=0, lt=2**10) = 0b11_1111_1111 # all
59
+ mask: Annotated[int, Field(ge=0, lt=2**10)] = 0b11_1111_1111 # all
60
60
  # ⤷ TODO: custom mask not implemented
61
- gpios: Optional[conlist(item_type=GPIO, min_items=1, max_items=10)] # = all
62
- # ⤷ TODO: list of GPIO to build mask, one of both should be internal
61
+ gpios: Optional[
62
+ Annotated[List[GPIO], Field(min_length=1, max_length=10)]
63
+ ] = None # = all
64
+ # ⤷ TODO: list of GPIO to build mask, one of both should be internal / computed field
63
65
 
64
66
  # time
65
67
  delay: timedelta = 0 # seconds
@@ -68,18 +70,18 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
68
70
  # post-processing,
69
71
  uart_decode: bool = False # todo: is currently done online by system-service
70
72
  uart_pin: GPIO = GPIO(name="GPIO8")
71
- uart_baudrate: conint(ge=2_400, le=921_600) = 115_200
73
+ uart_baudrate: Annotated[int, Field(ge=2_400, le=921_600)] = 115_200
72
74
  # TODO: add a "discard_gpio" (if only uart is wanted)
73
75
 
74
- @root_validator(pre=False)
75
- def post_validation(cls, values: dict) -> dict:
76
- if values.get("mask") == 0:
76
+ @model_validator(mode="after")
77
+ def post_validation(self):
78
+ if self.mask == 0:
77
79
  raise ValueError("Error in config -> tracing enabled but mask is 0")
78
- if values.get("delay") and values["delay"].total_seconds() < 0:
80
+ if self.delay and self.delay.total_seconds() < 0:
79
81
  raise ValueError("Delay can't be negative.")
80
- if values.get("duration") and values["duration"].total_seconds() < 0:
82
+ if self.duration and self.duration.total_seconds() < 0:
81
83
  raise ValueError("Duration can't be negative.")
82
- return values
84
+ return self
83
85
 
84
86
 
85
87
  class GpioLevel(str, Enum):
@@ -96,17 +98,17 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
96
98
  # ⤷ resolution 10 us (guaranteed, but finer steps are possible)
97
99
  gpio: GPIO
98
100
  level: GpioLevel
99
- period: confloat(ge=10e-6) = 1
101
+ period: Annotated[float, Field(ge=10e-6)] = 1
100
102
  # ⤷ time base of periodicity in s
101
- count: conint(ge=1, le=4096) = 1
103
+ count: Annotated[int, Field(ge=1, le=4096)] = 1
102
104
 
103
- @root_validator(pre=False)
104
- def post_validation(cls, values: dict) -> dict:
105
- if not values.get("gpio").user_controllable():
105
+ @model_validator(mode="after")
106
+ def post_validation(self):
107
+ if not self.gpio.user_controllable():
106
108
  raise ValueError(
107
- f"GPIO '{values.get('gpio').name}' in actuation-event not controllable by user"
109
+ f"GPIO '{self.gpio.name}' in actuation-event not controllable by user"
108
110
  )
109
- return values
111
+ return self
110
112
 
111
113
  def get_events(self) -> np.ndarray:
112
114
  stop = self.delay + self.count * self.period
@@ -120,7 +122,7 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
120
122
  - reverses pru-gpio (preferred if possible)
121
123
  """
122
124
 
123
- events: conlist(item_type=GpioEvent, min_items=1, max_items=1024)
125
+ events: Annotated[List[GpioEvent], Field(min_length=1, max_length=1024)]
124
126
 
125
127
  def get_gpios(self) -> set:
126
128
  return {_ev.gpio for _ev in self.events}
@@ -1,8 +1,9 @@
1
+ from typing import List
1
2
  from typing import Optional
2
3
 
3
- from pydantic import conint
4
- from pydantic import conlist
5
- from pydantic import root_validator
4
+ from pydantic import Field
5
+ from pydantic import model_validator
6
+ from typing_extensions import Annotated
6
7
 
7
8
  from ..base.content import IdInt
8
9
  from ..base.shepherd import ShpModel
@@ -19,33 +20,38 @@ from .observer_features import PowerTracing
19
20
  class TargetConfig(ShpModel, title="Target Config"):
20
21
  """Configuration for Target Nodes (DuT)"""
21
22
 
22
- target_IDs: conlist(item_type=IdInt, min_items=1, max_items=64)
23
- custom_IDs: Optional[conlist(item_type=IdInt16, min_items=1, max_items=64)]
23
+ target_IDs: Annotated[List[IdInt], Field(min_length=1, max_length=64)]
24
+ custom_IDs: Optional[
25
+ Annotated[List[IdInt16], Field(min_length=1, max_length=64)]
26
+ ] = None
24
27
  # ⤷ will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware
25
28
  # if no custom ID is provided, the original ID of target is used
26
29
 
27
30
  energy_env: EnergyEnvironment # alias: input
28
31
  virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
29
- target_delays: Optional[conlist(item_type=conint(ge=0), min_items=1, max_items=64)]
32
+ target_delays: Optional[
33
+ Annotated[List[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=64)]
34
+ ] = None
30
35
  # ⤷ individual starting times -> allows to use the same environment
36
+ # TODO: delays not used ATM
31
37
 
32
38
  firmware1: Firmware
33
39
  firmware2: Optional[Firmware] = None
34
40
 
35
- power_tracing: Optional[PowerTracing]
36
- gpio_tracing: Optional[GpioTracing]
37
- gpio_actuation: Optional[GpioActuation]
41
+ power_tracing: Optional[PowerTracing] = None
42
+ gpio_tracing: Optional[GpioTracing] = None
43
+ gpio_actuation: Optional[GpioActuation] = None
38
44
 
39
- @root_validator(pre=False)
40
- def post_validation(cls, values: dict) -> dict:
41
- if not values.get("energy_env").valid:
45
+ @model_validator(mode="after")
46
+ def post_validation(self):
47
+ if not self.energy_env.valid:
42
48
  raise ValueError(
43
- f"EnergyEnv '{values['energy_env'].name}' for target must be valid"
49
+ f"EnergyEnv '{self.energy_env.name}' for target must be valid"
44
50
  )
45
- for _id in values.get("target_IDs"):
51
+ for _id in self.target_IDs:
46
52
  target = Target(id=_id)
47
53
  for mcu_num in [1, 2]:
48
- val_fw = values.get(f"firmware{mcu_num}")
54
+ val_fw = getattr(self, f"firmware{mcu_num}")
49
55
  has_fw = val_fw is not None
50
56
  tgt_mcu = target[f"mcu{mcu_num}"]
51
57
  has_mcu = tgt_mcu is not None
@@ -65,15 +71,15 @@ class TargetConfig(ShpModel, title="Target Config"):
65
71
  f"is incompatible (={tgt_mcu.name})"
66
72
  )
67
73
 
68
- c_ids = values.get("custom_IDs")
69
- t_ids = values.get("target_IDs")
74
+ c_ids = self.custom_IDs
75
+ t_ids = self.target_IDs
70
76
  if c_ids is not None and (len(set(c_ids)) < len(set(t_ids))):
71
77
  raise ValueError(
72
78
  f"Provided custom IDs {c_ids} not enough "
73
79
  f"to cover target range {t_ids}"
74
80
  )
75
81
  # TODO: if custom ids present, firmware must be ELF
76
- return values
82
+ return self
77
83
 
78
84
  def get_custom_id(self, target_id: int) -> Optional[int]:
79
85
  if self.custom_IDs is not None and target_id in self.target_IDs:
@@ -50,16 +50,18 @@ def prepare_task(
50
50
  elif isinstance(config, ShpModel):
51
51
  shp_wrap = Wrapper(
52
52
  datatype=type(config).__name__,
53
- parameters=config.dict(),
53
+ parameters=config.model_dump(),
54
54
  )
55
55
  else:
56
56
  raise ValueError("had unknown input: %s", type(config))
57
57
 
58
58
  if shp_wrap.datatype == TestbedTasks:
59
59
  if observer is None:
60
- raise ValueError(
61
- "Task-Set contained TestbedTasks -> FN needs a observer-name"
60
+ logger.debug(
61
+ "Task-Set contained TestbedTasks & no observer was provided "
62
+ "-> will return TB-Tasks"
62
63
  )
64
+ return shp_wrap
63
65
  tbt = TestbedTasks(**shp_wrap.parameters)
64
66
  logger.debug("Loading Testbed-Tasks %s for %s", tbt.name, observer)
65
67
  obt = tbt.get_observer_tasks(observer)
@@ -67,12 +69,12 @@ def prepare_task(
67
69
  raise ValueError("Observer '%s' is not in TestbedTask-Set", observer)
68
70
  shp_wrap = Wrapper(
69
71
  datatype=type(obt).__name__,
70
- parameters=obt.dict(),
72
+ parameters=obt.model_dump(),
71
73
  )
72
74
  return shp_wrap
73
75
 
74
76
 
75
- def extract_tasks(shp_wrap: Wrapper) -> List[ShpModel]:
77
+ def extract_tasks(shp_wrap: Wrapper, no_task_sets: bool = True) -> List[ShpModel]:
76
78
  """ """
77
79
  if shp_wrap.datatype == ObserverTasks:
78
80
  obt = ObserverTasks(**shp_wrap.parameters)
@@ -85,6 +87,12 @@ def extract_tasks(shp_wrap: Wrapper) -> List[ShpModel]:
85
87
  content = [FirmwareModTask(**shp_wrap.parameters)]
86
88
  elif shp_wrap.datatype == ProgrammingTask.__name__:
87
89
  content = [ProgrammingTask(**shp_wrap.parameters)]
90
+ elif shp_wrap.datatype == TestbedTasks.__name__:
91
+ if no_task_sets:
92
+ raise ValueError(
93
+ "Model in Wrapper was TestbedTasks -> Task-Sets not allowed!"
94
+ )
95
+ content = [TestbedTasks(**shp_wrap.parameters)]
88
96
  else:
89
97
  raise ValueError("Extractor had unknown task: %s", shp_wrap.datatype)
90
98