shepherd-core 2024.4.1__py3-none-any.whl → 2024.5.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/__init__.py +3 -3
- shepherd_core/calibration_hw_def.py +9 -1
- shepherd_core/commons.py +2 -0
- shepherd_core/data_models/__init__.py +11 -0
- shepherd_core/data_models/base/__init__.py +4 -1
- shepherd_core/data_models/base/cal_measurement.py +18 -6
- shepherd_core/data_models/base/calibration.py +39 -15
- shepherd_core/data_models/base/content.py +10 -2
- shepherd_core/data_models/base/shepherd.py +21 -12
- shepherd_core/data_models/base/timezone.py +5 -0
- shepherd_core/data_models/base/wrapper.py +3 -3
- shepherd_core/data_models/content/__init__.py +5 -4
- shepherd_core/data_models/content/_external_fixtures.yaml +410 -0
- shepherd_core/data_models/content/energy_environment.py +7 -5
- shepherd_core/data_models/content/energy_environment_fixture.yaml +53 -0
- shepherd_core/data_models/content/firmware.py +12 -5
- shepherd_core/data_models/content/firmware_datatype.py +7 -0
- shepherd_core/data_models/content/virtual_harvester.py +24 -19
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml +160 -0
- shepherd_core/data_models/content/virtual_source.py +22 -10
- shepherd_core/data_models/content/virtual_source_fixture.yaml +230 -0
- shepherd_core/data_models/experiment/__init__.py +5 -4
- shepherd_core/data_models/experiment/experiment.py +7 -6
- shepherd_core/data_models/experiment/observer_features.py +14 -7
- shepherd_core/data_models/experiment/target_config.py +11 -7
- shepherd_core/data_models/readme.md +88 -0
- shepherd_core/data_models/task/__init__.py +10 -3
- shepherd_core/data_models/task/emulation.py +10 -7
- shepherd_core/data_models/task/firmware_mod.py +4 -2
- shepherd_core/data_models/task/harvest.py +5 -4
- shepherd_core/data_models/task/observer_tasks.py +3 -1
- shepherd_core/data_models/task/programming.py +3 -1
- shepherd_core/data_models/task/testbed_tasks.py +3 -1
- shepherd_core/data_models/testbed/__init__.py +5 -2
- shepherd_core/data_models/testbed/cape.py +7 -5
- shepherd_core/data_models/testbed/cape_fixture.yaml +94 -0
- shepherd_core/data_models/testbed/gpio.py +10 -8
- shepherd_core/data_models/testbed/gpio_fixture.yaml +166 -0
- shepherd_core/data_models/testbed/mcu.py +9 -9
- shepherd_core/data_models/testbed/mcu_fixture.yaml +19 -0
- shepherd_core/data_models/testbed/observer.py +9 -4
- shepherd_core/data_models/testbed/observer_fixture.yaml +221 -0
- shepherd_core/data_models/testbed/target.py +4 -2
- shepherd_core/data_models/testbed/target_fixture.yaml +137 -0
- shepherd_core/data_models/testbed/testbed.py +5 -2
- shepherd_core/data_models/testbed/testbed_fixture.yaml +25 -0
- shepherd_core/decoder_waveform/__init__.py +2 -0
- shepherd_core/decoder_waveform/uart.py +43 -25
- shepherd_core/fw_tools/__init__.py +2 -0
- shepherd_core/fw_tools/converter.py +20 -9
- shepherd_core/fw_tools/converter_elf.py +3 -0
- shepherd_core/fw_tools/patcher.py +17 -5
- shepherd_core/fw_tools/validation.py +27 -6
- shepherd_core/inventory/__init__.py +15 -5
- shepherd_core/inventory/python.py +4 -0
- shepherd_core/inventory/system.py +6 -2
- shepherd_core/inventory/target.py +4 -0
- shepherd_core/logger.py +5 -0
- shepherd_core/reader.py +38 -23
- shepherd_core/testbed_client/__init__.py +2 -0
- shepherd_core/testbed_client/cache_path.py +2 -0
- shepherd_core/testbed_client/client.py +15 -8
- shepherd_core/testbed_client/fixtures.py +27 -11
- shepherd_core/testbed_client/user_model.py +8 -3
- shepherd_core/vsource/__init__.py +2 -0
- shepherd_core/vsource/virtual_converter_model.py +10 -3
- shepherd_core/vsource/virtual_harvester_model.py +7 -1
- shepherd_core/vsource/virtual_source_model.py +9 -5
- shepherd_core/writer.py +26 -21
- {shepherd_core-2024.4.1.dist-info → shepherd_core-2024.5.1.dist-info}/METADATA +2 -1
- shepherd_core-2024.5.1.dist-info/RECORD +75 -0
- shepherd_core-2024.4.1.dist-info/RECORD +0 -64
- /shepherd_core/data_models/{doc_virtual_source.py → virtual_source_doc.txt} +0 -0
- {shepherd_core-2024.4.1.dist-info → shepherd_core-2024.5.1.dist-info}/WHEEL +0 -0
- {shepherd_core-2024.4.1.dist-info → shepherd_core-2024.5.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2024.4.1.dist-info → shepherd_core-2024.5.1.dist-info}/zip-safe +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Content-Converters for firmwares."""
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import hashlib
|
|
3
5
|
import shutil
|
|
@@ -15,21 +17,25 @@ from .validation import is_hex
|
|
|
15
17
|
|
|
16
18
|
@validate_call
|
|
17
19
|
def firmware_to_hex(file_path: Path) -> Path:
|
|
18
|
-
"""
|
|
20
|
+
"""Convert ELF-Files to HEX.
|
|
21
|
+
|
|
22
|
+
Generic converter that handles ELF & HEX.
|
|
23
|
+
"""
|
|
19
24
|
if not file_path.is_file():
|
|
20
|
-
raise
|
|
25
|
+
raise FileNotFoundError("Fn needs an existing file as input")
|
|
21
26
|
if is_elf(file_path):
|
|
22
27
|
return elf_to_hex(file_path)
|
|
23
28
|
if is_hex(file_path):
|
|
24
29
|
return file_path
|
|
25
|
-
raise
|
|
30
|
+
raise FileNotFoundError("FW2Hex: unknown file '%s', it should be ELF or HEX", file_path.name)
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
@validate_call
|
|
29
34
|
def file_to_base64(file_path: Path) -> str:
|
|
30
|
-
"""Compress and encode content of file
|
|
35
|
+
"""Compress and encode content of file.
|
|
36
|
+
|
|
31
37
|
- base64 adds ~33 % overhead
|
|
32
|
-
- zstd compression ~ 1:3
|
|
38
|
+
- zstd compression reduces to ~ 1:3
|
|
33
39
|
"""
|
|
34
40
|
if not file_path.is_file():
|
|
35
41
|
raise ValueError("Fn needs an existing file as input")
|
|
@@ -41,9 +47,10 @@ def file_to_base64(file_path: Path) -> str:
|
|
|
41
47
|
|
|
42
48
|
@validate_call
|
|
43
49
|
def base64_to_file(content: str, file_path: Path) -> None:
|
|
44
|
-
"""DeCompress and decode Content of file
|
|
50
|
+
"""DeCompress and decode Content of file.
|
|
51
|
+
|
|
45
52
|
- base64 adds ~33 % overhead
|
|
46
|
-
- zstd compression ~ 1:3
|
|
53
|
+
- zstd compression reduces to ~ 1:3
|
|
47
54
|
"""
|
|
48
55
|
file_cmpress = base64.b64decode(content)
|
|
49
56
|
file_content = zstd.ZstdDecompressor().decompress(file_cmpress)
|
|
@@ -55,6 +62,7 @@ def base64_to_file(content: str, file_path: Path) -> None:
|
|
|
55
62
|
|
|
56
63
|
@validate_call
|
|
57
64
|
def file_to_hash(file_path: Path) -> str:
|
|
65
|
+
"""Convert file-content to hash-value."""
|
|
58
66
|
if not file_path.is_file():
|
|
59
67
|
raise ValueError("Fn needs an existing file as input")
|
|
60
68
|
with file_path.resolve().open("rb") as file:
|
|
@@ -64,6 +72,7 @@ def file_to_hash(file_path: Path) -> str:
|
|
|
64
72
|
|
|
65
73
|
@validate_call
|
|
66
74
|
def base64_to_hash(content: str) -> str:
|
|
75
|
+
"""Convert base64-content to hash-value."""
|
|
67
76
|
file_cmpress = base64.b64decode(content)
|
|
68
77
|
file_content = zstd.ZstdDecompressor().decompress(file_cmpress)
|
|
69
78
|
return hashlib.sha3_224(file_content).hexdigest()
|
|
@@ -71,8 +80,10 @@ def base64_to_hash(content: str) -> str:
|
|
|
71
80
|
|
|
72
81
|
@validate_call
|
|
73
82
|
def extract_firmware(data: Union[str, Path], data_type: FirmwareDType, file_path: Path) -> Path:
|
|
74
|
-
"""
|
|
75
|
-
|
|
83
|
+
"""Make embedded firmware-data usable in filesystem.
|
|
84
|
+
|
|
85
|
+
- base64-string will be transformed to file
|
|
86
|
+
- if data is a path the file will be copied to the destination.
|
|
76
87
|
"""
|
|
77
88
|
if data_type == FirmwareDType.base64_elf:
|
|
78
89
|
file = file_path.with_suffix(".elf")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Converter for ELF-files."""
|
|
2
|
+
|
|
1
3
|
import subprocess
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import Optional
|
|
@@ -9,6 +11,7 @@ from pydantic import validate_call
|
|
|
9
11
|
|
|
10
12
|
@validate_call
|
|
11
13
|
def elf_to_hex(file_elf: Path, file_hex: Optional[Path] = None) -> Path:
|
|
14
|
+
"""Convert ELF to hex file using objcopy."""
|
|
12
15
|
if not file_elf.is_file():
|
|
13
16
|
raise ValueError("Fn needs an existing file as input")
|
|
14
17
|
if not file_hex:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Read and modify symbols in ELF-files."""
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
from typing import Optional
|
|
3
5
|
|
|
@@ -15,13 +17,14 @@ try:
|
|
|
15
17
|
except ImportError as e:
|
|
16
18
|
ELF = None
|
|
17
19
|
elf_error_text = (
|
|
18
|
-
"Please install functionality with 'pip install
|
|
20
|
+
"Please install functionality with 'pip install shepherd-core[elf] -U' first, "
|
|
19
21
|
f"underlying exception: {e.msg}"
|
|
20
22
|
)
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
@validate_call
|
|
24
26
|
def find_symbol(file_elf: Path, symbol: str) -> bool:
|
|
27
|
+
"""Find a symbol in the ELF file."""
|
|
25
28
|
if symbol is None or not is_elf(file_elf):
|
|
26
29
|
return False
|
|
27
30
|
if ELF is None:
|
|
@@ -47,7 +50,10 @@ def find_symbol(file_elf: Path, symbol: str) -> bool:
|
|
|
47
50
|
|
|
48
51
|
@validate_call
|
|
49
52
|
def read_symbol(file_elf: Path, symbol: str, length: int = uid_len_default) -> Optional[int]:
|
|
50
|
-
"""
|
|
53
|
+
"""Read value of symbol in ELF-File.
|
|
54
|
+
|
|
55
|
+
Will be interpreted as int.
|
|
56
|
+
"""
|
|
51
57
|
if not find_symbol(file_elf, symbol):
|
|
52
58
|
return None
|
|
53
59
|
if ELF is None:
|
|
@@ -60,10 +66,12 @@ def read_symbol(file_elf: Path, symbol: str, length: int = uid_len_default) -> O
|
|
|
60
66
|
|
|
61
67
|
|
|
62
68
|
def read_uid(file_elf: Path) -> Optional[int]:
|
|
69
|
+
"""Read value of UID-symbol for shepherd testbed."""
|
|
63
70
|
return read_symbol(file_elf, symbol=uid_str_default, length=uid_len_default)
|
|
64
71
|
|
|
65
72
|
|
|
66
73
|
def read_arch(file_elf: Path) -> Optional[str]:
|
|
74
|
+
"""Determine chip-architecture from elf-metadata."""
|
|
67
75
|
if not is_elf(file_elf):
|
|
68
76
|
return None
|
|
69
77
|
if ELF is None:
|
|
@@ -83,9 +91,12 @@ def modify_symbol_value(
|
|
|
83
91
|
*,
|
|
84
92
|
overwrite: bool = False,
|
|
85
93
|
) -> Optional[Path]:
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
"""Replace value of uint16-symbol in ELF-File.
|
|
95
|
+
|
|
96
|
+
Hardcoded for uint16_t (2 byte).
|
|
97
|
+
The testbed uses this to patch firmware with custom target-ID.
|
|
98
|
+
|
|
99
|
+
NOTE: can overwrite provided file.
|
|
89
100
|
|
|
90
101
|
"""
|
|
91
102
|
if not find_symbol(file_elf, symbol):
|
|
@@ -121,4 +132,5 @@ def modify_symbol_value(
|
|
|
121
132
|
|
|
122
133
|
|
|
123
134
|
def modify_uid(file_elf: Path, value: int) -> Optional[Path]:
|
|
135
|
+
"""Replace value of UID-symbol for shepherd testbed."""
|
|
124
136
|
return modify_symbol_value(file_elf, symbol=uid_str_default, value=value, overwrite=True)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Helper-functions for firmware-validation.
|
|
2
|
+
|
|
3
|
+
TODO: Work in Progress.
|
|
4
|
+
- Each arch should have its own file and
|
|
5
|
+
- detection-functions that register in main validator.
|
|
6
|
+
"""
|
|
2
7
|
|
|
3
8
|
import tempfile
|
|
4
9
|
from pathlib import Path
|
|
@@ -18,13 +23,14 @@ except ImportError as e:
|
|
|
18
23
|
ELF = None
|
|
19
24
|
ELFError = None
|
|
20
25
|
elf_error_text = (
|
|
21
|
-
"Please install functionality with 'pip install
|
|
26
|
+
"Please install functionality with 'pip install shepherd-core[elf] -U' first, "
|
|
22
27
|
f"underlying exception: {e.msg}"
|
|
23
28
|
)
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
@validate_call
|
|
27
32
|
def is_hex(file: Path) -> bool:
|
|
33
|
+
"""Check if file is containing intel-hex data."""
|
|
28
34
|
try:
|
|
29
35
|
_ = IntelHex(file.as_posix())
|
|
30
36
|
except ValueError: # parsing
|
|
@@ -35,7 +41,9 @@ def is_hex(file: Path) -> bool:
|
|
|
35
41
|
|
|
36
42
|
|
|
37
43
|
def is_hex_msp430(file: Path) -> bool:
|
|
38
|
-
"""
|
|
44
|
+
"""Try to detect specifics for that MCU.
|
|
45
|
+
|
|
46
|
+
Observations:
|
|
39
47
|
- addresses begin at 0x4000
|
|
40
48
|
- value @0xFFFE (IVT) is start_address (of pgm-code)
|
|
41
49
|
"""
|
|
@@ -56,7 +64,9 @@ def is_hex_msp430(file: Path) -> bool:
|
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
def is_hex_nrf52(file: Path) -> bool:
|
|
59
|
-
"""
|
|
67
|
+
"""Try to detect specifics for that MCU.
|
|
68
|
+
|
|
69
|
+
Observations:
|
|
60
70
|
- addresses begin at 0x0
|
|
61
71
|
- only one segment (.get_segments), todo
|
|
62
72
|
"""
|
|
@@ -79,6 +89,7 @@ def is_hex_nrf52(file: Path) -> bool:
|
|
|
79
89
|
|
|
80
90
|
@validate_call
|
|
81
91
|
def is_elf(file: Path) -> bool:
|
|
92
|
+
"""Check if file is an ELF file."""
|
|
82
93
|
if ELF is None:
|
|
83
94
|
raise RuntimeError(elf_error_text)
|
|
84
95
|
if not file.is_file():
|
|
@@ -91,7 +102,11 @@ def is_elf(file: Path) -> bool:
|
|
|
91
102
|
return True
|
|
92
103
|
|
|
93
104
|
|
|
94
|
-
def is_elf_msp430(file: Path) -> bool:
|
|
105
|
+
def is_elf_msp430(file: Path) -> bool:
|
|
106
|
+
"""Check if file is an ELF for that MCU.
|
|
107
|
+
|
|
108
|
+
TODO: allow detection without conversion
|
|
109
|
+
"""
|
|
95
110
|
if is_elf(file):
|
|
96
111
|
with tempfile.TemporaryDirectory() as path:
|
|
97
112
|
file_hex = Path(path) / "file.hex"
|
|
@@ -102,7 +117,11 @@ def is_elf_msp430(file: Path) -> bool: # TODO: allow detection without conversi
|
|
|
102
117
|
return False
|
|
103
118
|
|
|
104
119
|
|
|
105
|
-
def is_elf_nrf52(file: Path) -> bool:
|
|
120
|
+
def is_elf_nrf52(file: Path) -> bool:
|
|
121
|
+
"""Check if file is an ELF for that MCU.
|
|
122
|
+
|
|
123
|
+
TODO: allow detection without conversion
|
|
124
|
+
"""
|
|
106
125
|
if is_elf(file):
|
|
107
126
|
with tempfile.TemporaryDirectory() as path:
|
|
108
127
|
file_hex = Path(path) / "file.hex"
|
|
@@ -114,6 +133,7 @@ def is_elf_nrf52(file: Path) -> bool: # TODO: allow detection without conversio
|
|
|
114
133
|
|
|
115
134
|
|
|
116
135
|
def determine_type(file: Path) -> FirmwareDType:
|
|
136
|
+
"""Figure out file-type (hex or elf)."""
|
|
117
137
|
if not file.is_file():
|
|
118
138
|
raise ValueError("Fn needs an existing file as input")
|
|
119
139
|
if is_hex(file):
|
|
@@ -124,6 +144,7 @@ def determine_type(file: Path) -> FirmwareDType:
|
|
|
124
144
|
|
|
125
145
|
|
|
126
146
|
def determine_arch(file: Path) -> str:
|
|
147
|
+
"""Figure out arch (msp430 or nrf52)."""
|
|
127
148
|
file_t = determine_type(file)
|
|
128
149
|
if file_t == FirmwareDType.path_elf:
|
|
129
150
|
if is_elf_nrf52(file):
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
"""Creates an overview for shepherd-host-machines
|
|
1
|
+
"""Creates an overview for shepherd-host-machines.
|
|
2
|
+
|
|
3
|
+
This will collect:
|
|
2
4
|
- relevant software-versions
|
|
3
5
|
- system-parameters
|
|
4
|
-
- hardware-config
|
|
6
|
+
- hardware-config.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
from datetime import datetime
|
|
@@ -28,7 +30,11 @@ __all__ = [
|
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class Inventory(PythonInventory, SystemInventory, TargetInventory):
|
|
31
|
-
|
|
33
|
+
"""Complete inventory for one device.
|
|
34
|
+
|
|
35
|
+
Has all child-parameters.
|
|
36
|
+
"""
|
|
37
|
+
|
|
32
38
|
hostname: str
|
|
33
39
|
created: datetime
|
|
34
40
|
|
|
@@ -47,12 +53,16 @@ class Inventory(PythonInventory, SystemInventory, TargetInventory):
|
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
class InventoryList(ShpModel):
|
|
56
|
+
"""Collection of inventories for several devices."""
|
|
57
|
+
|
|
50
58
|
elements: Annotated[List[Inventory], Field(min_length=1)]
|
|
51
59
|
|
|
52
60
|
def to_csv(self, path: Path) -> None:
|
|
53
|
-
"""
|
|
61
|
+
"""Generate a CSV.
|
|
62
|
+
|
|
63
|
+
TODO: pretty messed up (raw lists and dicts for sub-elements)
|
|
54
64
|
numpy.savetxt -> too basic
|
|
55
|
-
np.concatenate(content).reshape((len(content), len(content[0])))
|
|
65
|
+
np.concatenate(content).reshape((len(content), len(content[0]))).
|
|
56
66
|
"""
|
|
57
67
|
if path.is_dir():
|
|
58
68
|
path = path / "inventory.yaml"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Python related inventory model."""
|
|
2
|
+
|
|
1
3
|
import platform
|
|
2
4
|
from contextlib import suppress
|
|
3
5
|
from importlib import import_module
|
|
@@ -10,6 +12,8 @@ from ..data_models import ShpModel
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class PythonInventory(ShpModel):
|
|
15
|
+
"""Python related inventory model."""
|
|
16
|
+
|
|
13
17
|
# program versions
|
|
14
18
|
h5py: Optional[str] = None
|
|
15
19
|
numpy: Optional[str] = None
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""System / OS related inventory model."""
|
|
2
|
+
|
|
1
3
|
import platform
|
|
2
4
|
import subprocess
|
|
3
5
|
import time
|
|
@@ -7,8 +9,8 @@ from typing import Optional
|
|
|
7
9
|
|
|
8
10
|
from typing_extensions import Self
|
|
9
11
|
|
|
10
|
-
from .. import local_now
|
|
11
|
-
from .. import logger
|
|
12
|
+
from ..data_models.base.timezone import local_now
|
|
13
|
+
from ..logger import logger
|
|
12
14
|
|
|
13
15
|
try:
|
|
14
16
|
import psutil
|
|
@@ -22,6 +24,8 @@ from ..data_models import ShpModel
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class SystemInventory(ShpModel):
|
|
27
|
+
"""System / OS related inventory model."""
|
|
28
|
+
|
|
25
29
|
uptime: PositiveInt
|
|
26
30
|
# ⤷ seconds
|
|
27
31
|
timestamp: datetime
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Hardware related inventory model."""
|
|
2
|
+
|
|
1
3
|
from typing import List
|
|
2
4
|
from typing import Optional
|
|
3
5
|
|
|
@@ -8,6 +10,8 @@ from ..data_models import ShpModel
|
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class TargetInventory(ShpModel):
|
|
13
|
+
"""Hardware related inventory model."""
|
|
14
|
+
|
|
11
15
|
cape: Optional[str] = None
|
|
12
16
|
targets: List[str] = [] # noqa: RUF012
|
|
13
17
|
|
shepherd_core/logger.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Log handler of shepherd."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import logging.handlers
|
|
3
5
|
from typing import Union
|
|
@@ -12,10 +14,12 @@ verbose_level: int = 2
|
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def get_verbose_level() -> int:
|
|
17
|
+
"""Get log level of shepherd."""
|
|
15
18
|
return verbose_level
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose: int) -> None:
|
|
22
|
+
"""Set log level of shepherd."""
|
|
19
23
|
if verbose == 0:
|
|
20
24
|
log_.setLevel(logging.ERROR)
|
|
21
25
|
logging.basicConfig(level=logging.ERROR)
|
|
@@ -39,6 +43,7 @@ def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose:
|
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
def increase_verbose_level(verbose: int) -> None:
|
|
46
|
+
"""Increase log level of shepherd."""
|
|
42
47
|
global verbose_level # noqa: PLW0603
|
|
43
48
|
if verbose >= verbose_level:
|
|
44
49
|
verbose_level = min(max(verbose, 0), 3)
|
shepherd_core/reader.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"""Reader-Baseclass"""
|
|
1
|
+
"""Reader-Baseclass."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
import contextlib
|
|
4
6
|
import errno
|
|
@@ -7,7 +9,7 @@ import math
|
|
|
7
9
|
import os
|
|
8
10
|
from itertools import product
|
|
9
11
|
from pathlib import Path
|
|
10
|
-
from
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
11
13
|
from typing import Any
|
|
12
14
|
from typing import ClassVar
|
|
13
15
|
from typing import Dict
|
|
@@ -30,6 +32,9 @@ from .data_models.base.calibration import CalibrationSeries
|
|
|
30
32
|
from .data_models.content.energy_environment import EnergyDType
|
|
31
33
|
from .decoder_waveform import Uart
|
|
32
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from types import TracebackType
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
class Reader:
|
|
35
40
|
"""Sequentially Reads shepherd-data from HDF5 file.
|
|
@@ -96,7 +101,8 @@ class Reader:
|
|
|
96
101
|
self.h5file = h5py.File(self.file_path, "r") # = readonly
|
|
97
102
|
self._reader_opened = True
|
|
98
103
|
except OSError as _xcp:
|
|
99
|
-
|
|
104
|
+
msg = f"Unable to open HDF5-File '{self.file_path.name}'"
|
|
105
|
+
raise TypeError(msg) from _xcp
|
|
100
106
|
|
|
101
107
|
if self.is_valid():
|
|
102
108
|
self._logger.debug("File is available now")
|
|
@@ -159,7 +165,7 @@ class Reader:
|
|
|
159
165
|
)
|
|
160
166
|
|
|
161
167
|
def _refresh_file_stats(self) -> None:
|
|
162
|
-
"""Update internal states, helpful after resampling or other changes in data-group"""
|
|
168
|
+
"""Update internal states, helpful after resampling or other changes in data-group."""
|
|
163
169
|
self.h5file.flush()
|
|
164
170
|
if (self.ds_time.shape[0] > 1) and (self.ds_time[1] != self.ds_time[0]):
|
|
165
171
|
# this assumes isochronal sampling
|
|
@@ -181,10 +187,11 @@ class Reader:
|
|
|
181
187
|
is_raw: bool = False,
|
|
182
188
|
omit_ts: bool = False,
|
|
183
189
|
) -> Generator[tuple, None, None]:
|
|
184
|
-
"""
|
|
185
|
-
|
|
190
|
+
"""Read the specified range of buffers from the hdf5 file.
|
|
191
|
+
|
|
192
|
+
Generator - can be configured on first call
|
|
186
193
|
TODO: reconstruct - start/end mark samples &
|
|
187
|
-
each call can request a certain number of samples
|
|
194
|
+
each call can request a certain number of samples.
|
|
188
195
|
|
|
189
196
|
Args:
|
|
190
197
|
----
|
|
@@ -218,7 +225,7 @@ class Reader:
|
|
|
218
225
|
)
|
|
219
226
|
|
|
220
227
|
def get_calibration_data(self) -> CalibrationSeries:
|
|
221
|
-
"""
|
|
228
|
+
"""Read calibration-data from hdf5 file.
|
|
222
229
|
|
|
223
230
|
:return: Calibration data as CalibrationSeries object
|
|
224
231
|
"""
|
|
@@ -248,12 +255,14 @@ class Reader:
|
|
|
248
255
|
try:
|
|
249
256
|
if "datatype" in self.h5file["data"].attrs:
|
|
250
257
|
return EnergyDType[self.h5file["data"].attrs["datatype"]]
|
|
251
|
-
return None
|
|
252
258
|
except KeyError:
|
|
253
259
|
return None
|
|
260
|
+
else:
|
|
261
|
+
return None
|
|
254
262
|
|
|
255
263
|
def get_hrv_config(self) -> dict:
|
|
256
|
-
"""Essential info for harvester
|
|
264
|
+
"""Essential info for harvester.
|
|
265
|
+
|
|
257
266
|
:return: config-dict directly for vHarvester to be used during emulation
|
|
258
267
|
"""
|
|
259
268
|
return {
|
|
@@ -262,7 +271,7 @@ class Reader:
|
|
|
262
271
|
}
|
|
263
272
|
|
|
264
273
|
def is_valid(self) -> bool:
|
|
265
|
-
"""
|
|
274
|
+
"""Check file for plausibility, validity / correctness.
|
|
266
275
|
|
|
267
276
|
:return: state of validity
|
|
268
277
|
"""
|
|
@@ -390,7 +399,7 @@ class Reader:
|
|
|
390
399
|
return True
|
|
391
400
|
|
|
392
401
|
def __getitem__(self, key: str) -> Any:
|
|
393
|
-
"""
|
|
402
|
+
"""Query attribute or (if none found) a handle for a group or dataset (if found).
|
|
394
403
|
|
|
395
404
|
:param key: attribute, group, dataset
|
|
396
405
|
:return: value of that key, or handle of object
|
|
@@ -402,9 +411,10 @@ class Reader:
|
|
|
402
411
|
raise KeyError
|
|
403
412
|
|
|
404
413
|
def energy(self) -> float:
|
|
405
|
-
"""Determine the recorded energy of the trace
|
|
414
|
+
"""Determine the recorded energy of the trace.
|
|
415
|
+
|
|
406
416
|
# multiprocessing: https://stackoverflow.com/a/71898911
|
|
407
|
-
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool
|
|
417
|
+
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool.
|
|
408
418
|
|
|
409
419
|
:return: sampled energy in Ws (watt-seconds)
|
|
410
420
|
"""
|
|
@@ -430,7 +440,8 @@ class Reader:
|
|
|
430
440
|
def _dset_statistics(
|
|
431
441
|
self, dset: h5py.Dataset, cal: Optional[CalibrationPair] = None
|
|
432
442
|
) -> Dict[str, float]:
|
|
433
|
-
"""
|
|
443
|
+
"""Create basic stats for a provided dataset.
|
|
444
|
+
|
|
434
445
|
:param dset: dataset to evaluate
|
|
435
446
|
:param cal: calibration (if wanted)
|
|
436
447
|
:return: dict with entries for mean, min, max, std
|
|
@@ -472,8 +483,9 @@ class Reader:
|
|
|
472
483
|
return stats
|
|
473
484
|
|
|
474
485
|
def _data_timediffs(self) -> List[float]:
|
|
475
|
-
"""Calculate list of
|
|
476
|
-
|
|
486
|
+
"""Calculate list of unique time-deltas [s] between buffers.
|
|
487
|
+
|
|
488
|
+
Optimized version that only looks at the start of each buffer.
|
|
477
489
|
|
|
478
490
|
:return: list of (unique) time-deltas between buffers [s]
|
|
479
491
|
"""
|
|
@@ -503,8 +515,9 @@ class Reader:
|
|
|
503
515
|
return list(diffs)
|
|
504
516
|
|
|
505
517
|
def check_timediffs(self) -> bool:
|
|
506
|
-
"""Validate equal time-deltas
|
|
507
|
-
|
|
518
|
+
"""Validate equal time-deltas.
|
|
519
|
+
|
|
520
|
+
Unexpected time-jumps hint at a corrupted file or faulty measurement.
|
|
508
521
|
|
|
509
522
|
:return: True if OK
|
|
510
523
|
"""
|
|
@@ -532,7 +545,8 @@ class Reader:
|
|
|
532
545
|
*,
|
|
533
546
|
minimal: bool = False,
|
|
534
547
|
) -> Dict[str, dict]:
|
|
535
|
-
"""Recursive FN to capture the structure of the file
|
|
548
|
+
"""Recursive FN to capture the structure of the file.
|
|
549
|
+
|
|
536
550
|
:param node: starting node, leave free to go through whole file
|
|
537
551
|
:param minimal: just provide a bare tree (much faster)
|
|
538
552
|
:return: structure of that node with everything inside it
|
|
@@ -583,7 +597,7 @@ class Reader:
|
|
|
583
597
|
return metadata
|
|
584
598
|
|
|
585
599
|
def save_metadata(self, node: Union[h5py.Dataset, h5py.Group, None] = None) -> dict:
|
|
586
|
-
"""Get structure of file and dump content to yaml-file with same name as original
|
|
600
|
+
"""Get structure of file and dump content to yaml-file with same name as original.
|
|
587
601
|
|
|
588
602
|
:param node: starting node, leave free to go through whole file
|
|
589
603
|
:return: structure of that node with everything inside it
|
|
@@ -612,8 +626,9 @@ class Reader:
|
|
|
612
626
|
|
|
613
627
|
@staticmethod
|
|
614
628
|
def get_filter_for_redundant_states(data: np.ndarray) -> np.ndarray:
|
|
615
|
-
"""Input is 1D state-vector, kep only first from identical & sequential states
|
|
616
|
-
|
|
629
|
+
"""Input is 1D state-vector, kep only first from identical & sequential states.
|
|
630
|
+
|
|
631
|
+
Algo: create an offset-by-one vector and compare against original.
|
|
617
632
|
"""
|
|
618
633
|
if len(data.shape) > 1:
|
|
619
634
|
ValueError("Array must be 1D")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Client-Class to access a testbed instance."""
|
|
2
|
+
|
|
1
3
|
from importlib import import_module
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import Optional
|
|
@@ -16,6 +18,8 @@ from .user_model import User
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class TestbedClient:
|
|
21
|
+
"""Client-Class to access a testbed instance."""
|
|
22
|
+
|
|
19
23
|
_instance: Optional[Self] = None
|
|
20
24
|
|
|
21
25
|
def __init__(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> None:
|
|
@@ -41,8 +45,10 @@ class TestbedClient:
|
|
|
41
45
|
|
|
42
46
|
@validate_call
|
|
43
47
|
def connect(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> bool:
|
|
44
|
-
"""
|
|
45
|
-
|
|
48
|
+
"""Establish connection to testbed-server.
|
|
49
|
+
|
|
50
|
+
server: either "local" to use demo-fixtures or something like "https://HOST:PORT"
|
|
51
|
+
token: your account validation.
|
|
46
52
|
"""
|
|
47
53
|
if isinstance(token, Path):
|
|
48
54
|
with token.resolve().open() as file:
|
|
@@ -77,19 +83,19 @@ class TestbedClient:
|
|
|
77
83
|
|
|
78
84
|
def query_ids(self, model_type: str) -> list:
|
|
79
85
|
if self._connected:
|
|
80
|
-
raise
|
|
86
|
+
raise NotImplementedError("TODO")
|
|
81
87
|
return list(self._fixtures[model_type].elements_by_id.keys())
|
|
82
88
|
|
|
83
89
|
def query_names(self, model_type: str) -> list:
|
|
84
90
|
if self._connected:
|
|
85
|
-
raise
|
|
91
|
+
raise NotImplementedError("TODO")
|
|
86
92
|
return list(self._fixtures[model_type].elements_by_name.keys())
|
|
87
93
|
|
|
88
94
|
def query_item(
|
|
89
95
|
self, model_type: str, uid: Optional[int] = None, name: Optional[str] = None
|
|
90
96
|
) -> dict:
|
|
91
97
|
if self._connected:
|
|
92
|
-
raise
|
|
98
|
+
raise NotImplementedError("TODO")
|
|
93
99
|
if uid is not None:
|
|
94
100
|
return self._fixtures[model_type].query_id(uid)
|
|
95
101
|
if name is not None:
|
|
@@ -115,11 +121,11 @@ class TestbedClient:
|
|
|
115
121
|
|
|
116
122
|
def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
|
|
117
123
|
if self._connected:
|
|
118
|
-
raise
|
|
124
|
+
raise NotImplementedError("TODO")
|
|
119
125
|
return self._fixtures[model_type].inheritance(values)
|
|
120
126
|
|
|
121
127
|
def try_completing_model(self, model_type: str, values: dict) -> (dict, list):
|
|
122
|
-
"""Init by name/id, for none existing instances raise Exception"""
|
|
128
|
+
"""Init by name/id, for none existing instances raise Exception."""
|
|
123
129
|
if len(values) == 1 and next(iter(values.keys())) in {"id", "name"}:
|
|
124
130
|
value = next(iter(values.values()))
|
|
125
131
|
if (
|
|
@@ -131,7 +137,8 @@ class TestbedClient:
|
|
|
131
137
|
# TODO: still depending on _fixture
|
|
132
138
|
values = self.query_item(model_type, uid=value)
|
|
133
139
|
else:
|
|
134
|
-
|
|
140
|
+
msg = f"Query {model_type} by name / ID failed - {values} is unknown!"
|
|
141
|
+
raise ValueError(msg)
|
|
135
142
|
return self.try_inheritance(model_type, values)
|
|
136
143
|
|
|
137
144
|
def fill_in_user_data(self, values: dict) -> dict:
|