shepherd-core 2025.5.3__py3-none-any.whl → 2025.6.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/commons.py +3 -5
- shepherd_core/config.py +34 -0
- shepherd_core/data_models/base/calibration.py +13 -8
- shepherd_core/data_models/base/wrapper.py +4 -4
- shepherd_core/data_models/content/energy_environment.py +1 -1
- shepherd_core/data_models/content/firmware.py +10 -5
- shepherd_core/data_models/content/virtual_harvester.py +10 -10
- shepherd_core/data_models/content/virtual_source.py +36 -28
- shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -1
- shepherd_core/data_models/experiment/experiment.py +28 -18
- shepherd_core/data_models/experiment/observer_features.py +30 -11
- shepherd_core/data_models/experiment/target_config.py +17 -7
- shepherd_core/data_models/task/emulation.py +38 -25
- shepherd_core/data_models/task/firmware_mod.py +1 -1
- shepherd_core/data_models/task/harvest.py +14 -13
- shepherd_core/data_models/task/observer_tasks.py +4 -3
- shepherd_core/data_models/task/programming.py +2 -2
- shepherd_core/data_models/task/testbed_tasks.py +4 -7
- shepherd_core/data_models/testbed/cape_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/observer.py +1 -1
- shepherd_core/data_models/testbed/observer_fixture.yaml +2 -2
- shepherd_core/data_models/testbed/target.py +1 -1
- 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 +8 -9
- shepherd_core/fw_tools/patcher.py +7 -8
- shepherd_core/inventory/system.py +1 -3
- shepherd_core/reader.py +9 -2
- shepherd_core/testbed_client/cache_path.py +1 -1
- shepherd_core/testbed_client/client_web.py +2 -2
- shepherd_core/version.py +1 -1
- shepherd_core/writer.py +2 -2
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/METADATA +12 -12
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/RECORD +37 -36
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/WHEEL +1 -1
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/zip-safe +0 -0
shepherd_core/commons.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Container for commonly shared constants."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from .config import config
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
TESTBED_SERVER_URI: str = "http://127.0.0.1:8000/shepherd"
|
|
5
|
+
# TODO: deprecated - replace with config in subprojects and then remove here
|
|
6
|
+
SAMPLERATE_SPS_DEFAULT: int = config.SAMPLERATE_SPS
|
shepherd_core/config.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Container for a common configuration.
|
|
2
|
+
|
|
3
|
+
This can be adapted by the user by importing 'config' and changing its variables.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pydantic import HttpUrl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigDefault(BaseModel):
|
|
11
|
+
"""Container for a common configuration."""
|
|
12
|
+
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
TESTBED: str = "shepherd_tud_nes"
|
|
16
|
+
"""name of the testbed to validate against - if enabled - see switch below"""
|
|
17
|
+
VALIDATE_INFRA: bool = True
|
|
18
|
+
"""switch to turn on / off deep validation of data models also considering the current
|
|
19
|
+
layout & infrastructure of the testbed.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
SAMPLERATE_SPS: int = 100_000
|
|
23
|
+
"""Rate of IV-Recording of the testbed."""
|
|
24
|
+
|
|
25
|
+
UID_NAME: str = "SHEPHERD_NODE_ID"
|
|
26
|
+
"""Variable name to patch in ELF-file"""
|
|
27
|
+
UID_SIZE: int = 2
|
|
28
|
+
"""Variable size in Byte"""
|
|
29
|
+
|
|
30
|
+
TESTBED_SERVER: HttpUrl = "https://shepherd.cfaed.tu-dresden.de:8000/"
|
|
31
|
+
"""Server that holds up to date testbed fixtures"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
config = ConfigDefault()
|
|
@@ -95,14 +95,19 @@ cal_hrv_legacy = { # legacy translator
|
|
|
95
95
|
"adc_voltage": "adc_V_Sense", # datalog voltage
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
# defaults (pre-init complex types)
|
|
99
|
+
cal_pair_dac_V = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
|
|
100
|
+
cal_pair_adc_V = CalibrationPair.from_fn(adc_voltage_to_raw, unit="V")
|
|
101
|
+
cal_pair_adc_C = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
|
|
102
|
+
|
|
98
103
|
|
|
99
104
|
class CalibrationHarvester(ShpModel):
|
|
100
105
|
"""Container for all calibration-pairs for that device."""
|
|
101
106
|
|
|
102
|
-
dac_V_Hrv: CalibrationPair =
|
|
103
|
-
dac_V_Sim: CalibrationPair =
|
|
104
|
-
adc_V_Sense: CalibrationPair =
|
|
105
|
-
adc_C_Hrv: CalibrationPair =
|
|
107
|
+
dac_V_Hrv: CalibrationPair = cal_pair_dac_V
|
|
108
|
+
dac_V_Sim: CalibrationPair = cal_pair_dac_V
|
|
109
|
+
adc_V_Sense: CalibrationPair = cal_pair_adc_V
|
|
110
|
+
adc_C_Hrv: CalibrationPair = cal_pair_adc_C
|
|
106
111
|
|
|
107
112
|
def export_for_sysfs(self) -> dict:
|
|
108
113
|
"""Convert and write the essential data.
|
|
@@ -143,10 +148,10 @@ class CalibrationEmulator(ShpModel):
|
|
|
143
148
|
Differentiates between both target-ports A/B.
|
|
144
149
|
"""
|
|
145
150
|
|
|
146
|
-
dac_V_A: CalibrationPair =
|
|
147
|
-
dac_V_B: CalibrationPair =
|
|
148
|
-
adc_C_A: CalibrationPair =
|
|
149
|
-
adc_C_B: CalibrationPair =
|
|
151
|
+
dac_V_A: CalibrationPair = cal_pair_dac_V
|
|
152
|
+
dac_V_B: CalibrationPair = cal_pair_dac_V
|
|
153
|
+
adc_C_A: CalibrationPair = cal_pair_adc_C
|
|
154
|
+
adc_C_B: CalibrationPair = cal_pair_adc_C
|
|
150
155
|
|
|
151
156
|
def export_for_sysfs(self) -> dict:
|
|
152
157
|
"""Convert and write the essential data.
|
|
@@ -17,11 +17,11 @@ class Wrapper(BaseModel):
|
|
|
17
17
|
"""Generalized web- & file-interface for all models with dynamic typecasting."""
|
|
18
18
|
|
|
19
19
|
datatype: str
|
|
20
|
-
|
|
20
|
+
""" ⤷ model-name"""
|
|
21
21
|
comment: Optional[SafeStrClone] = None
|
|
22
22
|
created: Optional[datetime] = None
|
|
23
|
-
|
|
23
|
+
""" ⤷ Optional metadata"""
|
|
24
24
|
lib_ver: Optional[str] = version
|
|
25
|
-
|
|
25
|
+
""" ⤷ for debug-purposes and later compatibility-checks"""
|
|
26
26
|
parameters: dict
|
|
27
|
-
|
|
27
|
+
""" ⤷ ShpModel"""
|
|
@@ -28,7 +28,7 @@ class EnergyEnvironment(ContentModel):
|
|
|
28
28
|
data_path: Path
|
|
29
29
|
data_type: EnergyDType
|
|
30
30
|
data_local: bool = True
|
|
31
|
-
|
|
31
|
+
""" ⤷ signals that file has to be copied to testbed"""
|
|
32
32
|
|
|
33
33
|
duration: PositiveFloat
|
|
34
34
|
energy_Ws: PositiveFloat
|
|
@@ -62,7 +62,7 @@ class Firmware(ContentModel, title="Firmware of Target"):
|
|
|
62
62
|
data_type: FirmwareDType
|
|
63
63
|
data_hash: Optional[str] = None
|
|
64
64
|
data_local: bool = True
|
|
65
|
-
|
|
65
|
+
""" ⤷ signals that file has to be copied to testbed"""
|
|
66
66
|
|
|
67
67
|
@model_validator(mode="before")
|
|
68
68
|
@classmethod
|
|
@@ -125,12 +125,17 @@ class Firmware(ContentModel, title="Firmware of Target"):
|
|
|
125
125
|
if "mcu" not in kwargs:
|
|
126
126
|
kwargs["mcu"] = arch_to_mcu[arch]
|
|
127
127
|
|
|
128
|
+
# verification of ELF - warn if something is off
|
|
129
|
+
# -> adds ARCH if it is able to derive
|
|
128
130
|
if kwargs["data_type"] == FirmwareDType.base64_elf:
|
|
129
131
|
arch = fw_tools.read_arch(file)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
try:
|
|
133
|
+
if "msp430" in arch and not fw_tools.is_elf_msp430(file):
|
|
134
|
+
raise ValueError("File is not a ELF for msp430")
|
|
135
|
+
if ("nrf52" in arch or "arm" in arch) and not fw_tools.is_elf_nrf52(file):
|
|
136
|
+
raise ValueError("File is not a ELF for nRF52")
|
|
137
|
+
except RuntimeError:
|
|
138
|
+
logger.warning("ObjCopy not found -> Arch of Firmware can't be verified")
|
|
134
139
|
logger.debug("ELF-File '%s' has arch: %s", file.name, arch)
|
|
135
140
|
if "mcu" not in kwargs:
|
|
136
141
|
kwargs["mcu"] = arch_to_mcu[arch]
|
|
@@ -10,7 +10,7 @@ from pydantic import Field
|
|
|
10
10
|
from pydantic import model_validator
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
|
|
13
|
-
from shepherd_core.
|
|
13
|
+
from shepherd_core.config import config
|
|
14
14
|
from shepherd_core.data_models.base.calibration import CalibrationHarvester
|
|
15
15
|
from shepherd_core.data_models.base.content import ContentModel
|
|
16
16
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
@@ -363,9 +363,9 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
363
363
|
def calc_timings_ms(self, *, for_emu: bool) -> tuple[float, float]:
|
|
364
364
|
"""factor-in model-internal timing-constraints."""
|
|
365
365
|
window_length = self.samples_n * (1 + self.wait_cycles)
|
|
366
|
-
time_min_ms = (1 + self.wait_cycles) * 1_000 /
|
|
366
|
+
time_min_ms = (1 + self.wait_cycles) * 1_000 / config.SAMPLERATE_SPS
|
|
367
367
|
if for_emu:
|
|
368
|
-
window_ms = window_length * 1_000 /
|
|
368
|
+
window_ms = window_length * 1_000 / config.SAMPLERATE_SPS
|
|
369
369
|
time_min_ms = max(time_min_ms, window_ms)
|
|
370
370
|
|
|
371
371
|
interval_ms = min(max(self.interval_ms, time_min_ms), 1_000_000)
|
|
@@ -453,16 +453,16 @@ class HarvesterPRUConfig(ShpModel):
|
|
|
453
453
|
voltage_min_uV: u32
|
|
454
454
|
voltage_max_uV: u32
|
|
455
455
|
voltage_step_uV: u32
|
|
456
|
-
|
|
456
|
+
""" ⤷ for window-based algo like ivcurve"""
|
|
457
457
|
current_limit_nA: u32
|
|
458
|
-
|
|
458
|
+
""" ⤷ lower bound to detect zero current"""
|
|
459
459
|
setpoint_n8: u32
|
|
460
460
|
interval_n: u32
|
|
461
|
-
|
|
461
|
+
""" ⤷ between measurements"""
|
|
462
462
|
duration_n: u32
|
|
463
|
-
|
|
463
|
+
""" ⤷ of measurement"""
|
|
464
464
|
wait_cycles_n: u32
|
|
465
|
-
|
|
465
|
+
""" ⤷ for DAC to settle"""
|
|
466
466
|
|
|
467
467
|
@classmethod
|
|
468
468
|
def from_vhrv(
|
|
@@ -517,7 +517,7 @@ class HarvesterPRUConfig(ShpModel):
|
|
|
517
517
|
voltage_step_uV=round(voltage_step_mV * 10**3),
|
|
518
518
|
current_limit_nA=round(data.current_limit_uA * 10**3),
|
|
519
519
|
setpoint_n8=round(min(255, data.setpoint_n * 2**8)),
|
|
520
|
-
interval_n=round(interval_ms *
|
|
521
|
-
duration_n=round(duration_ms *
|
|
520
|
+
interval_n=round(interval_ms * config.SAMPLERATE_SPS * 1e-3),
|
|
521
|
+
duration_n=round(duration_ms * config.SAMPLERATE_SPS * 1e-3),
|
|
522
522
|
wait_cycles_n=data.wait_cycles,
|
|
523
523
|
)
|
|
@@ -7,7 +7,7 @@ from pydantic import Field
|
|
|
7
7
|
from pydantic import model_validator
|
|
8
8
|
from typing_extensions import Self
|
|
9
9
|
|
|
10
|
-
from shepherd_core.
|
|
10
|
+
from shepherd_core.config import config
|
|
11
11
|
from shepherd_core.data_models.base.content import ContentModel
|
|
12
12
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
13
13
|
from shepherd_core.logger import logger
|
|
@@ -23,6 +23,9 @@ NormedNum = Annotated[float, Field(ge=0.0, le=1.0)]
|
|
|
23
23
|
LUT1D = Annotated[list[NormedNum], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
|
|
24
24
|
LUT2D = Annotated[list[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
|
|
25
25
|
|
|
26
|
+
# defaults (pre-init complex types for improved perf) TODO: is documentation still fine?
|
|
27
|
+
vhrv_mppt_opt = VirtualHarvesterConfig(name="mppt_opt")
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
28
31
|
"""The vSrc uses the energy environment (file) for supplying the Target Node.
|
|
@@ -41,46 +44,48 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
41
44
|
# General Metadata & Ownership -> ContentModel
|
|
42
45
|
|
|
43
46
|
enable_boost: bool = False
|
|
44
|
-
|
|
47
|
+
""" ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable"""
|
|
45
48
|
enable_buck: bool = False
|
|
46
|
-
|
|
49
|
+
""" ⤷ if false -> v_output = v_intermediate"""
|
|
47
50
|
enable_feedback_to_hrv: bool = False
|
|
48
|
-
|
|
51
|
+
""" src can control a cv-harvester for ivcurve"""
|
|
49
52
|
|
|
50
53
|
interval_startup_delay_drain_ms: Annotated[float, Field(ge=0, le=10_000)] = 0
|
|
51
54
|
|
|
52
|
-
harvester: VirtualHarvesterConfig =
|
|
55
|
+
harvester: VirtualHarvesterConfig = vhrv_mppt_opt
|
|
53
56
|
|
|
54
57
|
V_input_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
|
|
55
58
|
I_input_max_mA: Annotated[float, Field(ge=0, le=4.29e3)] = 4_200
|
|
56
59
|
V_input_drop_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 0
|
|
57
|
-
|
|
60
|
+
""" ⤷ simulate input-diode"""
|
|
58
61
|
R_input_mOhm: Annotated[float, Field(ge=0, le=4.29e6)] = 0
|
|
59
|
-
|
|
62
|
+
""" ⤷ resistance only active with disabled boost, range [1 mOhm; 1MOhm]"""
|
|
60
63
|
|
|
61
64
|
# primary storage-Cap
|
|
62
65
|
C_intermediate_uF: Annotated[float, Field(ge=0, le=100_000)] = 0
|
|
63
66
|
V_intermediate_init_mV: Annotated[float, Field(ge=0, le=10_000)] = 3_000
|
|
64
|
-
|
|
67
|
+
""" ⤷ allow a proper / fast startup"""
|
|
65
68
|
I_intermediate_leak_nA: Annotated[float, Field(ge=0, le=4.29e9)] = 0
|
|
66
69
|
|
|
67
70
|
V_intermediate_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 1
|
|
68
|
-
|
|
71
|
+
""" ⤷ target gets connected (hysteresis-combo with next value)"""
|
|
69
72
|
V_intermediate_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
|
|
70
|
-
|
|
73
|
+
""" ⤷ target gets disconnected"""
|
|
71
74
|
interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
|
|
72
|
-
|
|
75
|
+
""" ⤷ some ICs (BQ) check every 64 ms if output should be disconnected"""
|
|
73
76
|
# TODO: add intervals for input-disable, output-disable & power-good-signal
|
|
74
77
|
|
|
75
78
|
# pwr-good: target is informed on output-pin (hysteresis) -> for intermediate voltage
|
|
76
79
|
V_pwr_good_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2_800
|
|
77
80
|
V_pwr_good_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2200
|
|
78
81
|
immediate_pwr_good_signal: bool = True
|
|
79
|
-
|
|
82
|
+
""" ⤷ 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds"""
|
|
80
83
|
|
|
81
|
-
# final (always last) stage to compensate undetectable current spikes
|
|
82
|
-
# when enabling power for target
|
|
83
84
|
C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
|
|
85
|
+
"""
|
|
86
|
+
final (always last) stage to compensate undetectable current spikes when
|
|
87
|
+
enabling power for target
|
|
88
|
+
"""
|
|
84
89
|
# TODO: C_output is handled internally as delta-V, but should be a I_transient
|
|
85
90
|
# that makes it visible in simulation as additional i_out_drain
|
|
86
91
|
# TODO: potential weakness, ACD lowpass is capturing transient,
|
|
@@ -88,32 +93,35 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
88
93
|
|
|
89
94
|
# Extra
|
|
90
95
|
V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
|
|
91
|
-
|
|
96
|
+
""" ⤷ min voltage needed to enable recording changes in gpio-bank"""
|
|
92
97
|
|
|
93
98
|
# Boost Converter
|
|
94
99
|
V_input_boost_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
|
|
95
|
-
|
|
100
|
+
""" ⤷ min input-voltage for the boost converter to work"""
|
|
96
101
|
V_intermediate_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
|
|
97
|
-
|
|
102
|
+
""" ⤷ boost converter shuts off"""
|
|
98
103
|
|
|
99
104
|
LUT_input_efficiency: LUT2D = 12 * [12 * [1.00]]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
""" ⤷ rows are current -> first row a[V=0][:]
|
|
106
|
+
|
|
107
|
+
input-LUT[12][12] depending on array[inp_voltage][log(inp_current)],
|
|
108
|
+
influence of cap-voltage is not implemented
|
|
109
|
+
"""
|
|
110
|
+
|
|
103
111
|
LUT_input_V_min_log2_uV: Annotated[int, Field(ge=0, le=20)] = 0
|
|
104
|
-
|
|
112
|
+
""" ⤷ i.e. 2^7 = 128 uV -> LUT[0][:] is for inputs < 128 uV"""
|
|
105
113
|
LUT_input_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
|
|
106
|
-
|
|
114
|
+
""" ⤷ i.e. 2^8 = 256 nA -> LUT[:][0] is for inputs < 256 nA"""
|
|
107
115
|
|
|
108
116
|
# Buck Converter
|
|
109
117
|
V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
|
|
110
118
|
V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
|
|
111
|
-
|
|
119
|
+
""" ⤷ simulate LDO / diode min voltage differential or output-diode"""
|
|
112
120
|
|
|
113
121
|
LUT_output_efficiency: LUT1D = 12 * [1.00]
|
|
114
|
-
|
|
122
|
+
""" ⤷ array[12] depending on output_current"""
|
|
115
123
|
LUT_output_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
|
|
116
|
-
|
|
124
|
+
""" ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation"""
|
|
117
125
|
|
|
118
126
|
@model_validator(mode="before")
|
|
119
127
|
@classmethod
|
|
@@ -242,7 +250,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
242
250
|
dV[uV] = constant[us/nF] * current[nA] = constant[us*V/nAs] * current[nA]
|
|
243
251
|
"""
|
|
244
252
|
C_cap_uF = max(self.C_intermediate_uF, 0.001)
|
|
245
|
-
return int((10**3 * (2**28)) // (C_cap_uF *
|
|
253
|
+
return int((10**3 * (2**28)) // (C_cap_uF * config.SAMPLERATE_SPS))
|
|
246
254
|
|
|
247
255
|
|
|
248
256
|
u32 = Annotated[int, Field(ge=0, lt=2**32)]
|
|
@@ -317,7 +325,7 @@ class ConverterPRUConfig(ShpModel):
|
|
|
317
325
|
dtype_in, log_intermediate_node=log_intermediate_node
|
|
318
326
|
),
|
|
319
327
|
interval_startup_delay_drain_n=round(
|
|
320
|
-
data.interval_startup_delay_drain_ms *
|
|
328
|
+
data.interval_startup_delay_drain_ms * config.SAMPLERATE_SPS * 1e-3
|
|
321
329
|
),
|
|
322
330
|
V_input_max_uV=round(data.V_input_max_mV * 1e3),
|
|
323
331
|
I_input_max_nA=round(data.I_input_max_mA * 1e6),
|
|
@@ -330,7 +338,7 @@ class ConverterPRUConfig(ShpModel):
|
|
|
330
338
|
V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
|
|
331
339
|
dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
|
|
332
340
|
interval_check_thresholds_n=round(
|
|
333
|
-
data.interval_check_thresholds_ms *
|
|
341
|
+
data.interval_check_thresholds_ms * config.SAMPLERATE_SPS * 1e-3
|
|
334
342
|
),
|
|
335
343
|
V_pwr_good_enable_threshold_uV=round(data.V_pwr_good_enable_threshold_mV * 1e3),
|
|
336
344
|
V_pwr_good_disable_threshold_uV=round(data.V_pwr_good_disable_threshold_mV * 1e3),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# info:
|
|
2
2
|
# - compendium of all parameters & description
|
|
3
|
-
# - base for neutral fallback values if provided
|
|
3
|
+
# - base for neutral fallback values if provided yaml is sparse
|
|
4
4
|
# - -> it is encouraged to omit redundant parameters
|
|
5
5
|
---
|
|
6
6
|
- datatype: VirtualSourceConfig
|
|
@@ -11,11 +11,12 @@ from pydantic import model_validator
|
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
from typing_extensions import deprecated
|
|
13
13
|
|
|
14
|
+
from shepherd_core.config import config
|
|
14
15
|
from shepherd_core.data_models.base.content import IdInt
|
|
15
16
|
from shepherd_core.data_models.base.content import NameStr
|
|
16
17
|
from shepherd_core.data_models.base.content import SafeStr
|
|
17
|
-
from shepherd_core.data_models.base.content import id_default
|
|
18
18
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
19
|
+
from shepherd_core.data_models.base.timezone import local_now
|
|
19
20
|
from shepherd_core.data_models.testbed.target import Target
|
|
20
21
|
from shepherd_core.data_models.testbed.testbed import Testbed
|
|
21
22
|
from shepherd_core.version import version
|
|
@@ -23,33 +24,28 @@ from shepherd_core.version import version
|
|
|
23
24
|
from .observer_features import SystemLogging
|
|
24
25
|
from .target_config import TargetConfig
|
|
25
26
|
|
|
27
|
+
# defaults (pre-init complex types)
|
|
28
|
+
sys_log_all = SystemLogging() # = all active
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
class Experiment(ShpModel, title="Config of an Experiment"):
|
|
28
32
|
"""Config for experiments on the testbed emulating energy environments for target nodes."""
|
|
29
33
|
|
|
30
34
|
# General Properties
|
|
31
|
-
id: int = Field(description="Unique ID", default_factory=id_default)
|
|
32
|
-
# ⤷ TODO: automatic ID is problematic for identification by hash
|
|
33
|
-
|
|
34
35
|
name: NameStr
|
|
35
36
|
description: Annotated[
|
|
36
37
|
Optional[SafeStr], Field(description="Required for public instances")
|
|
37
38
|
] = None
|
|
38
39
|
comment: Optional[SafeStr] = None
|
|
39
|
-
created: datetime = Field(default_factory=datetime.now)
|
|
40
|
-
|
|
41
|
-
# Ownership & Access
|
|
42
|
-
owner_id: Optional[IdInt] = None
|
|
43
40
|
|
|
44
41
|
# feedback
|
|
45
|
-
email_results: bool =
|
|
42
|
+
email_results: bool = True
|
|
46
43
|
|
|
47
|
-
sys_logging: SystemLogging =
|
|
44
|
+
sys_logging: SystemLogging = sys_log_all
|
|
48
45
|
|
|
49
46
|
# schedule
|
|
50
47
|
time_start: Optional[datetime] = None # = ASAP
|
|
51
48
|
duration: Optional[timedelta] = None # = till EOF
|
|
52
|
-
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
53
49
|
|
|
54
50
|
# targets
|
|
55
51
|
target_configs: Annotated[list[TargetConfig], Field(min_length=1, max_length=128)]
|
|
@@ -57,13 +53,16 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
57
53
|
# debug
|
|
58
54
|
lib_ver: Optional[str] = version
|
|
59
55
|
|
|
56
|
+
# deprecated fields, TODO: remove before public release
|
|
57
|
+
id: Annotated[Optional[int], deprecated("not needed")] = None
|
|
58
|
+
created: Annotated[Optional[datetime], deprecated("not needed")] = None
|
|
59
|
+
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
60
|
+
owner_id: Annotated[Optional[IdInt], deprecated("not needed")] = None
|
|
61
|
+
|
|
60
62
|
@model_validator(mode="after")
|
|
61
63
|
def post_validation(self) -> Self:
|
|
62
|
-
|
|
63
|
-
# or with cached fixtures
|
|
64
|
-
testbed = Testbed() # this will query the first (and only) entry of client
|
|
64
|
+
self._validate_observers(self.target_configs)
|
|
65
65
|
self._validate_targets(self.target_configs)
|
|
66
|
-
self._validate_observers(self.target_configs, testbed)
|
|
67
66
|
if self.duration and self.duration.total_seconds() < 0:
|
|
68
67
|
raise ValueError("Duration of experiment can't be negative.")
|
|
69
68
|
return self
|
|
@@ -75,8 +74,9 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
75
74
|
for _config in configs:
|
|
76
75
|
for _id in _config.target_IDs:
|
|
77
76
|
target_ids.append(_id)
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
if config.VALIDATE_INFRA:
|
|
78
|
+
Target(id=_id)
|
|
79
|
+
# ⤷ this can raise exception for non-existing targets
|
|
80
80
|
if _config.custom_IDs is not None:
|
|
81
81
|
custom_ids = custom_ids + _config.custom_IDs[: len(_config.target_IDs)]
|
|
82
82
|
else:
|
|
@@ -87,7 +87,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
87
87
|
raise ValueError("Custom Target-ID are faulty (some form of id-collisions)!")
|
|
88
88
|
|
|
89
89
|
@staticmethod
|
|
90
|
-
def _validate_observers(configs: Iterable[TargetConfig]
|
|
90
|
+
def _validate_observers(configs: Iterable[TargetConfig]) -> None:
|
|
91
|
+
if not config.VALIDATE_INFRA:
|
|
92
|
+
return
|
|
93
|
+
testbed = Testbed()
|
|
91
94
|
target_ids = [_id for _config in configs for _id in _config.target_IDs]
|
|
92
95
|
obs_ids = [testbed.get_observer(_id).id for _id in target_ids]
|
|
93
96
|
if len(target_ids) > len(set(obs_ids)):
|
|
@@ -105,3 +108,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
105
108
|
# gets already caught in target_config - but keep:
|
|
106
109
|
msg = f"Target-ID {target_id} was not found in Experiment '{self.name}'"
|
|
107
110
|
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
def folder_name(self, custom_date: Optional[datetime] = None) -> str:
|
|
113
|
+
date = custom_date if custom_date is not None else self.time_start
|
|
114
|
+
timestamp = local_now() if date is None else date
|
|
115
|
+
timestrng = timestamp.strftime("%Y-%m-%d_%H-%M-%S")
|
|
116
|
+
# ⤷ closest to ISO 8601, avoids ":"
|
|
117
|
+
return f"{timestrng}_{self.name.replace(' ', '_')}"
|
|
@@ -17,6 +17,9 @@ from shepherd_core import logger
|
|
|
17
17
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
18
18
|
from shepherd_core.data_models.testbed.gpio import GPIO
|
|
19
19
|
|
|
20
|
+
# defaults (pre-init complex types)
|
|
21
|
+
zero_duration = timedelta(seconds=0)
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
22
25
|
"""Configuration for recording the Power-Consumption of the Target Nodes.
|
|
@@ -25,19 +28,27 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
30
|
intermediate_voltage: bool = False
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
"""
|
|
32
|
+
⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
|
|
33
|
+
this also includes current!
|
|
34
|
+
"""
|
|
31
35
|
# time
|
|
32
|
-
delay: timedelta =
|
|
36
|
+
delay: timedelta = zero_duration
|
|
37
|
+
"""start recording after experiment started"""
|
|
33
38
|
duration: Optional[timedelta] = None # till EOF
|
|
39
|
+
"""duration of recording after delay starts the process.
|
|
40
|
+
|
|
41
|
+
default is None, recording till EOF"""
|
|
34
42
|
|
|
35
43
|
# post-processing
|
|
36
44
|
calculate_power: bool = False
|
|
37
|
-
|
|
45
|
+
""" ⤷ reduce file-size by calculating power -> not implemented ATM"""
|
|
46
|
+
samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000
|
|
47
|
+
""" ⤷ reduce file-size by down-sampling -> not implemented ATM"""
|
|
38
48
|
discard_current: bool = False
|
|
49
|
+
""" ⤷ reduce file-size by omitting current -> not implemented ATM"""
|
|
39
50
|
discard_voltage: bool = False
|
|
40
|
-
|
|
51
|
+
""" ⤷ reduce file-size by omitting voltage -> not implemented ATM"""
|
|
41
52
|
|
|
42
53
|
@model_validator(mode="after")
|
|
43
54
|
def post_validation(self) -> Self:
|
|
@@ -163,11 +174,12 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
163
174
|
"""
|
|
164
175
|
|
|
165
176
|
# time
|
|
166
|
-
delay: timedelta =
|
|
177
|
+
delay: timedelta = zero_duration
|
|
167
178
|
duration: Optional[timedelta] = None # till EOF
|
|
168
179
|
|
|
169
180
|
# post-processing,
|
|
170
181
|
uart_decode: bool = False
|
|
182
|
+
"""Automatic decoding from gpio-trace not implemented ATM."""
|
|
171
183
|
uart_pin: GPIO = GPIO(name="GPIO8")
|
|
172
184
|
uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
|
|
173
185
|
|
|
@@ -205,12 +217,14 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
|
|
|
205
217
|
"""Configuration for a single GPIO-Event (Actuation)."""
|
|
206
218
|
|
|
207
219
|
delay: PositiveFloat
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
""" ⤷ from start_time
|
|
221
|
+
|
|
222
|
+
- resolution 10 us (guaranteed, but finer steps are possible)
|
|
223
|
+
"""
|
|
210
224
|
gpio: GPIO
|
|
211
225
|
level: GpioLevel
|
|
212
226
|
period: Annotated[float, Field(ge=10e-6)] = 1
|
|
213
|
-
|
|
227
|
+
""" ⤷ time base of periodicity in s"""
|
|
214
228
|
count: Annotated[int, Field(ge=1, le=4096)] = 1
|
|
215
229
|
|
|
216
230
|
@model_validator(mode="after")
|
|
@@ -233,6 +247,11 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
|
233
247
|
|
|
234
248
|
events: Annotated[list[GpioEvent], Field(min_length=1, max_length=1024)]
|
|
235
249
|
|
|
250
|
+
@model_validator(mode="after")
|
|
251
|
+
def post_validation(self) -> Self:
|
|
252
|
+
msg = "not implemented ATM"
|
|
253
|
+
raise ValueError(msg)
|
|
254
|
+
|
|
236
255
|
def get_gpios(self) -> set:
|
|
237
256
|
return {_ev.gpio for _ev in self.events}
|
|
238
257
|
|
|
@@ -245,7 +264,7 @@ class SystemLogging(ShpModel, title="Config for System-Logging"):
|
|
|
245
264
|
sheep: bool = True
|
|
246
265
|
sys_util: bool = True
|
|
247
266
|
|
|
248
|
-
# TODO: remove lines below
|
|
267
|
+
# deprecated, TODO: remove lines below before public release
|
|
249
268
|
dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
|
|
250
269
|
ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
|
|
251
270
|
shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
|
|
@@ -20,26 +20,36 @@ from .observer_features import GpioTracing
|
|
|
20
20
|
from .observer_features import PowerTracing
|
|
21
21
|
from .observer_features import UartLogging
|
|
22
22
|
|
|
23
|
+
# defaults (pre-init complex types)
|
|
24
|
+
vsrc_neutral = VirtualSourceConfig(name="neutral")
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
class TargetConfig(ShpModel, title="Target Config"):
|
|
25
28
|
"""Configuration related to Target Nodes (DuT)."""
|
|
26
29
|
|
|
27
30
|
target_IDs: Annotated[list[IdInt], Field(min_length=1, max_length=128)]
|
|
28
31
|
custom_IDs: Optional[Annotated[list[IdInt16], Field(min_length=1, max_length=128)]] = None
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
""" ⤷ custom ID will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware.
|
|
33
|
+
|
|
34
|
+
if no custom ID is provided, the original ID of target is used
|
|
35
|
+
"""
|
|
31
36
|
|
|
32
|
-
energy_env: EnergyEnvironment
|
|
33
|
-
|
|
37
|
+
energy_env: EnergyEnvironment
|
|
38
|
+
""" input for the virtual source """
|
|
39
|
+
virtual_source: VirtualSourceConfig = vsrc_neutral
|
|
34
40
|
target_delays: Optional[
|
|
35
41
|
Annotated[list[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
|
|
36
42
|
] = None
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
""" ⤷ individual starting times
|
|
44
|
+
|
|
45
|
+
- allows to use the same environment
|
|
46
|
+
- not implemented ATM
|
|
47
|
+
"""
|
|
39
48
|
|
|
40
49
|
firmware1: Firmware
|
|
50
|
+
""" ⤷ omitted FW gets set to neutral deep-sleep"""
|
|
41
51
|
firmware2: Optional[Firmware] = None
|
|
42
|
-
|
|
52
|
+
""" ⤷ omitted FW gets set to neutral deep-sleep"""
|
|
43
53
|
|
|
44
54
|
power_tracing: Optional[PowerTracing] = None
|
|
45
55
|
gpio_tracing: Optional[GpioTracing] = None
|