shepherd-core 2023.11.1__py3-none-any.whl → 2023.12.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 +1 -1
- shepherd_core/data_models/base/content.py +3 -8
- shepherd_core/data_models/base/shepherd.py +2 -6
- shepherd_core/data_models/content/virtual_source.py +13 -40
- shepherd_core/data_models/experiment/experiment.py +3 -8
- shepherd_core/data_models/experiment/observer_features.py +4 -9
- shepherd_core/data_models/experiment/target_config.py +4 -11
- shepherd_core/data_models/task/__init__.py +2 -6
- shepherd_core/data_models/task/emulation.py +7 -14
- shepherd_core/data_models/task/firmware_mod.py +1 -3
- shepherd_core/data_models/task/harvest.py +1 -3
- shepherd_core/data_models/testbed/observer.py +6 -22
- shepherd_core/data_models/testbed/testbed.py +1 -3
- shepherd_core/decoder_waveform/uart.py +8 -26
- shepherd_core/fw_tools/__init__.py +1 -3
- shepherd_core/fw_tools/converter.py +4 -12
- shepherd_core/fw_tools/patcher.py +4 -12
- shepherd_core/inventory/__init__.py +3 -9
- shepherd_core/inventory/system.py +2 -5
- shepherd_core/logger.py +1 -3
- shepherd_core/reader.py +26 -41
- shepherd_core/testbed_client/client.py +5 -16
- shepherd_core/testbed_client/fixtures.py +4 -14
- shepherd_core/testbed_client/user_model.py +1 -3
- shepherd_core/vsource/virtual_converter_model.py +4 -14
- shepherd_core/vsource/virtual_harvester_model.py +1 -3
- shepherd_core/vsource/virtual_source_model.py +2 -6
- shepherd_core/writer.py +11 -22
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2023.12.1.dist-info}/METADATA +2 -5
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2023.12.1.dist-info}/RECORD +42 -42
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2023.12.1.dist-info}/WHEEL +1 -1
- tests/data_models/test_content_models.py +3 -9
- tests/data_models/test_experiment_models.py +2 -4
- tests/data_models/test_task_generation.py +1 -1
- tests/inventory/test_inventory.py +1 -3
- tests/test_cal_hw.py +2 -6
- tests/test_writer.py +2 -2
- tests/vsource/conftest.py +1 -3
- tests/vsource/test_converter.py +2 -6
- tests/vsource/test_harvester.py +3 -9
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2023.12.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2023.12.1.dist-info}/zip-safe +0 -0
shepherd_core/__init__.py
CHANGED
|
@@ -14,9 +14,7 @@ from .timezone import local_now
|
|
|
14
14
|
# constr -> to_lower=True, max_length=16, regex=r"^[\w]+$"
|
|
15
15
|
# ⤷ Regex = AlphaNum
|
|
16
16
|
IdInt = Annotated[int, Field(ge=0, lt=2**128)]
|
|
17
|
-
NameStr = Annotated[
|
|
18
|
-
str, StringConstraints(max_length=32, pattern=r'^[^<>:;,?"*|\/\\]+$')
|
|
19
|
-
]
|
|
17
|
+
NameStr = Annotated[str, StringConstraints(max_length=32, pattern=r'^[^<>:;,?"*|\/\\]+$')]
|
|
20
18
|
# ⤷ Regex = FileSystem-Compatible ASCII
|
|
21
19
|
SafeStr = Annotated[str, StringConstraints(pattern=r"^[ -~]+$")]
|
|
22
20
|
# ⤷ Regex = All Printable ASCII-Characters with Space
|
|
@@ -35,9 +33,7 @@ class ContentModel(ShpModel):
|
|
|
35
33
|
default_factory=id_default,
|
|
36
34
|
)
|
|
37
35
|
name: NameStr
|
|
38
|
-
description: Annotated[
|
|
39
|
-
Optional[SafeStr], Field(description="Required when public")
|
|
40
|
-
] = None
|
|
36
|
+
description: Annotated[Optional[SafeStr], Field(description="Required when public")] = None
|
|
41
37
|
comment: Optional[SafeStr] = None
|
|
42
38
|
created: datetime = Field(default_factory=datetime.now)
|
|
43
39
|
|
|
@@ -57,7 +53,6 @@ class ContentModel(ShpModel):
|
|
|
57
53
|
is_visible = self.visible2group or self.visible2all
|
|
58
54
|
if is_visible and self.description is None:
|
|
59
55
|
raise ValueError(
|
|
60
|
-
"Public instances require a description "
|
|
61
|
-
"(check visible2*- and description-field)"
|
|
56
|
+
"Public instances require a description (check visible2*- and description-field)"
|
|
62
57
|
)
|
|
63
58
|
return self
|
|
@@ -27,9 +27,7 @@ def path2str(
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def time2int(dumper: Dumper, data: timedelta) -> ScalarNode:
|
|
30
|
-
return dumper.represent_scalar(
|
|
31
|
-
"tag:yaml.org,2002:int", str(int(data.total_seconds()))
|
|
32
|
-
)
|
|
30
|
+
return dumper.represent_scalar("tag:yaml.org,2002:int", str(int(data.total_seconds())))
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
def generic2str(dumper: Dumper, data: Any) -> ScalarNode:
|
|
@@ -109,9 +107,7 @@ class ShpModel(BaseModel):
|
|
|
109
107
|
def schema_to_file(cls, path: Union[str, Path]) -> None:
|
|
110
108
|
"""Store schema to yaml (for frontend-generators)"""
|
|
111
109
|
model_dict = cls.model_json_schema()
|
|
112
|
-
model_yaml = yaml.safe_dump(
|
|
113
|
-
model_dict, default_flow_style=False, sort_keys=False
|
|
114
|
-
)
|
|
110
|
+
model_yaml = yaml.safe_dump(model_dict, default_flow_style=False, sort_keys=False)
|
|
115
111
|
with Path(path).resolve().with_suffix(".yaml").open("w") as f:
|
|
116
112
|
f.write(model_yaml)
|
|
117
113
|
|
|
@@ -153,15 +153,9 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
153
153
|
dV_output_imed_low_mV = 0
|
|
154
154
|
|
|
155
155
|
# protect from complex solutions (non valid input combinations)
|
|
156
|
-
if not (
|
|
157
|
-
isinstance(dV_output_en_thrs_mV, (int, float))
|
|
158
|
-
and (dV_output_en_thrs_mV >= 0)
|
|
159
|
-
):
|
|
156
|
+
if not (isinstance(dV_output_en_thrs_mV, (int, float)) and (dV_output_en_thrs_mV >= 0)):
|
|
160
157
|
dV_output_en_thrs_mV = 0
|
|
161
|
-
if not (
|
|
162
|
-
isinstance(dV_output_imed_low_mV, (int, float))
|
|
163
|
-
and (dV_output_imed_low_mV >= 0)
|
|
164
|
-
):
|
|
158
|
+
if not (isinstance(dV_output_imed_low_mV, (int, float)) and (dV_output_imed_low_mV >= 0)):
|
|
165
159
|
logger.warning("VSrc: C_output shouldn't be larger than C_intermediate")
|
|
166
160
|
dV_output_imed_low_mV = 0
|
|
167
161
|
|
|
@@ -171,9 +165,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
171
165
|
|
|
172
166
|
if self.V_intermediate_enable_threshold_mV > V_pre_output_mV:
|
|
173
167
|
values["dV_enable_output_mV"] = dV_output_en_thrs_mV
|
|
174
|
-
values[
|
|
175
|
-
"V_enable_output_threshold_mV"
|
|
176
|
-
] = self.V_intermediate_enable_threshold_mV
|
|
168
|
+
values["V_enable_output_threshold_mV"] = self.V_intermediate_enable_threshold_mV
|
|
177
169
|
|
|
178
170
|
else:
|
|
179
171
|
values["dV_enable_output_mV"] = dV_output_imed_low_mV
|
|
@@ -182,20 +174,14 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
182
174
|
)
|
|
183
175
|
|
|
184
176
|
if self.V_intermediate_disable_threshold_mV > V_pre_output_mV:
|
|
185
|
-
values[
|
|
186
|
-
"V_disable_output_threshold_mV"
|
|
187
|
-
] = self.V_intermediate_disable_threshold_mV
|
|
177
|
+
values["V_disable_output_threshold_mV"] = self.V_intermediate_disable_threshold_mV
|
|
188
178
|
else:
|
|
189
179
|
values["V_disable_output_threshold_mV"] = V_pre_output_mV
|
|
190
180
|
|
|
191
181
|
else:
|
|
192
182
|
values["dV_enable_output_mV"] = dV_output_en_thrs_mV
|
|
193
|
-
values[
|
|
194
|
-
|
|
195
|
-
] = self.V_intermediate_enable_threshold_mV
|
|
196
|
-
values[
|
|
197
|
-
"V_disable_output_threshold_mV"
|
|
198
|
-
] = self.V_intermediate_disable_threshold_mV
|
|
183
|
+
values["V_enable_output_threshold_mV"] = self.V_intermediate_enable_threshold_mV
|
|
184
|
+
values["V_disable_output_threshold_mV"] = self.V_intermediate_disable_threshold_mV
|
|
199
185
|
return values
|
|
200
186
|
|
|
201
187
|
def calc_converter_mode(self, *, log_intermediate_node: bool) -> int:
|
|
@@ -286,9 +272,7 @@ class ConverterPRUConfig(ShpModel):
|
|
|
286
272
|
states = data.calc_internal_states()
|
|
287
273
|
return cls(
|
|
288
274
|
# General
|
|
289
|
-
converter_mode=data.calc_converter_mode(
|
|
290
|
-
log_intermediate_node=log_intermediate_node
|
|
291
|
-
),
|
|
275
|
+
converter_mode=data.calc_converter_mode(log_intermediate_node=log_intermediate_node),
|
|
292
276
|
interval_startup_delay_drain_n=round(
|
|
293
277
|
data.interval_startup_delay_drain_ms * samplerate_sps_default * 1e-3
|
|
294
278
|
),
|
|
@@ -299,26 +283,16 @@ class ConverterPRUConfig(ShpModel):
|
|
|
299
283
|
Constant_us_per_nF_n28=data.calc_cap_constant_us_per_nF_n28(),
|
|
300
284
|
V_intermediate_init_uV=round(data.V_intermediate_init_mV * 1e3),
|
|
301
285
|
I_intermediate_leak_nA=round(data.I_intermediate_leak_nA),
|
|
302
|
-
V_enable_output_threshold_uV=round(
|
|
303
|
-
|
|
304
|
-
),
|
|
305
|
-
V_disable_output_threshold_uV=round(
|
|
306
|
-
states["V_disable_output_threshold_mV"] * 1e3
|
|
307
|
-
),
|
|
286
|
+
V_enable_output_threshold_uV=round(states["V_enable_output_threshold_mV"] * 1e3),
|
|
287
|
+
V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
|
|
308
288
|
dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
|
|
309
289
|
interval_check_thresholds_n=round(
|
|
310
290
|
data.interval_check_thresholds_ms * samplerate_sps_default * 1e-3
|
|
311
291
|
),
|
|
312
|
-
V_pwr_good_enable_threshold_uV=round(
|
|
313
|
-
|
|
314
|
-
),
|
|
315
|
-
V_pwr_good_disable_threshold_uV=round(
|
|
316
|
-
data.V_pwr_good_disable_threshold_mV * 1e3
|
|
317
|
-
),
|
|
292
|
+
V_pwr_good_enable_threshold_uV=round(data.V_pwr_good_enable_threshold_mV * 1e3),
|
|
293
|
+
V_pwr_good_disable_threshold_uV=round(data.V_pwr_good_disable_threshold_mV * 1e3),
|
|
318
294
|
immediate_pwr_good_signal=data.immediate_pwr_good_signal,
|
|
319
|
-
V_output_log_gpio_threshold_uV=round(
|
|
320
|
-
data.V_output_log_gpio_threshold_mV * 1e3
|
|
321
|
-
),
|
|
295
|
+
V_output_log_gpio_threshold_uV=round(data.V_output_log_gpio_threshold_mV * 1e3),
|
|
322
296
|
# Boost-Converter
|
|
323
297
|
V_input_boost_threshold_uV=round(data.V_input_boost_threshold_mV * 1e3),
|
|
324
298
|
V_intermediate_max_uV=round(data.V_intermediate_max_mV * 1e3),
|
|
@@ -330,8 +304,7 @@ class ConverterPRUConfig(ShpModel):
|
|
|
330
304
|
LUT_input_I_min_log2_nA=data.LUT_input_I_min_log2_nA,
|
|
331
305
|
LUT_output_I_min_log2_nA=data.LUT_output_I_min_log2_nA,
|
|
332
306
|
LUT_inp_efficiency_n8=[
|
|
333
|
-
[min(255, round(256 * ival)) for ival in il]
|
|
334
|
-
for il in data.LUT_input_efficiency
|
|
307
|
+
[min(255, round(256 * ival)) for ival in il] for il in data.LUT_input_efficiency
|
|
335
308
|
],
|
|
336
309
|
LUT_out_inv_efficiency_n4=[
|
|
337
310
|
min((2**14), round((2**4) / value)) if (value > 0) else int(2**14)
|
|
@@ -80,9 +80,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
80
80
|
if len(target_ids) > len(set(target_ids)):
|
|
81
81
|
raise ValueError("Target-ID used more than once in Experiment!")
|
|
82
82
|
if len(target_ids) > len(set(custom_ids)):
|
|
83
|
-
raise ValueError(
|
|
84
|
-
"Custom Target-ID are faulty (some form of id-collisions)!"
|
|
85
|
-
)
|
|
83
|
+
raise ValueError("Custom Target-ID are faulty (some form of id-collisions)!")
|
|
86
84
|
|
|
87
85
|
@staticmethod
|
|
88
86
|
def validate_observers(configs: List[TargetConfig]) -> None:
|
|
@@ -92,8 +90,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
92
90
|
obs_ids = [testbed.get_observer(_id).id for _id in target_ids]
|
|
93
91
|
if len(target_ids) > len(set(obs_ids)):
|
|
94
92
|
raise ValueError(
|
|
95
|
-
"Observer used more than once in Experiment "
|
|
96
|
-
"-> only 1 target per observer!"
|
|
93
|
+
"Observer used more than once in Experiment -> only 1 target per observer!"
|
|
97
94
|
)
|
|
98
95
|
|
|
99
96
|
def get_target_ids(self) -> list:
|
|
@@ -104,6 +101,4 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
104
101
|
if target_id in _config.target_IDs:
|
|
105
102
|
return _config
|
|
106
103
|
# gets already caught in target_config - but keep:
|
|
107
|
-
raise ValueError(
|
|
108
|
-
f"Target-ID {target_id} was not found in Experiment '{self.name}'"
|
|
109
|
-
)
|
|
104
|
+
raise ValueError(f"Target-ID {target_id} was not found in Experiment '{self.name}'")
|
|
@@ -43,9 +43,7 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
43
43
|
|
|
44
44
|
discard_all = self.discard_current and self.discard_voltage
|
|
45
45
|
if not self.calculate_power and discard_all:
|
|
46
|
-
raise ValueError(
|
|
47
|
-
"Error in config -> tracing enabled, but output gets discarded"
|
|
48
|
-
)
|
|
46
|
+
raise ValueError("Error in config -> tracing enabled, but output gets discarded")
|
|
49
47
|
if self.calculate_power:
|
|
50
48
|
raise ValueError("postprocessing not implemented ATM")
|
|
51
49
|
return self
|
|
@@ -59,9 +57,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
59
57
|
# initial recording
|
|
60
58
|
mask: Annotated[int, Field(ge=0, lt=2**10)] = 0b11_1111_1111 # all
|
|
61
59
|
# ⤷ TODO: custom mask not implemented in PRU, ATM
|
|
62
|
-
gpios: Optional[
|
|
63
|
-
Annotated[List[GPIO], Field(min_length=1, max_length=10)]
|
|
64
|
-
] = None # = all
|
|
60
|
+
gpios: Optional[Annotated[List[GPIO], Field(min_length=1, max_length=10)]] = None # = all
|
|
65
61
|
# ⤷ TODO: list of GPIO to build mask, one of both should be internal / computed field
|
|
66
62
|
|
|
67
63
|
# time
|
|
@@ -108,9 +104,7 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
|
|
|
108
104
|
@model_validator(mode="after")
|
|
109
105
|
def post_validation(self) -> Self:
|
|
110
106
|
if not self.gpio.user_controllable():
|
|
111
|
-
raise ValueError(
|
|
112
|
-
f"GPIO '{self.gpio.name}' in actuation-event not controllable by user"
|
|
113
|
-
)
|
|
107
|
+
raise ValueError(f"GPIO '{self.gpio.name}' in actuation-event not controllable by user")
|
|
114
108
|
return self
|
|
115
109
|
|
|
116
110
|
def get_events(self) -> np.ndarray:
|
|
@@ -138,6 +132,7 @@ class SystemLogging(ShpModel, title="Config for System-Logging"):
|
|
|
138
132
|
ptp: bool = True
|
|
139
133
|
shepherd: bool = True
|
|
140
134
|
# TODO: rename to kernel, timesync, sheep
|
|
135
|
+
# TODO: add utilization as option
|
|
141
136
|
|
|
142
137
|
|
|
143
138
|
# TODO: some more interaction would be good
|
|
@@ -22,18 +22,14 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
22
22
|
"""Configuration for Target Nodes (DuT)"""
|
|
23
23
|
|
|
24
24
|
target_IDs: Annotated[List[IdInt], Field(min_length=1, max_length=128)]
|
|
25
|
-
custom_IDs: Optional[
|
|
26
|
-
Annotated[List[IdInt16], Field(min_length=1, max_length=128)]
|
|
27
|
-
] = None
|
|
25
|
+
custom_IDs: Optional[Annotated[List[IdInt16], Field(min_length=1, max_length=128)]] = None
|
|
28
26
|
# ⤷ will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware
|
|
29
27
|
# if no custom ID is provided, the original ID of target is used
|
|
30
28
|
|
|
31
29
|
energy_env: EnergyEnvironment # alias: input
|
|
32
30
|
virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
|
|
33
31
|
target_delays: Optional[
|
|
34
|
-
Annotated[
|
|
35
|
-
List[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)
|
|
36
|
-
]
|
|
32
|
+
Annotated[List[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
|
|
37
33
|
] = None
|
|
38
34
|
# ⤷ individual starting times -> allows to use the same environment
|
|
39
35
|
# TODO: delays not used ATM
|
|
@@ -48,9 +44,7 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
48
44
|
@model_validator(mode="after")
|
|
49
45
|
def post_validation(self) -> Self:
|
|
50
46
|
if not self.energy_env.valid:
|
|
51
|
-
raise ValueError(
|
|
52
|
-
f"EnergyEnv '{self.energy_env.name}' for target must be valid"
|
|
53
|
-
)
|
|
47
|
+
raise ValueError(f"EnergyEnv '{self.energy_env.name}' for target must be valid")
|
|
54
48
|
for _id in self.target_IDs:
|
|
55
49
|
target = Target(id=_id)
|
|
56
50
|
for mcu_num in [1, 2]:
|
|
@@ -78,8 +72,7 @@ class TargetConfig(ShpModel, title="Target Config"):
|
|
|
78
72
|
t_ids = self.target_IDs
|
|
79
73
|
if c_ids is not None and (len(set(c_ids)) < len(set(t_ids))):
|
|
80
74
|
raise ValueError(
|
|
81
|
-
f"Provided custom IDs {c_ids} not enough "
|
|
82
|
-
f"to cover target range {t_ids}"
|
|
75
|
+
f"Provided custom IDs {c_ids} not enough to cover target range {t_ids}"
|
|
83
76
|
)
|
|
84
77
|
# TODO: if custom ids present, firmware must be ELF
|
|
85
78
|
return self
|
|
@@ -32,9 +32,7 @@ __all__ = [
|
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def prepare_task(
|
|
36
|
-
config: Union[ShpModel, Path, str], observer: Optional[str] = None
|
|
37
|
-
) -> Wrapper:
|
|
35
|
+
def prepare_task(config: Union[ShpModel, Path, str], observer: Optional[str] = None) -> Wrapper:
|
|
38
36
|
"""- Opens file (from Path or str of Path)
|
|
39
37
|
- wraps task-model
|
|
40
38
|
- and if it's an TestbedTasks it will extract the correct ObserverTask
|
|
@@ -88,9 +86,7 @@ def extract_tasks(shp_wrap: Wrapper, *, no_task_sets: bool = True) -> List[ShpMo
|
|
|
88
86
|
content = [ProgrammingTask(**shp_wrap.parameters)]
|
|
89
87
|
elif shp_wrap.datatype == TestbedTasks.__name__:
|
|
90
88
|
if no_task_sets:
|
|
91
|
-
raise ValueError(
|
|
92
|
-
"Model in Wrapper was TestbedTasks -> Task-Sets not allowed!"
|
|
93
|
-
)
|
|
89
|
+
raise ValueError("Model in Wrapper was TestbedTasks -> Task-Sets not allowed!")
|
|
94
90
|
content = [TestbedTasks(**shp_wrap.parameters)]
|
|
95
91
|
else:
|
|
96
92
|
raise ValueError("Extractor had unknown task: %s", shp_wrap.datatype)
|
|
@@ -29,8 +29,9 @@ class Compression(str, Enum):
|
|
|
29
29
|
lzf = "lzf" # not native hdf5
|
|
30
30
|
gzip1 = 1 # higher compr & load
|
|
31
31
|
gzip = 1
|
|
32
|
-
default =
|
|
32
|
+
default = "lzf"
|
|
33
33
|
null = None
|
|
34
|
+
# NOTE: changed to lzf as BBB needs every straw it can get
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
compressions_allowed: list = [None, "lzf", 1]
|
|
@@ -52,8 +53,7 @@ class EmulationTask(ShpModel):
|
|
|
52
53
|
force_overwrite: bool = False
|
|
53
54
|
# ⤷ Overwrite existing file
|
|
54
55
|
output_compression: Optional[Compression] = Compression.default
|
|
55
|
-
# ⤷ should be 1 (level 1
|
|
56
|
-
|
|
56
|
+
# ⤷ should be lzf, 1 (gzip level 1) or None (order of recommendation)
|
|
57
57
|
time_start: Optional[datetime] = None
|
|
58
58
|
# timestamp or unix epoch time, None = ASAP
|
|
59
59
|
duration: Optional[timedelta] = None
|
|
@@ -98,9 +98,7 @@ class EmulationTask(ShpModel):
|
|
|
98
98
|
# convert & add local timezone-data
|
|
99
99
|
has_time = values.get("time_start") is not None
|
|
100
100
|
if has_time and isinstance(values["time_start"], (int, float)):
|
|
101
|
-
values["time_start"] = datetime.fromtimestamp(
|
|
102
|
-
values["time_start"], tz=local_tz()
|
|
103
|
-
)
|
|
101
|
+
values["time_start"] = datetime.fromtimestamp(values["time_start"], tz=local_tz())
|
|
104
102
|
if has_time and isinstance(values["time_start"], str):
|
|
105
103
|
values["time_start"] = datetime.fromisoformat(values["time_start"])
|
|
106
104
|
if has_time and values["time_start"].tzinfo is None:
|
|
@@ -123,18 +121,14 @@ class EmulationTask(ShpModel):
|
|
|
123
121
|
"main",
|
|
124
122
|
"buffer",
|
|
125
123
|
}:
|
|
126
|
-
raise ValueError(
|
|
127
|
-
"Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'."
|
|
128
|
-
)
|
|
124
|
+
raise ValueError("Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'.")
|
|
129
125
|
if self.gpio_actuation is not None:
|
|
130
126
|
raise ValueError("GPIO Actuation not yet implemented!")
|
|
131
127
|
return self
|
|
132
128
|
|
|
133
129
|
@classmethod
|
|
134
130
|
@validate_call
|
|
135
|
-
def from_xp(
|
|
136
|
-
cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path
|
|
137
|
-
) -> Self:
|
|
131
|
+
def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path) -> Self:
|
|
138
132
|
obs = tb.get_observer(tgt_id)
|
|
139
133
|
tgt_cfg = xp.get_target_config(tgt_id)
|
|
140
134
|
|
|
@@ -144,8 +138,7 @@ class EmulationTask(ShpModel):
|
|
|
144
138
|
time_start=copy.copy(xp.time_start),
|
|
145
139
|
duration=xp.duration,
|
|
146
140
|
abort_on_error=xp.abort_on_error,
|
|
147
|
-
enable_io=(tgt_cfg.gpio_tracing is not None)
|
|
148
|
-
or (tgt_cfg.gpio_actuation is not None),
|
|
141
|
+
enable_io=(tgt_cfg.gpio_tracing is not None) or (tgt_cfg.gpio_actuation is not None),
|
|
149
142
|
io_port=obs.get_target_port(tgt_id),
|
|
150
143
|
pwr_port=obs.get_target_port(tgt_id),
|
|
151
144
|
virtual_source=tgt_cfg.virtual_source,
|
|
@@ -40,9 +40,7 @@ class FirmwareModTask(ShpModel):
|
|
|
40
40
|
FirmwareDType.base64_hex,
|
|
41
41
|
FirmwareDType.path_hex,
|
|
42
42
|
}:
|
|
43
|
-
logger.warning(
|
|
44
|
-
"Firmware is scheduled to get custom-ID but is not in elf-format"
|
|
45
|
-
)
|
|
43
|
+
logger.warning("Firmware is scheduled to get custom-ID but is not in elf-format")
|
|
46
44
|
return self
|
|
47
45
|
|
|
48
46
|
@classmethod
|
|
@@ -60,9 +60,7 @@ class HarvestTask(ShpModel):
|
|
|
60
60
|
# convert & add local timezone-data, TODO: used twice, refactor
|
|
61
61
|
has_time = values.get("time_start") is not None
|
|
62
62
|
if has_time and isinstance(values["time_start"], (int, float)):
|
|
63
|
-
values["time_start"] = datetime.fromtimestamp(
|
|
64
|
-
values["time_start"], tz=local_tz()
|
|
65
|
-
)
|
|
63
|
+
values["time_start"] = datetime.fromtimestamp(values["time_start"], tz=local_tz())
|
|
66
64
|
if has_time and isinstance(values["time_start"], str):
|
|
67
65
|
values["time_start"] = datetime.fromisoformat(values["time_start"])
|
|
68
66
|
if has_time and values["time_start"].tzinfo is None:
|
|
@@ -19,9 +19,7 @@ from .target import Target
|
|
|
19
19
|
|
|
20
20
|
MACStr = Annotated[
|
|
21
21
|
str,
|
|
22
|
-
StringConstraints(
|
|
23
|
-
max_length=17, pattern=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
|
|
24
|
-
),
|
|
22
|
+
StringConstraints(max_length=17, pattern=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"),
|
|
25
23
|
]
|
|
26
24
|
|
|
27
25
|
|
|
@@ -65,23 +63,13 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
65
63
|
has_cape = self.cape is not None
|
|
66
64
|
has_target = (self.target_a is not None) or (self.target_b is not None)
|
|
67
65
|
if not has_cape and has_target:
|
|
68
|
-
raise ValueError(
|
|
69
|
-
f"Observer '{self.name}' is faulty " f"-> has targets but no cape"
|
|
70
|
-
)
|
|
66
|
+
raise ValueError(f"Observer '{self.name}' is faulty -> has targets but no cape")
|
|
71
67
|
return self
|
|
72
68
|
|
|
73
69
|
def has_target(self, target_id: int) -> bool:
|
|
74
|
-
if
|
|
75
|
-
self.target_a is not None
|
|
76
|
-
and target_id == self.target_a.id
|
|
77
|
-
and self.target_a.active
|
|
78
|
-
):
|
|
70
|
+
if self.target_a is not None and target_id == self.target_a.id and self.target_a.active:
|
|
79
71
|
return True
|
|
80
|
-
if
|
|
81
|
-
self.target_b is not None
|
|
82
|
-
and target_id == self.target_b.id
|
|
83
|
-
and self.target_b.active
|
|
84
|
-
):
|
|
72
|
+
if self.target_b is not None and target_id == self.target_b.id and self.target_b.active:
|
|
85
73
|
return True
|
|
86
74
|
return False
|
|
87
75
|
|
|
@@ -91,9 +79,7 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
91
79
|
return TargetPort.A
|
|
92
80
|
if self.target_b is not None and target_id == self.target_b.id:
|
|
93
81
|
return TargetPort.B
|
|
94
|
-
raise ValueError(
|
|
95
|
-
f"Target-ID {target_id} was not found in Observer '{self.name}'"
|
|
96
|
-
)
|
|
82
|
+
raise ValueError(f"Target-ID {target_id} was not found in Observer '{self.name}'")
|
|
97
83
|
|
|
98
84
|
def get_target(self, target_id: int) -> Target:
|
|
99
85
|
if self.has_target(target_id):
|
|
@@ -101,6 +87,4 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
101
87
|
return self.target_a
|
|
102
88
|
if self.target_b is not None and target_id == self.target_b.id:
|
|
103
89
|
return self.target_b
|
|
104
|
-
raise ValueError(
|
|
105
|
-
f"Target-ID {target_id} was not found in Observer '{self.name}'"
|
|
106
|
-
)
|
|
90
|
+
raise ValueError(f"Target-ID {target_id} was not found in Observer '{self.name}'")
|
|
@@ -87,6 +87,4 @@ class Testbed(ShpModel):
|
|
|
87
87
|
continue
|
|
88
88
|
if _observer.has_target(target_id):
|
|
89
89
|
return _observer
|
|
90
|
-
raise ValueError(
|
|
91
|
-
f"Target-ID {target_id} was not found in Testbed '{self.name}'"
|
|
92
|
-
)
|
|
90
|
+
raise ValueError(f"Target-ID {target_id} was not found in Testbed '{self.name}'")
|
|
@@ -56,9 +56,7 @@ class Uart:
|
|
|
56
56
|
(some detectors still missing)
|
|
57
57
|
"""
|
|
58
58
|
if isinstance(content, Path):
|
|
59
|
-
self.events_sig: np.ndarray = np.loadtxt(
|
|
60
|
-
content.as_posix(), delimiter=",", skiprows=1
|
|
61
|
-
)
|
|
59
|
+
self.events_sig: np.ndarray = np.loadtxt(content.as_posix(), delimiter=",", skiprows=1)
|
|
62
60
|
# TODO: if float fails load as str -
|
|
63
61
|
# cast first col as np.datetime64 with ns-resolution, convert to delta
|
|
64
62
|
else:
|
|
@@ -66,9 +64,7 @@ class Uart:
|
|
|
66
64
|
|
|
67
65
|
# verify table
|
|
68
66
|
if self.events_sig.shape[1] != 2:
|
|
69
|
-
raise TypeError(
|
|
70
|
-
"Input file should have 2 rows -> (comma-separated) timestamp & value"
|
|
71
|
-
)
|
|
67
|
+
raise TypeError("Input file should have 2 rows -> (comma-separated) timestamp & value")
|
|
72
68
|
if self.events_sig.shape[0] < 8:
|
|
73
69
|
raise TypeError("Input file is too short (< state-changes)")
|
|
74
70
|
# verify timestamps
|
|
@@ -80,23 +76,17 @@ class Uart:
|
|
|
80
76
|
self._convert_analog2digital()
|
|
81
77
|
self._filter_redundant_states()
|
|
82
78
|
|
|
83
|
-
self.baud_rate: int = (
|
|
84
|
-
baud_rate if baud_rate is not None else self.detect_baud_rate()
|
|
85
|
-
)
|
|
79
|
+
self.baud_rate: int = baud_rate if baud_rate is not None else self.detect_baud_rate()
|
|
86
80
|
self.dur_tick: float = 1.0 / self.baud_rate
|
|
87
81
|
|
|
88
82
|
self._add_duration()
|
|
89
83
|
|
|
90
|
-
self.inversion: bool = (
|
|
91
|
-
inversion if inversion is not None else self.detect_inversion()
|
|
92
|
-
)
|
|
84
|
+
self.inversion: bool = inversion if inversion is not None else self.detect_inversion()
|
|
93
85
|
self.half_stop: bool = self.detect_half_stop() # not needed ATM
|
|
94
86
|
|
|
95
87
|
# TODO: add detectors
|
|
96
88
|
self.parity: Parity = parity if parity is not None else Parity.no
|
|
97
|
-
self.bit_order: BitOrder =
|
|
98
|
-
bit_order if bit_order is not None else BitOrder.lsb_first
|
|
99
|
-
)
|
|
89
|
+
self.bit_order: BitOrder = bit_order if bit_order is not None else BitOrder.lsb_first
|
|
100
90
|
self.frame_length: int = frame_length if frame_length is not None else 8
|
|
101
91
|
|
|
102
92
|
if not (0 < self.frame_length <= 64):
|
|
@@ -146,14 +136,10 @@ class Uart:
|
|
|
146
136
|
logger.warning("Tried to add state-duration, but it seems already present")
|
|
147
137
|
return
|
|
148
138
|
if not hasattr(self, "dur_tick"):
|
|
149
|
-
raise ValueError(
|
|
150
|
-
"Make sure that baud-rate was calculated before running add_dur()"
|
|
151
|
-
)
|
|
139
|
+
raise ValueError("Make sure that baud-rate was calculated before running add_dur()")
|
|
152
140
|
dur_steps = self.events_sig[1:, 0] - self.events_sig[:-1, 0]
|
|
153
141
|
dur_steps = np.reshape(dur_steps, (dur_steps.size, 1))
|
|
154
|
-
self.events_sig = np.append(
|
|
155
|
-
self.events_sig[:-1, :], dur_steps / self.dur_tick, axis=1
|
|
156
|
-
)
|
|
142
|
+
self.events_sig = np.append(self.events_sig[:-1, :], dur_steps / self.dur_tick, axis=1)
|
|
157
143
|
|
|
158
144
|
def detect_inversion(self) -> bool:
|
|
159
145
|
"""Analyze bit-state during long pauses (unchanged states)
|
|
@@ -182,16 +168,12 @@ class Uart:
|
|
|
182
168
|
def detect_half_stop(self) -> bool:
|
|
183
169
|
"""Looks into the spacing between time-steps"""
|
|
184
170
|
events = self.events_sig[:1000, :] # speedup for large datasets
|
|
185
|
-
return (
|
|
186
|
-
np.sum((events > 1.333 * self.dur_tick) & (events < 1.667 * self.dur_tick))
|
|
187
|
-
> 0
|
|
188
|
-
)
|
|
171
|
+
return np.sum((events > 1.333 * self.dur_tick) & (events < 1.667 * self.dur_tick)) > 0
|
|
189
172
|
|
|
190
173
|
def detect_dataframe_length(self) -> int:
|
|
191
174
|
"""Look after longest pauses
|
|
192
175
|
- accumulate steps until a state with uneven step-size is found
|
|
193
176
|
"""
|
|
194
|
-
pass
|
|
195
177
|
|
|
196
178
|
def get_symbols(self, *, force_redo: bool = False) -> np.ndarray:
|
|
197
179
|
"""Ways to detect EOF:
|
|
@@ -25,9 +25,7 @@ except ImportError:
|
|
|
25
25
|
"cffi",
|
|
26
26
|
]
|
|
27
27
|
# only update when module is not avail
|
|
28
|
-
MOCK_MODULES = [
|
|
29
|
-
mod_name for mod_name in MOCK_MODULES if find_spec(mod_name) is None
|
|
30
|
-
]
|
|
28
|
+
MOCK_MODULES = [mod_name for mod_name in MOCK_MODULES if find_spec(mod_name) is None]
|
|
31
29
|
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
|
32
30
|
|
|
33
31
|
from .converter import base64_to_file
|
|
@@ -22,9 +22,7 @@ def firmware_to_hex(file_path: Path) -> Path:
|
|
|
22
22
|
return elf_to_hex(file_path)
|
|
23
23
|
if is_hex(file_path):
|
|
24
24
|
return file_path
|
|
25
|
-
raise ValueError(
|
|
26
|
-
"FW2Hex: unknown file '%s', it should be ELF or HEX", file_path.name
|
|
27
|
-
)
|
|
25
|
+
raise ValueError("FW2Hex: unknown file '%s', it should be ELF or HEX", file_path.name)
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
@validate_call
|
|
@@ -72,9 +70,7 @@ def base64_to_hash(content: str) -> str:
|
|
|
72
70
|
|
|
73
71
|
|
|
74
72
|
@validate_call
|
|
75
|
-
def extract_firmware(
|
|
76
|
-
data: Union[str, Path], data_type: FirmwareDType, file_path: Path
|
|
77
|
-
) -> Path:
|
|
73
|
+
def extract_firmware(data: Union[str, Path], data_type: FirmwareDType, file_path: Path) -> Path:
|
|
78
74
|
"""- base64-string will be transformed into file
|
|
79
75
|
- if data is a path the file will be copied to the destination
|
|
80
76
|
"""
|
|
@@ -90,14 +86,10 @@ def extract_firmware(
|
|
|
90
86
|
elif data_type == FirmwareDType.path_hex:
|
|
91
87
|
file = file_path.with_suffix(".hex")
|
|
92
88
|
else:
|
|
93
|
-
raise ValueError(
|
|
94
|
-
"FW-Extraction failed due to unknown datatype '%s'", data_type
|
|
95
|
-
)
|
|
89
|
+
raise ValueError("FW-Extraction failed due to unknown datatype '%s'", data_type)
|
|
96
90
|
if not file.parent.exists():
|
|
97
91
|
file.parent.mkdir(parents=True)
|
|
98
92
|
shutil.copy(data, file)
|
|
99
93
|
else:
|
|
100
|
-
raise ValueError(
|
|
101
|
-
"FW-Extraction failed due to unknown data-type '%s'", type(data)
|
|
102
|
-
)
|
|
94
|
+
raise ValueError("FW-Extraction failed due to unknown data-type '%s'", type(data))
|
|
103
95
|
return file
|
|
@@ -46,9 +46,7 @@ def find_symbol(file_elf: Path, symbol: str) -> bool:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@validate_call
|
|
49
|
-
def read_symbol(
|
|
50
|
-
file_elf: Path, symbol: str, length: int = uid_len_default
|
|
51
|
-
) -> Optional[int]:
|
|
49
|
+
def read_symbol(file_elf: Path, symbol: str, length: int = uid_len_default) -> Optional[int]:
|
|
52
50
|
"""Interpreted as int"""
|
|
53
51
|
if not find_symbol(file_elf, symbol):
|
|
54
52
|
return None
|
|
@@ -99,9 +97,7 @@ def modify_symbol_value(
|
|
|
99
97
|
value_raw = elf.read(address=addr, count=uid_len_default)[-uid_len_default:]
|
|
100
98
|
# ⤷ cutting needed -> msp produces 4b instead of 2
|
|
101
99
|
value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
|
|
102
|
-
value_raw = value.to_bytes(
|
|
103
|
-
length=uid_len_default, byteorder=elf.endian, signed=False
|
|
104
|
-
)
|
|
100
|
+
value_raw = value.to_bytes(length=uid_len_default, byteorder=elf.endian, signed=False)
|
|
105
101
|
try:
|
|
106
102
|
elf.write(address=addr, data=value_raw)
|
|
107
103
|
except AttributeError:
|
|
@@ -110,9 +106,7 @@ def modify_symbol_value(
|
|
|
110
106
|
if overwrite:
|
|
111
107
|
file_new = file_elf
|
|
112
108
|
else:
|
|
113
|
-
file_new = file_elf.with_name(
|
|
114
|
-
file_elf.stem + "_" + str(value) + file_elf.suffix
|
|
115
|
-
)
|
|
109
|
+
file_new = file_elf.with_name(file_elf.stem + "_" + str(value) + file_elf.suffix)
|
|
116
110
|
# could be simplified, but py3.8-- doesn't know .with_stem()
|
|
117
111
|
elf.save(path=file_new)
|
|
118
112
|
elf.close()
|
|
@@ -127,6 +121,4 @@ def modify_symbol_value(
|
|
|
127
121
|
|
|
128
122
|
|
|
129
123
|
def modify_uid(file_elf: Path, value: int) -> Optional[Path]:
|
|
130
|
-
return modify_symbol_value(
|
|
131
|
-
file_elf, symbol=uid_str_default, value=value, overwrite=True
|
|
132
|
-
)
|
|
124
|
+
return modify_symbol_value(file_elf, symbol=uid_str_default, value=value, overwrite=True)
|
|
@@ -30,15 +30,9 @@ class Inventory(PythonInventory, SystemInventory, TargetInventory):
|
|
|
30
30
|
@classmethod
|
|
31
31
|
def collect(cls) -> Self:
|
|
32
32
|
# one by one for more precise error messages
|
|
33
|
-
pid = PythonInventory.collect().model_dump(
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
sid = SystemInventory.collect().model_dump(
|
|
37
|
-
exclude_unset=True, exclude_defaults=True
|
|
38
|
-
)
|
|
39
|
-
tid = TargetInventory.collect().model_dump(
|
|
40
|
-
exclude_unset=True, exclude_defaults=True
|
|
41
|
-
)
|
|
33
|
+
pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
34
|
+
sid = SystemInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
35
|
+
tid = TargetInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
42
36
|
model = {**pid, **sid, **tid}
|
|
43
37
|
return cls(**model)
|
|
44
38
|
|