shepherd-core 2025.6.3__py3-none-any.whl → 2025.8.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/data_models/base/cal_measurement.py +4 -5
- shepherd_core/data_models/base/calibration.py +8 -10
- shepherd_core/data_models/base/content.py +2 -3
- shepherd_core/data_models/base/shepherd.py +6 -8
- shepherd_core/data_models/base/wrapper.py +3 -4
- shepherd_core/data_models/content/energy_environment.py +4 -5
- shepherd_core/data_models/content/firmware.py +3 -5
- shepherd_core/data_models/content/virtual_harvester.py +5 -6
- shepherd_core/data_models/experiment/experiment.py +9 -17
- shepherd_core/data_models/experiment/observer_features.py +22 -38
- shepherd_core/data_models/experiment/target_config.py +10 -11
- shepherd_core/data_models/task/__init__.py +1 -3
- shepherd_core/data_models/task/emulation.py +18 -19
- shepherd_core/data_models/task/firmware_mod.py +3 -4
- shepherd_core/data_models/task/harvest.py +7 -10
- shepherd_core/data_models/task/observer_tasks.py +12 -10
- shepherd_core/data_models/task/programming.py +2 -2
- shepherd_core/data_models/task/testbed_tasks.py +8 -10
- shepherd_core/data_models/testbed/cape.py +3 -5
- shepherd_core/data_models/testbed/gpio.py +7 -8
- shepherd_core/data_models/testbed/mcu.py +1 -2
- shepherd_core/data_models/testbed/observer.py +5 -6
- shepherd_core/data_models/testbed/target.py +4 -6
- shepherd_core/data_models/testbed/testbed.py +2 -3
- shepherd_core/decoder_waveform/uart.py +12 -13
- shepherd_core/fw_tools/converter.py +1 -2
- shepherd_core/fw_tools/converter_elf.py +1 -2
- shepherd_core/fw_tools/patcher.py +65 -40
- shepherd_core/fw_tools/validation.py +7 -1
- shepherd_core/inventory/python.py +8 -9
- shepherd_core/inventory/system.py +1 -2
- shepherd_core/inventory/target.py +1 -2
- shepherd_core/logger.py +1 -2
- shepherd_core/reader.py +18 -23
- shepherd_core/testbed_client/client_abc_fix.py +2 -7
- shepherd_core/testbed_client/client_web.py +5 -9
- shepherd_core/testbed_client/fixtures.py +3 -5
- shepherd_core/testbed_client/user_model.py +4 -5
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/virtual_converter_model.py +1 -2
- shepherd_core/vsource/virtual_harvester_simulation.py +1 -2
- shepherd_core/vsource/virtual_source_model.py +3 -5
- shepherd_core/vsource/virtual_source_simulation.py +2 -3
- shepherd_core/writer.py +12 -14
- {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/METADATA +4 -8
- shepherd_core-2025.8.1.dist-info/RECORD +83 -0
- shepherd_core-2025.6.3.dist-info/RECORD +0 -83
- {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/WHEEL +0 -0
- {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/zip-safe +0 -0
|
@@ -4,9 +4,7 @@ from collections.abc import Set as AbstractSet
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from pathlib import PurePosixPath
|
|
6
6
|
from typing import Annotated
|
|
7
|
-
from typing import Optional
|
|
8
7
|
from typing import TypedDict
|
|
9
|
-
from typing import Union
|
|
10
8
|
|
|
11
9
|
from pydantic import Field
|
|
12
10
|
from pydantic import model_validator
|
|
@@ -31,9 +29,9 @@ from .helper_paths import path_posix
|
|
|
31
29
|
class FirmwareModTask(ShpModel):
|
|
32
30
|
"""Config for Task that adds the custom ID to the firmware & stores it into a file."""
|
|
33
31
|
|
|
34
|
-
data:
|
|
32
|
+
data: FirmwareStr | Path
|
|
35
33
|
data_type: FirmwareDType
|
|
36
|
-
custom_id:
|
|
34
|
+
custom_id: IdInt16 | None = None
|
|
37
35
|
firmware_file: Path
|
|
38
36
|
|
|
39
37
|
verbose: Annotated[int, Field(ge=0, le=4)] = 2
|
|
@@ -95,6 +93,7 @@ class FirmwareModTask(ShpModel):
|
|
|
95
93
|
return cls(**kwargs)
|
|
96
94
|
|
|
97
95
|
def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
|
|
96
|
+
"""Limit paths to allowed directories."""
|
|
98
97
|
all_ok = any(self.firmware_file.is_relative_to(path) for path in paths)
|
|
99
98
|
if isinstance(self.data, Path):
|
|
100
99
|
all_ok = any(self.data.is_relative_to(path) for path in paths)
|
|
@@ -6,12 +6,10 @@ from datetime import timedelta
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from pathlib import PurePosixPath
|
|
8
8
|
from typing import Annotated
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
10
|
from pydantic import Field
|
|
12
11
|
from pydantic import model_validator
|
|
13
12
|
from typing_extensions import Self
|
|
14
|
-
from typing_extensions import deprecated
|
|
15
13
|
|
|
16
14
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
17
15
|
from shepherd_core.data_models.base.timezone import local_tz
|
|
@@ -35,14 +33,13 @@ class HarvestTask(ShpModel):
|
|
|
35
33
|
"""
|
|
36
34
|
force_overwrite: bool = False
|
|
37
35
|
""" ⤷ Overwrite existing file"""
|
|
38
|
-
output_compression:
|
|
36
|
+
output_compression: Compression | None = Compression.default
|
|
39
37
|
""" ⤷ should be 1 (level 1 gzip), lzf, or None (order of recommendation)"""
|
|
40
38
|
|
|
41
|
-
time_start:
|
|
39
|
+
time_start: datetime | None = None
|
|
42
40
|
""" timestamp or unix epoch time, None = ASAP"""
|
|
43
|
-
duration:
|
|
41
|
+
duration: timedelta | None = None
|
|
44
42
|
""" ⤷ Duration of recording in seconds, None = till EOFSys"""
|
|
45
|
-
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
46
43
|
|
|
47
44
|
# emulation-specific
|
|
48
45
|
use_cal_default: bool = False
|
|
@@ -52,7 +49,7 @@ class HarvestTask(ShpModel):
|
|
|
52
49
|
""" ⤷ Choose one of the predefined virtual harvesters or configure a new one
|
|
53
50
|
"""
|
|
54
51
|
power_tracing: PowerTracing = PowerTracing()
|
|
55
|
-
sys_logging:
|
|
52
|
+
sys_logging: SystemLogging | None = SystemLogging()
|
|
56
53
|
|
|
57
54
|
verbose: Annotated[int, Field(ge=0, le=4)] = 2
|
|
58
55
|
""" ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
|
|
@@ -75,12 +72,11 @@ class HarvestTask(ShpModel):
|
|
|
75
72
|
@model_validator(mode="after")
|
|
76
73
|
def post_validation(self) -> Self:
|
|
77
74
|
# TODO: limit paths
|
|
78
|
-
has_time = self.time_start is not None
|
|
75
|
+
has_time = False # TODO: deactivated, self.time_start is not None
|
|
79
76
|
time_now = datetime.now().astimezone()
|
|
80
77
|
if has_time and self.time_start < time_now:
|
|
81
78
|
msg = (
|
|
82
|
-
"Start-Time for
|
|
83
|
-
f"('{self.time_start}' vs '{time_now}'."
|
|
79
|
+
f"Start-Time for Harvest can't be in the past ('{self.time_start}' vs '{time_now}'."
|
|
84
80
|
)
|
|
85
81
|
raise ValueError(msg)
|
|
86
82
|
if self.duration and self.duration.total_seconds() < 0:
|
|
@@ -88,4 +84,5 @@ class HarvestTask(ShpModel):
|
|
|
88
84
|
return self
|
|
89
85
|
|
|
90
86
|
def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
|
|
87
|
+
"""Limit paths to allowed directories."""
|
|
91
88
|
return any(self.output_path.is_relative_to(path) for path in paths)
|
|
@@ -5,7 +5,6 @@ from datetime import datetime
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from pathlib import PurePosixPath
|
|
7
7
|
from typing import Annotated
|
|
8
|
-
from typing import Optional
|
|
9
8
|
|
|
10
9
|
from pydantic import validate_call
|
|
11
10
|
from typing_extensions import Self
|
|
@@ -29,20 +28,20 @@ class ObserverTasks(ShpModel):
|
|
|
29
28
|
observer: NameStr
|
|
30
29
|
|
|
31
30
|
# PRE PROCESS
|
|
32
|
-
time_prep:
|
|
31
|
+
time_prep: datetime | None = None # TODO: currently not used
|
|
33
32
|
root_path: Path
|
|
34
33
|
|
|
35
34
|
# fw mod, store as hex-file and program
|
|
36
|
-
fw1_mod:
|
|
37
|
-
fw2_mod:
|
|
38
|
-
fw1_prog:
|
|
39
|
-
fw2_prog:
|
|
35
|
+
fw1_mod: FirmwareModTask | None = None
|
|
36
|
+
fw2_mod: FirmwareModTask | None = None
|
|
37
|
+
fw1_prog: ProgrammingTask | None = None
|
|
38
|
+
fw2_prog: ProgrammingTask | None = None
|
|
40
39
|
|
|
41
40
|
# MAIN PROCESS
|
|
42
|
-
emulation:
|
|
41
|
+
emulation: EmulationTask | None = None
|
|
43
42
|
|
|
44
43
|
# deprecations, TODO: remove before public release
|
|
45
|
-
owner_id: Annotated[
|
|
44
|
+
owner_id: Annotated[IdInt | None, deprecated("not needed anymore")] = None
|
|
46
45
|
abort_on_error: Annotated[bool, deprecated("has no effect")] = False
|
|
47
46
|
|
|
48
47
|
# post_copy / cleanup, Todo: could also just intake emuTask
|
|
@@ -53,12 +52,14 @@ class ObserverTasks(ShpModel):
|
|
|
53
52
|
|
|
54
53
|
@classmethod
|
|
55
54
|
@validate_call
|
|
56
|
-
def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt) -> Self:
|
|
55
|
+
def from_xp(cls, xp: Experiment, xp_folder: str | None, tb: Testbed, tgt_id: IdInt) -> Self:
|
|
57
56
|
if not tb.shared_storage:
|
|
58
57
|
raise ValueError("Implementation currently relies on shared storage!")
|
|
59
58
|
|
|
60
59
|
obs = tb.get_observer(tgt_id)
|
|
61
|
-
|
|
60
|
+
if xp_folder is None:
|
|
61
|
+
xp_folder = xp.folder_name() # moved a layer up for consistent naming
|
|
62
|
+
root_path = tb.data_on_observer / "experiments" / xp_folder
|
|
62
63
|
fw_paths = [root_path / f"fw{_i}_{obs.name}.hex" for _i in [1, 2]]
|
|
63
64
|
|
|
64
65
|
return cls(
|
|
@@ -94,6 +95,7 @@ class ObserverTasks(ShpModel):
|
|
|
94
95
|
return values
|
|
95
96
|
|
|
96
97
|
def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
|
|
98
|
+
"""Limit paths to allowed directories."""
|
|
97
99
|
all_ok = any(self.root_path.is_relative_to(path) for path in paths)
|
|
98
100
|
all_ok &= self.fw1_mod.is_contained(paths)
|
|
99
101
|
all_ok &= self.fw2_mod.is_contained(paths)
|
|
@@ -4,7 +4,6 @@ from collections.abc import Set as AbstractSet
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from pathlib import PurePosixPath
|
|
6
6
|
from typing import Annotated
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
from pydantic import Field
|
|
10
9
|
from pydantic import model_validator
|
|
@@ -60,7 +59,7 @@ class ProgrammingTask(ShpModel):
|
|
|
60
59
|
tgt_id: IdInt,
|
|
61
60
|
mcu_port: MCUPort,
|
|
62
61
|
fw_path: Path,
|
|
63
|
-
) ->
|
|
62
|
+
) -> Self | None:
|
|
64
63
|
obs = tb.get_observer(tgt_id)
|
|
65
64
|
tgt_cfg = xp.get_target_config(tgt_id)
|
|
66
65
|
|
|
@@ -79,4 +78,5 @@ class ProgrammingTask(ShpModel):
|
|
|
79
78
|
)
|
|
80
79
|
|
|
81
80
|
def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
|
|
81
|
+
"""Limit paths to allowed directories."""
|
|
82
82
|
return any(self.firmware_file.is_relative_to(path) for path in paths)
|
|
@@ -4,14 +4,11 @@ from pathlib import Path
|
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
from typing import Annotated
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
from pydantic import Field
|
|
10
9
|
from pydantic import validate_call
|
|
11
10
|
from typing_extensions import Self
|
|
12
|
-
from typing_extensions import deprecated
|
|
13
11
|
|
|
14
|
-
from shepherd_core.data_models.base.content import IdInt
|
|
15
12
|
from shepherd_core.data_models.base.content import NameStr
|
|
16
13
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
17
14
|
from shepherd_core.data_models.experiment.experiment import Experiment
|
|
@@ -29,30 +26,30 @@ class TestbedTasks(ShpModel):
|
|
|
29
26
|
name: NameStr
|
|
30
27
|
observer_tasks: Annotated[list[ObserverTasks], Field(min_length=1, max_length=128)]
|
|
31
28
|
|
|
32
|
-
# deprecated, TODO: remove before public release
|
|
33
|
-
email_results: Annotated[Optional[bool], deprecated("not needed anymore")] = False
|
|
34
|
-
owner_id: Annotated[Optional[IdInt], deprecated("not needed anymore")] = None
|
|
35
|
-
|
|
36
29
|
@classmethod
|
|
37
30
|
@validate_call
|
|
38
|
-
def from_xp(cls, xp: Experiment, tb:
|
|
31
|
+
def from_xp(cls, xp: Experiment, tb: Testbed | None = None) -> Self:
|
|
39
32
|
if tb is None:
|
|
40
33
|
# TODO: is tb-argument really needed? prob. not
|
|
41
34
|
tb = Testbed() # this will query the first (and only) entry of client
|
|
42
35
|
|
|
43
36
|
tgt_ids = xp.get_target_ids()
|
|
44
|
-
|
|
37
|
+
xp_folder = xp.folder_name()
|
|
38
|
+
obs_tasks = [ObserverTasks.from_xp(xp, xp_folder, tb, _id) for _id in tgt_ids]
|
|
45
39
|
return cls(
|
|
46
40
|
name=xp.name,
|
|
47
41
|
observer_tasks=obs_tasks,
|
|
48
42
|
)
|
|
49
43
|
|
|
50
|
-
def get_observer_tasks(self, observer: str) ->
|
|
44
|
+
def get_observer_tasks(self, observer: str) -> ObserverTasks | None:
|
|
51
45
|
for tasks in self.observer_tasks:
|
|
52
46
|
if observer == tasks.observer:
|
|
53
47
|
return tasks
|
|
54
48
|
return None
|
|
55
49
|
|
|
50
|
+
def get_observers(self) -> set[str]:
|
|
51
|
+
return {tasks.observer for tasks in self.observer_tasks}
|
|
52
|
+
|
|
56
53
|
def get_output_paths(self) -> dict[str, Path]:
|
|
57
54
|
# TODO: computed field preferred, but they don't work here, as
|
|
58
55
|
# - they are always stored in yaml despite "repr=False"
|
|
@@ -63,6 +60,7 @@ class TestbedTasks(ShpModel):
|
|
|
63
60
|
return values
|
|
64
61
|
|
|
65
62
|
def is_contained(self) -> bool:
|
|
63
|
+
"""Limit paths to allowed directories."""
|
|
66
64
|
paths_allowed: AbstractSet[PurePosixPath] = {
|
|
67
65
|
PurePosixPath("/var/shepherd/"),
|
|
68
66
|
PurePosixPath("/tmp/"), # noqa: S108
|
|
@@ -4,8 +4,6 @@ from datetime import date
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import Any
|
|
7
|
-
from typing import Optional
|
|
8
|
-
from typing import Union
|
|
9
7
|
|
|
10
8
|
from pydantic import Field
|
|
11
9
|
from pydantic import model_validator
|
|
@@ -31,12 +29,12 @@ class Cape(ShpModel, title="Shepherd-Cape"):
|
|
|
31
29
|
name: NameStr
|
|
32
30
|
version: NameStr
|
|
33
31
|
description: SafeStr
|
|
34
|
-
comment:
|
|
32
|
+
comment: SafeStr | None = None
|
|
35
33
|
# TODO: wake_interval, calibration
|
|
36
34
|
|
|
37
35
|
active: bool = True
|
|
38
|
-
created:
|
|
39
|
-
calibrated:
|
|
36
|
+
created: date | datetime = Field(default_factory=datetime.now)
|
|
37
|
+
calibrated: date | datetime | None = None
|
|
40
38
|
|
|
41
39
|
def __str__(self) -> str:
|
|
42
40
|
return self.name
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
7
|
from pydantic import Field
|
|
9
8
|
from pydantic import StringConstraints
|
|
@@ -30,16 +29,16 @@ class GPIO(ShpModel, title="GPIO of Observer Node"):
|
|
|
30
29
|
|
|
31
30
|
id: IdInt
|
|
32
31
|
name: NameStr
|
|
33
|
-
description:
|
|
34
|
-
comment:
|
|
32
|
+
description: SafeStr | None = None
|
|
33
|
+
comment: SafeStr | None = None
|
|
35
34
|
|
|
36
35
|
direction: Direction = Direction.Input
|
|
37
|
-
dir_switch:
|
|
36
|
+
dir_switch: Annotated[str, StringConstraints(max_length=32)] | None = None
|
|
38
37
|
|
|
39
|
-
reg_pru:
|
|
40
|
-
pin_pru:
|
|
41
|
-
reg_sys:
|
|
42
|
-
pin_sys:
|
|
38
|
+
reg_pru: Annotated[str, StringConstraints(max_length=10)] | None = None
|
|
39
|
+
pin_pru: Annotated[str, StringConstraints(max_length=10)] | None = None
|
|
40
|
+
reg_sys: Annotated[int, Field(ge=0)] | None = None
|
|
41
|
+
pin_sys: Annotated[str, StringConstraints(max_length=10)] | None = None
|
|
43
42
|
|
|
44
43
|
def __str__(self) -> str:
|
|
45
44
|
return self.name
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
7
|
from pydantic import Field
|
|
9
8
|
from pydantic import model_validator
|
|
@@ -30,7 +29,7 @@ class MCU(ShpModel, title="Microcontroller of the Target Node"):
|
|
|
30
29
|
id: IdInt
|
|
31
30
|
name: NameStr
|
|
32
31
|
description: SafeStr
|
|
33
|
-
comment:
|
|
32
|
+
comment: SafeStr | None = None
|
|
34
33
|
|
|
35
34
|
platform: NameStr
|
|
36
35
|
core: NameStr
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
7
|
from pydantic import Field
|
|
9
8
|
from pydantic import IPvAnyAddress
|
|
@@ -33,7 +32,7 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
33
32
|
id: IdInt
|
|
34
33
|
name: NameStr
|
|
35
34
|
description: SafeStr
|
|
36
|
-
comment:
|
|
35
|
+
comment: SafeStr | None = None
|
|
37
36
|
|
|
38
37
|
ip: IPvAnyAddress
|
|
39
38
|
mac: MACStr
|
|
@@ -46,12 +45,12 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
46
45
|
""" ⤷ cfaed-floor"""
|
|
47
46
|
|
|
48
47
|
active: bool = True
|
|
49
|
-
cape:
|
|
50
|
-
target_a:
|
|
51
|
-
target_b:
|
|
48
|
+
cape: Cape | None = None
|
|
49
|
+
target_a: Target | None = None
|
|
50
|
+
target_b: Target | None = None
|
|
52
51
|
|
|
53
52
|
created: datetime = Field(default_factory=datetime.now)
|
|
54
|
-
alive_last:
|
|
53
|
+
alive_last: datetime | None = None
|
|
55
54
|
|
|
56
55
|
def __str__(self) -> str:
|
|
57
56
|
return self.name
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
-
from typing import Optional
|
|
7
|
-
from typing import Union
|
|
8
6
|
|
|
9
7
|
from pydantic import Field
|
|
10
8
|
from pydantic import model_validator
|
|
@@ -30,15 +28,15 @@ class Target(ShpModel, title="Target Node (DuT)"):
|
|
|
30
28
|
version: NameStr
|
|
31
29
|
description: SafeStr
|
|
32
30
|
|
|
33
|
-
comment:
|
|
31
|
+
comment: SafeStr | None = None
|
|
34
32
|
|
|
35
33
|
active: bool = True
|
|
36
34
|
created: datetime = Field(default_factory=datetime.now)
|
|
37
35
|
|
|
38
|
-
testbed_id:
|
|
36
|
+
testbed_id: IdInt16 | None = None
|
|
39
37
|
""" ⤷ is derived from ID (targets are still selected by id!)"""
|
|
40
|
-
mcu1:
|
|
41
|
-
mcu2:
|
|
38
|
+
mcu1: MCU | NameStr
|
|
39
|
+
mcu2: MCU | NameStr | None = None
|
|
42
40
|
|
|
43
41
|
# TODO: programming pins per mcu should be here (or better in Cape)
|
|
44
42
|
|
|
@@ -4,7 +4,6 @@ from datetime import timedelta
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Annotated
|
|
6
6
|
from typing import Any
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
from pydantic import Field
|
|
10
9
|
from pydantic import HttpUrl
|
|
@@ -29,9 +28,9 @@ class Testbed(ShpModel):
|
|
|
29
28
|
id: IdInt
|
|
30
29
|
name: NameStr
|
|
31
30
|
description: SafeStr
|
|
32
|
-
comment:
|
|
31
|
+
comment: SafeStr | None = None
|
|
33
32
|
|
|
34
|
-
url:
|
|
33
|
+
url: HttpUrl | None = None
|
|
35
34
|
|
|
36
35
|
observers: Annotated[list[Observer], Field(min_length=1, max_length=128)]
|
|
37
36
|
|
|
@@ -23,8 +23,6 @@ https://sigrok.org/wiki/Protocol_decoder:Uart
|
|
|
23
23
|
|
|
24
24
|
from enum import Enum
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import Optional
|
|
27
|
-
from typing import Union
|
|
28
26
|
|
|
29
27
|
import numpy as np
|
|
30
28
|
|
|
@@ -51,12 +49,13 @@ class Uart:
|
|
|
51
49
|
|
|
52
50
|
def __init__(
|
|
53
51
|
self,
|
|
54
|
-
content:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
content: Path | np.ndarray,
|
|
53
|
+
*,
|
|
54
|
+
baud_rate: int | None = None,
|
|
55
|
+
frame_length: int | None = 8,
|
|
56
|
+
inversion: bool | None = None,
|
|
57
|
+
parity: Parity | None = Parity.no,
|
|
58
|
+
bit_order: BitOrder | None = BitOrder.lsb_first,
|
|
60
59
|
) -> None:
|
|
61
60
|
"""Provide a file with two columns: TS & Signal.
|
|
62
61
|
|
|
@@ -115,9 +114,9 @@ class Uart:
|
|
|
115
114
|
log.error("Signal still inverted?!? Check parameters and input")
|
|
116
115
|
|
|
117
116
|
# results
|
|
118
|
-
self.events_symbols:
|
|
119
|
-
self.events_lines:
|
|
120
|
-
self.text:
|
|
117
|
+
self.events_symbols: np.ndarray | None = None
|
|
118
|
+
self.events_lines: np.ndarray | None = None
|
|
119
|
+
self.text: str | None = None
|
|
121
120
|
|
|
122
121
|
def _convert_analog2digital(self, *, invert: bool = False) -> None:
|
|
123
122
|
"""Divide dimension in two, divided by mean-value."""
|
|
@@ -208,9 +207,9 @@ class Uart:
|
|
|
208
207
|
if self.events_symbols is not None:
|
|
209
208
|
return self.events_symbols
|
|
210
209
|
|
|
211
|
-
pos_df:
|
|
210
|
+
pos_df: int | None = None
|
|
212
211
|
symbol: int = 0
|
|
213
|
-
t_start:
|
|
212
|
+
t_start: float | None = None
|
|
214
213
|
content: list = []
|
|
215
214
|
|
|
216
215
|
for time, value, steps in self.events_sig:
|
|
@@ -4,7 +4,6 @@ import base64
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import shutil
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Union
|
|
8
7
|
|
|
9
8
|
import zstandard as zstd
|
|
10
9
|
from pydantic import validate_call
|
|
@@ -81,7 +80,7 @@ def base64_to_hash(content: str) -> str:
|
|
|
81
80
|
|
|
82
81
|
|
|
83
82
|
@validate_call
|
|
84
|
-
def extract_firmware(data:
|
|
83
|
+
def extract_firmware(data: str | Path, data_type: FirmwareDType, file_path: Path) -> Path:
|
|
85
84
|
"""Make embedded firmware-data usable in filesystem.
|
|
86
85
|
|
|
87
86
|
- base64-string will be transformed to file
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from pydantic import validate_call
|
|
8
7
|
|
|
@@ -10,7 +9,7 @@ from pydantic import validate_call
|
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
@validate_call
|
|
13
|
-
def elf_to_hex(file_elf: Path, file_hex:
|
|
12
|
+
def elf_to_hex(file_elf: Path, file_hex: Path | None = None) -> Path:
|
|
14
13
|
"""Convert ELF to hex file using objcopy."""
|
|
15
14
|
if not file_elf.is_file():
|
|
16
15
|
raise ValueError("Fn needs an existing file as input")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Read and modify symbols in ELF-files."""
|
|
2
2
|
|
|
3
|
+
import shutil
|
|
3
4
|
from pathlib import Path
|
|
5
|
+
from tempfile import TemporaryDirectory
|
|
4
6
|
from typing import Annotated
|
|
5
|
-
from typing import Optional
|
|
6
7
|
|
|
7
8
|
from pydantic import Field
|
|
8
9
|
from pydantic import validate_call
|
|
@@ -29,27 +30,32 @@ def find_symbol(file_elf: Path, symbol: str) -> bool:
|
|
|
29
30
|
return False
|
|
30
31
|
if ELF is None:
|
|
31
32
|
raise RuntimeError(elf_error_text)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
|
34
|
+
# switcheroo that might prevent windows bug - overwrite fails in modify_symbol_value()
|
|
35
|
+
file_tmp = Path(tmp) / file_elf.name
|
|
36
|
+
shutil.copy(file_elf, file_tmp)
|
|
37
|
+
elf = ELF(path=file_tmp)
|
|
38
|
+
try:
|
|
39
|
+
addr = elf.symbols[symbol]
|
|
40
|
+
except KeyError:
|
|
41
|
+
addr = None
|
|
42
|
+
if addr is None:
|
|
43
|
+
elf.close() # better be safe
|
|
44
|
+
log.debug("Symbol '%s' not found in ELF-File %s", symbol, file_elf.name)
|
|
45
|
+
return False
|
|
46
|
+
log.debug(
|
|
47
|
+
"Symbol '%s' found in ELF-File %s, arch=%s, order=%s",
|
|
48
|
+
symbol,
|
|
49
|
+
file_elf.name,
|
|
50
|
+
elf.arch,
|
|
51
|
+
elf.endian,
|
|
52
|
+
)
|
|
53
|
+
elf.close()
|
|
48
54
|
return True
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
@validate_call
|
|
52
|
-
def read_symbol(file_elf: Path, symbol: str, length: int) ->
|
|
58
|
+
def read_symbol(file_elf: Path, symbol: str, length: int) -> int | None:
|
|
53
59
|
"""Read value of symbol in ELF-File.
|
|
54
60
|
|
|
55
61
|
Will be interpreted as int.
|
|
@@ -61,24 +67,28 @@ def read_symbol(file_elf: Path, symbol: str, length: int) -> Optional[int]:
|
|
|
61
67
|
elf = ELF(path=file_elf)
|
|
62
68
|
addr = elf.symbols[symbol]
|
|
63
69
|
value_raw = elf.read(address=addr, count=length)[-length:]
|
|
70
|
+
endian = elf.endian
|
|
64
71
|
elf.close()
|
|
65
|
-
return int.from_bytes(bytes=value_raw, byteorder=
|
|
72
|
+
return int.from_bytes(bytes=value_raw, byteorder=endian, signed=False)
|
|
66
73
|
|
|
67
74
|
|
|
68
|
-
def read_uid(file_elf: Path) ->
|
|
75
|
+
def read_uid(file_elf: Path) -> int | None:
|
|
69
76
|
"""Read value of UID-symbol for shepherd testbed."""
|
|
70
77
|
return read_symbol(file_elf, symbol=config.UID_NAME, length=config.UID_SIZE)
|
|
71
78
|
|
|
72
79
|
|
|
73
|
-
def read_arch(file_elf: Path) ->
|
|
80
|
+
def read_arch(file_elf: Path) -> str | None:
|
|
74
81
|
"""Determine chip-architecture from elf-metadata."""
|
|
75
82
|
if not is_elf(file_elf):
|
|
76
83
|
return None
|
|
77
84
|
if ELF is None:
|
|
78
85
|
raise RuntimeError(elf_error_text)
|
|
79
86
|
elf = ELF(path=file_elf)
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
elf_type = elf.elftype.lower()
|
|
88
|
+
elf_arch = elf.arch.lower()
|
|
89
|
+
elf.close()
|
|
90
|
+
if "exec" in elf_type:
|
|
91
|
+
return elf_arch
|
|
82
92
|
log.error("ELF is not Executable")
|
|
83
93
|
return None
|
|
84
94
|
|
|
@@ -90,7 +100,7 @@ def modify_symbol_value(
|
|
|
90
100
|
value: Annotated[int, Field(ge=0, lt=2 ** (8 * config.UID_SIZE))],
|
|
91
101
|
*,
|
|
92
102
|
overwrite: bool = False,
|
|
93
|
-
) ->
|
|
103
|
+
) -> Path | None:
|
|
94
104
|
"""Replace value of uint16-symbol in ELF-File.
|
|
95
105
|
|
|
96
106
|
Hardcoded for uint16_t (2 byte).
|
|
@@ -103,22 +113,37 @@ def modify_symbol_value(
|
|
|
103
113
|
return None
|
|
104
114
|
if ELF is None:
|
|
105
115
|
raise RuntimeError(elf_error_text)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
|
117
|
+
# switcheroo that also prevents windows bug (overwrite fails)
|
|
118
|
+
file_tmp = Path(tmp) / file_elf.name
|
|
119
|
+
shutil.copy(file_elf, file_tmp)
|
|
120
|
+
|
|
121
|
+
elf = ELF(path=file_elf)
|
|
122
|
+
addr = elf.symbols[symbol]
|
|
123
|
+
value_raw = elf.read(address=addr, count=config.UID_SIZE)[-config.UID_SIZE :]
|
|
124
|
+
# ⤷ cutting needed -> msp produces 4b instead of 2
|
|
125
|
+
value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
|
|
126
|
+
value_raw = value.to_bytes(length=config.UID_SIZE, byteorder=elf.endian, signed=False)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
elf.write(address=addr, data=value_raw)
|
|
130
|
+
except AttributeError:
|
|
131
|
+
log.warning("ELF-Modifier failed @%s for symbol '%s'", f"0x{addr:X}", symbol)
|
|
132
|
+
elf.close()
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
file_new = file_elf if overwrite else file_elf.with_stem(file_elf.stem + "_" + str(value))
|
|
136
|
+
try:
|
|
137
|
+
file_new.unlink(missing_ok=True)
|
|
138
|
+
except PermissionError:
|
|
139
|
+
elf.close()
|
|
140
|
+
log.error(
|
|
141
|
+
"Failed to overwrite file, because it's somehow still in use (typical for WinOS)."
|
|
142
|
+
)
|
|
143
|
+
return None
|
|
144
|
+
elf.save(path=file_new)
|
|
145
|
+
elf.close()
|
|
118
146
|
|
|
119
|
-
file_new = file_elf if overwrite else file_elf.with_stem(file_elf.stem + "_" + str(value))
|
|
120
|
-
elf.save(path=file_new)
|
|
121
|
-
elf.close()
|
|
122
147
|
log.debug(
|
|
123
148
|
"Value of Symbol '%s' modified: %s -> %s @%s",
|
|
124
149
|
symbol,
|
|
@@ -129,6 +154,6 @@ def modify_symbol_value(
|
|
|
129
154
|
return file_new
|
|
130
155
|
|
|
131
156
|
|
|
132
|
-
def modify_uid(file_elf: Path, value: int) ->
|
|
157
|
+
def modify_uid(file_elf: Path, value: int) -> Path | None:
|
|
133
158
|
"""Replace value of UID-symbol for shepherd testbed."""
|
|
134
159
|
return modify_symbol_value(file_elf, symbol=config.UID_NAME, value=value, overwrite=True)
|
|
@@ -5,6 +5,7 @@ TODO: Work in Progress.
|
|
|
5
5
|
- detection-functions that register in main validator.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import shutil
|
|
8
9
|
import tempfile
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -92,7 +93,12 @@ def is_elf(file: Path) -> bool:
|
|
|
92
93
|
if not file.is_file():
|
|
93
94
|
return False
|
|
94
95
|
try:
|
|
95
|
-
|
|
96
|
+
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
|
97
|
+
# switcheroo that might prevent windows bug - overwrite fails in modify_symbol_value()
|
|
98
|
+
file_tmp = Path(tmp) / file.name
|
|
99
|
+
shutil.copy(file, file_tmp)
|
|
100
|
+
elf = ELF(path=file_tmp)
|
|
101
|
+
elf.close()
|
|
96
102
|
except ELFError:
|
|
97
103
|
log.debug("File %s is not ELF - Magic number does not match", file.name)
|
|
98
104
|
return False
|