shepherd-core 2023.8.6__py3-none-any.whl → 2023.8.8__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/__init__.py +1 -1
- shepherd_core/data_models/__init__.py +3 -1
- shepherd_core/data_models/base/cal_measurement.py +17 -14
- shepherd_core/data_models/base/calibration.py +41 -8
- shepherd_core/data_models/base/content.py +17 -13
- shepherd_core/data_models/base/shepherd.py +29 -22
- shepherd_core/data_models/base/wrapper.py +5 -4
- shepherd_core/data_models/content/energy_environment.py +3 -2
- shepherd_core/data_models/content/firmware.py +10 -6
- shepherd_core/data_models/content/virtual_harvester.py +42 -39
- shepherd_core/data_models/content/virtual_source.py +83 -72
- shepherd_core/data_models/doc_virtual_source.py +7 -14
- shepherd_core/data_models/experiment/experiment.py +20 -15
- shepherd_core/data_models/experiment/observer_features.py +33 -31
- shepherd_core/data_models/experiment/target_config.py +24 -18
- shepherd_core/data_models/task/__init__.py +13 -5
- shepherd_core/data_models/task/emulation.py +35 -23
- shepherd_core/data_models/task/firmware_mod.py +14 -13
- shepherd_core/data_models/task/harvest.py +28 -13
- shepherd_core/data_models/task/observer_tasks.py +17 -7
- shepherd_core/data_models/task/programming.py +13 -13
- shepherd_core/data_models/task/testbed_tasks.py +16 -6
- shepherd_core/data_models/testbed/cape.py +3 -2
- shepherd_core/data_models/testbed/gpio.py +18 -15
- shepherd_core/data_models/testbed/mcu.py +7 -6
- shepherd_core/data_models/testbed/observer.py +23 -19
- shepherd_core/data_models/testbed/target.py +15 -14
- shepherd_core/data_models/testbed/testbed.py +14 -11
- shepherd_core/fw_tools/converter.py +7 -7
- shepherd_core/fw_tools/converter_elf.py +2 -2
- shepherd_core/fw_tools/patcher.py +7 -6
- shepherd_core/fw_tools/validation.py +3 -3
- shepherd_core/inventory/__init__.py +16 -8
- shepherd_core/inventory/python.py +4 -3
- shepherd_core/inventory/system.py +5 -5
- shepherd_core/inventory/target.py +4 -4
- shepherd_core/reader.py +3 -3
- shepherd_core/testbed_client/client.py +6 -4
- shepherd_core/testbed_client/user_model.py +14 -10
- shepherd_core/writer.py +2 -2
- {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/METADATA +9 -2
- {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/RECORD +49 -49
- {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/WHEEL +1 -1
- tests/data_models/example_cal_data.yaml +2 -1
- tests/data_models/example_cal_meas.yaml +2 -1
- tests/data_models/test_base_models.py +19 -2
- tests/inventory/test_inventory.py +1 -1
- {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/top_level.txt +0 -0
- {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/zip-safe +0 -0
|
@@ -6,10 +6,10 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
from typing import Union
|
|
8
8
|
|
|
9
|
-
from pydantic import
|
|
10
|
-
from pydantic import
|
|
11
|
-
from pydantic import
|
|
12
|
-
from
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from pydantic import model_validator
|
|
11
|
+
from pydantic import validate_call
|
|
12
|
+
from typing_extensions import Annotated
|
|
13
13
|
|
|
14
14
|
from ..base.content import IdInt
|
|
15
15
|
from ..base.shepherd import ShpModel
|
|
@@ -41,12 +41,12 @@ class EmulationTask(ShpModel):
|
|
|
41
41
|
# General config
|
|
42
42
|
input_path: Path
|
|
43
43
|
# ⤷ hdf5 file containing harvesting data
|
|
44
|
-
output_path: Optional[Path]
|
|
44
|
+
output_path: Optional[Path] = None
|
|
45
45
|
# ⤷ dir- or file-path for storing the recorded data:
|
|
46
46
|
# - providing a directory -> file is named emu_timestamp.h5
|
|
47
47
|
# - for a complete path the filename is not changed except it exists and
|
|
48
48
|
# overwrite is disabled -> emu#num.h5
|
|
49
|
-
# TODO: should the path be mandatory?
|
|
49
|
+
# TODO: should the output-path be mandatory?
|
|
50
50
|
force_overwrite: bool = False
|
|
51
51
|
# ⤷ Overwrite existing file
|
|
52
52
|
output_compression: Optional[Compression] = Compression.default
|
|
@@ -70,7 +70,7 @@ class EmulationTask(ShpModel):
|
|
|
70
70
|
pwr_port: TargetPort = TargetPort.A
|
|
71
71
|
# ⤷ chosen port will be current-monitored (main, connected to virtual Source),
|
|
72
72
|
# the other port is aux
|
|
73
|
-
voltage_aux: Union[
|
|
73
|
+
voltage_aux: Union[Annotated[float, Field(ge=0, le=4.5)], str] = 0
|
|
74
74
|
# ⤷ aux_voltage options:
|
|
75
75
|
# - 0-4.5 for specific const Voltage (0 V = disabled),
|
|
76
76
|
# - "buffer" will output intermediate voltage (storage cap of vsource),
|
|
@@ -86,35 +86,47 @@ class EmulationTask(ShpModel):
|
|
|
86
86
|
gpio_actuation: Optional[GpioActuation] = None
|
|
87
87
|
sys_logging: Optional[SystemLogging] = SystemLogging()
|
|
88
88
|
|
|
89
|
-
verbose:
|
|
89
|
+
verbose: Annotated[int, Field(ge=0, le=4)] = 2
|
|
90
90
|
# ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
|
|
91
91
|
|
|
92
|
-
@
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if
|
|
92
|
+
@model_validator(mode="before")
|
|
93
|
+
@classmethod
|
|
94
|
+
def pre_correction(cls, values: dict) -> dict:
|
|
95
|
+
# convert & add local timezone-data
|
|
96
|
+
has_time = values.get("time_start") is not None
|
|
97
|
+
if has_time and isinstance(values["time_start"], (int, float)):
|
|
98
|
+
values["time_start"] = datetime.fromtimestamp(values["time_start"])
|
|
99
|
+
if has_time and isinstance(values["time_start"], str):
|
|
100
|
+
values["time_start"] = datetime.fromisoformat(values["time_start"])
|
|
101
|
+
if has_time and values["time_start"].tzinfo is None:
|
|
98
102
|
values["time_start"] = values["time_start"].astimezone()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
return values
|
|
104
|
+
|
|
105
|
+
@model_validator(mode="after")
|
|
106
|
+
def post_validation(self):
|
|
107
|
+
# TODO: limit paths
|
|
108
|
+
has_time = self.time_start is not None
|
|
109
|
+
time_now = datetime.now().astimezone()
|
|
110
|
+
if has_time and self.time_start < time_now:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"Start-Time for Emulation can't be in the past "
|
|
113
|
+
f"('{self.time_start}' vs '{time_now}'."
|
|
114
|
+
)
|
|
115
|
+
if self.duration and self.duration.total_seconds() < 0:
|
|
102
116
|
raise ValueError("Task-Duration can't be negative.")
|
|
103
|
-
if isinstance(
|
|
104
|
-
"voltage_aux"
|
|
105
|
-
) not in [
|
|
117
|
+
if isinstance(self.voltage_aux, str) and self.voltage_aux not in [
|
|
106
118
|
"main",
|
|
107
119
|
"buffer",
|
|
108
120
|
]:
|
|
109
121
|
raise ValueError(
|
|
110
122
|
"Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'."
|
|
111
123
|
)
|
|
112
|
-
if
|
|
124
|
+
if self.gpio_actuation is not None:
|
|
113
125
|
raise ValueError("GPIO Actuation not yet implemented!")
|
|
114
|
-
return
|
|
126
|
+
return self
|
|
115
127
|
|
|
116
128
|
@classmethod
|
|
117
|
-
@
|
|
129
|
+
@validate_call
|
|
118
130
|
def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path):
|
|
119
131
|
obs = tb.get_observer(tgt_id)
|
|
120
132
|
tgt_cfg = xp.get_target_config(tgt_id)
|
|
@@ -3,16 +3,17 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
from typing import Union
|
|
5
5
|
|
|
6
|
-
from pydantic import
|
|
7
|
-
from pydantic import
|
|
8
|
-
from pydantic import
|
|
9
|
-
from
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from pydantic import model_validator
|
|
8
|
+
from pydantic import validate_call
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
10
|
|
|
11
11
|
from ...logger import logger
|
|
12
12
|
from ..base.content import IdInt
|
|
13
13
|
from ..base.shepherd import ShpModel
|
|
14
14
|
from ..content.firmware import Firmware
|
|
15
15
|
from ..content.firmware import FirmwareDType
|
|
16
|
+
from ..content.firmware import FirmwareStr
|
|
16
17
|
from ..experiment.experiment import Experiment
|
|
17
18
|
from ..testbed import Testbed
|
|
18
19
|
from ..testbed.target import IdInt16
|
|
@@ -22,27 +23,27 @@ from ..testbed.target import MCUPort
|
|
|
22
23
|
class FirmwareModTask(ShpModel):
|
|
23
24
|
"""Config for Task that adds the custom ID to the firmware & stores it into a file"""
|
|
24
25
|
|
|
25
|
-
data: Union[
|
|
26
|
+
data: Union[FirmwareStr, Path]
|
|
26
27
|
data_type: FirmwareDType
|
|
27
|
-
custom_id: Optional[IdInt16]
|
|
28
|
+
custom_id: Optional[IdInt16] = None
|
|
28
29
|
firmware_file: Path
|
|
29
30
|
|
|
30
|
-
verbose:
|
|
31
|
+
verbose: Annotated[int, Field(ge=0, le=4)] = 2
|
|
31
32
|
# ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
|
|
32
33
|
|
|
33
|
-
@
|
|
34
|
-
def post_validation(
|
|
35
|
-
if
|
|
34
|
+
@model_validator(mode="after")
|
|
35
|
+
def post_validation(self):
|
|
36
|
+
if self.data_type in [
|
|
36
37
|
FirmwareDType.base64_hex,
|
|
37
38
|
FirmwareDType.path_hex,
|
|
38
39
|
]:
|
|
39
40
|
logger.warning(
|
|
40
41
|
"Firmware is scheduled to get custom-ID but is not in elf-format"
|
|
41
42
|
)
|
|
42
|
-
return
|
|
43
|
+
return self
|
|
43
44
|
|
|
44
45
|
@classmethod
|
|
45
|
-
@
|
|
46
|
+
@validate_call
|
|
46
47
|
def from_xp(
|
|
47
48
|
cls,
|
|
48
49
|
xp: Experiment,
|
|
@@ -70,7 +71,7 @@ class FirmwareModTask(ShpModel):
|
|
|
70
71
|
)
|
|
71
72
|
|
|
72
73
|
@classmethod
|
|
73
|
-
@
|
|
74
|
+
@validate_call
|
|
74
75
|
def from_firmware(cls, fw: Firmware, **kwargs):
|
|
75
76
|
kwargs["data"] = fw.data
|
|
76
77
|
kwargs["data_type"] = fw.data_type
|
|
@@ -3,8 +3,9 @@ from datetime import timedelta
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
|
-
from pydantic import
|
|
7
|
-
from pydantic import
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from pydantic import model_validator
|
|
8
|
+
from typing_extensions import Annotated
|
|
8
9
|
|
|
9
10
|
from ..base.shepherd import ShpModel
|
|
10
11
|
from ..content.virtual_harvester import VirtualHarvesterConfig
|
|
@@ -46,20 +47,34 @@ class HarvestTask(ShpModel):
|
|
|
46
47
|
power_tracing: PowerTracing = PowerTracing()
|
|
47
48
|
sys_logging: Optional[SystemLogging] = SystemLogging()
|
|
48
49
|
|
|
49
|
-
verbose:
|
|
50
|
+
verbose: Annotated[int, Field(ge=0, le=4)] = 2
|
|
50
51
|
# ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
|
|
51
52
|
|
|
52
53
|
# TODO: there is an unused DAC-Output patched to the harvesting-port
|
|
53
54
|
|
|
54
|
-
@
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
@model_validator(mode="before")
|
|
56
|
+
@classmethod
|
|
57
|
+
def pre_correction(cls, values: dict) -> dict:
|
|
58
|
+
# convert & add local timezone-data, TODO: used twice, refactor
|
|
59
|
+
has_time = values.get("time_start") is not None
|
|
60
|
+
if has_time and isinstance(values["time_start"], (int, float)):
|
|
61
|
+
values["time_start"] = datetime.fromtimestamp(values["time_start"])
|
|
62
|
+
if has_time and isinstance(values["time_start"], str):
|
|
63
|
+
values["time_start"] = datetime.fromisoformat(values["time_start"])
|
|
64
|
+
if has_time and values["time_start"].tzinfo is None:
|
|
60
65
|
values["time_start"] = values["time_start"].astimezone()
|
|
61
|
-
if has_start and values["time_start"] < datetime.now().astimezone():
|
|
62
|
-
raise ValueError("Start-Time for Harvest can't be in the past.")
|
|
63
|
-
if values.get("duration") and values["duration"].total_seconds() < 0:
|
|
64
|
-
raise ValueError("Task-Duration can't be negative.")
|
|
65
66
|
return values
|
|
67
|
+
|
|
68
|
+
@model_validator(mode="after")
|
|
69
|
+
def post_validation(self):
|
|
70
|
+
# TODO: limit paths
|
|
71
|
+
has_time = self.time_start is not None
|
|
72
|
+
time_now = datetime.now().astimezone()
|
|
73
|
+
if has_time and self.time_start < time_now:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"Start-Time for Emulation can't be in the past "
|
|
76
|
+
f"('{self.time_start}' vs '{time_now}'."
|
|
77
|
+
)
|
|
78
|
+
if self.duration and self.duration.total_seconds() < 0:
|
|
79
|
+
raise ValueError("Task-Duration can't be negative.")
|
|
80
|
+
return self
|
|
@@ -3,7 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import List
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
|
-
from pydantic import
|
|
6
|
+
from pydantic import computed_field
|
|
7
|
+
from pydantic import validate_call
|
|
7
8
|
|
|
8
9
|
from ..base.content import IdInt
|
|
9
10
|
from ..base.content import NameStr
|
|
@@ -27,13 +28,13 @@ class ObserverTasks(ShpModel):
|
|
|
27
28
|
abort_on_error: bool
|
|
28
29
|
|
|
29
30
|
# fw mod, store as hex-file and program
|
|
30
|
-
fw1_mod: Optional[FirmwareModTask]
|
|
31
|
-
fw2_mod: Optional[FirmwareModTask]
|
|
32
|
-
fw1_prog: Optional[ProgrammingTask]
|
|
33
|
-
fw2_prog: Optional[ProgrammingTask]
|
|
31
|
+
fw1_mod: Optional[FirmwareModTask] = None
|
|
32
|
+
fw2_mod: Optional[FirmwareModTask] = None
|
|
33
|
+
fw1_prog: Optional[ProgrammingTask] = None
|
|
34
|
+
fw2_prog: Optional[ProgrammingTask] = None
|
|
34
35
|
|
|
35
36
|
# MAIN PROCESS
|
|
36
|
-
emulation: Optional[EmulationTask]
|
|
37
|
+
emulation: Optional[EmulationTask] = None
|
|
37
38
|
|
|
38
39
|
# post_copy / cleanup, Todo: could also just intake emuTask
|
|
39
40
|
# - delete firmwares
|
|
@@ -42,7 +43,7 @@ class ObserverTasks(ShpModel):
|
|
|
42
43
|
# - zip it
|
|
43
44
|
|
|
44
45
|
@classmethod
|
|
45
|
-
@
|
|
46
|
+
@validate_call
|
|
46
47
|
def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt):
|
|
47
48
|
if not tb.shared_storage:
|
|
48
49
|
raise ValueError("Implementation currently relies on shared storage!")
|
|
@@ -78,3 +79,12 @@ class ObserverTasks(ShpModel):
|
|
|
78
79
|
continue
|
|
79
80
|
tasks.append(task)
|
|
80
81
|
return tasks
|
|
82
|
+
|
|
83
|
+
@computed_field
|
|
84
|
+
def output_paths(self) -> dict:
|
|
85
|
+
values = {}
|
|
86
|
+
if isinstance(self.emulation, EmulationTask):
|
|
87
|
+
if self.emulation.output_path is None:
|
|
88
|
+
raise ValueError("Emu-Task should have a valid output-path")
|
|
89
|
+
values[self.observer] = self.emulation.output_path
|
|
90
|
+
return values
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from pydantic import
|
|
5
|
-
from pydantic import
|
|
6
|
-
from pydantic import
|
|
7
|
-
from
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pydantic import model_validator
|
|
6
|
+
from pydantic import validate_call
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
8
|
|
|
9
9
|
from ..base.content import IdInt
|
|
10
10
|
from ..base.content import SafeStr
|
|
@@ -24,23 +24,23 @@ class ProgrammingTask(ShpModel):
|
|
|
24
24
|
mcu_port: MCUPort = 1
|
|
25
25
|
mcu_type: SafeStr
|
|
26
26
|
# ⤷ for later
|
|
27
|
-
voltage:
|
|
28
|
-
datarate:
|
|
27
|
+
voltage: Annotated[float, Field(ge=1, lt=5)] = 3
|
|
28
|
+
datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 500_000
|
|
29
29
|
protocol: ProgrammerProtocol
|
|
30
30
|
|
|
31
31
|
simulate: bool = False
|
|
32
32
|
|
|
33
|
-
verbose:
|
|
33
|
+
verbose: Annotated[int, Field(ge=0, le=4)] = 2
|
|
34
34
|
# ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
|
|
35
35
|
|
|
36
|
-
@
|
|
37
|
-
def post_validation(
|
|
38
|
-
if
|
|
39
|
-
ValueError(f"Firmware is not intel-.hex ('{
|
|
40
|
-
return
|
|
36
|
+
@model_validator(mode="after")
|
|
37
|
+
def post_validation(self):
|
|
38
|
+
if self.firmware_file.suffix.lower() != ".hex":
|
|
39
|
+
ValueError(f"Firmware is not intel-.hex ('{self.firmware_file}')")
|
|
40
|
+
return self
|
|
41
41
|
|
|
42
42
|
@classmethod
|
|
43
|
-
@
|
|
43
|
+
@validate_call
|
|
44
44
|
def from_xp(
|
|
45
45
|
cls,
|
|
46
46
|
xp: Experiment,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
from typing import List
|
|
1
2
|
from typing import Optional
|
|
2
3
|
|
|
3
4
|
from pydantic import EmailStr
|
|
4
|
-
from pydantic import
|
|
5
|
-
from pydantic import
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
from pydantic import computed_field
|
|
7
|
+
from pydantic import validate_call
|
|
8
|
+
from typing_extensions import Annotated
|
|
6
9
|
|
|
7
10
|
from ..base.content import NameStr
|
|
8
11
|
from ..base.shepherd import ShpModel
|
|
@@ -15,13 +18,13 @@ class TestbedTasks(ShpModel):
|
|
|
15
18
|
"""Collection of tasks for all observers included in experiment"""
|
|
16
19
|
|
|
17
20
|
name: NameStr
|
|
18
|
-
observer_tasks:
|
|
21
|
+
observer_tasks: Annotated[List[ObserverTasks], Field(min_length=1, max_length=64)]
|
|
19
22
|
|
|
20
23
|
# POST PROCESS
|
|
21
|
-
email: Optional[EmailStr]
|
|
24
|
+
email: Optional[EmailStr] = None
|
|
22
25
|
|
|
23
26
|
@classmethod
|
|
24
|
-
@
|
|
27
|
+
@validate_call
|
|
25
28
|
def from_xp(cls, xp: Experiment, tb: Optional[Testbed] = None):
|
|
26
29
|
if tb is None:
|
|
27
30
|
# TODO: just for testing OK
|
|
@@ -32,6 +35,13 @@ class TestbedTasks(ShpModel):
|
|
|
32
35
|
|
|
33
36
|
def get_observer_tasks(self, observer) -> Optional[ObserverTasks]:
|
|
34
37
|
for tasks in self.observer_tasks:
|
|
35
|
-
if observer
|
|
38
|
+
if observer == tasks.observer:
|
|
36
39
|
return tasks
|
|
37
40
|
return None
|
|
41
|
+
|
|
42
|
+
@computed_field
|
|
43
|
+
def output_paths(self) -> dict:
|
|
44
|
+
values = {}
|
|
45
|
+
for obt in self.observer_tasks:
|
|
46
|
+
values = {**values, **obt.output_paths}
|
|
47
|
+
return values
|
|
@@ -5,7 +5,7 @@ from typing import Optional
|
|
|
5
5
|
from typing import Union
|
|
6
6
|
|
|
7
7
|
from pydantic import Field
|
|
8
|
-
from pydantic import
|
|
8
|
+
from pydantic import model_validator
|
|
9
9
|
|
|
10
10
|
from ...testbed_client import tb_client
|
|
11
11
|
from ..base.content import IdInt
|
|
@@ -37,7 +37,8 @@ class Cape(ShpModel, title="Shepherd-Cape"):
|
|
|
37
37
|
def __str__(self):
|
|
38
38
|
return self.name
|
|
39
39
|
|
|
40
|
-
@
|
|
40
|
+
@model_validator(mode="before")
|
|
41
|
+
@classmethod
|
|
41
42
|
def query_database(cls, values: dict) -> dict:
|
|
42
43
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
43
44
|
return values
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from pydantic import
|
|
5
|
-
from pydantic import
|
|
6
|
-
from pydantic import
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pydantic import StringConstraints
|
|
6
|
+
from pydantic import model_validator
|
|
7
|
+
from typing_extensions import Annotated
|
|
7
8
|
|
|
8
9
|
from ...testbed_client import tb_client
|
|
9
10
|
from ..base.content import IdInt
|
|
@@ -30,31 +31,33 @@ class GPIO(ShpModel, title="GPIO of Observer Node"):
|
|
|
30
31
|
comment: Optional[SafeStr] = None
|
|
31
32
|
|
|
32
33
|
direction: Direction = Direction.Input
|
|
33
|
-
dir_switch: Optional[
|
|
34
|
+
dir_switch: Optional[Annotated[str, StringConstraints(max_length=32)]] = None
|
|
34
35
|
|
|
35
|
-
reg_pru: Optional[
|
|
36
|
-
pin_pru: Optional[
|
|
37
|
-
reg_sys: Optional[
|
|
38
|
-
pin_sys: Optional[
|
|
36
|
+
reg_pru: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
|
|
37
|
+
pin_pru: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
|
|
38
|
+
reg_sys: Optional[Annotated[int, Field(ge=0)]] = None
|
|
39
|
+
pin_sys: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
|
|
39
40
|
|
|
40
41
|
def __str__(self):
|
|
41
42
|
return self.name
|
|
42
43
|
|
|
43
|
-
@
|
|
44
|
+
@model_validator(mode="before")
|
|
45
|
+
@classmethod
|
|
44
46
|
def query_database(cls, values: dict) -> dict:
|
|
45
47
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
46
48
|
return values
|
|
47
49
|
|
|
48
|
-
@
|
|
49
|
-
def post_validation(
|
|
50
|
+
@model_validator(mode="after")
|
|
51
|
+
def post_validation(self):
|
|
50
52
|
# ensure that either pru or sys is used, otherwise instance is considered faulty
|
|
51
|
-
no_pru = (
|
|
52
|
-
no_sys = (
|
|
53
|
+
no_pru = (self.reg_pru is None) or (self.pin_pru is None)
|
|
54
|
+
no_sys = (self.reg_sys is None) or (self.pin_sys is None)
|
|
53
55
|
if no_pru and no_sys:
|
|
54
56
|
raise ValueError(
|
|
55
|
-
|
|
57
|
+
"GPIO-Instance is faulty -> "
|
|
58
|
+
f"it needs to use pru or sys, content: {self.model_dump()}"
|
|
56
59
|
)
|
|
57
|
-
return
|
|
60
|
+
return self
|
|
58
61
|
|
|
59
62
|
def user_controllable(self) -> bool:
|
|
60
63
|
return ("gpio" in self.name.lower()) and (self.direction in ["IO", "OUT"])
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from pydantic import
|
|
5
|
-
from pydantic import
|
|
6
|
-
from
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pydantic import model_validator
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
7
|
|
|
8
8
|
from ...testbed_client import tb_client
|
|
9
9
|
from ..base.content import IdInt
|
|
@@ -34,8 +34,8 @@ class MCU(ShpModel, title="Microcontroller of the Target Node"):
|
|
|
34
34
|
platform: NameStr
|
|
35
35
|
core: NameStr
|
|
36
36
|
prog_protocol: ProgrammerProtocol
|
|
37
|
-
prog_voltage:
|
|
38
|
-
prog_datarate:
|
|
37
|
+
prog_voltage: Annotated[float, Field(ge=1, le=5)] = 3
|
|
38
|
+
prog_datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 500_000
|
|
39
39
|
|
|
40
40
|
fw_name_default: str
|
|
41
41
|
# ⤷ can't be FW-Object (circular import)
|
|
@@ -43,7 +43,8 @@ class MCU(ShpModel, title="Microcontroller of the Target Node"):
|
|
|
43
43
|
def __str__(self):
|
|
44
44
|
return self.name
|
|
45
45
|
|
|
46
|
-
@
|
|
46
|
+
@model_validator(mode="before")
|
|
47
|
+
@classmethod
|
|
47
48
|
def query_database(cls, values: dict) -> dict:
|
|
48
49
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
49
50
|
return values
|
|
@@ -3,9 +3,9 @@ from typing import Optional
|
|
|
3
3
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
from pydantic import IPvAnyAddress
|
|
6
|
-
from pydantic import
|
|
7
|
-
from pydantic import
|
|
8
|
-
from
|
|
6
|
+
from pydantic import StringConstraints
|
|
7
|
+
from pydantic import model_validator
|
|
8
|
+
from typing_extensions import Annotated
|
|
9
9
|
|
|
10
10
|
from ...testbed_client import tb_client
|
|
11
11
|
from ..base.content import IdInt
|
|
@@ -16,7 +16,12 @@ from .cape import Cape
|
|
|
16
16
|
from .cape import TargetPort
|
|
17
17
|
from .target import Target
|
|
18
18
|
|
|
19
|
-
MACStr =
|
|
19
|
+
MACStr = Annotated[
|
|
20
|
+
str,
|
|
21
|
+
StringConstraints(
|
|
22
|
+
max_length=17, pattern=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
|
|
23
|
+
),
|
|
24
|
+
]
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
@@ -33,36 +38,35 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
33
38
|
room: NameStr
|
|
34
39
|
eth_port: NameStr
|
|
35
40
|
|
|
36
|
-
latitude:
|
|
37
|
-
longitude:
|
|
41
|
+
latitude: Annotated[float, Field(ge=-90, le=90)] = 51.026573
|
|
42
|
+
longitude: Annotated[float, Field(ge=-180, le=180)] = 13.723291
|
|
43
|
+
# ⤷ cfaed-floor
|
|
38
44
|
|
|
39
|
-
cape: Optional[Cape]
|
|
40
|
-
target_a: Optional[Target]
|
|
45
|
+
cape: Optional[Cape] = None
|
|
46
|
+
target_a: Optional[Target] = None
|
|
41
47
|
target_b: Optional[Target] = None
|
|
42
48
|
|
|
43
49
|
created: datetime = Field(default_factory=datetime.now)
|
|
44
|
-
alive_last: Optional[datetime]
|
|
50
|
+
alive_last: Optional[datetime] = None
|
|
45
51
|
|
|
46
52
|
def __str__(self):
|
|
47
53
|
return self.name
|
|
48
54
|
|
|
49
|
-
@
|
|
55
|
+
@model_validator(mode="before")
|
|
56
|
+
@classmethod
|
|
50
57
|
def query_database(cls, values: dict) -> dict:
|
|
51
58
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
52
59
|
return values
|
|
53
60
|
|
|
54
|
-
@
|
|
55
|
-
def post_validation(
|
|
56
|
-
has_cape =
|
|
57
|
-
has_target = (
|
|
58
|
-
values.get("target_b") is not None
|
|
59
|
-
)
|
|
61
|
+
@model_validator(mode="after")
|
|
62
|
+
def post_validation(self):
|
|
63
|
+
has_cape = self.cape is not None
|
|
64
|
+
has_target = (self.target_a is not None) or (self.target_b is not None)
|
|
60
65
|
if not has_cape and has_target:
|
|
61
66
|
raise ValueError(
|
|
62
|
-
f"Observer '{
|
|
63
|
-
f"-> has targets but no cape"
|
|
67
|
+
f"Observer '{self.name}' is faulty " f"-> has targets but no cape"
|
|
64
68
|
)
|
|
65
|
-
return
|
|
69
|
+
return self
|
|
66
70
|
|
|
67
71
|
def get_target_port(self, target_id: int) -> TargetPort:
|
|
68
72
|
if self.target_a is not None and target_id == self.target_a.id:
|
|
@@ -3,8 +3,8 @@ from typing import Optional
|
|
|
3
3
|
from typing import Union
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
|
-
from pydantic import
|
|
7
|
-
from
|
|
6
|
+
from pydantic import model_validator
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
8
|
|
|
9
9
|
from ...testbed_client import tb_client
|
|
10
10
|
from ..base.content import IdInt
|
|
@@ -13,9 +13,9 @@ from ..base.content import SafeStr
|
|
|
13
13
|
from ..base.shepherd import ShpModel
|
|
14
14
|
from .mcu import MCU
|
|
15
15
|
|
|
16
|
-
IdInt16 =
|
|
16
|
+
IdInt16 = Annotated[int, Field(ge=0, lt=2**16)]
|
|
17
17
|
|
|
18
|
-
MCUPort =
|
|
18
|
+
MCUPort = Annotated[int, Field(ge=1, le=2)]
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class Target(ShpModel, title="Target Node (DuT)"):
|
|
@@ -30,7 +30,7 @@ class Target(ShpModel, title="Target Node (DuT)"):
|
|
|
30
30
|
|
|
31
31
|
created: datetime = Field(default_factory=datetime.now)
|
|
32
32
|
|
|
33
|
-
fw_id: Optional[IdInt16]
|
|
33
|
+
fw_id: Optional[IdInt16] = None
|
|
34
34
|
mcu1: Union[MCU, NameStr]
|
|
35
35
|
mcu2: Union[MCU, NameStr, None] = None
|
|
36
36
|
#
|
|
@@ -40,18 +40,19 @@ class Target(ShpModel, title="Target Node (DuT)"):
|
|
|
40
40
|
def __str__(self):
|
|
41
41
|
return self.name
|
|
42
42
|
|
|
43
|
-
@
|
|
43
|
+
@model_validator(mode="before")
|
|
44
|
+
@classmethod
|
|
44
45
|
def query_database(cls, values: dict) -> dict:
|
|
45
46
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
46
|
-
return values
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
# post correction
|
|
49
|
+
for _mcu in ["mcu1", "mcu2"]:
|
|
50
|
+
if isinstance(values.get(_mcu), str):
|
|
51
|
+
values[_mcu] = MCU(name=values[_mcu])
|
|
52
|
+
# ⤷ this will raise if default is faulty
|
|
53
|
+
elif isinstance(values.get(_mcu), dict):
|
|
54
|
+
values[_mcu] = MCU(**values[_mcu])
|
|
55
55
|
if values.get("fw_id") is None:
|
|
56
56
|
values["fw_id"] = values.get("id") % 2**16
|
|
57
|
+
|
|
57
58
|
return values
|