shepherd-core 2025.4.2__py3-none-any.whl → 2025.5.3__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/data_models/__init__.py +2 -0
- shepherd_core/data_models/base/content.py +4 -13
- shepherd_core/data_models/content/_external_fixtures.yaml +43 -43
- shepherd_core/data_models/content/energy_environment.py +2 -2
- shepherd_core/data_models/content/virtual_harvester.py +245 -16
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml +2 -2
- shepherd_core/data_models/content/virtual_source.py +5 -2
- shepherd_core/data_models/content/virtual_source_fixture.yaml +3 -3
- shepherd_core/data_models/experiment/experiment.py +8 -8
- shepherd_core/data_models/experiment/observer_features.py +129 -18
- shepherd_core/data_models/experiment/target_config.py +5 -0
- shepherd_core/data_models/task/__init__.py +6 -3
- shepherd_core/data_models/task/emulation.py +21 -5
- shepherd_core/data_models/task/harvest.py +3 -2
- shepherd_core/data_models/task/observer_tasks.py +5 -4
- shepherd_core/data_models/task/programming.py +3 -1
- shepherd_core/data_models/task/testbed_tasks.py +3 -2
- shepherd_core/data_models/testbed/cape_fixture.yaml +8 -0
- shepherd_core/data_models/testbed/gpio.py +7 -0
- shepherd_core/data_models/testbed/mcu_fixture.yaml +4 -4
- shepherd_core/data_models/testbed/observer_fixture.yaml +17 -0
- shepherd_core/data_models/testbed/target_fixture.yaml +13 -0
- shepherd_core/data_models/testbed/testbed_fixture.yaml +11 -0
- shepherd_core/data_models/virtual_source_doc.txt +3 -3
- shepherd_core/fw_tools/converter.py +6 -3
- shepherd_core/fw_tools/validation.py +8 -4
- shepherd_core/reader.py +77 -47
- shepherd_core/testbed_client/client_abc_fix.py +2 -3
- shepherd_core/testbed_client/fixtures.py +15 -17
- shepherd_core/testbed_client/user_model.py +3 -6
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/virtual_harvester_simulation.py +1 -1
- shepherd_core/vsource/virtual_source_simulation.py +1 -1
- shepherd_core/writer.py +8 -8
- {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/METADATA +1 -1
- {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/RECORD +39 -39
- {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/WHEEL +1 -1
- {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.4.2.dist-info → shepherd_core-2025.5.3.dist-info}/zip-safe +0 -0
|
@@ -33,7 +33,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
33
33
|
The converter-stage is software defined and offers:
|
|
34
34
|
- buck-boost-combinations,
|
|
35
35
|
- a simple diode + resistor and
|
|
36
|
-
- an intermediate
|
|
36
|
+
- an intermediate storage capacitor.
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
39
|
# TODO: I,V,R should be in regular unit (V, A, Ohm)
|
|
@@ -70,6 +70,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
70
70
|
# ⤷ target gets disconnected
|
|
71
71
|
interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
|
|
72
72
|
# ⤷ some ICs (BQ) check every 64 ms if output should be disconnected
|
|
73
|
+
# TODO: add intervals for input-disable, output-disable & power-good-signal
|
|
73
74
|
|
|
74
75
|
# pwr-good: target is informed on output-pin (hysteresis) -> for intermediate voltage
|
|
75
76
|
V_pwr_good_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2_800
|
|
@@ -82,6 +83,8 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
82
83
|
C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
|
|
83
84
|
# TODO: C_output is handled internally as delta-V, but should be a I_transient
|
|
84
85
|
# that makes it visible in simulation as additional i_out_drain
|
|
86
|
+
# TODO: potential weakness, ACD lowpass is capturing transient,
|
|
87
|
+
# but energy is LOST with this model
|
|
85
88
|
|
|
86
89
|
# Extra
|
|
87
90
|
V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
|
|
@@ -105,7 +108,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
105
108
|
# Buck Converter
|
|
106
109
|
V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
|
|
107
110
|
V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
|
|
108
|
-
# ⤷ simulate LDO min voltage differential or output-diode
|
|
111
|
+
# ⤷ simulate LDO / diode min voltage differential or output-diode
|
|
109
112
|
|
|
110
113
|
LUT_output_efficiency: LUT1D = 12 * [1.00]
|
|
111
114
|
# ⤷ array[12] depending on output_current
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
parameters:
|
|
94
94
|
id: 1011
|
|
95
95
|
name: diode+capacitor
|
|
96
|
-
description: Simple Converter based on diode and
|
|
96
|
+
description: Simple Converter based on diode and storage capacitor
|
|
97
97
|
inherit_from: neutral
|
|
98
98
|
V_input_drop_mV: 300 # simulate input-diode
|
|
99
99
|
C_intermediate_uF: 47 # primary storage-Cap
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
parameters:
|
|
115
115
|
id: 1013
|
|
116
116
|
name: diode+resistor+capacitor
|
|
117
|
-
description: Simple Converter based on diode, current limiting resistor and
|
|
117
|
+
description: Simple Converter based on diode, current limiting resistor and storage capacitor
|
|
118
118
|
inherit_from: diode+capacitor
|
|
119
119
|
R_input_mOhm: 10000
|
|
120
120
|
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
enable_boost: true # if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
|
|
134
134
|
|
|
135
135
|
harvester:
|
|
136
|
-
name: mppt_bq_solar # harvester only active if input is
|
|
136
|
+
name: mppt_bq_solar # harvester only active if input is ivsurface / curves
|
|
137
137
|
|
|
138
138
|
V_input_max_mV: 3000
|
|
139
139
|
I_input_max_mA: 100
|
|
@@ -5,17 +5,16 @@ from datetime import datetime
|
|
|
5
5
|
from datetime import timedelta
|
|
6
6
|
from typing import Annotated
|
|
7
7
|
from typing import Optional
|
|
8
|
-
from typing import Union
|
|
9
|
-
from uuid import uuid4
|
|
10
8
|
|
|
11
|
-
from pydantic import UUID4
|
|
12
9
|
from pydantic import Field
|
|
13
10
|
from pydantic import model_validator
|
|
14
11
|
from typing_extensions import Self
|
|
12
|
+
from typing_extensions import deprecated
|
|
15
13
|
|
|
16
14
|
from shepherd_core.data_models.base.content import IdInt
|
|
17
15
|
from shepherd_core.data_models.base.content import NameStr
|
|
18
16
|
from shepherd_core.data_models.base.content import SafeStr
|
|
17
|
+
from shepherd_core.data_models.base.content import id_default
|
|
19
18
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
20
19
|
from shepherd_core.data_models.testbed.target import Target
|
|
21
20
|
from shepherd_core.data_models.testbed.testbed import Testbed
|
|
@@ -29,8 +28,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
29
28
|
"""Config for experiments on the testbed emulating energy environments for target nodes."""
|
|
30
29
|
|
|
31
30
|
# General Properties
|
|
32
|
-
|
|
33
|
-
id: Union[UUID4, int] = Field(default_factory=uuid4)
|
|
31
|
+
id: int = Field(description="Unique ID", default_factory=id_default)
|
|
34
32
|
# ⤷ TODO: automatic ID is problematic for identification by hash
|
|
35
33
|
|
|
36
34
|
name: NameStr
|
|
@@ -46,21 +44,23 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
46
44
|
# feedback
|
|
47
45
|
email_results: bool = False
|
|
48
46
|
|
|
49
|
-
sys_logging: SystemLogging = SystemLogging(
|
|
47
|
+
sys_logging: SystemLogging = SystemLogging() # = all active
|
|
50
48
|
|
|
51
49
|
# schedule
|
|
52
50
|
time_start: Optional[datetime] = None # = ASAP
|
|
53
51
|
duration: Optional[timedelta] = None # = till EOF
|
|
54
|
-
abort_on_error: bool = False
|
|
52
|
+
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
55
53
|
|
|
56
54
|
# targets
|
|
57
55
|
target_configs: Annotated[list[TargetConfig], Field(min_length=1, max_length=128)]
|
|
58
56
|
|
|
59
|
-
#
|
|
57
|
+
# debug
|
|
60
58
|
lib_ver: Optional[str] = version
|
|
61
59
|
|
|
62
60
|
@model_validator(mode="after")
|
|
63
61
|
def post_validation(self) -> Self:
|
|
62
|
+
# TODO: only do deep validation with active connection to TB-client
|
|
63
|
+
# or with cached fixtures
|
|
64
64
|
testbed = Testbed() # this will query the first (and only) entry of client
|
|
65
65
|
self._validate_targets(self.target_configs)
|
|
66
66
|
self._validate_observers(self.target_configs, testbed)
|
|
@@ -6,11 +6,14 @@ from typing import Annotated
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
|
+
from annotated_types import Interval
|
|
9
10
|
from pydantic import Field
|
|
10
11
|
from pydantic import PositiveFloat
|
|
11
12
|
from pydantic import model_validator
|
|
12
13
|
from typing_extensions import Self
|
|
14
|
+
from typing_extensions import deprecated
|
|
13
15
|
|
|
16
|
+
from shepherd_core import logger
|
|
14
17
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
15
18
|
from shepherd_core.data_models.testbed.gpio import GPIO
|
|
16
19
|
|
|
@@ -22,7 +25,7 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
22
25
|
"""
|
|
23
26
|
|
|
24
27
|
intermediate_voltage: bool = False
|
|
25
|
-
# ⤷ for EMU: record
|
|
28
|
+
# ⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
|
|
26
29
|
# this also includes current!
|
|
27
30
|
|
|
28
31
|
# time
|
|
@@ -47,21 +50,117 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
47
50
|
if not self.calculate_power and discard_all:
|
|
48
51
|
raise ValueError("Error in config -> tracing enabled, but output gets discarded")
|
|
49
52
|
if self.calculate_power:
|
|
50
|
-
raise NotImplementedError(
|
|
53
|
+
raise NotImplementedError(
|
|
54
|
+
"Feature PowerTracing.calculate_power reserved for future use."
|
|
55
|
+
)
|
|
56
|
+
if self.samplerate != 100_000:
|
|
57
|
+
raise NotImplementedError("Feature PowerTracing.samplerate reserved for future use.")
|
|
58
|
+
if self.discard_current:
|
|
59
|
+
raise NotImplementedError(
|
|
60
|
+
"Feature PowerTracing.discard_current reserved for future use."
|
|
61
|
+
)
|
|
62
|
+
if self.discard_voltage:
|
|
63
|
+
raise NotImplementedError(
|
|
64
|
+
"Feature PowerTracing.discard_voltage reserved for future use."
|
|
65
|
+
)
|
|
51
66
|
return self
|
|
52
67
|
|
|
53
68
|
|
|
69
|
+
# NOTE: this was taken from pyserial (removes one dependency)
|
|
70
|
+
BAUDRATES = (
|
|
71
|
+
50,
|
|
72
|
+
75,
|
|
73
|
+
110,
|
|
74
|
+
134,
|
|
75
|
+
150,
|
|
76
|
+
200,
|
|
77
|
+
300,
|
|
78
|
+
600,
|
|
79
|
+
1200,
|
|
80
|
+
1800,
|
|
81
|
+
2400,
|
|
82
|
+
4800,
|
|
83
|
+
9600,
|
|
84
|
+
19200,
|
|
85
|
+
38400,
|
|
86
|
+
57600,
|
|
87
|
+
115200,
|
|
88
|
+
230400,
|
|
89
|
+
460800,
|
|
90
|
+
500000,
|
|
91
|
+
576000,
|
|
92
|
+
921600,
|
|
93
|
+
1000000,
|
|
94
|
+
1152000,
|
|
95
|
+
1500000,
|
|
96
|
+
2000000,
|
|
97
|
+
2500000,
|
|
98
|
+
3000000,
|
|
99
|
+
3500000,
|
|
100
|
+
4000000,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = "N", "E", "O", "M", "S"
|
|
104
|
+
PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
|
|
105
|
+
|
|
106
|
+
STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
|
|
107
|
+
STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class UartLogging(ShpModel, title="Config for UART Logging"):
|
|
111
|
+
"""Configuration for recording UART-Output of the Target Nodes.
|
|
112
|
+
|
|
113
|
+
Note that the Communication has to be on a specific port that
|
|
114
|
+
reaches the hardware-module of the SBC.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
baudrate: Annotated[int, Field(ge=2_400, le=460_800)] = 115_200
|
|
118
|
+
# ⤷ TODO: find maximum that the system can handle
|
|
119
|
+
bytesize: Annotated[int, Field(ge=5, le=8)] = 8
|
|
120
|
+
stopbits: Annotated[float, Field(ge=1, le=2)] = 1
|
|
121
|
+
parity: str = PARITY_NONE
|
|
122
|
+
|
|
123
|
+
@model_validator(mode="after")
|
|
124
|
+
def post_validation(self) -> Self:
|
|
125
|
+
if self.baudrate not in BAUDRATES:
|
|
126
|
+
msg = f"Error in config -> baud-rate must be one of: {BAUDRATES}"
|
|
127
|
+
raise ValueError(msg)
|
|
128
|
+
if self.stopbits not in STOPBITS:
|
|
129
|
+
msg = f"Error in config -> stop-bits must be one of: {STOPBITS}"
|
|
130
|
+
raise ValueError(msg)
|
|
131
|
+
if self.parity not in PARITIES:
|
|
132
|
+
msg = f"Error in config -> parity must be one of: {PARITIES}"
|
|
133
|
+
raise ValueError(msg)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
GpioInt = Annotated[int, Interval(ge=0, le=17)]
|
|
138
|
+
GpioList = Annotated[list[GpioInt], Field(min_length=1, max_length=18)]
|
|
139
|
+
all_gpio = list(range(18))
|
|
140
|
+
|
|
141
|
+
|
|
54
142
|
class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
55
143
|
"""Configuration for recording the GPIO-Output of the Target Nodes.
|
|
56
144
|
|
|
57
145
|
TODO: postprocessing not implemented ATM
|
|
58
146
|
"""
|
|
59
147
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
148
|
+
gpios: GpioList = all_gpio
|
|
149
|
+
"""List of GPIO to record.
|
|
150
|
+
|
|
151
|
+
This feature allows to remove unwanted pins from recording,
|
|
152
|
+
i.e. for chatty pins with separate UART Logging enabled.
|
|
153
|
+
Numbering is based on the Target-Port and its 16x GPIO and two PwrGood-Signals.
|
|
154
|
+
See doc for nRF_FRAM_Target_v1.3+ to see mapping of target port.
|
|
155
|
+
|
|
156
|
+
Example for skipping UART (pin 0 & 1):
|
|
157
|
+
.gpio = range(2,18)
|
|
158
|
+
|
|
159
|
+
Note:
|
|
160
|
+
- Cape 2.4 (2023) and lower only has 9x GPIO + 1x PwrGood
|
|
161
|
+
- Cape 2.5 (2025) has first 12 GPIO & both PwrGood
|
|
162
|
+
- this will be mapped accordingly by the observer
|
|
163
|
+
"""
|
|
65
164
|
|
|
66
165
|
# time
|
|
67
166
|
delay: timedelta = timedelta(seconds=0)
|
|
@@ -69,22 +168,30 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
69
168
|
|
|
70
169
|
# post-processing,
|
|
71
170
|
uart_decode: bool = False
|
|
72
|
-
# TODO: quickfix - uart-log currently done online in userspace
|
|
73
|
-
# NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
|
|
74
171
|
uart_pin: GPIO = GPIO(name="GPIO8")
|
|
75
|
-
uart_baudrate: Annotated[int, Field(ge=2_400, le=
|
|
76
|
-
# TODO: add a "discard_gpio" (if only uart is wanted)
|
|
172
|
+
uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
|
|
77
173
|
|
|
78
174
|
@model_validator(mode="after")
|
|
79
175
|
def post_validation(self) -> Self:
|
|
80
|
-
if self.mask == 0:
|
|
81
|
-
raise ValueError("Error in config -> tracing enabled but mask is 0")
|
|
82
176
|
if self.delay and self.delay.total_seconds() < 0:
|
|
83
177
|
raise ValueError("Delay can't be negative.")
|
|
84
178
|
if self.duration and self.duration.total_seconds() < 0:
|
|
85
179
|
raise ValueError("Duration can't be negative.")
|
|
180
|
+
if self.uart_decode:
|
|
181
|
+
logger.error(
|
|
182
|
+
"Feature GpioTracing.uart_decode reserved for future use. "
|
|
183
|
+
"Use UartLogging or manually decode serial with the provided waveform decoder."
|
|
184
|
+
)
|
|
86
185
|
return self
|
|
87
186
|
|
|
187
|
+
@property
|
|
188
|
+
def gpio_mask(self) -> int:
|
|
189
|
+
# valid for cape v2.5
|
|
190
|
+
mask = 0
|
|
191
|
+
for gpio in set(self.gpios):
|
|
192
|
+
mask |= 2**gpio
|
|
193
|
+
return mask
|
|
194
|
+
|
|
88
195
|
|
|
89
196
|
class GpioLevel(str, Enum):
|
|
90
197
|
"""Options for setting the gpio-level or state."""
|
|
@@ -133,11 +240,15 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
|
133
240
|
class SystemLogging(ShpModel, title="Config for System-Logging"):
|
|
134
241
|
"""Configuration for recording Debug-Output of the Observers System-Services."""
|
|
135
242
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
243
|
+
kernel: bool = True
|
|
244
|
+
time_sync: bool = True
|
|
245
|
+
sheep: bool = True
|
|
246
|
+
sys_util: bool = True
|
|
247
|
+
|
|
248
|
+
# TODO: remove lines below in 2026
|
|
249
|
+
dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
|
|
250
|
+
ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
|
|
251
|
+
shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
|
|
141
252
|
|
|
142
253
|
|
|
143
254
|
# TODO: some more interaction would be good
|
|
@@ -18,6 +18,7 @@ from shepherd_core.data_models.testbed.target import Target
|
|
|
18
18
|
from .observer_features import GpioActuation
|
|
19
19
|
from .observer_features import GpioTracing
|
|
20
20
|
from .observer_features import PowerTracing
|
|
21
|
+
from .observer_features import UartLogging
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class TargetConfig(ShpModel, title="Target Config"):
|
|
@@ -38,10 +39,12 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
38
39
|
|
|
39
40
|
firmware1: Firmware
|
|
40
41
|
firmware2: Optional[Firmware] = None
|
|
42
|
+
# ⤷ omitted FW gets set to neutral deep-sleep
|
|
41
43
|
|
|
42
44
|
power_tracing: Optional[PowerTracing] = None
|
|
43
45
|
gpio_tracing: Optional[GpioTracing] = None
|
|
44
46
|
gpio_actuation: Optional[GpioActuation] = None
|
|
47
|
+
uart_logging: Optional[UartLogging] = None
|
|
45
48
|
|
|
46
49
|
@model_validator(mode="after")
|
|
47
50
|
def post_validation(self) -> Self:
|
|
@@ -79,6 +82,8 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
79
82
|
msg = f"Provided custom IDs {c_ids} not enough to cover target range {t_ids}"
|
|
80
83
|
raise ValueError(msg)
|
|
81
84
|
# TODO: if custom ids present, firmware must be ELF
|
|
85
|
+
if self.gpio_actuation is not None:
|
|
86
|
+
raise NotImplementedError("Feature GpioActuation reserved for future use.")
|
|
82
87
|
return self
|
|
83
88
|
|
|
84
89
|
def get_custom_id(self, target_id: int) -> Optional[int]:
|
|
@@ -54,7 +54,8 @@ def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = N
|
|
|
54
54
|
parameters=config.model_dump(),
|
|
55
55
|
)
|
|
56
56
|
else:
|
|
57
|
-
|
|
57
|
+
msg = f"had unknown input: {type(config)}"
|
|
58
|
+
raise TypeError(msg)
|
|
58
59
|
|
|
59
60
|
if shp_wrap.datatype == TestbedTasks.__name__:
|
|
60
61
|
if observer is None:
|
|
@@ -66,7 +67,8 @@ def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = N
|
|
|
66
67
|
logger.debug("Loading Testbed-Tasks %s for %s", tbt.name, observer)
|
|
67
68
|
obt = tbt.get_observer_tasks(observer)
|
|
68
69
|
if obt is None:
|
|
69
|
-
|
|
70
|
+
msg = f"Observer '{observer}' is not in TestbedTask-Set"
|
|
71
|
+
raise ValueError(msg)
|
|
70
72
|
shp_wrap = Wrapper(
|
|
71
73
|
datatype=type(obt).__name__,
|
|
72
74
|
parameters=obt.model_dump(),
|
|
@@ -92,6 +94,7 @@ def extract_tasks(shp_wrap: Wrapper, *, no_task_sets: bool = True) -> list[ShpMo
|
|
|
92
94
|
raise ValueError("Model in Wrapper was TestbedTasks -> Task-Sets not allowed!")
|
|
93
95
|
content = [TestbedTasks(**shp_wrap.parameters)]
|
|
94
96
|
else:
|
|
95
|
-
|
|
97
|
+
msg = f"Extractor had unknown task: {shp_wrap.datatype}"
|
|
98
|
+
raise ValueError(msg)
|
|
96
99
|
|
|
97
100
|
return content
|
|
@@ -13,6 +13,7 @@ from pydantic import Field
|
|
|
13
13
|
from pydantic import model_validator
|
|
14
14
|
from pydantic import validate_call
|
|
15
15
|
from typing_extensions import Self
|
|
16
|
+
from typing_extensions import deprecated
|
|
16
17
|
|
|
17
18
|
from shepherd_core.data_models.base.content import IdInt
|
|
18
19
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
@@ -23,8 +24,10 @@ from shepherd_core.data_models.experiment.observer_features import GpioActuation
|
|
|
23
24
|
from shepherd_core.data_models.experiment.observer_features import GpioTracing
|
|
24
25
|
from shepherd_core.data_models.experiment.observer_features import PowerTracing
|
|
25
26
|
from shepherd_core.data_models.experiment.observer_features import SystemLogging
|
|
27
|
+
from shepherd_core.data_models.experiment.observer_features import UartLogging
|
|
26
28
|
from shepherd_core.data_models.testbed import Testbed
|
|
27
29
|
from shepherd_core.data_models.testbed.cape import TargetPort
|
|
30
|
+
from shepherd_core.logger import logger
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class Compression(str, Enum):
|
|
@@ -61,16 +64,16 @@ class EmulationTask(ShpModel):
|
|
|
61
64
|
# timestamp or unix epoch time, None = ASAP
|
|
62
65
|
duration: Optional[timedelta] = None
|
|
63
66
|
# ⤷ Duration of recording in seconds, None = till EOF
|
|
64
|
-
abort_on_error: bool
|
|
67
|
+
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
65
68
|
|
|
66
69
|
# emulation-specific
|
|
67
70
|
use_cal_default: bool = False
|
|
68
71
|
# ⤷ Use default calibration values, skip loading from EEPROM
|
|
69
72
|
|
|
70
|
-
enable_io: bool =
|
|
73
|
+
enable_io: bool = True
|
|
71
74
|
# TODO: add direction of pins! also it seems error-prone when only setting _tracing
|
|
72
75
|
# ⤷ Switch the GPIO level converter to targets on/off
|
|
73
|
-
# pre-req for sampling gpio,
|
|
76
|
+
# pre-req for sampling gpio / uart,
|
|
74
77
|
io_port: TargetPort = TargetPort.A
|
|
75
78
|
# ⤷ Either Port A or B that gets connected to IO
|
|
76
79
|
pwr_port: TargetPort = TargetPort.A
|
|
@@ -89,6 +92,7 @@ class EmulationTask(ShpModel):
|
|
|
89
92
|
|
|
90
93
|
power_tracing: Optional[PowerTracing] = PowerTracing()
|
|
91
94
|
gpio_tracing: Optional[GpioTracing] = GpioTracing()
|
|
95
|
+
uart_logging: Optional[UartLogging] = UartLogging()
|
|
92
96
|
gpio_actuation: Optional[GpioActuation] = None
|
|
93
97
|
sys_logging: Optional[SystemLogging] = SystemLogging()
|
|
94
98
|
|
|
@@ -127,6 +131,14 @@ class EmulationTask(ShpModel):
|
|
|
127
131
|
raise ValueError("Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'.")
|
|
128
132
|
if self.gpio_actuation is not None:
|
|
129
133
|
raise ValueError("GPIO Actuation not yet implemented!")
|
|
134
|
+
|
|
135
|
+
io_requested = any(
|
|
136
|
+
_io is not None for _io in (self.gpio_actuation, self.gpio_tracing, self.uart_logging)
|
|
137
|
+
)
|
|
138
|
+
if self.enable_io and not io_requested:
|
|
139
|
+
logger.warning("Target IO enabled, but no feature requested IO")
|
|
140
|
+
if not self.enable_io and io_requested:
|
|
141
|
+
logger.warning("Target IO not enabled, but a feature requested IO")
|
|
130
142
|
return self
|
|
131
143
|
|
|
132
144
|
@classmethod
|
|
@@ -134,19 +146,23 @@ class EmulationTask(ShpModel):
|
|
|
134
146
|
def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path) -> Self:
|
|
135
147
|
obs = tb.get_observer(tgt_id)
|
|
136
148
|
tgt_cfg = xp.get_target_config(tgt_id)
|
|
149
|
+
io_requested = any(
|
|
150
|
+
_io is not None
|
|
151
|
+
for _io in (tgt_cfg.gpio_actuation, tgt_cfg.gpio_tracing, tgt_cfg.uart_logging)
|
|
152
|
+
)
|
|
137
153
|
|
|
138
154
|
return cls(
|
|
139
155
|
input_path=tgt_cfg.energy_env.data_path,
|
|
140
156
|
output_path=root_path / f"emu_{obs.name}.h5",
|
|
141
157
|
time_start=copy.copy(xp.time_start),
|
|
142
158
|
duration=xp.duration,
|
|
143
|
-
|
|
144
|
-
enable_io=(tgt_cfg.gpio_tracing is not None) or (tgt_cfg.gpio_actuation is not None),
|
|
159
|
+
enable_io=io_requested,
|
|
145
160
|
io_port=obs.get_target_port(tgt_id),
|
|
146
161
|
pwr_port=obs.get_target_port(tgt_id),
|
|
147
162
|
virtual_source=tgt_cfg.virtual_source,
|
|
148
163
|
power_tracing=tgt_cfg.power_tracing,
|
|
149
164
|
gpio_tracing=tgt_cfg.gpio_tracing,
|
|
165
|
+
uart_logging=tgt_cfg.uart_logging,
|
|
150
166
|
gpio_actuation=tgt_cfg.gpio_actuation,
|
|
151
167
|
sys_logging=xp.sys_logging,
|
|
152
168
|
)
|
|
@@ -9,6 +9,7 @@ from typing import Optional
|
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
from pydantic import model_validator
|
|
11
11
|
from typing_extensions import Self
|
|
12
|
+
from typing_extensions import deprecated
|
|
12
13
|
|
|
13
14
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
14
15
|
from shepherd_core.data_models.base.timezone import local_tz
|
|
@@ -36,8 +37,8 @@ class HarvestTask(ShpModel):
|
|
|
36
37
|
time_start: Optional[datetime] = None
|
|
37
38
|
# timestamp or unix epoch time, None = ASAP
|
|
38
39
|
duration: Optional[timedelta] = None
|
|
39
|
-
# ⤷ Duration of recording in seconds, None = till
|
|
40
|
-
abort_on_error: bool = False
|
|
40
|
+
# ⤷ Duration of recording in seconds, None = till EOFSys
|
|
41
|
+
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
41
42
|
|
|
42
43
|
# emulation-specific
|
|
43
44
|
use_cal_default: bool = False
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
8
9
|
from pydantic import validate_call
|
|
9
10
|
from typing_extensions import Self
|
|
11
|
+
from typing_extensions import deprecated
|
|
10
12
|
|
|
11
13
|
from shepherd_core.data_models.base.content import IdInt
|
|
12
14
|
from shepherd_core.data_models.base.content import NameStr
|
|
@@ -28,7 +30,7 @@ class ObserverTasks(ShpModel):
|
|
|
28
30
|
# PRE PROCESS
|
|
29
31
|
time_prep: datetime # TODO: should be optional
|
|
30
32
|
root_path: Path
|
|
31
|
-
abort_on_error: bool
|
|
33
|
+
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
32
34
|
|
|
33
35
|
# fw mod, store as hex-file and program
|
|
34
36
|
fw1_mod: Optional[FirmwareModTask] = None
|
|
@@ -70,7 +72,6 @@ class ObserverTasks(ShpModel):
|
|
|
70
72
|
owner_id=xp.owner_id,
|
|
71
73
|
time_prep=t_start - tb.prep_duration,
|
|
72
74
|
root_path=root_path,
|
|
73
|
-
abort_on_error=xp.abort_on_error,
|
|
74
75
|
fw1_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
|
|
75
76
|
fw2_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 2, fw_paths[1]),
|
|
76
77
|
fw1_prog=ProgrammingTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
|
|
@@ -91,8 +92,8 @@ class ObserverTasks(ShpModel):
|
|
|
91
92
|
tasks.append(task)
|
|
92
93
|
return tasks
|
|
93
94
|
|
|
94
|
-
def get_output_paths(self) -> dict:
|
|
95
|
-
values = {}
|
|
95
|
+
def get_output_paths(self) -> dict[str, Path]:
|
|
96
|
+
values: dict[str, Path] = {}
|
|
96
97
|
if isinstance(self.emulation, EmulationTask):
|
|
97
98
|
if self.emulation.output_path is None:
|
|
98
99
|
raise ValueError("Emu-Task should have a valid output-path")
|
|
@@ -33,6 +33,7 @@ class ProgrammingTask(ShpModel):
|
|
|
33
33
|
voltage: Annotated[float, Field(ge=1, lt=5)] = 3
|
|
34
34
|
datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 200_000
|
|
35
35
|
protocol: ProgrammerProtocol
|
|
36
|
+
# TODO: eradicate - should not exist. derive protocol from mcu_type
|
|
36
37
|
|
|
37
38
|
simulate: bool = False
|
|
38
39
|
|
|
@@ -43,7 +44,8 @@ class ProgrammingTask(ShpModel):
|
|
|
43
44
|
def post_validation(self) -> Self:
|
|
44
45
|
d_type = suffix_to_DType.get(self.firmware_file.suffix.lower())
|
|
45
46
|
if d_type != FirmwareDType.base64_hex:
|
|
46
|
-
|
|
47
|
+
msg = (f"Firmware is not intel-hex ({self.firmware_file.as_posix()})",)
|
|
48
|
+
raise ValueError(msg)
|
|
47
49
|
return self
|
|
48
50
|
|
|
49
51
|
@classmethod
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Collection of tasks for all observers included in experiment."""
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import Annotated
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
@@ -50,11 +51,11 @@ class TestbedTasks(ShpModel):
|
|
|
50
51
|
return tasks
|
|
51
52
|
return None
|
|
52
53
|
|
|
53
|
-
def get_output_paths(self) -> dict:
|
|
54
|
+
def get_output_paths(self) -> dict[str, Path]:
|
|
54
55
|
# TODO: computed field preferred, but they don't work here, as
|
|
55
56
|
# - they are always stored in yaml despite "repr=False"
|
|
56
57
|
# - solution will shift to some kind of "result"-datatype that is combinable
|
|
57
|
-
values = {}
|
|
58
|
+
values: dict[str, Path] = {}
|
|
58
59
|
for obt in self.observer_tasks:
|
|
59
60
|
values = {**values, **obt.get_output_paths()}
|
|
60
61
|
return values
|
|
@@ -65,3 +65,10 @@ class GPIO(ShpModel, title="GPIO of Observer Node"):
|
|
|
65
65
|
|
|
66
66
|
def user_controllable(self) -> bool:
|
|
67
67
|
return ("gpio" in self.name.lower()) and (self.direction in {"IO", "OUT"})
|
|
68
|
+
|
|
69
|
+
def user_recordable(self) -> bool:
|
|
70
|
+
return (
|
|
71
|
+
("gpio" in self.name.lower())
|
|
72
|
+
and (self.direction in {"IO", "IN"})
|
|
73
|
+
and (self.pin_pru is not None)
|
|
74
|
+
)
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
parameters:
|
|
4
4
|
id: 1001
|
|
5
5
|
name: nRF52
|
|
6
|
-
description:
|
|
6
|
+
description: MCU with RF, 802.15.4, Bluetooth v5.0, 2.4GHz
|
|
7
7
|
platform: nRF52
|
|
8
8
|
core: nRF52840
|
|
9
9
|
prog_protocol: SWD
|
|
10
|
-
fw_name_default:
|
|
10
|
+
fw_name_default: nrf52_deep_sleep
|
|
11
11
|
- datatype: mcu
|
|
12
12
|
parameters:
|
|
13
13
|
id: 1002
|
|
14
14
|
name: MSP430FR
|
|
15
|
-
description: 16MHz Ultra-Low-Pwr MCU with
|
|
15
|
+
description: 16MHz Ultra-Low-Pwr MCU with 256 KB FRAM
|
|
16
16
|
platform: MSP430
|
|
17
|
-
core:
|
|
17
|
+
core: MSP430FR5994
|
|
18
18
|
prog_protocol: SBW
|
|
19
19
|
fw_name_default: msp430_deep_sleep
|
|
@@ -221,3 +221,20 @@
|
|
|
221
221
|
target_a:
|
|
222
222
|
name: nRF52_FRAM_1392_390
|
|
223
223
|
created: 2023-09-22 12:12:12
|
|
224
|
+
- datatype: observer
|
|
225
|
+
parameters:
|
|
226
|
+
id: 42
|
|
227
|
+
name: unit_testing_sheep
|
|
228
|
+
ip: 0.0.0.0
|
|
229
|
+
mac: 00:00:00:00:00:00
|
|
230
|
+
room: none
|
|
231
|
+
eth_port: none
|
|
232
|
+
description: unit testing
|
|
233
|
+
longitude: 0
|
|
234
|
+
latitude: 0
|
|
235
|
+
cape:
|
|
236
|
+
name: unit_testing_cape
|
|
237
|
+
target_a:
|
|
238
|
+
name: unit_testing_target
|
|
239
|
+
created: 2025-04-29 13:07:00
|
|
240
|
+
active: true
|
|
@@ -161,3 +161,16 @@
|
|
|
161
161
|
name: nRF52
|
|
162
162
|
mcu2: null
|
|
163
163
|
created: 2022-12-12 12:12:12
|
|
164
|
+
|
|
165
|
+
- datatype: target
|
|
166
|
+
parameters:
|
|
167
|
+
id: 42
|
|
168
|
+
name: unit_testing_target
|
|
169
|
+
version: v0
|
|
170
|
+
description: for unit testing
|
|
171
|
+
comment: no comment
|
|
172
|
+
created: 2025-04-29
|
|
173
|
+
mcu1:
|
|
174
|
+
name: nRF52
|
|
175
|
+
mcu2:
|
|
176
|
+
name: MSP430FR
|
|
@@ -23,3 +23,14 @@
|
|
|
23
23
|
- name: sheep12
|
|
24
24
|
- name: sheep13
|
|
25
25
|
- name: sheep14
|
|
26
|
+
|
|
27
|
+
- datatype: testbed
|
|
28
|
+
parameters:
|
|
29
|
+
id: 42
|
|
30
|
+
name: unit_testing_testbed
|
|
31
|
+
description: "Artificial Testbed used in unit testing"
|
|
32
|
+
shared_storage: true
|
|
33
|
+
data_on_server: /tmp/
|
|
34
|
+
data_on_observer: /tmp/
|
|
35
|
+
observers:
|
|
36
|
+
- name: unit_testing_sheep
|