MTPPy2.0 0.1.5__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.
- examples/__init__.py +0 -0
- examples/minimal+pid.py +82 -0
- examples/minimal.py +32 -0
- examples/minimal_recipe.py +117 -0
- examples/pea_alarm_base.py +47 -0
- examples/pea_minimal.py +258 -0
- mtppy/__init__.py +1 -0
- mtppy/part1/__init__.py +4 -0
- mtppy/part1/empty_folder_test.py +7 -0
- mtppy/part3/__init__.py +4 -0
- mtppy/part3/data_assembly_set/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/active_element.py +22 -0
- mtppy/part3/data_assembly_set/active_elements/drives/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv.py +446 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv_test.py +567 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv.py +365 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv_test.py +503 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv.py +469 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv_test.py +418 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv.py +295 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv_test.py +265 -0
- mtppy/part3/data_assembly_set/active_elements/pid/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl.py +333 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl_test.py +156 -0
- mtppy/part3/data_assembly_set/active_elements/valves/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv_test.py +732 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv.py +279 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv_test.py +585 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv_test.py +270 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv.py +286 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv_test.py +224 -0
- mtppy/part3/data_assembly_set/base_functions/__init__.py +4 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode.py +252 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode_test.py +260 -0
- mtppy/part3/data_assembly_set/base_functions/oslevel.py +35 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode.py +189 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode_test.py +112 -0
- mtppy/part3/data_assembly_set/base_functions/wqc.py +75 -0
- mtppy/part3/data_assembly_set/base_functions/wqc_test.py +86 -0
- mtppy/part3/data_assembly_set/data_assembly.py +65 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/diagnostic_element.py +19 -0
- mtppy/part3/data_assembly_set/indicator_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/indicator_element.py +22 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/ana_view.py +68 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/bin_view.py +73 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/dint_view.py +76 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/string_view.py +55 -0
- mtppy/part3/data_assembly_set/operation_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/operation_elements/operation_element.py +19 -0
- mtppy/part3/data_assembly_set/pea_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_element.py +28 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/pea_information_label.py +231 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/web_server_url_info.py +42 -0
- mtppy/part3/definitions/__init__.py +4 -0
- mtppy/part3/definitions/mtp_access.py +15 -0
- mtppy/part4/__init__.py +4 -0
- mtppy/part4/automation_services/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode.py +368 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode_test.py +242 -0
- mtppy/part4/automation_services/base_functions/service_operator_interaction.py +62 -0
- mtppy/part4/automation_services/base_functions/service_source_mode.py +138 -0
- mtppy/part4/automation_services/base_functions/service_source_mode_test.py +179 -0
- mtppy/part4/automation_services/helpers/__init__.py +4 -0
- mtppy/part4/automation_services/helpers/command_en_control.py +330 -0
- mtppy/part4/automation_services/helpers/command_en_control_test.py +302 -0
- mtppy/part4/automation_services/helpers/state_machine.py +167 -0
- mtppy/part4/automation_services/helpers/state_machine_test.py +294 -0
- mtppy/part4/automation_services/helpers/thread_control.py +39 -0
- mtppy/part4/automation_services/helpers/thread_control_test.py +80 -0
- mtppy/part4/automation_services/procedure.py +111 -0
- mtppy/part4/automation_services/procedure_test.py +185 -0
- mtppy/part4/automation_services/service.py +463 -0
- mtppy/part4/automation_services/service_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param.py +108 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param_test.py +59 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param.py +70 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param_test.py +39 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/common_serv_param.py +111 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param.py +77 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param_test.py +58 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element.py +175 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element_test.py +246 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param.py +48 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param_test.py +37 -0
- mtppy/part4/automation_services/service_elements/procedure_health_view.py +36 -0
- mtppy/part4/automation_services/service_elements/service_control.py +473 -0
- mtppy/part4/automation_services/service_elements/service_control_test.py +150 -0
- mtppy/part4/automation_services/service_elements/service_element.py +20 -0
- mtppy/part4/automation_services/service_test.py +407 -0
- mtppy/part4/definitions/__init__.py +4 -0
- mtppy/part4/definitions/command_codes.py +58 -0
- mtppy/part4/definitions/command_codes_test.py +44 -0
- mtppy/part4/definitions/state_codes.py +65 -0
- mtppy/part4/definitions/state_codes_test.py +44 -0
- mtppy/part4/process_values/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/vqc.py +48 -0
- mtppy/part4/process_values/input_elements/__init__.py +4 -0
- mtppy/part4/process_values/input_elements/ana_process_value_in.py +160 -0
- mtppy/part4/process_values/input_elements/bin_process_value_in.py +83 -0
- mtppy/part4/process_values/input_elements/dint_process_value_in.py +159 -0
- mtppy/part4/process_values/input_elements/input_element.py +40 -0
- mtppy/part4/process_values/input_elements/string_process_value_in.py +45 -0
- mtppy/part5/__init__.py +4 -0
- mtppy/part5/communication_object.py +99 -0
- mtppy/part5/communication_object_test.py +59 -0
- mtppy/part5/definitions/__init__.py +4 -0
- mtppy/part5/definitions/iec61131_data_types.py +31 -0
- mtppy/part5/definitions/iec61131_data_types_test.py +82 -0
- mtppy/part5/definitions/opcua_data_types.py +36 -0
- mtppy/part5/definitions/opcua_data_types_test.py +90 -0
- mtppy/part5/definitions/w3c_xml_data_types.py +177 -0
- mtppy/part5/definitions/w3c_xml_data_types_test.py +143 -0
- mtppy/part5/server_assembly_set/__init__.py +5 -0
- mtppy/part6/__init__.py +4 -0
- mtppy/part6/alarm_set/__init__.py +4 -0
- mtppy/part6/alarm_set/alarm.py +132 -0
- mtppy/part6/alarm_set/alarm_test.py +265 -0
- mtppy/pea/__init__.py +19 -0
- mtppy/pea/data_assembly_factory.py +124 -0
- mtppy/pea/data_assembly_factory_test.py +229 -0
- mtppy/pea/mtp_generator/__init__.py +4 -0
- mtppy/pea/mtp_generator/mtp_generator.py +1183 -0
- mtppy/pea/mtp_generator/mtp_generator_test.py +518 -0
- mtppy/pea/pea.py +139 -0
- mtppy/pea/pea_opcua_server/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/marshalling.py +60 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/instances/basic_opcua_options_provider.py +50 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/opcua_options_provider.py +103 -0
- mtppy/pea/pea_opcua_server/pea_opcua_server.py +492 -0
- mtppy/pea/pea_opcua_server/subscription_list.py +55 -0
- mtppy/pea/pea_test.py +59 -0
- mtppy/pea/rmtp_pea.py +155 -0
- mtppy/pea/rmtp_pea_test.py +27 -0
- mtppy/pea/service_factory.py +178 -0
- mtppy/pea/service_factory_test.py +156 -0
- mtppy/pea/user_logic/__init__.py +4 -0
- mtppy/pea/user_logic/user_logic_pea.py +72 -0
- mtppy/pea/user_logic/user_logic_registry.py +106 -0
- mtppy/pea/user_logic/user_logic_registry_test.py +167 -0
- mtppy/pea/user_logic/user_logic_service.py +228 -0
- mtppy/pea/user_logic/user_logic_service_test.py +289 -0
- mtppy/utils/__init__.py +4 -0
- mtppy/utils/data_assembly_dict.py +61 -0
- mtppy/utils/data_assembly_dict_test.py +107 -0
- mtppy/utils/data_item.py +446 -0
- mtppy/utils/data_item_com_test.py +82 -0
- mtppy/utils/data_item_test.py +325 -0
- mtppy/utils/logging.py +23 -0
- mtppy/utils/logging_test.py +26 -0
- mtppy/utils/mtp_file_handling/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/extract_manifest_from_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/extract_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_mtp.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_path_for_manifest.py +15 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_manifest.py +17 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_mtp.py +17 -0
- mtppy/utils/mtp_file_handling/load_manifest_file.py +24 -0
- mtppy/utils/mtp_file_handling/mtp_reader/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader.py +103 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader_test.py +96 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml.py +226 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml_test.py +108 -0
- mtppy/utils/mtp_file_handling/mtp_writer/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_writer/mtp_writer.py +77 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_content_types.py +10 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_rels.py +11 -0
- mtppy/utils/mtp_file_handling/mtp_writer/test_mtp_writer.py +25 -0
- mtppy/utils/mtp_file_handling/write_manifest_file.py +29 -0
- mtppy/utils/mtp_file_handling/write_manifest_file_test.py +22 -0
- mtppy/utils/mtp_file_handling/write_mtp_file.py +43 -0
- mtppy/utils/mtp_file_handling/write_mtp_file_test.py +22 -0
- mtppy/utils/testing.py +31 -0
- mtppy2_0-0.1.5.dist-info/METADATA +75 -0
- mtppy2_0-0.1.5.dist-info/RECORD +214 -0
- mtppy2_0-0.1.5.dist-info/WHEEL +5 -0
- mtppy2_0-0.1.5.dist-info/entry_points.txt +3 -0
- mtppy2_0-0.1.5.dist-info/licenses/LICENSE +21 -0
- mtppy2_0-0.1.5.dist-info/top_level.txt +4 -0
- tools/__init__.py +0 -0
- tools/create_bugreport.py +150 -0
- tools/inspect_mtp.py +110 -0
- utils/__init__.py +1 -0
- utils/file_handling/__init__.py +4 -0
- utils/file_handling/cleanup_folder.py +9 -0
- utils/file_handling/get_utc_foldername_with_ms.py +15 -0
- utils/file_handling/get_work_dir.py +9 -0
- utils/file_handling/is_valid_path_for_json.py +11 -0
- utils/file_handling/is_valid_path_to_json.py +17 -0
- utils/file_handling/load_json_file.py +18 -0
- utils/file_handling/read_file_from_file_archive.py +43 -0
- utils/file_handling/read_file_string.py +10 -0
- utils/file_handling/validate_json_data.py +28 -0
- utils/file_handling/write_json_file.py +23 -0
- utils/get_uuid.py +18 -0
- utils/get_uuid_test.py +17 -0
- utils/logging.py +100 -0
- utils/logging_test.py +67 -0
- utils/opcua/check_node.py +79 -0
- utils/resolve_unit/resolve_unit.py +11 -0
- utils/resolve_unit/unit_lib.py +2101 -0
- utils/resolve_unit/unit_lib_test.py +36 -0
- utils/type_conversion.py +236 -0
- utils/type_conversion_test.py +114 -0
mtppy/pea/rmtp_pea.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""PEA implementation that loads an MTP package and user logic module."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import asyncio
|
|
10
|
+
import importlib
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
from mtppy.pea.data_assembly_factory import DataAssemblyFactory
|
|
15
|
+
from mtppy.pea.pea import PEA
|
|
16
|
+
from mtppy.pea.service_factory import ServiceConfig, ServiceFactory
|
|
17
|
+
from mtppy.part4.automation_services.service import Service
|
|
18
|
+
from mtppy.pea.user_logic.user_logic_registry import global_registry
|
|
19
|
+
from mtppy.pea.user_logic.user_logic_service import UserLogicService
|
|
20
|
+
from mtppy.utils.logging import CHOICES, logger, setLogLevel
|
|
21
|
+
from mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader import MtpReader
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RMTPPea(PEA):
|
|
25
|
+
"""PEA assembled from an MTP package and a user logic module."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
endpoint: str = "opc.tcp://localhost:4840/",
|
|
30
|
+
mtp: str = "",
|
|
31
|
+
ssd: str = "",
|
|
32
|
+
code: str = "",
|
|
33
|
+
cycle_time: float = 1.0,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(endpoint)
|
|
36
|
+
self._cycle_time = cycle_time
|
|
37
|
+
if mtp:
|
|
38
|
+
self.load_requirements(mtp, ssd, code)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _import_user_code(module_name: str) -> ModuleType:
|
|
42
|
+
"""Import the user logic module by its Python module name."""
|
|
43
|
+
if not module_name:
|
|
44
|
+
raise ValueError("user logic module name must not be empty")
|
|
45
|
+
return importlib.import_module(module_name)
|
|
46
|
+
|
|
47
|
+
def load_requirements(self, mtp: str, ssd: str = "", code: str = "") -> None:
|
|
48
|
+
"""Load user logic, data assemblies, and services from an MTP package."""
|
|
49
|
+
del ssd
|
|
50
|
+
module = self._import_user_code(code)
|
|
51
|
+
logger.app(f"successfully loaded module {module.__name__}")
|
|
52
|
+
|
|
53
|
+
reader = MtpReader(mtp)
|
|
54
|
+
data_assemblies = DataAssemblyFactory.create_data_assemblies(
|
|
55
|
+
reader.data_assemblies
|
|
56
|
+
)
|
|
57
|
+
self.add_data_assembly(data_assemblies)
|
|
58
|
+
|
|
59
|
+
services = ServiceFactory.create_services(
|
|
60
|
+
cast(dict[str, ServiceConfig], reader.services), data_assemblies
|
|
61
|
+
)
|
|
62
|
+
for service_group in services.values():
|
|
63
|
+
service = service_group.get(ServiceFactory.RBSUB_SERVICE)
|
|
64
|
+
if isinstance(service, UserLogicService):
|
|
65
|
+
service.set_registry(global_registry)
|
|
66
|
+
service.set_cycle_time(self._cycle_time)
|
|
67
|
+
if isinstance(service, Service):
|
|
68
|
+
self.add_service(service)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
72
|
+
"""Parse command-line options for starting an RMTP-backed PEA."""
|
|
73
|
+
parser = argparse.ArgumentParser("rmtp_pea")
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"-r",
|
|
76
|
+
"--rmtp",
|
|
77
|
+
required=True,
|
|
78
|
+
help="basename of the requirements MTP files (without .mtp or .json)",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"-u",
|
|
82
|
+
"--user-logic",
|
|
83
|
+
dest="user_logic",
|
|
84
|
+
required=True,
|
|
85
|
+
help="Python module containing the user logic",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"-l",
|
|
89
|
+
"--log-level",
|
|
90
|
+
default="MTP",
|
|
91
|
+
choices=["DEBUG", "INFO", *CHOICES, "WARNING", "ERROR", "CRITICAL"],
|
|
92
|
+
help="logging level (default: MTP)",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-d",
|
|
96
|
+
"--duration",
|
|
97
|
+
type=float,
|
|
98
|
+
default=10.0,
|
|
99
|
+
help="seconds to keep the PEA running when not using --daemon",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"-c",
|
|
103
|
+
"--cycle-time",
|
|
104
|
+
type=float,
|
|
105
|
+
default=1.0,
|
|
106
|
+
help="cycle time in seconds for the procedures execution, default=1.0",
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"-e",
|
|
110
|
+
"--endpoint",
|
|
111
|
+
default="opc.tcp://localhost:4840",
|
|
112
|
+
help="OPC UA endpoint",
|
|
113
|
+
)
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--daemon",
|
|
116
|
+
action="store_true",
|
|
117
|
+
help="keep the PEA running until interrupted",
|
|
118
|
+
)
|
|
119
|
+
return parser.parse_args(args)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def run(parsed_args: argparse.Namespace) -> None:
|
|
123
|
+
setLogLevel(parsed_args.log_level)
|
|
124
|
+
logger.app(f"--- Create PEA at {parsed_args.endpoint} ---")
|
|
125
|
+
|
|
126
|
+
pea = RMTPPea(
|
|
127
|
+
endpoint=parsed_args.endpoint,
|
|
128
|
+
mtp=f"{parsed_args.rmtp}.mtp",
|
|
129
|
+
ssd=f"{parsed_args.rmtp}.json",
|
|
130
|
+
code=parsed_args.user_logic,
|
|
131
|
+
cycle_time=parsed_args.cycle_time,
|
|
132
|
+
)
|
|
133
|
+
pea.start()
|
|
134
|
+
logger.app("--- PEA OPC UA server started ---")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
if parsed_args.daemon:
|
|
138
|
+
await asyncio.Event().wait()
|
|
139
|
+
else:
|
|
140
|
+
await asyncio.sleep(parsed_args.duration)
|
|
141
|
+
except (KeyboardInterrupt, asyncio.exceptions.CancelledError):
|
|
142
|
+
logger.app("--- PEA interrupted ---")
|
|
143
|
+
finally:
|
|
144
|
+
logger.app("--- Stop PEA ---")
|
|
145
|
+
pea.stop()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(args: list[str] | None = None) -> None:
|
|
149
|
+
"""Start an RMTP PEA from command-line arguments."""
|
|
150
|
+
parsed_args = parse_args(args)
|
|
151
|
+
asyncio.run(run(parsed_args))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tests for the MTP-backed PEA facade."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from mtppy.pea import RMTPPea
|
|
7
|
+
from mtppy.pea.rmtp_pea import RMTPPea as DirectRMTPPea
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_rmtp_pea_is_exported_from_pea_package() -> None:
|
|
11
|
+
assert RMTPPea is DirectRMTPPea
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_rmtp_pea_can_import_user_logic_module(monkeypatch) -> None:
|
|
15
|
+
project_root = Path(__file__).parents[3]
|
|
16
|
+
monkeypatch.syspath_prepend(str(project_root))
|
|
17
|
+
|
|
18
|
+
module = RMTPPea._import_user_code("examples.minimal")
|
|
19
|
+
|
|
20
|
+
assert module is importlib.import_module("examples.minimal")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_rmtp_pea_without_mtp_starts_empty() -> None:
|
|
24
|
+
pea = RMTPPea()
|
|
25
|
+
|
|
26
|
+
assert pea.data_assemblies == {}
|
|
27
|
+
assert pea.service_set == {}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Factory for creating services from a ServiceConfig dictionary.
|
|
2
|
+
|
|
3
|
+
This module provides a factory class for creating service instances
|
|
4
|
+
based on the configuration dictionaries of type DataAssemblyConfig and ServiceConfig.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__copyright__ = (
|
|
9
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
10
|
+
)
|
|
11
|
+
__license__ = "MIT"
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import ClassVar
|
|
15
|
+
from itertools import chain
|
|
16
|
+
|
|
17
|
+
from mtppy.utils.logging import logger
|
|
18
|
+
import utils.type_conversion as tc
|
|
19
|
+
|
|
20
|
+
from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
|
|
21
|
+
from mtppy.part4.automation_services.service import Service
|
|
22
|
+
from mtppy.part4.automation_services.procedure import Procedure
|
|
23
|
+
from mtppy.pea.user_logic.user_logic_service import UserLogicService
|
|
24
|
+
|
|
25
|
+
ServiceConfig = dict[str, str | dict[str, str | None | dict[str, str | None]]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ServiceFactory:
|
|
29
|
+
"""Factory class for creating DataAssembly instances."""
|
|
30
|
+
|
|
31
|
+
# TODO: move string constants to mtppy.part4.definitions.automation_ml.AML
|
|
32
|
+
RBSUB_ROOT: str = "MTPServiceSUCLib"
|
|
33
|
+
RBSUB_SERVICE: str = f"{RBSUB_ROOT}/Service"
|
|
34
|
+
RBSUB_PROCEDURE: str = f"{RBSUB_ROOT}/Service/Procedure"
|
|
35
|
+
RBSUB_REQUIRED_EQUIPMENT: str = f"{RBSUB_ROOT}/RequiredEquipment"
|
|
36
|
+
RBSUB_REPORT_VALUE: str = f"{RBSUB_ROOT}/ReportValue"
|
|
37
|
+
RBSUB_PROCEDURE_PARAMETER: str = f"{RBSUB_ROOT}/ServiceParameter/ProcedureParameter"
|
|
38
|
+
|
|
39
|
+
_FACTORY: ClassVar[
|
|
40
|
+
dict[
|
|
41
|
+
str,
|
|
42
|
+
Callable[
|
|
43
|
+
[dict[str, ServiceConfig], dict[str, DataAssembly]],
|
|
44
|
+
dict[str, dict[str, Service | Procedure | DataAssembly]],
|
|
45
|
+
],
|
|
46
|
+
]
|
|
47
|
+
] = {}
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def initialize_factory(cls):
|
|
51
|
+
cls._FACTORY = {
|
|
52
|
+
cls.RBSUB_SERVICE: ServiceFactory.create_service,
|
|
53
|
+
cls.RBSUB_PROCEDURE: ServiceFactory.create_procedure,
|
|
54
|
+
cls.RBSUB_PROCEDURE_PARAMETER: ServiceFactory.get_data_assembly,
|
|
55
|
+
cls.RBSUB_REPORT_VALUE: ServiceFactory.get_data_assembly,
|
|
56
|
+
cls.RBSUB_REQUIRED_EQUIPMENT: ServiceFactory.get_data_assembly,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def create_services(
|
|
61
|
+
cls,
|
|
62
|
+
service_dict: dict[str, ServiceConfig],
|
|
63
|
+
data_assemblies: dict[str, DataAssembly] = {},
|
|
64
|
+
) -> dict[str, dict[str, Service | Procedure | DataAssembly]]:
|
|
65
|
+
return cls.create_instances(service_dict, data_assemblies)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def create_instances(
|
|
69
|
+
cls,
|
|
70
|
+
service_dict: dict[str, ServiceConfig],
|
|
71
|
+
data_assemblies: dict[str, DataAssembly] = {},
|
|
72
|
+
) -> dict[str, dict[str, Service | Procedure | DataAssembly]]:
|
|
73
|
+
"""recursively create instances from a configration dictionary.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
service_dict: Dictionary mapping names to ServiceConfig dictionaries.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary mapping names to created instances.
|
|
80
|
+
"""
|
|
81
|
+
result: dict[str, dict[str, Service | Procedure | DataAssembly]] = {}
|
|
82
|
+
if isinstance(service_dict, dict):
|
|
83
|
+
for name, config in service_dict.items():
|
|
84
|
+
if "RefBaseSystemUnitPath" in config:
|
|
85
|
+
if name not in result:
|
|
86
|
+
result[name] = {}
|
|
87
|
+
kind = config["RefBaseSystemUnitPath"]
|
|
88
|
+
if isinstance(kind, str):
|
|
89
|
+
result[name][kind] = cls._FACTORY[kind](config, data_assemblies)
|
|
90
|
+
elif isinstance(kind, list):
|
|
91
|
+
for k in kind:
|
|
92
|
+
result[name][k] = cls._FACTORY[k](config, data_assemblies)
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError("unknown type RefBaseSystemUnitPath")
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def create_service(
|
|
99
|
+
config: ServiceConfig,
|
|
100
|
+
data_assemblies: dict[str, DataAssembly] = {},
|
|
101
|
+
) -> Service:
|
|
102
|
+
"""Create a single Service instance from a configuration entry.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
config: Configuration dictionary for the service.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Created service instance.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
tag_name: str = config["Name"]
|
|
112
|
+
tag_description: str = ""
|
|
113
|
+
|
|
114
|
+
service = UserLogicService(tag_name, tag_description)
|
|
115
|
+
procedures = ServiceFactory.create_instances(config, data_assemblies)
|
|
116
|
+
service.add_procedure(
|
|
117
|
+
list(chain.from_iterable(sub.values() for sub in procedures.values()))
|
|
118
|
+
)
|
|
119
|
+
return service
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def create_procedure(
|
|
123
|
+
cls,
|
|
124
|
+
config: ServiceConfig,
|
|
125
|
+
data_assemblies: dict[str, DataAssembly] = {},
|
|
126
|
+
) -> Procedure:
|
|
127
|
+
"""Create a single Procedure instance from a configuration entry.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
config: Configuration dictionary for the service.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Created service instance.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
tag_name: str = config["Name"]
|
|
137
|
+
is_self_completing: bool = tc.cast_to_bool(config["IsSelfCompleting"]["value"])
|
|
138
|
+
procedure_id: int = int(config["ProcedureID"]["value"])
|
|
139
|
+
|
|
140
|
+
procedure = Procedure(
|
|
141
|
+
procedure_id=procedure_id,
|
|
142
|
+
tag_name=tag_name,
|
|
143
|
+
is_self_completing=is_self_completing,
|
|
144
|
+
)
|
|
145
|
+
equipment = ServiceFactory.create_instances(config, data_assemblies)
|
|
146
|
+
|
|
147
|
+
dispatcher: dict[
|
|
148
|
+
str,
|
|
149
|
+
Callable[
|
|
150
|
+
[
|
|
151
|
+
list[DataAssembly | None],
|
|
152
|
+
],
|
|
153
|
+
None,
|
|
154
|
+
],
|
|
155
|
+
] = {
|
|
156
|
+
cls.RBSUB_REQUIRED_EQUIPMENT: procedure.add_required_equipment,
|
|
157
|
+
cls.RBSUB_PROCEDURE_PARAMETER: procedure.add_procedure_parameter,
|
|
158
|
+
cls.RBSUB_REPORT_VALUE: procedure.add_report_value,
|
|
159
|
+
}
|
|
160
|
+
for key in dispatcher:
|
|
161
|
+
dispatcher[key]([d[key] for d in equipment.values() if key in d])
|
|
162
|
+
|
|
163
|
+
return procedure
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def get_data_assembly(
|
|
167
|
+
config: ServiceConfig,
|
|
168
|
+
data_assemblies: dict[str, DataAssembly] = {},
|
|
169
|
+
) -> DataAssembly | None:
|
|
170
|
+
name: str = config["Name"]
|
|
171
|
+
if name in data_assemblies:
|
|
172
|
+
return data_assemblies[name]
|
|
173
|
+
else:
|
|
174
|
+
logger.warning(f"No data assembly defined for {name}")
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
ServiceFactory.initialize_factory()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the ServiceFactory class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from mtppy.pea.service_factory import ServiceFactory
|
|
8
|
+
from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
|
|
9
|
+
from mtppy.part3.data_assembly_set.indicator_elements.views.ana_view import AnaView
|
|
10
|
+
from mtppy.part4.automation_services.service import Service
|
|
11
|
+
from mtppy.part4.automation_services.procedure import Procedure
|
|
12
|
+
from mtppy.part4.automation_services.service_elements.parameter_elements.dint_serv_param import DIntServParam
|
|
13
|
+
|
|
14
|
+
MINIMAL_SERVICE_CONFIG = {
|
|
15
|
+
"rand_num_gen": {
|
|
16
|
+
"Classification": {"default": "", "value": ""},
|
|
17
|
+
"ID": "d65bb96a-f5af-457e-8fa6-63cdbb5a8a80",
|
|
18
|
+
"Name": "rand_num_gen",
|
|
19
|
+
"RefBaseSystemUnitPath": "MTPServiceSUCLib/Service",
|
|
20
|
+
"RefID": {"default": "", "value": "542a4ea1-baa2-4e34-af95-9b3c2a605ae3"},
|
|
21
|
+
"cont": {
|
|
22
|
+
"Classification": {"default": "", "value": ""},
|
|
23
|
+
"ID": "202d0321-7bbe-42f3-84c8-e871527b4656",
|
|
24
|
+
"IsSelfCompleting": {"default": "", "value": "true"},
|
|
25
|
+
"Name": "cont",
|
|
26
|
+
"ProcedureID": {"default": "", "value": "1"},
|
|
27
|
+
"RefBaseSystemUnitPath": "MTPServiceSUCLib/Service/Procedure",
|
|
28
|
+
"RefID": {"default": "", "value": "c0048311-3a6f-4339-9f29-7a87dc020e17"},
|
|
29
|
+
"generated_value": {
|
|
30
|
+
"ID": "cbc5c1e8-96da-4311-8763-6caadf85e0b9",
|
|
31
|
+
"Name": "generated_value",
|
|
32
|
+
"RefBaseSystemUnitPath": [
|
|
33
|
+
"MTPServiceSUCLib/ReportValue",
|
|
34
|
+
"MTPServiceSUCLib/RequiredEquipment",
|
|
35
|
+
],
|
|
36
|
+
"RefID": {
|
|
37
|
+
"default": "",
|
|
38
|
+
"value": "43bcd4e3-f9e5-40e5-b573-e67470ad134c",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"lower_bound": {
|
|
42
|
+
"ID": [
|
|
43
|
+
"fe89d547-851d-4be8-9c94-ee085a6dd867",
|
|
44
|
+
],
|
|
45
|
+
"Name": "lower_bound",
|
|
46
|
+
"RefBaseSystemUnitPath": [
|
|
47
|
+
"MTPServiceSUCLib/ServiceParameter/ProcedureParameter",
|
|
48
|
+
"MTPServiceSUCLib/RequiredEquipment",
|
|
49
|
+
],
|
|
50
|
+
"RefID": {
|
|
51
|
+
"default": "",
|
|
52
|
+
"value": "b3e75452-613e-4fca-be18-b11a62bcb1f7",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
"upper_bound": {
|
|
56
|
+
"ID": "455570ca-7835-4eb8-b09b-29ec59737573",
|
|
57
|
+
"Name": "upper_bound",
|
|
58
|
+
"RefBaseSystemUnitPath": [
|
|
59
|
+
"MTPServiceSUCLib/ServiceParameter/ProcedureParameter",
|
|
60
|
+
"MTPServiceSUCLib/RequiredEquipment",
|
|
61
|
+
],
|
|
62
|
+
"RefID": {
|
|
63
|
+
"default": "",
|
|
64
|
+
"value": "8df4853a-1b1a-4b86-9079-b5d4203cbe46",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
MINIMAL_DATA_ASSEMBLIES = {
|
|
72
|
+
"upper_bound": DIntServParam(tag_name="upper_bound"),
|
|
73
|
+
"lower_bound": DIntServParam(tag_name="lower_bound"),
|
|
74
|
+
"generated_value": AnaView(tag_name="generated_value"),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_factory() -> None:
|
|
79
|
+
"""Test that all methods referred to in the _FACTORY exists."""
|
|
80
|
+
assert len(ServiceFactory._FACTORY) == 5
|
|
81
|
+
for elem in ServiceFactory._FACTORY.values():
|
|
82
|
+
assert hasattr(ServiceFactory, elem.__name__) == True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_get_data_assembly() -> None:
|
|
86
|
+
"""test retrieval of equipment"""
|
|
87
|
+
config = MINIMAL_SERVICE_CONFIG["rand_num_gen"]["cont"]["upper_bound"]
|
|
88
|
+
equipment = ServiceFactory.get_data_assembly(config, MINIMAL_DATA_ASSEMBLIES)
|
|
89
|
+
assert isinstance(equipment, DataAssembly)
|
|
90
|
+
assert equipment.TagName == "upper_bound"
|
|
91
|
+
assert equipment.tag_name == "upper_bound"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_get_data_assembly_missing() -> None:
|
|
95
|
+
config = MINIMAL_SERVICE_CONFIG["rand_num_gen"]["cont"]["generated_value"]
|
|
96
|
+
equipment = ServiceFactory.get_data_assembly(config)
|
|
97
|
+
assert equipment is None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_create_procedure() -> None:
|
|
101
|
+
procedure = ServiceFactory.create_procedure(
|
|
102
|
+
MINIMAL_SERVICE_CONFIG["rand_num_gen"]["cont"],
|
|
103
|
+
MINIMAL_DATA_ASSEMBLIES,
|
|
104
|
+
)
|
|
105
|
+
assert isinstance(procedure, Procedure) == True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.parametrize(
|
|
109
|
+
("value", "expected"),
|
|
110
|
+
[("true", True), ("false", False), (True, True), (False, False)],
|
|
111
|
+
)
|
|
112
|
+
def test_create_procedure_parses_is_self_completing(
|
|
113
|
+
value: object, expected: bool
|
|
114
|
+
) -> None:
|
|
115
|
+
config = {
|
|
116
|
+
"Name": "procedure",
|
|
117
|
+
"IsSelfCompleting": {"value": value},
|
|
118
|
+
"ProcedureID": {"value": "1"},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
procedure = ServiceFactory.create_procedure(config) # type: ignore[arg-type]
|
|
122
|
+
|
|
123
|
+
assert procedure.is_self_completing is expected
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_create_procedure_rejects_invalid_boolean() -> None:
|
|
127
|
+
config = {
|
|
128
|
+
"Name": "procedure",
|
|
129
|
+
"IsSelfCompleting": {"value": "yes"},
|
|
130
|
+
"ProcedureID": {"value": "1"},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
with pytest.raises(ValueError, match="Invalid boolean value"):
|
|
134
|
+
ServiceFactory.create_procedure(config) # type: ignore[arg-type]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_create_service() -> None:
|
|
138
|
+
service = ServiceFactory.create_service(
|
|
139
|
+
MINIMAL_SERVICE_CONFIG["rand_num_gen"],
|
|
140
|
+
MINIMAL_DATA_ASSEMBLIES,
|
|
141
|
+
)
|
|
142
|
+
assert isinstance(service, Service) == True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_create_services() -> None:
|
|
146
|
+
services = ServiceFactory.create_services(
|
|
147
|
+
MINIMAL_SERVICE_CONFIG,
|
|
148
|
+
MINIMAL_DATA_ASSEMBLIES,
|
|
149
|
+
)
|
|
150
|
+
assert len(services) == 1
|
|
151
|
+
assert "rand_num_gen" in services
|
|
152
|
+
assert ServiceFactory.RBSUB_SERVICE in services["rand_num_gen"]
|
|
153
|
+
assert (
|
|
154
|
+
isinstance(services["rand_num_gen"][ServiceFactory.RBSUB_SERVICE], Service)
|
|
155
|
+
== True
|
|
156
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
__copyright__ = (
|
|
2
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
3
|
+
)
|
|
4
|
+
__license__ = "MIT"
|
|
5
|
+
|
|
6
|
+
import importlib
|
|
7
|
+
|
|
8
|
+
from mtppy.pea.data_assembly_factory import DataAssemblyFactory
|
|
9
|
+
from mtppy.pea.pea import PEA
|
|
10
|
+
from mtppy.pea.service_factory import ServiceFactory
|
|
11
|
+
from mtppy.pea.user_logic.user_logic_registry import global_registry
|
|
12
|
+
from mtppy.pea.user_logic.user_logic_service import UserLogicService
|
|
13
|
+
from mtppy.utils.logging import logger
|
|
14
|
+
from mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader import MtpReader
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UserLogicPEA(PEA):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
endpoint: str = "opc.tcp://localhost:4840/",
|
|
21
|
+
mtp: str = "",
|
|
22
|
+
ssd: str = "",
|
|
23
|
+
code: str = "",
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initialize UserLogicPEA"""
|
|
26
|
+
super().__init__(endpoint)
|
|
27
|
+
if mtp is not None and len(mtp):
|
|
28
|
+
self.load_requirements(mtp, ssd, code)
|
|
29
|
+
|
|
30
|
+
def _import_user_code(self, module_name: str):
|
|
31
|
+
"""Dynamically import the user module by name and inject the registry decorator
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
module_name: The name of the module to import.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
module: The imported module.
|
|
38
|
+
"""
|
|
39
|
+
# Import the module dynamically
|
|
40
|
+
module = importlib.import_module(module_name)
|
|
41
|
+
|
|
42
|
+
return module
|
|
43
|
+
|
|
44
|
+
def load_requirements(self, mtp: str, ssd: str, code: str):
|
|
45
|
+
"""Loads requirements mtp and user logic. ignores namespace for the moment."""
|
|
46
|
+
# load the user logic
|
|
47
|
+
module = self._import_user_code(code)
|
|
48
|
+
logger.app(f"successfully loaded module {module.__name__}")
|
|
49
|
+
|
|
50
|
+
# Log all function names from the module
|
|
51
|
+
# function_names = [name for name, obj in vars(module).items() if callable(obj)]
|
|
52
|
+
# logger.app(f"Functions in module: {', '.join(function_names)}")
|
|
53
|
+
|
|
54
|
+
logger.app(
|
|
55
|
+
f"registered {",".join(f"{v.__name__} as {k}" for k,v in global_registry._registry.items())}",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
reader = MtpReader(mtp)
|
|
59
|
+
|
|
60
|
+
# create and attach the data_assemblies
|
|
61
|
+
data_assemblies = DataAssemblyFactory.create_data_assemblies(
|
|
62
|
+
reader.data_assemblies,
|
|
63
|
+
)
|
|
64
|
+
self.add_data_assembly(data_assemblies)
|
|
65
|
+
|
|
66
|
+
# create and attach the services
|
|
67
|
+
services = ServiceFactory.create_services(reader.services, data_assemblies)
|
|
68
|
+
for k in services:
|
|
69
|
+
service = services[k][ServiceFactory.RBSUB_SERVICE]
|
|
70
|
+
if isinstance(service, UserLogicService):
|
|
71
|
+
service.set_registry(global_registry)
|
|
72
|
+
self.add_service(service)
|