shepherd-core 2023.11.1__py3-none-any.whl → 2024.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. shepherd_core/__init__.py +3 -2
  2. shepherd_core/data_models/base/calibration.py +3 -1
  3. shepherd_core/data_models/base/content.py +13 -11
  4. shepherd_core/data_models/base/shepherd.py +4 -6
  5. shepherd_core/data_models/content/energy_environment.py +1 -1
  6. shepherd_core/data_models/content/virtual_harvester.py +1 -1
  7. shepherd_core/data_models/content/virtual_source.py +31 -53
  8. shepherd_core/data_models/experiment/experiment.py +13 -18
  9. shepherd_core/data_models/experiment/observer_features.py +9 -15
  10. shepherd_core/data_models/experiment/target_config.py +4 -11
  11. shepherd_core/data_models/task/__init__.py +2 -6
  12. shepherd_core/data_models/task/emulation.py +7 -14
  13. shepherd_core/data_models/task/firmware_mod.py +1 -3
  14. shepherd_core/data_models/task/harvest.py +1 -3
  15. shepherd_core/data_models/task/observer_tasks.py +1 -1
  16. shepherd_core/data_models/task/testbed_tasks.py +7 -3
  17. shepherd_core/data_models/testbed/cape.py +1 -1
  18. shepherd_core/data_models/testbed/gpio.py +1 -1
  19. shepherd_core/data_models/testbed/mcu.py +1 -1
  20. shepherd_core/data_models/testbed/observer.py +7 -23
  21. shepherd_core/data_models/testbed/target.py +1 -1
  22. shepherd_core/data_models/testbed/testbed.py +2 -4
  23. shepherd_core/decoder_waveform/uart.py +9 -26
  24. shepherd_core/fw_tools/__init__.py +1 -3
  25. shepherd_core/fw_tools/converter.py +4 -12
  26. shepherd_core/fw_tools/patcher.py +4 -12
  27. shepherd_core/fw_tools/validation.py +1 -2
  28. shepherd_core/inventory/__init__.py +53 -9
  29. shepherd_core/inventory/system.py +10 -5
  30. shepherd_core/logger.py +1 -3
  31. shepherd_core/reader.py +45 -57
  32. shepherd_core/testbed_client/cache_path.py +15 -0
  33. shepherd_core/testbed_client/client.py +7 -19
  34. shepherd_core/testbed_client/fixtures.py +8 -15
  35. shepherd_core/testbed_client/user_model.py +7 -7
  36. shepherd_core/vsource/virtual_converter_model.py +5 -15
  37. shepherd_core/vsource/virtual_harvester_model.py +2 -3
  38. shepherd_core/vsource/virtual_source_model.py +3 -6
  39. shepherd_core/writer.py +16 -24
  40. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/METADATA +50 -38
  41. shepherd_core-2024.4.1.dist-info/RECORD +64 -0
  42. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/WHEEL +1 -1
  43. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/top_level.txt +0 -1
  44. shepherd_core/data_models/content/_external_fixtures.yaml +0 -394
  45. shepherd_core/data_models/content/energy_environment_fixture.yaml +0 -50
  46. shepherd_core/data_models/content/virtual_harvester_fixture.yaml +0 -159
  47. shepherd_core/data_models/content/virtual_source_fixture.yaml +0 -229
  48. shepherd_core/data_models/testbed/cape_fixture.yaml +0 -94
  49. shepherd_core/data_models/testbed/gpio_fixture.yaml +0 -166
  50. shepherd_core/data_models/testbed/mcu_fixture.yaml +0 -19
  51. shepherd_core/data_models/testbed/observer_fixture.yaml +0 -220
  52. shepherd_core/data_models/testbed/target_fixture.yaml +0 -137
  53. shepherd_core/data_models/testbed/testbed_fixture.yaml +0 -25
  54. shepherd_core-2023.11.1.dist-info/RECORD +0 -117
  55. tests/__init__.py +0 -0
  56. tests/conftest.py +0 -64
  57. tests/data_models/__init__.py +0 -0
  58. tests/data_models/conftest.py +0 -14
  59. tests/data_models/example_cal_data.yaml +0 -31
  60. tests/data_models/example_cal_data_faulty.yaml +0 -29
  61. tests/data_models/example_cal_meas.yaml +0 -178
  62. tests/data_models/example_cal_meas_faulty1.yaml +0 -142
  63. tests/data_models/example_cal_meas_faulty2.yaml +0 -136
  64. tests/data_models/example_config_emulator.yaml +0 -41
  65. tests/data_models/example_config_experiment.yaml +0 -16
  66. tests/data_models/example_config_experiment_alternative.yaml +0 -14
  67. tests/data_models/example_config_harvester.yaml +0 -15
  68. tests/data_models/example_config_testbed.yaml +0 -26
  69. tests/data_models/example_config_virtsource.yaml +0 -78
  70. tests/data_models/test_base_models.py +0 -205
  71. tests/data_models/test_content_fixtures.py +0 -41
  72. tests/data_models/test_content_models.py +0 -288
  73. tests/data_models/test_examples.py +0 -48
  74. tests/data_models/test_experiment_models.py +0 -279
  75. tests/data_models/test_task_generation.py +0 -52
  76. tests/data_models/test_task_models.py +0 -131
  77. tests/data_models/test_testbed_fixtures.py +0 -47
  78. tests/data_models/test_testbed_models.py +0 -187
  79. tests/decoder_waveform/__init__.py +0 -0
  80. tests/decoder_waveform/test_decoder.py +0 -34
  81. tests/fw_tools/__init__.py +0 -0
  82. tests/fw_tools/conftest.py +0 -5
  83. tests/fw_tools/test_converter.py +0 -76
  84. tests/fw_tools/test_patcher.py +0 -66
  85. tests/fw_tools/test_validation.py +0 -56
  86. tests/inventory/__init__.py +0 -0
  87. tests/inventory/test_inventory.py +0 -22
  88. tests/test_cal_hw.py +0 -38
  89. tests/test_examples.py +0 -40
  90. tests/test_logger.py +0 -15
  91. tests/test_reader.py +0 -283
  92. tests/test_writer.py +0 -169
  93. tests/testbed_client/__init__.py +0 -0
  94. tests/vsource/__init__.py +0 -0
  95. tests/vsource/conftest.py +0 -51
  96. tests/vsource/test_converter.py +0 -165
  97. tests/vsource/test_harvester.py +0 -79
  98. tests/vsource/test_z.py +0 -5
  99. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/zip-safe +0 -0
shepherd_core/__init__.py CHANGED
@@ -4,6 +4,7 @@ Provides classes for storing and retrieving sampled IV data to/from
4
4
  HDF5 files.
5
5
 
6
6
  """
7
+
7
8
  from .data_models.base.calibration import Calc_t
8
9
  from .data_models.base.calibration import CalibrationCape
9
10
  from .data_models.base.calibration import CalibrationEmulator
@@ -12,7 +13,7 @@ from .data_models.base.calibration import CalibrationPair
12
13
  from .data_models.base.calibration import CalibrationSeries
13
14
  from .data_models.base.timezone import local_now
14
15
  from .data_models.base.timezone import local_tz
15
- from .data_models.task import Compression
16
+ from .data_models.task.emulation import Compression
16
17
  from .inventory import Inventory
17
18
  from .logger import get_verbose_level
18
19
  from .logger import increase_verbose_level
@@ -22,7 +23,7 @@ from .testbed_client.client import TestbedClient
22
23
  from .testbed_client.client import tb_client
23
24
  from .writer import Writer
24
25
 
25
- __version__ = "2023.11.1"
26
+ __version__ = "2024.04.1"
26
27
 
27
28
  __all__ = [
28
29
  "Reader",
@@ -165,7 +165,7 @@ class CapeData(ShpModel):
165
165
  `See<https://github.com/beagleboard/beaglebone-black/wiki/System-Reference-Manual#824_EEPROM_Data_Format>`_
166
166
  """
167
167
 
168
- header: conbytes(max_length=4) = b"\xAA\x55\x33\xEE"
168
+ header: conbytes(max_length=4) = b"\xaa\x55\x33\xee"
169
169
  eeprom_revision: constr(max_length=2) = "A2"
170
170
  board_name: constr(max_length=32) = "BeagleBone SHEPHERD2 Cape"
171
171
  version: constr(max_length=4) = "24B0"
@@ -207,6 +207,7 @@ class CalibrationCape(ShpModel):
207
207
  cape: data can be supplied
208
208
  Returns:
209
209
  CalibrationCape object with extracted calibration data.
210
+
210
211
  """
211
212
  dv = cls().model_dump(include={"harvester", "emulator"})
212
213
  lw = list(dict_generator(dv))
@@ -225,6 +226,7 @@ class CalibrationCape(ShpModel):
225
226
  Returns
226
227
  -------
227
228
  Byte string representation of calibration values.
229
+
228
230
  """
229
231
  lw = list(dict_generator(self.model_dump(include={"harvester", "emulator"})))
230
232
  values = [walk[-1] for walk in lw]
@@ -1,7 +1,10 @@
1
1
  import hashlib
2
2
  from datetime import datetime
3
3
  from typing import Optional
4
+ from typing import Union
5
+ from uuid import uuid4
4
6
 
7
+ from pydantic import UUID4
5
8
  from pydantic import Field
6
9
  from pydantic import StringConstraints
7
10
  from pydantic import model_validator
@@ -14,34 +17,34 @@ from .timezone import local_now
14
17
  # constr -> to_lower=True, max_length=16, regex=r"^[\w]+$"
15
18
  # ⤷ Regex = AlphaNum
16
19
  IdInt = Annotated[int, Field(ge=0, lt=2**128)]
17
- NameStr = Annotated[
18
- str, StringConstraints(max_length=32, pattern=r'^[^<>:;,?"*|\/\\]+$')
19
- ]
20
+ NameStr = Annotated[str, StringConstraints(max_length=32, pattern=r"^[^<>:;,?\"\*|\/\\]+$")]
20
21
  # ⤷ Regex = FileSystem-Compatible ASCII
21
22
  SafeStr = Annotated[str, StringConstraints(pattern=r"^[ -~]+$")]
22
23
  # ⤷ Regex = All Printable ASCII-Characters with Space
23
24
 
24
25
 
25
26
  def id_default() -> int:
27
+ # note: IdInt has space for 128 bit, so 128/4 = 32 hex-chars
26
28
  time_stamp = str(local_now()).encode("utf-8")
27
- time_hash = hashlib.sha3_224(time_stamp).hexdigest()[-16:]
29
+ time_hash = hashlib.sha3_224(time_stamp).hexdigest()[-32:]
28
30
  return int(time_hash, 16)
29
31
 
30
32
 
31
33
  class ContentModel(ShpModel):
32
34
  # General Properties
33
- id: IdInt = Field( # noqa: A003
35
+ # id: UUID4 = Field( # TODO: db-migration - temp fix for documentation
36
+ id: Union[UUID4, int] = Field(
34
37
  description="Unique ID",
35
- default_factory=id_default,
38
+ default_factory=uuid4,
36
39
  )
37
40
  name: NameStr
38
- description: Annotated[
39
- Optional[SafeStr], Field(description="Required when public")
40
- ] = None
41
+ description: Annotated[Optional[SafeStr], Field(description="Required when public")] = None
41
42
  comment: Optional[SafeStr] = None
42
43
  created: datetime = Field(default_factory=datetime.now)
44
+ updated_last: datetime = Field(default_factory=datetime.now)
43
45
 
44
46
  # Ownership & Access
47
+ # TODO: remove owner & group, only needed for DB
45
48
  owner: NameStr
46
49
  group: Annotated[NameStr, Field(description="University or Subgroup")]
47
50
  visible2group: bool = False
@@ -57,7 +60,6 @@ class ContentModel(ShpModel):
57
60
  is_visible = self.visible2group or self.visible2all
58
61
  if is_visible and self.description is None:
59
62
  raise ValueError(
60
- "Public instances require a description "
61
- "(check visible2*- and description-field)"
63
+ "Public instances require a description (check visible2*- and description-field)"
62
64
  )
63
65
  return self
@@ -7,6 +7,7 @@ from typing import Any
7
7
  from typing import Generator
8
8
  from typing import Optional
9
9
  from typing import Union
10
+ from uuid import UUID
10
11
 
11
12
  import yaml
12
13
  from pydantic import BaseModel
@@ -27,9 +28,7 @@ def path2str(
27
28
 
28
29
 
29
30
  def time2int(dumper: Dumper, data: timedelta) -> ScalarNode:
30
- return dumper.represent_scalar(
31
- "tag:yaml.org,2002:int", str(int(data.total_seconds()))
32
- )
31
+ return dumper.represent_scalar("tag:yaml.org,2002:int", str(int(data.total_seconds())))
33
32
 
34
33
 
35
34
  def generic2str(dumper: Dumper, data: Any) -> ScalarNode:
@@ -41,6 +40,7 @@ yaml.add_representer(pathlib.WindowsPath, path2str, SafeDumper)
41
40
  yaml.add_representer(pathlib.Path, path2str, SafeDumper)
42
41
  yaml.add_representer(timedelta, time2int, SafeDumper)
43
42
  yaml.add_representer(IPv4Address, generic2str, SafeDumper)
43
+ yaml.add_representer(UUID, generic2str, SafeDumper)
44
44
 
45
45
 
46
46
  class ShpModel(BaseModel):
@@ -109,9 +109,7 @@ class ShpModel(BaseModel):
109
109
  def schema_to_file(cls, path: Union[str, Path]) -> None:
110
110
  """Store schema to yaml (for frontend-generators)"""
111
111
  model_dict = cls.model_json_schema()
112
- model_yaml = yaml.safe_dump(
113
- model_dict, default_flow_style=False, sort_keys=False
114
- )
112
+ model_yaml = yaml.safe_dump(model_dict, default_flow_style=False, sort_keys=False)
115
113
  with Path(path).resolve().with_suffix(".yaml").open("w") as f:
116
114
  f.write(model_yaml)
117
115
 
@@ -13,7 +13,7 @@ class EnergyDType(str, Enum):
13
13
  ivsample = "ivsample"
14
14
  ivsamples = "ivsample"
15
15
  ivcurve = "ivcurve"
16
- ivcurves = "ivcurve"
16
+ ivcurves = "ivcurve" # a stream could also be called iv-surface
17
17
  isc_voc = "isc_voc"
18
18
 
19
19
 
@@ -76,7 +76,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
76
76
  logger.debug("VHrv-Inheritances: %s", chain)
77
77
 
78
78
  # post corrections -> should be in separate validator
79
- cal = CalibrationHarvester() # todo: as argument?
79
+ cal = CalibrationHarvester() # TODO: as argument?
80
80
  c_limit = values.get("current_limit_uA", 50_000) # cls.current_limit_uA)
81
81
  values["current_limit_uA"] = max(10**6 * cal.adc_C_Hrv.raw_to_si(4), c_limit)
82
82
 
@@ -25,12 +25,13 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
25
25
  for supplying the Target Node during the experiment.
26
26
  If not already done, the energy will be harvested and then converted.
27
27
  The converter-stage is software defined and offers:
28
- buck-boost-combinations,
29
- a simple diode + resistor and
30
- an intermediate buffer capacitor.
31
- TODO: I,V,R should be in regular unit (V, A, Ohm)
28
+ - buck-boost-combinations,
29
+ - a simple diode + resistor and
30
+ - an intermediate buffer capacitor.
32
31
  """
33
32
 
33
+ # TODO: I,V,R should be in regular unit (V, A, Ohm)
34
+
34
35
  # General Metadata & Ownership -> ContentModel
35
36
 
36
37
  enable_boost: bool = False
@@ -123,14 +124,17 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
123
124
  only has simpler formula, second enabling when V_Cap >= V_out
124
125
 
125
126
  Math behind this calculation:
126
- Energy-Change Storage Cap -> E_new = E_old - E_output
127
- with Energy of a Cap -> E_x = C_x * V_x^2 / 2
128
- combine formulas ->
129
- C_store * V_store_new^2 / 2 = C_store * V_store_old^2 / 2 - C_out * V_out^2 / 2
130
- convert formula to V_new -> V_store_new^2 = V_store_old^2 - (C_out / C_store) * V_out^2
131
- convert into dV -> dV = V_store_new - V_store_old
132
- in case of V_cap = V_out -> dV = V_store_old * (sqrt(1 - C_out / C_store) - 1)
133
- -> dV values will be reversed (negated), because dV is always negative (Voltage drop)
127
+
128
+ - Energy-Change Storage Cap -> E_new = E_old - E_output
129
+ - with Energy of a Cap -> E_x = C_x * V_x^2 / 2
130
+ - combine formulas -> C_store * V_store_new^2 / 2 =
131
+ C_store * V_store_old^2 / 2 - C_out * V_out^2 / 2
132
+ - convert formula to V_new -> V_store_new^2 =
133
+ V_store_old^2 - (C_out / C_store) * V_out^2
134
+ - convert into dV -> dV = V_store_new - V_store_old
135
+ - in case of V_cap = V_out -> dV = V_store_old * (sqrt(1 - C_out / C_store) - 1)
136
+
137
+ Note: dV values will be reversed (negated), because dV is always negative (Voltage drop)
134
138
  """
135
139
  values = {}
136
140
  if self.C_intermediate_uF > 0 and self.C_output_uF > 0:
@@ -153,15 +157,9 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
153
157
  dV_output_imed_low_mV = 0
154
158
 
155
159
  # protect from complex solutions (non valid input combinations)
156
- if not (
157
- isinstance(dV_output_en_thrs_mV, (int, float))
158
- and (dV_output_en_thrs_mV >= 0)
159
- ):
160
+ if not (isinstance(dV_output_en_thrs_mV, (int, float)) and (dV_output_en_thrs_mV >= 0)):
160
161
  dV_output_en_thrs_mV = 0
161
- if not (
162
- isinstance(dV_output_imed_low_mV, (int, float))
163
- and (dV_output_imed_low_mV >= 0)
164
- ):
162
+ if not (isinstance(dV_output_imed_low_mV, (int, float)) and (dV_output_imed_low_mV >= 0)):
165
163
  logger.warning("VSrc: C_output shouldn't be larger than C_intermediate")
166
164
  dV_output_imed_low_mV = 0
167
165
 
@@ -171,9 +169,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
171
169
 
172
170
  if self.V_intermediate_enable_threshold_mV > V_pre_output_mV:
173
171
  values["dV_enable_output_mV"] = dV_output_en_thrs_mV
174
- values[
175
- "V_enable_output_threshold_mV"
176
- ] = self.V_intermediate_enable_threshold_mV
172
+ values["V_enable_output_threshold_mV"] = self.V_intermediate_enable_threshold_mV
177
173
 
178
174
  else:
179
175
  values["dV_enable_output_mV"] = dV_output_imed_low_mV
@@ -182,26 +178,21 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
182
178
  )
183
179
 
184
180
  if self.V_intermediate_disable_threshold_mV > V_pre_output_mV:
185
- values[
186
- "V_disable_output_threshold_mV"
187
- ] = self.V_intermediate_disable_threshold_mV
181
+ values["V_disable_output_threshold_mV"] = self.V_intermediate_disable_threshold_mV
188
182
  else:
189
183
  values["V_disable_output_threshold_mV"] = V_pre_output_mV
190
184
 
191
185
  else:
192
186
  values["dV_enable_output_mV"] = dV_output_en_thrs_mV
193
- values[
194
- "V_enable_output_threshold_mV"
195
- ] = self.V_intermediate_enable_threshold_mV
196
- values[
197
- "V_disable_output_threshold_mV"
198
- ] = self.V_intermediate_disable_threshold_mV
187
+ values["V_enable_output_threshold_mV"] = self.V_intermediate_enable_threshold_mV
188
+ values["V_disable_output_threshold_mV"] = self.V_intermediate_disable_threshold_mV
199
189
  return values
200
190
 
201
191
  def calc_converter_mode(self, *, log_intermediate_node: bool) -> int:
202
192
  """Assembles bitmask from discrete values
193
+
203
194
  log_intermediate_node: record / log virtual intermediate (cap-)voltage and
204
- -current (out) instead of output-voltage and -current
195
+ -current (out) instead of output-voltage and -current
205
196
  """
206
197
  enable_storage = self.C_intermediate_uF > 0
207
198
  enable_boost = self.enable_boost and enable_storage
@@ -286,9 +277,7 @@ class ConverterPRUConfig(ShpModel):
286
277
  states = data.calc_internal_states()
287
278
  return cls(
288
279
  # General
289
- converter_mode=data.calc_converter_mode(
290
- log_intermediate_node=log_intermediate_node
291
- ),
280
+ converter_mode=data.calc_converter_mode(log_intermediate_node=log_intermediate_node),
292
281
  interval_startup_delay_drain_n=round(
293
282
  data.interval_startup_delay_drain_ms * samplerate_sps_default * 1e-3
294
283
  ),
@@ -299,26 +288,16 @@ class ConverterPRUConfig(ShpModel):
299
288
  Constant_us_per_nF_n28=data.calc_cap_constant_us_per_nF_n28(),
300
289
  V_intermediate_init_uV=round(data.V_intermediate_init_mV * 1e3),
301
290
  I_intermediate_leak_nA=round(data.I_intermediate_leak_nA),
302
- V_enable_output_threshold_uV=round(
303
- states["V_enable_output_threshold_mV"] * 1e3
304
- ),
305
- V_disable_output_threshold_uV=round(
306
- states["V_disable_output_threshold_mV"] * 1e3
307
- ),
291
+ V_enable_output_threshold_uV=round(states["V_enable_output_threshold_mV"] * 1e3),
292
+ V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
308
293
  dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
309
294
  interval_check_thresholds_n=round(
310
295
  data.interval_check_thresholds_ms * samplerate_sps_default * 1e-3
311
296
  ),
312
- V_pwr_good_enable_threshold_uV=round(
313
- data.V_pwr_good_enable_threshold_mV * 1e3
314
- ),
315
- V_pwr_good_disable_threshold_uV=round(
316
- data.V_pwr_good_disable_threshold_mV * 1e3
317
- ),
297
+ V_pwr_good_enable_threshold_uV=round(data.V_pwr_good_enable_threshold_mV * 1e3),
298
+ V_pwr_good_disable_threshold_uV=round(data.V_pwr_good_disable_threshold_mV * 1e3),
318
299
  immediate_pwr_good_signal=data.immediate_pwr_good_signal,
319
- V_output_log_gpio_threshold_uV=round(
320
- data.V_output_log_gpio_threshold_mV * 1e3
321
- ),
300
+ V_output_log_gpio_threshold_uV=round(data.V_output_log_gpio_threshold_mV * 1e3),
322
301
  # Boost-Converter
323
302
  V_input_boost_threshold_uV=round(data.V_input_boost_threshold_mV * 1e3),
324
303
  V_intermediate_max_uV=round(data.V_intermediate_max_mV * 1e3),
@@ -330,8 +309,7 @@ class ConverterPRUConfig(ShpModel):
330
309
  LUT_input_I_min_log2_nA=data.LUT_input_I_min_log2_nA,
331
310
  LUT_output_I_min_log2_nA=data.LUT_output_I_min_log2_nA,
332
311
  LUT_inp_efficiency_n8=[
333
- [min(255, round(256 * ival)) for ival in il]
334
- for il in data.LUT_input_efficiency
312
+ [min(255, round(256 * ival)) for ival in il] for il in data.LUT_input_efficiency
335
313
  ],
336
314
  LUT_out_inv_efficiency_n4=[
337
315
  min((2**14), round((2**4) / value)) if (value > 0) else int(2**14)
@@ -2,8 +2,10 @@ from datetime import datetime
2
2
  from datetime import timedelta
3
3
  from typing import List
4
4
  from typing import Optional
5
+ from typing import Union
6
+ from uuid import uuid4
5
7
 
6
- from pydantic import EmailStr
8
+ from pydantic import UUID4
7
9
  from pydantic import Field
8
10
  from pydantic import model_validator
9
11
  from typing_extensions import Annotated
@@ -12,7 +14,6 @@ from typing_extensions import Self
12
14
  from ..base.content import IdInt
13
15
  from ..base.content import NameStr
14
16
  from ..base.content import SafeStr
15
- from ..base.content import id_default
16
17
  from ..base.shepherd import ShpModel
17
18
  from ..testbed.target import Target
18
19
  from ..testbed.testbed import Testbed
@@ -26,11 +27,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
26
27
  """
27
28
 
28
29
  # General Properties
29
- id: IdInt = Field( # noqa: A003
30
- description="Unique ID",
31
- default_factory=id_default,
32
- )
30
+ # id: UUID4 ... # TODO db-migration - temp fix for documentation
31
+ id: Union[UUID4, int] = Field(default_factory=uuid4)
33
32
  # ⤷ TODO: automatic ID is problematic for identification by hash
33
+
34
34
  name: NameStr
35
35
  description: Annotated[
36
36
  Optional[SafeStr], Field(description="Required for public instances")
@@ -38,12 +38,12 @@ class Experiment(ShpModel, title="Config of an Experiment"):
38
38
  comment: Optional[SafeStr] = None
39
39
  created: datetime = Field(default_factory=datetime.now)
40
40
 
41
- # Ownership & Access, TODO
42
- owner_id: Optional[IdInt] = 5472 # UUID?
41
+ # Ownership & Access
42
+ owner_id: Optional[IdInt] = None
43
43
 
44
44
  # feedback
45
- email_results: Optional[EmailStr] = None
46
- # ⤷ TODO: can be bool, as its linked to account
45
+ email_results: bool = False
46
+
47
47
  sys_logging: SystemLogging = SystemLogging(dmesg=True, ptp=True, shepherd=True)
48
48
 
49
49
  # schedule
@@ -80,9 +80,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
80
80
  if len(target_ids) > len(set(target_ids)):
81
81
  raise ValueError("Target-ID used more than once in Experiment!")
82
82
  if len(target_ids) > len(set(custom_ids)):
83
- raise ValueError(
84
- "Custom Target-ID are faulty (some form of id-collisions)!"
85
- )
83
+ raise ValueError("Custom Target-ID are faulty (some form of id-collisions)!")
86
84
 
87
85
  @staticmethod
88
86
  def validate_observers(configs: List[TargetConfig]) -> None:
@@ -92,8 +90,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
92
90
  obs_ids = [testbed.get_observer(_id).id for _id in target_ids]
93
91
  if len(target_ids) > len(set(obs_ids)):
94
92
  raise ValueError(
95
- "Observer used more than once in Experiment "
96
- "-> only 1 target per observer!"
93
+ "Observer used more than once in Experiment -> only 1 target per observer!"
97
94
  )
98
95
 
99
96
  def get_target_ids(self) -> list:
@@ -104,6 +101,4 @@ class Experiment(ShpModel, title="Config of an Experiment"):
104
101
  if target_id in _config.target_IDs:
105
102
  return _config
106
103
  # gets already caught in target_config - but keep:
107
- raise ValueError(
108
- f"Target-ID {target_id} was not found in Experiment '{self.name}'"
109
- )
104
+ raise ValueError(f"Target-ID {target_id} was not found in Experiment '{self.name}'")
@@ -43,9 +43,7 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
43
43
 
44
44
  discard_all = self.discard_current and self.discard_voltage
45
45
  if not self.calculate_power and discard_all:
46
- raise ValueError(
47
- "Error in config -> tracing enabled, but output gets discarded"
48
- )
46
+ raise ValueError("Error in config -> tracing enabled, but output gets discarded")
49
47
  if self.calculate_power:
50
48
  raise ValueError("postprocessing not implemented ATM")
51
49
  return self
@@ -59,9 +57,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
59
57
  # initial recording
60
58
  mask: Annotated[int, Field(ge=0, lt=2**10)] = 0b11_1111_1111 # all
61
59
  # ⤷ TODO: custom mask not implemented in PRU, ATM
62
- gpios: Optional[
63
- Annotated[List[GPIO], Field(min_length=1, max_length=10)]
64
- ] = None # = all
60
+ gpios: Optional[Annotated[List[GPIO], Field(min_length=1, max_length=10)]] = None # = all
65
61
  # ⤷ TODO: list of GPIO to build mask, one of both should be internal / computed field
66
62
 
67
63
  # time
@@ -70,7 +66,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
70
66
 
71
67
  # post-processing,
72
68
  uart_decode: bool = False
73
- # todo: quickfix - uart-log currently done online in userspace
69
+ # TODO: quickfix - uart-log currently done online in userspace
74
70
  # NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
75
71
  uart_pin: GPIO = GPIO(name="GPIO8")
76
72
  uart_baudrate: Annotated[int, Field(ge=2_400, le=921_600)] = 115_200
@@ -108,9 +104,7 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
108
104
  @model_validator(mode="after")
109
105
  def post_validation(self) -> Self:
110
106
  if not self.gpio.user_controllable():
111
- raise ValueError(
112
- f"GPIO '{self.gpio.name}' in actuation-event not controllable by user"
113
- )
107
+ raise ValueError(f"GPIO '{self.gpio.name}' in actuation-event not controllable by user")
114
108
  return self
115
109
 
116
110
  def get_events(self) -> np.ndarray:
@@ -119,11 +113,10 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
119
113
 
120
114
 
121
115
  class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
122
- """Configuration for a GPIO-Actuation-Sequence
123
- TODO: not implemented ATM:
124
- - decide if pru control sys-gpio or
125
- - reverses pru-gpio (preferred if possible)
126
- """
116
+ """Configuration for a GPIO-Actuation-Sequence"""
117
+
118
+ # TODO: not implemented ATM - decide if pru control sys-gpio or
119
+ # TODO: not implemented ATM - reverses pru-gpio (preferred if possible)
127
120
 
128
121
  events: Annotated[List[GpioEvent], Field(min_length=1, max_length=1024)]
129
122
 
@@ -138,6 +131,7 @@ class SystemLogging(ShpModel, title="Config for System-Logging"):
138
131
  ptp: bool = True
139
132
  shepherd: bool = True
140
133
  # TODO: rename to kernel, timesync, sheep
134
+ # TODO: add utilization as option
141
135
 
142
136
 
143
137
  # TODO: some more interaction would be good
@@ -22,18 +22,14 @@ class TargetConfig(ShpModel, title="Target Config"):
22
22
  """Configuration for Target Nodes (DuT)"""
23
23
 
24
24
  target_IDs: Annotated[List[IdInt], Field(min_length=1, max_length=128)]
25
- custom_IDs: Optional[
26
- Annotated[List[IdInt16], Field(min_length=1, max_length=128)]
27
- ] = None
25
+ custom_IDs: Optional[Annotated[List[IdInt16], Field(min_length=1, max_length=128)]] = None
28
26
  # ⤷ will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware
29
27
  # if no custom ID is provided, the original ID of target is used
30
28
 
31
29
  energy_env: EnergyEnvironment # alias: input
32
30
  virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
33
31
  target_delays: Optional[
34
- Annotated[
35
- List[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)
36
- ]
32
+ Annotated[List[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
37
33
  ] = None
38
34
  # ⤷ individual starting times -> allows to use the same environment
39
35
  # TODO: delays not used ATM
@@ -48,9 +44,7 @@ class TargetConfig(ShpModel, title="Target Config"):
48
44
  @model_validator(mode="after")
49
45
  def post_validation(self) -> Self:
50
46
  if not self.energy_env.valid:
51
- raise ValueError(
52
- f"EnergyEnv '{self.energy_env.name}' for target must be valid"
53
- )
47
+ raise ValueError(f"EnergyEnv '{self.energy_env.name}' for target must be valid")
54
48
  for _id in self.target_IDs:
55
49
  target = Target(id=_id)
56
50
  for mcu_num in [1, 2]:
@@ -78,8 +72,7 @@ class TargetConfig(ShpModel, title="Target Config"):
78
72
  t_ids = self.target_IDs
79
73
  if c_ids is not None and (len(set(c_ids)) < len(set(t_ids))):
80
74
  raise ValueError(
81
- f"Provided custom IDs {c_ids} not enough "
82
- f"to cover target range {t_ids}"
75
+ f"Provided custom IDs {c_ids} not enough to cover target range {t_ids}"
83
76
  )
84
77
  # TODO: if custom ids present, firmware must be ELF
85
78
  return self
@@ -32,9 +32,7 @@ __all__ = [
32
32
  ]
33
33
 
34
34
 
35
- def prepare_task(
36
- config: Union[ShpModel, Path, str], observer: Optional[str] = None
37
- ) -> Wrapper:
35
+ def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = None) -> Wrapper:
38
36
  """- Opens file (from Path or str of Path)
39
37
  - wraps task-model
40
38
  - and if it's an TestbedTasks it will extract the correct ObserverTask
@@ -88,9 +86,7 @@ def extract_tasks(shp_wrap: Wrapper, *, no_task_sets: bool = True) -> List[ShpMo
88
86
  content = [ProgrammingTask(**shp_wrap.parameters)]
89
87
  elif shp_wrap.datatype == TestbedTasks.__name__:
90
88
  if no_task_sets:
91
- raise ValueError(
92
- "Model in Wrapper was TestbedTasks -> Task-Sets not allowed!"
93
- )
89
+ raise ValueError("Model in Wrapper was TestbedTasks -> Task-Sets not allowed!")
94
90
  content = [TestbedTasks(**shp_wrap.parameters)]
95
91
  else:
96
92
  raise ValueError("Extractor had unknown task: %s", shp_wrap.datatype)
@@ -29,8 +29,9 @@ class Compression(str, Enum):
29
29
  lzf = "lzf" # not native hdf5
30
30
  gzip1 = 1 # higher compr & load
31
31
  gzip = 1
32
- default = 1
32
+ default = "lzf"
33
33
  null = None
34
+ # NOTE: changed to lzf as BBB needs every straw it can get
34
35
 
35
36
 
36
37
  compressions_allowed: list = [None, "lzf", 1]
@@ -52,8 +53,7 @@ class EmulationTask(ShpModel):
52
53
  force_overwrite: bool = False
53
54
  # ⤷ Overwrite existing file
54
55
  output_compression: Optional[Compression] = Compression.default
55
- # ⤷ should be 1 (level 1 gzip), lzf, or None (order of recommendation)
56
-
56
+ # ⤷ should be lzf, 1 (gzip level 1) or None (order of recommendation)
57
57
  time_start: Optional[datetime] = None
58
58
  # timestamp or unix epoch time, None = ASAP
59
59
  duration: Optional[timedelta] = None
@@ -98,9 +98,7 @@ class EmulationTask(ShpModel):
98
98
  # convert & add local timezone-data
99
99
  has_time = values.get("time_start") is not None
100
100
  if has_time and isinstance(values["time_start"], (int, float)):
101
- values["time_start"] = datetime.fromtimestamp(
102
- values["time_start"], tz=local_tz()
103
- )
101
+ values["time_start"] = datetime.fromtimestamp(values["time_start"], tz=local_tz())
104
102
  if has_time and isinstance(values["time_start"], str):
105
103
  values["time_start"] = datetime.fromisoformat(values["time_start"])
106
104
  if has_time and values["time_start"].tzinfo is None:
@@ -123,18 +121,14 @@ class EmulationTask(ShpModel):
123
121
  "main",
124
122
  "buffer",
125
123
  }:
126
- raise ValueError(
127
- "Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'."
128
- )
124
+ raise ValueError("Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'.")
129
125
  if self.gpio_actuation is not None:
130
126
  raise ValueError("GPIO Actuation not yet implemented!")
131
127
  return self
132
128
 
133
129
  @classmethod
134
130
  @validate_call
135
- def from_xp(
136
- cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path
137
- ) -> Self:
131
+ def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path) -> Self:
138
132
  obs = tb.get_observer(tgt_id)
139
133
  tgt_cfg = xp.get_target_config(tgt_id)
140
134
 
@@ -144,8 +138,7 @@ class EmulationTask(ShpModel):
144
138
  time_start=copy.copy(xp.time_start),
145
139
  duration=xp.duration,
146
140
  abort_on_error=xp.abort_on_error,
147
- enable_io=(tgt_cfg.gpio_tracing is not None)
148
- or (tgt_cfg.gpio_actuation is not None),
141
+ enable_io=(tgt_cfg.gpio_tracing is not None) or (tgt_cfg.gpio_actuation is not None),
149
142
  io_port=obs.get_target_port(tgt_id),
150
143
  pwr_port=obs.get_target_port(tgt_id),
151
144
  virtual_source=tgt_cfg.virtual_source,
@@ -40,9 +40,7 @@ class FirmwareModTask(ShpModel):
40
40
  FirmwareDType.base64_hex,
41
41
  FirmwareDType.path_hex,
42
42
  }:
43
- logger.warning(
44
- "Firmware is scheduled to get custom-ID but is not in elf-format"
45
- )
43
+ logger.warning("Firmware is scheduled to get custom-ID but is not in elf-format")
46
44
  return self
47
45
 
48
46
  @classmethod
@@ -60,9 +60,7 @@ class HarvestTask(ShpModel):
60
60
  # convert & add local timezone-data, TODO: used twice, refactor
61
61
  has_time = values.get("time_start") is not None
62
62
  if has_time and isinstance(values["time_start"], (int, float)):
63
- values["time_start"] = datetime.fromtimestamp(
64
- values["time_start"], tz=local_tz()
65
- )
63
+ values["time_start"] = datetime.fromtimestamp(values["time_start"], tz=local_tz())
66
64
  if has_time and isinstance(values["time_start"], str):
67
65
  values["time_start"] = datetime.fromisoformat(values["time_start"])
68
66
  if has_time and values["time_start"].tzinfo is None:
@@ -21,7 +21,7 @@ class ObserverTasks(ShpModel):
21
21
  """Collection of tasks for selected observer included in experiment"""
22
22
 
23
23
  observer: NameStr
24
- owner_id: IdInt
24
+ owner_id: Optional[IdInt] # TODO: set to optional for now, shouldn't be
25
25
 
26
26
  # PRE PROCESS
27
27
  time_prep: datetime # TODO: should be optional