shepherd-core 2025.8.1__py3-none-any.whl → 2026.2.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 (82) hide show
  1. shepherd_core/config.py +1 -1
  2. shepherd_core/data_models/__init__.py +8 -4
  3. shepherd_core/data_models/base/cal_measurement.py +7 -2
  4. shepherd_core/data_models/base/calibration.py +23 -12
  5. shepherd_core/data_models/base/content.py +12 -2
  6. shepherd_core/data_models/base/shepherd.py +13 -4
  7. shepherd_core/data_models/base/wrapper.py +2 -0
  8. shepherd_core/data_models/content/__init__.py +8 -4
  9. shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
  10. shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
  11. shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
  12. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
  13. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
  14. shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
  15. shepherd_core/data_models/content/energy_environment.py +341 -23
  16. shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
  17. shepherd_core/data_models/content/enum_datatypes.py +109 -0
  18. shepherd_core/data_models/content/firmware.py +44 -16
  19. shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
  20. shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
  21. shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
  22. shepherd_core/data_models/content/virtual_storage_config.py +429 -0
  23. shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
  24. shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
  25. shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
  26. shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
  27. shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
  28. shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
  29. shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
  30. shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
  31. shepherd_core/data_models/experiment/experiment.py +38 -13
  32. shepherd_core/data_models/experiment/observer_features.py +17 -4
  33. shepherd_core/data_models/experiment/target_config.py +56 -8
  34. shepherd_core/data_models/task/__init__.py +13 -2
  35. shepherd_core/data_models/task/emulation.py +10 -6
  36. shepherd_core/data_models/task/firmware_mod.py +3 -1
  37. shepherd_core/data_models/task/harvest.py +3 -1
  38. shepherd_core/data_models/task/helper_paths.py +2 -2
  39. shepherd_core/data_models/task/observer_tasks.py +8 -6
  40. shepherd_core/data_models/task/programming.py +4 -2
  41. shepherd_core/data_models/task/testbed_tasks.py +8 -2
  42. shepherd_core/data_models/testbed/cape.py +2 -0
  43. shepherd_core/data_models/testbed/gpio.py +2 -0
  44. shepherd_core/data_models/testbed/mcu.py +2 -0
  45. shepherd_core/data_models/testbed/observer.py +2 -0
  46. shepherd_core/data_models/testbed/target.py +7 -5
  47. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  48. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  49. shepherd_core/data_models/testbed/testbed.py +17 -15
  50. shepherd_core/decoder_waveform/uart.py +1 -1
  51. shepherd_core/exit_handler.py +22 -0
  52. shepherd_core/fw_tools/converter.py +2 -2
  53. shepherd_core/fw_tools/validation.py +1 -1
  54. shepherd_core/inventory/__init__.py +23 -21
  55. shepherd_core/inventory/system.py +3 -3
  56. shepherd_core/logger.py +0 -1
  57. shepherd_core/reader.py +32 -27
  58. shepherd_core/testbed_client/cache_path.py +3 -3
  59. shepherd_core/testbed_client/client_abc_fix.py +14 -3
  60. shepherd_core/testbed_client/client_web.py +7 -5
  61. shepherd_core/testbed_client/fixtures.py +7 -7
  62. shepherd_core/version.py +1 -1
  63. shepherd_core/vsource/__init__.py +4 -0
  64. shepherd_core/vsource/virtual_converter_model.py +29 -28
  65. shepherd_core/vsource/virtual_harvester_model.py +29 -21
  66. shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
  67. shepherd_core/vsource/virtual_source_model.py +18 -14
  68. shepherd_core/vsource/virtual_source_simulation.py +71 -73
  69. shepherd_core/vsource/virtual_storage_model.py +164 -0
  70. shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
  71. shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
  72. shepherd_core/vsource/virtual_storage_simulator.py +104 -0
  73. shepherd_core/writer.py +16 -9
  74. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
  75. shepherd_core-2026.2.1.dist-info/RECORD +102 -0
  76. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
  77. shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
  78. shepherd_core/data_models/content/firmware_datatype.py +0 -15
  79. shepherd_core/data_models/virtual_source_doc.txt +0 -207
  80. shepherd_core-2025.8.1.dist-info/RECORD +0 -83
  81. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
  82. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/zip-safe +0 -0
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from typing import Annotated
8
8
  from typing import Any
9
9
  from typing import TypedDict
10
+ from typing import final
10
11
 
11
12
  from pydantic import StringConstraints
12
13
  from pydantic import model_validator
@@ -20,7 +21,7 @@ from shepherd_core.data_models.testbed.mcu import MCU
20
21
  from shepherd_core.logger import log
21
22
  from shepherd_core.testbed_client import tb_client
22
23
 
23
- from .firmware_datatype import FirmwareDType
24
+ from .enum_datatypes import FirmwareDType
24
25
 
25
26
  suffix_to_DType: dict = {
26
27
  # derived from wikipedia
@@ -49,17 +50,18 @@ arch_to_mcu: dict = {
49
50
  FirmwareStr = Annotated[str, StringConstraints(min_length=3, max_length=8_000_000)]
50
51
 
51
52
 
53
+ @final
52
54
  class Firmware(ContentModel, title="Firmware of Target"):
53
55
  """meta-data representation of a data-component."""
54
56
 
55
- # General Metadata & Ownership -> ContentModel
57
+ # General Metadata & Ownership -> see ContentModel
56
58
 
57
59
  mcu: MCU
58
60
 
59
61
  data: FirmwareStr | Path
60
62
  data_type: FirmwareDType
61
63
  data_hash: str | None = None
62
- data_local: bool = True
64
+ data_2_copy: bool = True
63
65
  """ ⤷ signals that file has to be copied to testbed"""
64
66
 
65
67
  @model_validator(mode="before")
@@ -67,18 +69,18 @@ class Firmware(ContentModel, title="Firmware of Target"):
67
69
  def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
68
70
  values, _ = tb_client.try_completing_model(cls.__name__, values)
69
71
  # crosscheck type with actual data
70
- _type = values.get("data_type")
71
- if _type in {
72
+ dtype = values.get("data_type")
73
+ if dtype in {
72
74
  FirmwareDType.base64_hex,
73
75
  FirmwareDType.base64_elf,
74
76
  }:
75
77
  try:
76
- _hash = fw_tools.base64_to_hash(values.get("data"))
78
+ dhash = fw_tools.base64_to_hash(values.get("data"))
77
79
  except ValueError:
78
80
  raise ValueError("Embedded Firmware seems to be faulty") from None
79
- if values.get("data_hash") is not None and _hash != values.get("data_hash"):
81
+ if values.get("data_hash") is not None and dhash != values.get("data_hash"):
80
82
  raise ValueError("Embedded Firmware and Hash do not match!")
81
- elif _type in {
83
+ elif dtype in {
82
84
  FirmwareDType.path_hex,
83
85
  FirmwareDType.path_elf,
84
86
  }:
@@ -105,10 +107,10 @@ class Firmware(ContentModel, title="Firmware of Target"):
105
107
  kwargs["data_hash"] = fw_tools.file_to_hash(file)
106
108
  if embed:
107
109
  kwargs["data"] = fw_tools.file_to_base64(file)
108
- kwargs["data_local"] = False
110
+ kwargs["data_2_copy"] = False
109
111
  else:
110
112
  kwargs["data"] = Path(file).as_posix()
111
- kwargs["data_local"] = True
113
+ kwargs["data_2_copy"] = True
112
114
 
113
115
  if "data_type" not in kwargs:
114
116
  kwargs["data_type"] = suffix_to_DType[file.suffix.lower()]
@@ -145,16 +147,22 @@ class Firmware(ContentModel, title="Firmware of Target"):
145
147
  kwargs["name"] = file.name
146
148
  return cls(**kwargs)
147
149
 
148
- def compare_hash(self, path: Path | None = None) -> bool:
150
+ def compare_hash(self, data: Path | str | None = None) -> bool:
149
151
  if self.data_hash is None:
150
152
  return True
151
153
 
152
- if path is not None and path.is_file():
153
- hash_new = fw_tools.file_to_hash(path)
154
+ if data is None:
155
+ # use included data if nothing is provided
156
+ data = self.data
157
+
158
+ if isinstance(data, Path) and data.is_file():
159
+ hash_new = fw_tools.file_to_hash(data)
154
160
  match = self.data_hash == hash_new
155
- else:
156
- hash_new = fw_tools.base64_to_hash(self.data)
161
+ elif isinstance(data, str):
162
+ hash_new = fw_tools.base64_to_hash(data)
157
163
  match = self.data_hash == hash_new
164
+ else:
165
+ match = False
158
166
 
159
167
  if not match:
160
168
  log.warning("FW-Hash does not match with stored value!")
@@ -169,7 +177,27 @@ class Firmware(ContentModel, title="Firmware of Target"):
169
177
  - if provided path is a directory, the firmware-name is used
170
178
  """
171
179
  if file.is_dir():
172
- file = file / self.name
180
+ file /= self.name
173
181
  file_new = fw_tools.extract_firmware(self.data, self.data_type, file)
174
182
  self.compare_hash(file_new)
175
183
  return file_new
184
+
185
+ def exists(self) -> bool:
186
+ """Check if embedded file exists."""
187
+ if self.data_type in [FirmwareDType.path_hex, FirmwareDType.path_elf]:
188
+ if not isinstance(self.data, Path):
189
+ raise ValueError("Firmware.data is not a Path (but type-property claims so)")
190
+ return self.data.exists()
191
+ return True
192
+
193
+ def check(self) -> bool:
194
+ """Check if embedded file is still valid or unchanged."""
195
+ valid = True
196
+ if self.data_type in [FirmwareDType.path_hex, FirmwareDType.path_elf]:
197
+ valid &= isinstance(self.data, Path) and self.data.exists()
198
+ if self.data_type in [FirmwareDType.base64_elf, FirmwareDType.base64_hex]:
199
+ valid &= isinstance(self.data, str)
200
+ # TODO: could also begin unpacking base64
201
+ # TODO: could also verify hex, elf
202
+
203
+ return valid & self.compare_hash()
@@ -1,9 +1,9 @@
1
1
  """Generalized energy harvester data models."""
2
2
 
3
3
  from collections.abc import Mapping
4
- from enum import Enum
5
4
  from typing import Annotated
6
5
  from typing import Any
6
+ from typing import final
7
7
 
8
8
  from pydantic import Field
9
9
  from pydantic import model_validator
@@ -16,95 +16,11 @@ from shepherd_core.data_models.base.shepherd import ShpModel
16
16
  from shepherd_core.logger import log
17
17
  from shepherd_core.testbed_client import tb_client
18
18
 
19
- from .energy_environment import EnergyDType
20
-
21
-
22
- class AlgorithmDType(str, Enum):
23
- """Options for choosing a harvesting algorithm."""
24
-
25
- direct = disable = neutral = "neutral"
26
- """
27
- Reads an energy environment as is without selecting a harvesting
28
- voltage.
29
-
30
- Used to play "constant-power" energy environments or simple
31
- "on-off-patterns". Generally, not useful for virtual source
32
- emulation.
33
-
34
- Not applicable to real harvesting, only emulation with IVTrace / samples.
35
- """
36
-
37
- isc_voc = "isc_voc"
38
- """
39
- Short Circuit Current, Open Circuit Voltage.
40
-
41
- This is not relevant for emulation, but used to configure recording of
42
- energy environments.
43
-
44
- This mode samples the two extremes of an IV curve, which may be
45
- interesting to characterize a transducer/energy environment.
46
-
47
- Not applicable to emulation - only recordable during harvest-recording ATM.
48
- """
49
-
50
- ivcurve = ivcurves = ivsurface = "ivcurve"
51
- """
52
- Used during harvesting to record the full IV surface.
53
-
54
- When configuring the energy environment recording, this algorithm
55
- records the IV surface by repeatedly recording voltage and current
56
- while ramping the voltage.
57
-
58
- Cannot be used as output of emulation.
59
- """
60
-
61
- constant = cv = "cv"
62
- """
63
- Harvest energy at a fixed predefined voltage ('voltage_mV').
64
-
65
- For harvesting, this records the IV samples at the specified voltage.
66
- For emulation, this virtually harvests the IV surface at the specified voltage.
67
-
68
- In addition to constant voltage harvesting, this can be used together
69
- with the 'feedback_to_hrv' flag to implement a "Capacitor and Diode"
70
- topology, where the harvesting voltage depends dynamically on the
71
- capacitor voltage.
72
- """
73
-
74
- # ci .. constant current -> is this desired?
75
-
76
- mppt_voc = "mppt_voc"
77
- """
78
- Emulate a harvester with maximum power point (MPP) tracking based on
79
- open circuit voltage measurements.
80
-
81
- This MPPT heuristic estimates the MPP as a constant ratio of the open
82
- circuit voltage.
83
-
84
- Used in conjunction with 'setpoint_n', 'interval_ms', and 'duration_ms'.
85
- """
86
-
87
- mppt_po = perturb_observe = "mppt_po"
88
- """
89
- Emulate a harvester with perturb and observe maximum power point
90
- tracking.
91
-
92
- This MPPT heuristic adjusts the harvesting voltage by small amounts and
93
- checks if the power increases. Eventually, the tracking changes the
94
- direction of adjustments and oscillates around the MPP.
95
- """
96
-
97
- mppt_opt = optimal = "mppt_opt"
98
- """
99
- A theoretical harvester that identifies the MPP by reading it from the
100
- IV curve during emulation.
101
-
102
- Note that this is not possible for real-world harvesting as the system would
103
- not know the entire IV curve. In that case a very fast and detailed mppt_po is
104
- used.
105
- """
19
+ from .enum_datatypes import EnergyDType
20
+ from .enum_datatypes import HarvestAlgorithmDType
106
21
 
107
22
 
23
+ @final
108
24
  class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
109
25
  """The virtual harvester configuration characterizes usage of an energy environment.
110
26
 
@@ -150,9 +66,9 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
150
66
  storage.
151
67
  """
152
68
 
153
- # General Metadata & Ownership -> ContentModel
69
+ # General Metadata & Ownership -> see ContentModel
154
70
 
155
- algorithm: AlgorithmDType
71
+ algorithm: HarvestAlgorithmDType
156
72
  """The algorithm determines how the harvester chooses the harvesting voltage.
157
73
  """
158
74
 
@@ -332,11 +248,11 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
332
248
  @model_validator(mode="after")
333
249
  def post_validation(self) -> Self:
334
250
  if self.voltage_min_mV > self.voltage_max_mV:
335
- raise ValueError("Voltage min > max")
251
+ raise ValueError("Voltage minimum > max")
336
252
  if self.voltage_mV < self.voltage_min_mV:
337
- raise ValueError("Voltage below min")
253
+ raise ValueError("Voltage below minimum")
338
254
  if self.voltage_mV > self.voltage_max_mV:
339
- raise ValueError("Voltage above max")
255
+ raise ValueError("Voltage above maximum")
340
256
 
341
257
  return self
342
258
 
@@ -369,12 +285,12 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
369
285
 
370
286
  interval_ms = min(max(self.interval_ms, time_min_ms), 1_000_000)
371
287
  duration_ms = min(max(self.duration_ms, time_min_ms), interval_ms)
372
- _ratio = (duration_ms / interval_ms) / (self.duration_ms / self.interval_ms)
373
- if (_ratio - 1) > 0.1:
288
+ ratio = (duration_ms / interval_ms) / (self.duration_ms / self.interval_ms)
289
+ if (ratio - 1) > 0.1:
374
290
  log.debug(
375
291
  "Ratio between interval & duration has changed "
376
292
  "more than 10%% due to constraints (%.4f)",
377
- _ratio,
293
+ ratio,
378
294
  )
379
295
  return interval_ms, duration_ms
380
296
 
@@ -436,6 +352,7 @@ ALGO_TO_DTYPE: Mapping[str, EnergyDType] = {
436
352
  }
437
353
 
438
354
 
355
+ @final
439
356
  class HarvesterPRUConfig(ShpModel):
440
357
  """Map settings-list to internal state-vars struct HarvesterConfig for PRU.
441
358
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import Annotated
4
4
  from typing import Any
5
+ from typing import final
5
6
 
6
7
  from pydantic import Field
7
8
  from pydantic import model_validator
@@ -13,9 +14,10 @@ from shepherd_core.data_models.base.shepherd import ShpModel
13
14
  from shepherd_core.logger import log
14
15
  from shepherd_core.testbed_client import tb_client
15
16
 
16
- from .energy_environment import EnergyDType
17
- from .virtual_harvester import HarvesterPRUConfig
18
- from .virtual_harvester import VirtualHarvesterConfig
17
+ from .enum_datatypes import EnergyDType
18
+ from .virtual_harvester_config import HarvesterPRUConfig
19
+ from .virtual_harvester_config import VirtualHarvesterConfig
20
+ from .virtual_storage_config import VirtualStorageConfig
19
21
 
20
22
  # Custom Types
21
23
  LUT_SIZE: int = 12
@@ -27,6 +29,7 @@ LUT2D = Annotated[list[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
27
29
  vhrv_mppt_opt = VirtualHarvesterConfig(name="mppt_opt")
28
30
 
29
31
 
32
+ @final
30
33
  class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
31
34
  """The vSrc uses the energy environment (file) for supplying the Target Node.
32
35
 
@@ -41,42 +44,49 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
41
44
 
42
45
  # TODO: I,V,R should be in regular unit (V, A, Ohm)
43
46
 
44
- # General Metadata & Ownership -> ContentModel
47
+ # General Metadata & Ownership -> see ContentModel
45
48
 
46
49
  enable_boost: bool = False
47
50
  """ ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable"""
48
51
  enable_buck: bool = False
49
52
  """ ⤷ if false -> v_output = v_intermediate"""
50
53
  enable_feedback_to_hrv: bool = False
51
- """ src can control a cv-harvester for ivcurve"""
54
+ """ Source can control a cv-harvester for ivsurface.
55
+ Feedback is essential for some harvesters, i.e. diode-circuitry.
56
+ """
52
57
 
53
58
  interval_startup_delay_drain_ms: Annotated[float, Field(ge=0, le=10_000)] = 0
59
+ """ ⤷ Model begins running, but Target is not draining the storage capacitor
60
+ until this delay is over.
61
+ """
54
62
 
55
63
  harvester: VirtualHarvesterConfig = vhrv_mppt_opt
64
+ """ ⤷ Only active / needed if input is ivsurface. """
56
65
 
57
66
  V_input_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
67
+ """ ⤷ Maximum input Voltage [mV] -> will be clipped."""
58
68
  I_input_max_mA: Annotated[float, Field(ge=0, le=4.29e3)] = 4_200
69
+ """ ⤷ Maximum input Current [mA] -> will be clipped."""
59
70
  V_input_drop_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 0
60
- """ ⤷ simulate input-diode"""
71
+ """ ⤷ simulate voltage drop for input-diode or LDO."""
61
72
  R_input_mOhm: Annotated[float, Field(ge=0, le=4.29e6)] = 0
62
73
  """ ⤷ resistance only active with disabled boost, range [1 mOhm; 1MOhm]"""
63
74
 
64
- # primary storage-Cap
65
- C_intermediate_uF: Annotated[float, Field(ge=0, le=100_000)] = 0
66
- V_intermediate_init_mV: Annotated[float, Field(ge=0, le=10_000)] = 3_000
67
- """ ⤷ allow a proper / fast startup"""
68
- I_intermediate_leak_nA: Annotated[float, Field(ge=0, le=4.29e9)] = 0
69
-
70
- V_intermediate_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 1
75
+ storage: VirtualStorageConfig | None = None
76
+ """ primary intermediate energy storage between boost- and buck-converter stage.
77
+ Selecting "None" disables the storage and directly connects input to output.
78
+ """
79
+ V_intermediate_enable_output_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 1
71
80
  """ ⤷ target gets connected (hysteresis-combo with next value)"""
72
- V_intermediate_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
81
+ V_intermediate_disable_output_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
73
82
  """ ⤷ target gets disconnected"""
74
83
  interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
75
84
  """ ⤷ some ICs (BQ) check every 64 ms if output should be disconnected"""
76
85
  # TODO: add intervals for input-disable, output-disable & power-good-signal
77
86
 
78
- # pwr-good: target is informed on output-pin (hysteresis) -> for intermediate voltage
79
87
  V_pwr_good_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2_800
88
+ """ pwr-good: target is informed on output-pin (hysteresis)
89
+ -> reference is the intermediate voltage """
80
90
  V_pwr_good_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2200
81
91
  immediate_pwr_good_signal: bool = True
82
92
  """ ⤷ 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds"""
@@ -93,13 +103,13 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
93
103
 
94
104
  # Extra
95
105
  V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
96
- """ ⤷ min voltage needed to enable recording changes in gpio-bank"""
106
+ """ ⤷ minimum voltage threshold needed to enable recording changes in gpio-bank"""
97
107
 
98
108
  # Boost Converter
99
109
  V_input_boost_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
100
- """ ⤷ min input-voltage for the boost converter to work"""
110
+ """ ⤷ minimum input-voltage for the boost converter to work"""
101
111
  V_intermediate_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
102
- """ ⤷ boost converter shuts off"""
112
+ """ ⤷ threshold for shutting off boost converter """
103
113
 
104
114
  LUT_input_efficiency: LUT2D = 12 * [12 * [1.00]]
105
115
  """ ⤷ rows are current -> first row a[V=0][:]
@@ -115,13 +125,18 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
115
125
 
116
126
  # Buck Converter
117
127
  V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
128
+ """ Fixed Voltage of Buck-Converter.
129
+ (as long as Input is > Output + Drop-Voltage)
130
+ """
118
131
  V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
119
- """ ⤷ simulate LDO / diode min voltage differential or output-diode"""
132
+ """ ⤷ simulate LDO / diode minimum voltage differential or output-diode"""
120
133
 
121
134
  LUT_output_efficiency: LUT1D = 12 * [1.00]
122
- """ ⤷ array[12] depending on output_current"""
135
+ """ ⤷ array[12] depending on output_current, In- & Output is linear."""
123
136
  LUT_output_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
124
- """ ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation"""
137
+ """ ⤷ i.e. 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA,
138
+ see notes on LUT_input for explanation
139
+ """
125
140
 
126
141
  @model_validator(mode="before")
127
142
  @classmethod
@@ -129,12 +144,16 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
129
144
  values, chain = tb_client.try_completing_model(cls.__name__, values)
130
145
  values = tb_client.fill_in_user_data(values)
131
146
  log.debug("VSrc-Inheritances: %s", chain)
147
+ # TODO: most "internal states" should be corrected here
148
+
132
149
  return values
133
150
 
134
151
  @model_validator(mode="after")
135
152
  def post_validation(self) -> Self:
136
153
  # trigger stricter test of harv-parameters
137
154
  HarvesterPRUConfig.from_vhrv(self.harvester, for_emu=True)
155
+ # TODO: enable threshold < mid_max
156
+ # TODO: mid_max < mid_soc1
138
157
  return self
139
158
 
140
159
  def calc_internal_states(self) -> dict:
@@ -148,6 +167,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
148
167
  - the converter always turns on with "V_storage_enable_threshold_uV".
149
168
 
150
169
  TODO: currently neglecting delay after disabling converter, boost
170
+ TODO: warn and explain when altering config due to boundaries (transparency)
151
171
  only has simpler formula, second enabling when V_Cap >= V_out
152
172
 
153
173
  Math behind this calculation:
@@ -164,11 +184,11 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
164
184
  Note: dV values will be reversed (negated), because dV is always negative (Voltage drop)
165
185
  """
166
186
  values = {}
167
- if self.C_intermediate_uF > 0 and self.C_output_uF > 0:
187
+ if (self.storage is not None) and self.C_output_uF > 0:
168
188
  # first case: storage cap outside of en/dis-thresholds
169
- v_old = self.V_intermediate_enable_threshold_mV
189
+ v_old = self.V_intermediate_enable_output_threshold_mV
170
190
  v_out = self.V_output_mV
171
- c_store = self.C_intermediate_uF
191
+ c_store = self.storage.capacity_in_uF
172
192
  c_out = self.C_output_uF
173
193
  dV_output_en_thrs_mV = v_old - pow(
174
194
  pow(v_old, 2) - (c_out / c_store) * pow(v_out, 2),
@@ -183,6 +203,16 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
183
203
  dV_output_en_thrs_mV = 0
184
204
  dV_output_imed_low_mV = 0
185
205
 
206
+ if self.enable_boost and self.storage is not None:
207
+ # TODO: storage could have maximum at a different SoC, is this needed at all?
208
+ values["V_mid_max_mV"] = min(
209
+ self.V_intermediate_max_mV,
210
+ 1e3 * self.storage.calc_V_OC(SoC=1.0),
211
+ 10_000,
212
+ )
213
+ else:
214
+ values["V_mid_max_mV"] = self.V_intermediate_max_mV
215
+
186
216
  # protect from complex solutions (non valid input combinations)
187
217
  if not (isinstance(dV_output_en_thrs_mV, (int, float)) and (dV_output_en_thrs_mV >= 0)):
188
218
  dV_output_en_thrs_mV = 0
@@ -194,25 +224,33 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
194
224
  if self.enable_buck > 0:
195
225
  V_pre_output_mV = self.V_output_mV + self.V_buck_drop_mV
196
226
 
197
- if self.V_intermediate_enable_threshold_mV > V_pre_output_mV:
198
- values["dV_enable_output_mV"] = dV_output_en_thrs_mV
199
- values["V_enable_output_threshold_mV"] = self.V_intermediate_enable_threshold_mV
227
+ if self.V_intermediate_enable_output_threshold_mV > V_pre_output_mV:
228
+ values["dV_mid_enable_output_mV"] = dV_output_en_thrs_mV
229
+ values["V_mid_enable_output_threshold_mV"] = (
230
+ self.V_intermediate_enable_output_threshold_mV
231
+ )
200
232
 
201
233
  else:
202
- values["dV_enable_output_mV"] = dV_output_imed_low_mV
203
- values["V_enable_output_threshold_mV"] = (
204
- V_pre_output_mV + values["dV_enable_output_mV"]
234
+ values["dV_mid_enable_output_mV"] = dV_output_imed_low_mV
235
+ values["V_mid_enable_output_threshold_mV"] = (
236
+ V_pre_output_mV + values["dV_mid_enable_output_mV"]
205
237
  )
206
238
 
207
- if self.V_intermediate_disable_threshold_mV > V_pre_output_mV:
208
- values["V_disable_output_threshold_mV"] = self.V_intermediate_disable_threshold_mV
239
+ if self.V_intermediate_disable_output_threshold_mV > V_pre_output_mV:
240
+ values["V_mid_disable_output_threshold_mV"] = (
241
+ self.V_intermediate_disable_output_threshold_mV
242
+ )
209
243
  else:
210
- values["V_disable_output_threshold_mV"] = V_pre_output_mV
244
+ values["V_mid_disable_output_threshold_mV"] = V_pre_output_mV
211
245
 
212
246
  else:
213
- values["dV_enable_output_mV"] = dV_output_en_thrs_mV
214
- values["V_enable_output_threshold_mV"] = self.V_intermediate_enable_threshold_mV
215
- values["V_disable_output_threshold_mV"] = self.V_intermediate_disable_threshold_mV
247
+ values["dV_mid_enable_output_mV"] = dV_output_en_thrs_mV
248
+ values["V_mid_enable_output_threshold_mV"] = (
249
+ self.V_intermediate_enable_output_threshold_mV
250
+ )
251
+ values["V_mid_disable_output_threshold_mV"] = (
252
+ self.V_intermediate_disable_output_threshold_mV
253
+ )
216
254
  return values
217
255
 
218
256
  def calc_converter_mode(self, dtype_in: EnergyDType, *, log_intermediate_node: bool) -> int:
@@ -221,7 +259,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
221
259
  log_intermediate_node: record / log virtual intermediate (cap-)voltage and
222
260
  -current (out) instead of output-voltage and -current
223
261
  """
224
- enable_storage = self.C_intermediate_uF > 0
262
+ enable_storage = self.storage is not None
225
263
  enable_boost = self.enable_boost and enable_storage
226
264
  if enable_boost != self.enable_boost:
227
265
  log.warning("VSrc - boost was disabled due to missing storage capacitor!")
@@ -244,14 +282,6 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
244
282
  + 16 * int(enable_feedback)
245
283
  )
246
284
 
247
- def calc_cap_constant_us_per_nF_n28(self) -> int:
248
- """Calc constant to convert capacitor-current to Voltage-delta.
249
-
250
- dV[uV] = constant[us/nF] * current[nA] = constant[us*V/nAs] * current[nA]
251
- """
252
- C_cap_uF = max(self.C_intermediate_uF, 0.001)
253
- return int((10**3 * (2**28)) // (C_cap_uF * config.SAMPLERATE_SPS))
254
-
255
285
 
256
286
  u32 = Annotated[int, Field(ge=0, lt=2**32)]
257
287
  u8 = Annotated[int, Field(ge=0, lt=2**8)]
@@ -265,6 +295,7 @@ lut_i = Annotated[
265
295
  lut_o = Annotated[list[u32], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
266
296
 
267
297
 
298
+ @final
268
299
  class ConverterPRUConfig(ShpModel):
269
300
  """Map settings-list to internal state-vars struct ConverterConfig.
270
301
 
@@ -283,13 +314,9 @@ class ConverterPRUConfig(ShpModel):
283
314
  R_input_kOhm_n22: u32
284
315
  # ⤷ TODO: possible optimization: n32 (range 1uOhm to 1 kOhm) is easier to calc in pru
285
316
 
286
- Constant_us_per_nF_n28: u32
287
- V_intermediate_init_uV: u32
288
- I_intermediate_leak_nA: u32
289
-
290
- V_enable_output_threshold_uV: u32
291
- V_disable_output_threshold_uV: u32
292
- dV_enable_output_uV: u32
317
+ V_mid_enable_output_threshold_uV: u32
318
+ V_mid_disable_output_threshold_uV: u32
319
+ dV_mid_enable_output_uV: u32
293
320
  interval_check_thresholds_n: u32
294
321
 
295
322
  V_pwr_good_enable_threshold_uV: u32
@@ -299,7 +326,7 @@ class ConverterPRUConfig(ShpModel):
299
326
  V_output_log_gpio_threshold_uV: u32
300
327
 
301
328
  V_input_boost_threshold_uV: u32
302
- V_intermediate_max_uV: u32
329
+ V_mid_max_uV: u32
303
330
 
304
331
  V_output_uV: u32
305
332
  V_buck_drop_uV: u32
@@ -331,12 +358,13 @@ class ConverterPRUConfig(ShpModel):
331
358
  I_input_max_nA=round(data.I_input_max_mA * 1e6),
332
359
  V_input_drop_uV=round(data.V_input_drop_mV * 1e3),
333
360
  R_input_kOhm_n22=round(data.R_input_mOhm * (1e-6 * 2**22)),
334
- Constant_us_per_nF_n28=data.calc_cap_constant_us_per_nF_n28(),
335
- V_intermediate_init_uV=round(data.V_intermediate_init_mV * 1e3),
336
- I_intermediate_leak_nA=round(data.I_intermediate_leak_nA),
337
- V_enable_output_threshold_uV=round(states["V_enable_output_threshold_mV"] * 1e3),
338
- V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
339
- dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
361
+ V_mid_enable_output_threshold_uV=round(
362
+ states["V_mid_enable_output_threshold_mV"] * 1e3
363
+ ),
364
+ V_mid_disable_output_threshold_uV=round(
365
+ states["V_mid_disable_output_threshold_mV"] * 1e3
366
+ ),
367
+ dV_mid_enable_output_uV=round(states["dV_mid_enable_output_mV"] * 1e3),
340
368
  interval_check_thresholds_n=round(
341
369
  data.interval_check_thresholds_ms * config.SAMPLERATE_SPS * 1e-3
342
370
  ),
@@ -346,7 +374,7 @@ class ConverterPRUConfig(ShpModel):
346
374
  V_output_log_gpio_threshold_uV=round(data.V_output_log_gpio_threshold_mV * 1e3),
347
375
  # Boost-Converter
348
376
  V_input_boost_threshold_uV=round(data.V_input_boost_threshold_mV * 1e3),
349
- V_intermediate_max_uV=round(data.V_intermediate_max_mV * 1e3),
377
+ V_mid_max_uV=round(states["V_mid_max_mV"] * 1e3),
350
378
  # Buck-Converter
351
379
  V_output_uV=round(data.V_output_mV * 1e3),
352
380
  V_buck_drop_uV=round(data.V_buck_drop_mV * 1e3),
@@ -362,3 +390,18 @@ class ConverterPRUConfig(ShpModel):
362
390
  for value in data.LUT_output_efficiency
363
391
  ],
364
392
  )
393
+
394
+ def storage_is_enabled(self) -> bool:
395
+ return bool(self.converter_mode & 1)
396
+
397
+ def boost_is_enabled(self) -> bool:
398
+ return bool(self.converter_mode & 2)
399
+
400
+ def buck_is_enabled(self) -> bool:
401
+ return bool(self.converter_mode & 4)
402
+
403
+ def logging_intermediate_node_is_enabled(self) -> bool:
404
+ return bool(self.converter_mode & 8)
405
+
406
+ def feedback_is_enabled(self) -> bool:
407
+ return bool(self.converter_mode & 16)