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.
- shepherd_core/config.py +1 -1
- shepherd_core/data_models/__init__.py +8 -4
- shepherd_core/data_models/base/cal_measurement.py +7 -2
- shepherd_core/data_models/base/calibration.py +23 -12
- shepherd_core/data_models/base/content.py +12 -2
- shepherd_core/data_models/base/shepherd.py +13 -4
- shepherd_core/data_models/base/wrapper.py +2 -0
- shepherd_core/data_models/content/__init__.py +8 -4
- shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
- shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
- shepherd_core/data_models/content/energy_environment.py +341 -23
- shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
- shepherd_core/data_models/content/enum_datatypes.py +109 -0
- shepherd_core/data_models/content/firmware.py +44 -16
- shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
- shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
- shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
- shepherd_core/data_models/content/virtual_storage_config.py +429 -0
- shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
- shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
- shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
- shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
- shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
- shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
- shepherd_core/data_models/experiment/experiment.py +38 -13
- shepherd_core/data_models/experiment/observer_features.py +17 -4
- shepherd_core/data_models/experiment/target_config.py +56 -8
- shepherd_core/data_models/task/__init__.py +13 -2
- shepherd_core/data_models/task/emulation.py +10 -6
- shepherd_core/data_models/task/firmware_mod.py +3 -1
- shepherd_core/data_models/task/harvest.py +3 -1
- shepherd_core/data_models/task/helper_paths.py +2 -2
- shepherd_core/data_models/task/observer_tasks.py +8 -6
- shepherd_core/data_models/task/programming.py +4 -2
- shepherd_core/data_models/task/testbed_tasks.py +8 -2
- shepherd_core/data_models/testbed/cape.py +2 -0
- shepherd_core/data_models/testbed/gpio.py +2 -0
- shepherd_core/data_models/testbed/mcu.py +2 -0
- shepherd_core/data_models/testbed/observer.py +2 -0
- shepherd_core/data_models/testbed/target.py +7 -5
- shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
- shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/testbed.py +17 -15
- shepherd_core/decoder_waveform/uart.py +1 -1
- shepherd_core/exit_handler.py +22 -0
- shepherd_core/fw_tools/converter.py +2 -2
- shepherd_core/fw_tools/validation.py +1 -1
- shepherd_core/inventory/__init__.py +23 -21
- shepherd_core/inventory/system.py +3 -3
- shepherd_core/logger.py +0 -1
- shepherd_core/reader.py +32 -27
- shepherd_core/testbed_client/cache_path.py +3 -3
- shepherd_core/testbed_client/client_abc_fix.py +14 -3
- shepherd_core/testbed_client/client_web.py +7 -5
- shepherd_core/testbed_client/fixtures.py +7 -7
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/__init__.py +4 -0
- shepherd_core/vsource/virtual_converter_model.py +29 -28
- shepherd_core/vsource/virtual_harvester_model.py +29 -21
- shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
- shepherd_core/vsource/virtual_source_model.py +18 -14
- shepherd_core/vsource/virtual_source_simulation.py +71 -73
- shepherd_core/vsource/virtual_storage_model.py +164 -0
- shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
- shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
- shepherd_core/vsource/virtual_storage_simulator.py +104 -0
- shepherd_core/writer.py +16 -9
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
- shepherd_core-2026.2.1.dist-info/RECORD +102 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
- shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
- shepherd_core/data_models/content/firmware_datatype.py +0 -15
- shepherd_core/data_models/virtual_source_doc.txt +0 -207
- shepherd_core-2025.8.1.dist-info/RECORD +0 -83
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
- {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 .
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
if
|
|
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
|
-
|
|
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
|
|
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
|
|
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["
|
|
110
|
+
kwargs["data_2_copy"] = False
|
|
109
111
|
else:
|
|
110
112
|
kwargs["data"] = Path(file).as_posix()
|
|
111
|
-
kwargs["
|
|
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,
|
|
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
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
hash_new = fw_tools.base64_to_hash(
|
|
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
|
|
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 .
|
|
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:
|
|
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
|
|
251
|
+
raise ValueError("Voltage minimum > max")
|
|
336
252
|
if self.voltage_mV < self.voltage_min_mV:
|
|
337
|
-
raise ValueError("Voltage below
|
|
253
|
+
raise ValueError("Voltage below minimum")
|
|
338
254
|
if self.voltage_mV > self.voltage_max_mV:
|
|
339
|
-
raise ValueError("Voltage above
|
|
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
|
-
|
|
373
|
-
if (
|
|
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
|
-
|
|
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 .
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
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
|
-
"""
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"""
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
""" ⤷
|
|
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
|
-
""" ⤷
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
189
|
+
v_old = self.V_intermediate_enable_output_threshold_mV
|
|
170
190
|
v_out = self.V_output_mV
|
|
171
|
-
c_store = self.
|
|
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.
|
|
198
|
-
values["
|
|
199
|
-
values["
|
|
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["
|
|
203
|
-
values["
|
|
204
|
-
V_pre_output_mV + values["
|
|
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.
|
|
208
|
-
values["
|
|
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["
|
|
244
|
+
values["V_mid_disable_output_threshold_mV"] = V_pre_output_mV
|
|
211
245
|
|
|
212
246
|
else:
|
|
213
|
-
values["
|
|
214
|
-
values["
|
|
215
|
-
|
|
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.
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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)
|