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.
Files changed (214) hide show
  1. examples/__init__.py +0 -0
  2. examples/minimal+pid.py +82 -0
  3. examples/minimal.py +32 -0
  4. examples/minimal_recipe.py +117 -0
  5. examples/pea_alarm_base.py +47 -0
  6. examples/pea_minimal.py +258 -0
  7. mtppy/__init__.py +1 -0
  8. mtppy/part1/__init__.py +4 -0
  9. mtppy/part1/empty_folder_test.py +7 -0
  10. mtppy/part3/__init__.py +4 -0
  11. mtppy/part3/data_assembly_set/__init__.py +4 -0
  12. mtppy/part3/data_assembly_set/active_elements/__init__.py +4 -0
  13. mtppy/part3/data_assembly_set/active_elements/active_element.py +22 -0
  14. mtppy/part3/data_assembly_set/active_elements/drives/__init__.py +4 -0
  15. mtppy/part3/data_assembly_set/active_elements/drives/ana_drv.py +446 -0
  16. mtppy/part3/data_assembly_set/active_elements/drives/ana_drv_test.py +567 -0
  17. mtppy/part3/data_assembly_set/active_elements/drives/bin_drv.py +365 -0
  18. mtppy/part3/data_assembly_set/active_elements/drives/bin_drv_test.py +503 -0
  19. mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv.py +469 -0
  20. mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv_test.py +418 -0
  21. mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv.py +295 -0
  22. mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv_test.py +265 -0
  23. mtppy/part3/data_assembly_set/active_elements/pid/__init__.py +4 -0
  24. mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl.py +333 -0
  25. mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl_test.py +156 -0
  26. mtppy/part3/data_assembly_set/active_elements/valves/__init__.py +4 -0
  27. mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv.py +402 -0
  28. mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv_test.py +732 -0
  29. mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv.py +279 -0
  30. mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv_test.py +585 -0
  31. mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv.py +402 -0
  32. mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv_test.py +270 -0
  33. mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv.py +286 -0
  34. mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv_test.py +224 -0
  35. mtppy/part3/data_assembly_set/base_functions/__init__.py +4 -0
  36. mtppy/part3/data_assembly_set/base_functions/operation_mode.py +252 -0
  37. mtppy/part3/data_assembly_set/base_functions/operation_mode_test.py +260 -0
  38. mtppy/part3/data_assembly_set/base_functions/oslevel.py +35 -0
  39. mtppy/part3/data_assembly_set/base_functions/source_mode.py +189 -0
  40. mtppy/part3/data_assembly_set/base_functions/source_mode_test.py +112 -0
  41. mtppy/part3/data_assembly_set/base_functions/wqc.py +75 -0
  42. mtppy/part3/data_assembly_set/base_functions/wqc_test.py +86 -0
  43. mtppy/part3/data_assembly_set/data_assembly.py +65 -0
  44. mtppy/part3/data_assembly_set/diagnostic_elements/__init__.py +4 -0
  45. mtppy/part3/data_assembly_set/diagnostic_elements/diagnostic_element.py +19 -0
  46. mtppy/part3/data_assembly_set/indicator_elements/__init__.py +4 -0
  47. mtppy/part3/data_assembly_set/indicator_elements/indicator_element.py +22 -0
  48. mtppy/part3/data_assembly_set/indicator_elements/views/__init__.py +4 -0
  49. mtppy/part3/data_assembly_set/indicator_elements/views/ana_view.py +68 -0
  50. mtppy/part3/data_assembly_set/indicator_elements/views/bin_view.py +73 -0
  51. mtppy/part3/data_assembly_set/indicator_elements/views/dint_view.py +76 -0
  52. mtppy/part3/data_assembly_set/indicator_elements/views/string_view.py +55 -0
  53. mtppy/part3/data_assembly_set/operation_elements/__init__.py +4 -0
  54. mtppy/part3/data_assembly_set/operation_elements/operation_element.py +19 -0
  55. mtppy/part3/data_assembly_set/pea_elements/__init__.py +4 -0
  56. mtppy/part3/data_assembly_set/pea_elements/pea_element.py +28 -0
  57. mtppy/part3/data_assembly_set/pea_elements/pea_information_label/__init__.py +4 -0
  58. mtppy/part3/data_assembly_set/pea_elements/pea_information_label/pea_information_label.py +231 -0
  59. mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/__init__.py +4 -0
  60. mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/web_server_url_info.py +42 -0
  61. mtppy/part3/definitions/__init__.py +4 -0
  62. mtppy/part3/definitions/mtp_access.py +15 -0
  63. mtppy/part4/__init__.py +4 -0
  64. mtppy/part4/automation_services/__init__.py +4 -0
  65. mtppy/part4/automation_services/base_functions/__init__.py +4 -0
  66. mtppy/part4/automation_services/base_functions/service_operation_mode.py +368 -0
  67. mtppy/part4/automation_services/base_functions/service_operation_mode_test.py +242 -0
  68. mtppy/part4/automation_services/base_functions/service_operator_interaction.py +62 -0
  69. mtppy/part4/automation_services/base_functions/service_source_mode.py +138 -0
  70. mtppy/part4/automation_services/base_functions/service_source_mode_test.py +179 -0
  71. mtppy/part4/automation_services/helpers/__init__.py +4 -0
  72. mtppy/part4/automation_services/helpers/command_en_control.py +330 -0
  73. mtppy/part4/automation_services/helpers/command_en_control_test.py +302 -0
  74. mtppy/part4/automation_services/helpers/state_machine.py +167 -0
  75. mtppy/part4/automation_services/helpers/state_machine_test.py +294 -0
  76. mtppy/part4/automation_services/helpers/thread_control.py +39 -0
  77. mtppy/part4/automation_services/helpers/thread_control_test.py +80 -0
  78. mtppy/part4/automation_services/procedure.py +111 -0
  79. mtppy/part4/automation_services/procedure_test.py +185 -0
  80. mtppy/part4/automation_services/service.py +463 -0
  81. mtppy/part4/automation_services/service_elements/__init__.py +4 -0
  82. mtppy/part4/automation_services/service_elements/parameter_elements/__init__.py +4 -0
  83. mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param.py +108 -0
  84. mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param_test.py +59 -0
  85. mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param.py +70 -0
  86. mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param_test.py +39 -0
  87. mtppy/part4/automation_services/service_elements/parameter_elements/common_serv_param.py +111 -0
  88. mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param.py +77 -0
  89. mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param_test.py +58 -0
  90. mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element.py +175 -0
  91. mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element_test.py +246 -0
  92. mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param.py +48 -0
  93. mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param_test.py +37 -0
  94. mtppy/part4/automation_services/service_elements/procedure_health_view.py +36 -0
  95. mtppy/part4/automation_services/service_elements/service_control.py +473 -0
  96. mtppy/part4/automation_services/service_elements/service_control_test.py +150 -0
  97. mtppy/part4/automation_services/service_elements/service_element.py +20 -0
  98. mtppy/part4/automation_services/service_test.py +407 -0
  99. mtppy/part4/definitions/__init__.py +4 -0
  100. mtppy/part4/definitions/command_codes.py +58 -0
  101. mtppy/part4/definitions/command_codes_test.py +44 -0
  102. mtppy/part4/definitions/state_codes.py +65 -0
  103. mtppy/part4/definitions/state_codes_test.py +44 -0
  104. mtppy/part4/process_values/__init__.py +4 -0
  105. mtppy/part4/process_values/base_functions/__init__.py +4 -0
  106. mtppy/part4/process_values/base_functions/vqc.py +48 -0
  107. mtppy/part4/process_values/input_elements/__init__.py +4 -0
  108. mtppy/part4/process_values/input_elements/ana_process_value_in.py +160 -0
  109. mtppy/part4/process_values/input_elements/bin_process_value_in.py +83 -0
  110. mtppy/part4/process_values/input_elements/dint_process_value_in.py +159 -0
  111. mtppy/part4/process_values/input_elements/input_element.py +40 -0
  112. mtppy/part4/process_values/input_elements/string_process_value_in.py +45 -0
  113. mtppy/part5/__init__.py +4 -0
  114. mtppy/part5/communication_object.py +99 -0
  115. mtppy/part5/communication_object_test.py +59 -0
  116. mtppy/part5/definitions/__init__.py +4 -0
  117. mtppy/part5/definitions/iec61131_data_types.py +31 -0
  118. mtppy/part5/definitions/iec61131_data_types_test.py +82 -0
  119. mtppy/part5/definitions/opcua_data_types.py +36 -0
  120. mtppy/part5/definitions/opcua_data_types_test.py +90 -0
  121. mtppy/part5/definitions/w3c_xml_data_types.py +177 -0
  122. mtppy/part5/definitions/w3c_xml_data_types_test.py +143 -0
  123. mtppy/part5/server_assembly_set/__init__.py +5 -0
  124. mtppy/part6/__init__.py +4 -0
  125. mtppy/part6/alarm_set/__init__.py +4 -0
  126. mtppy/part6/alarm_set/alarm.py +132 -0
  127. mtppy/part6/alarm_set/alarm_test.py +265 -0
  128. mtppy/pea/__init__.py +19 -0
  129. mtppy/pea/data_assembly_factory.py +124 -0
  130. mtppy/pea/data_assembly_factory_test.py +229 -0
  131. mtppy/pea/mtp_generator/__init__.py +4 -0
  132. mtppy/pea/mtp_generator/mtp_generator.py +1183 -0
  133. mtppy/pea/mtp_generator/mtp_generator_test.py +518 -0
  134. mtppy/pea/pea.py +139 -0
  135. mtppy/pea/pea_opcua_server/__init__.py +4 -0
  136. mtppy/pea/pea_opcua_server/marshalling.py +60 -0
  137. mtppy/pea/pea_opcua_server/opcua_options_provider/__init__.py +4 -0
  138. mtppy/pea/pea_opcua_server/opcua_options_provider/instances/basic_opcua_options_provider.py +50 -0
  139. mtppy/pea/pea_opcua_server/opcua_options_provider/opcua_options_provider.py +103 -0
  140. mtppy/pea/pea_opcua_server/pea_opcua_server.py +492 -0
  141. mtppy/pea/pea_opcua_server/subscription_list.py +55 -0
  142. mtppy/pea/pea_test.py +59 -0
  143. mtppy/pea/rmtp_pea.py +155 -0
  144. mtppy/pea/rmtp_pea_test.py +27 -0
  145. mtppy/pea/service_factory.py +178 -0
  146. mtppy/pea/service_factory_test.py +156 -0
  147. mtppy/pea/user_logic/__init__.py +4 -0
  148. mtppy/pea/user_logic/user_logic_pea.py +72 -0
  149. mtppy/pea/user_logic/user_logic_registry.py +106 -0
  150. mtppy/pea/user_logic/user_logic_registry_test.py +167 -0
  151. mtppy/pea/user_logic/user_logic_service.py +228 -0
  152. mtppy/pea/user_logic/user_logic_service_test.py +289 -0
  153. mtppy/utils/__init__.py +4 -0
  154. mtppy/utils/data_assembly_dict.py +61 -0
  155. mtppy/utils/data_assembly_dict_test.py +107 -0
  156. mtppy/utils/data_item.py +446 -0
  157. mtppy/utils/data_item_com_test.py +82 -0
  158. mtppy/utils/data_item_test.py +325 -0
  159. mtppy/utils/logging.py +23 -0
  160. mtppy/utils/logging_test.py +26 -0
  161. mtppy/utils/mtp_file_handling/__init__.py +4 -0
  162. mtppy/utils/mtp_file_handling/extract_manifest_from_mtp_file.py +22 -0
  163. mtppy/utils/mtp_file_handling/extract_mtp_file.py +22 -0
  164. mtppy/utils/mtp_file_handling/is_valid_mtp.py +22 -0
  165. mtppy/utils/mtp_file_handling/is_valid_path_for_manifest.py +15 -0
  166. mtppy/utils/mtp_file_handling/is_valid_path_to_manifest.py +17 -0
  167. mtppy/utils/mtp_file_handling/is_valid_path_to_mtp.py +17 -0
  168. mtppy/utils/mtp_file_handling/load_manifest_file.py +24 -0
  169. mtppy/utils/mtp_file_handling/mtp_reader/__init__.py +4 -0
  170. mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader.py +103 -0
  171. mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader_test.py +96 -0
  172. mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml.py +226 -0
  173. mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml_test.py +108 -0
  174. mtppy/utils/mtp_file_handling/mtp_writer/__init__.py +4 -0
  175. mtppy/utils/mtp_file_handling/mtp_writer/mtp_writer.py +77 -0
  176. mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_content_types.py +10 -0
  177. mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_rels.py +11 -0
  178. mtppy/utils/mtp_file_handling/mtp_writer/test_mtp_writer.py +25 -0
  179. mtppy/utils/mtp_file_handling/write_manifest_file.py +29 -0
  180. mtppy/utils/mtp_file_handling/write_manifest_file_test.py +22 -0
  181. mtppy/utils/mtp_file_handling/write_mtp_file.py +43 -0
  182. mtppy/utils/mtp_file_handling/write_mtp_file_test.py +22 -0
  183. mtppy/utils/testing.py +31 -0
  184. mtppy2_0-0.1.5.dist-info/METADATA +75 -0
  185. mtppy2_0-0.1.5.dist-info/RECORD +214 -0
  186. mtppy2_0-0.1.5.dist-info/WHEEL +5 -0
  187. mtppy2_0-0.1.5.dist-info/entry_points.txt +3 -0
  188. mtppy2_0-0.1.5.dist-info/licenses/LICENSE +21 -0
  189. mtppy2_0-0.1.5.dist-info/top_level.txt +4 -0
  190. tools/__init__.py +0 -0
  191. tools/create_bugreport.py +150 -0
  192. tools/inspect_mtp.py +110 -0
  193. utils/__init__.py +1 -0
  194. utils/file_handling/__init__.py +4 -0
  195. utils/file_handling/cleanup_folder.py +9 -0
  196. utils/file_handling/get_utc_foldername_with_ms.py +15 -0
  197. utils/file_handling/get_work_dir.py +9 -0
  198. utils/file_handling/is_valid_path_for_json.py +11 -0
  199. utils/file_handling/is_valid_path_to_json.py +17 -0
  200. utils/file_handling/load_json_file.py +18 -0
  201. utils/file_handling/read_file_from_file_archive.py +43 -0
  202. utils/file_handling/read_file_string.py +10 -0
  203. utils/file_handling/validate_json_data.py +28 -0
  204. utils/file_handling/write_json_file.py +23 -0
  205. utils/get_uuid.py +18 -0
  206. utils/get_uuid_test.py +17 -0
  207. utils/logging.py +100 -0
  208. utils/logging_test.py +67 -0
  209. utils/opcua/check_node.py +79 -0
  210. utils/resolve_unit/resolve_unit.py +11 -0
  211. utils/resolve_unit/unit_lib.py +2101 -0
  212. utils/resolve_unit/unit_lib_test.py +36 -0
  213. utils/type_conversion.py +236 -0
  214. 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,4 @@
1
+ __copyright__ = (
2
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
3
+ )
4
+ __license__ = "MIT"
@@ -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)