From 03459ce60e5419c23049b6e455fb8ec3d26e2661 Mon Sep 17 00:00:00 2001 From: Reed Johnson Date: Thu, 19 Dec 2024 17:23:42 -0600 Subject: [PATCH 1/6] PLC Block --- inference/core/workflows/core_steps/loader.py | 4 + .../sinks/PLCethernetIP/__init__.py | 0 .../core_steps/sinks/PLCethernetIP/v1.py | 139 ++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 inference/core/workflows/core_steps/sinks/PLCethernetIP/__init__.py create mode 100644 inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py diff --git a/inference/core/workflows/core_steps/loader.py b/inference/core/workflows/core_steps/loader.py index a705d23f02..d2554400fe 100644 --- a/inference/core/workflows/core_steps/loader.py +++ b/inference/core/workflows/core_steps/loader.py @@ -370,6 +370,9 @@ from inference.core.workflows.core_steps.visualizations.triangle.v1 import ( TriangleVisualizationBlockV1, ) +from inference.core.workflows.core_steps.sinks.PLCethernetIP.v1 import ( + PLCBlockV1, +) from inference.core.workflows.execution_engine.entities.types import ( BAR_CODE_DETECTION_KIND, BOOLEAN_KIND, @@ -582,6 +585,7 @@ def load_blocks() -> List[Type[WorkflowBlock]]: EnvironmentSecretsStoreBlockV1, SlackNotificationBlockV1, TwilioSMSNotificationBlockV1, + PLCBlockV1, ] diff --git a/inference/core/workflows/core_steps/sinks/PLCethernetIP/__init__.py b/inference/core/workflows/core_steps/sinks/PLCethernetIP/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py b/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py new file mode 100644 index 0000000000..00eeaccd5c --- /dev/null +++ b/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py @@ -0,0 +1,139 @@ +from typing import Dict, List, Optional, Type, Union +from pydantic import ConfigDict, Field +from typing_extensions import Literal + +#TODO: Add to requirements. +import pylogix + +from inference.core.workflows.execution_engine.entities.base import ( + OutputDefinition, + WorkflowImageData, + VideoMetadata, +) +from inference.core.workflows.execution_engine.entities.types import ( + LIST_OF_VALUES_KIND, + STRING_KIND, + WorkflowParameterSelector, +) +from inference.core.workflows.prototypes.block import ( + WorkflowBlock, + WorkflowBlockManifest, +) + + +class PLCBlockManifest(WorkflowBlockManifest): + """Manifest class for the PLC Communication Block. + + This specifies the parameters that the block needs: + - plc_ip: The PLC IP address. + - tags_to_read: A list of tag names to read from the PLC. + - tags_to_write: A dictionary of tags and values to write to the PLC. + """ + + model_config = ConfigDict( + json_schema_extra={ + "name": "PLC Communication", + "version": "v1", + "short_description": "Block that reads/writes tags from/to a PLC using pylogix.", + "long_description": "The PLCBlock allows reading and writing of tags from a PLC. " + "This can be used to integrate model results into a factory automation workflow.", + "license": "Apache-2.0", + "block_type": "analytics", + } + ) + + type: Literal["roboflow_core/plc_communication@v1"] + + plc_ip: Union[str, WorkflowParameterSelector(kind=[STRING_KIND])] = Field( + description="IP address of the PLC", + examples=["192.168.1.10"] + ) + + tags_to_read: Union[List[str], WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND])] = Field( + default=[], + description="List of PLC tags to read", + examples=[["tag1", "tag2", "tag3"]] + ) + + tags_to_write: Union[Dict[str, Union[int, float, str]], WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND])] = Field( + default={}, + description="Dictionary of PLC tags to write and their corresponding values", + examples=[{"class_name": "car", "class_count": 5}] + ) + + @classmethod + def describe_outputs(cls) -> List[OutputDefinition]: + return [ + OutputDefinition( + name="plc_results", + kind=[LIST_OF_VALUES_KIND], + ), + ] + + @classmethod + def get_execution_engine_compatibility(cls) -> Optional[str]: + return ">=1.0.0,<2.0.0" + + +class PLCBlockV1(WorkflowBlock): + """A workflow block for PLC communication. + + This block: + - Connects to a PLC using pylogix. + - Reads specified tags from the PLC. + - Writes specified values to the PLC. + - Returns a dictionary containing read results and write confirmations. + """ + + @classmethod + def get_manifest(cls) -> Type[WorkflowBlockManifest]: + return PLCBlockManifest + + def run( + self, + plc_ip: str, + tags_to_read: List[str], + tags_to_write: Dict[str, Union[int, float, str]], + image: Optional[WorkflowImageData] = None, + metadata: Optional[VideoMetadata] = None, + ) -> dict: + """Connect to the PLC, read and write tags, and return the results. + + Args: + plc_ip (str): The PLC IP address. + tags_to_read (List[str]): Tag names to read from PLC. + tags_to_write (Dict[str, Union[int, float, str]]): Key-value pairs of tags and values to write. + image (Optional[WorkflowImageData]): Not required, included for framework compliance. + metadata (Optional[VideoMetadata]): Not required, included for framework compliance. + + Returns: + dict: A dictionary with 'plc_results' containing read and write results. + """ + read_results = {} + write_results = {} + + with pylogix.PLC() as comm: + comm.IPAddress = plc_ip + + # Read tags + for tag in tags_to_read: + read_response = comm.Read(tag) + if read_response.Status == "Success": + read_results[tag] = read_response.Value + else: + read_results[tag] = f"ReadError: {read_response.Status}" + + # Write tags + for tag, value in tags_to_write.items(): + write_response = comm.Write(tag, value) + if write_response.Status == "Success": + write_results[tag] = "WriteSuccess" + else: + write_results[tag] = f"WriteError: {write_response.Status}" + + return { + "plc_results": { + "read": read_results, + "write": write_results + } + } From 12d70948588a0c14e6e7a354007b61833ac72762 Mon Sep 17 00:00:00 2001 From: Reed Johnson Date: Fri, 20 Dec 2024 19:33:16 -0600 Subject: [PATCH 2/6] PLC block updates --- .../core_steps/sinks/PLCethernetIP/v1.py | 141 +++++++++++------- requirements/_requirements.txt | 1 + 2 files changed, 90 insertions(+), 52 deletions(-) diff --git a/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py b/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py index 00eeaccd5c..aa9b5876aa 100644 --- a/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py +++ b/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py @@ -2,7 +2,6 @@ from pydantic import ConfigDict, Field from typing_extensions import Literal -#TODO: Add to requirements. import pylogix from inference.core.workflows.execution_engine.entities.base import ( @@ -14,51 +13,78 @@ LIST_OF_VALUES_KIND, STRING_KIND, WorkflowParameterSelector, + Selector, ) from inference.core.workflows.prototypes.block import ( WorkflowBlock, WorkflowBlockManifest, ) +LONG_DESCRIPTION = """ +This **PLC Communication** block integrates a Roboflow Workflow with a PLC using Ethernet/IP communication. +It can: +- Read tags from a PLC if `mode='read'`. +- Write tags to a PLC if `mode='write'`. +- Perform both read and write in a single run if `mode='read_and_write'`. + +**Parameters depending on mode:** +- If `mode='read'` or `mode='read_and_write'`, `tags_to_read` must be provided. +- If `mode='write'` or `mode='read_and_write'`, `tags_to_write` must be provided. + +If a read or write operation fails, an error message is printed to the terminal, +and the corresponding entry in the output dictionary is set to a generic "ReadFailure" or "WriteFailure" message. +""" + class PLCBlockManifest(WorkflowBlockManifest): - """Manifest class for the PLC Communication Block. + """Manifest for a PLC communication block using Ethernet/IP. - This specifies the parameters that the block needs: - - plc_ip: The PLC IP address. - - tags_to_read: A list of tag names to read from the PLC. - - tags_to_write: A dictionary of tags and values to write to the PLC. + The block can be used in one of three modes: + - 'read': Only reads specified tags. + - 'write': Only writes specified tags. + - 'read_and_write': Performs both reading and writing in one execution. + + `tags_to_read` and `tags_to_write` are applicable depending on the mode chosen. """ model_config = ConfigDict( json_schema_extra={ - "name": "PLC Communication", + "name": "PLC EthernetIP", "version": "v1", - "short_description": "Block that reads/writes tags from/to a PLC using pylogix.", - "long_description": "The PLCBlock allows reading and writing of tags from a PLC. " - "This can be used to integrate model results into a factory automation workflow.", + "short_description": "Generic PLC read/write block using pylogix over Ethernet/IP.", + "long_description": LONG_DESCRIPTION, "license": "Apache-2.0", - "block_type": "analytics", + "block_type": "sinks", } ) - type: Literal["roboflow_core/plc_communication@v1"] + type: Literal["roboflow_core/sinks@v1"] plc_ip: Union[str, WorkflowParameterSelector(kind=[STRING_KIND])] = Field( - description="IP address of the PLC", + description="IP address of the target PLC.", examples=["192.168.1.10"] ) + mode: Literal["read", "write", "read_and_write"] = Field( + description="Mode of operation: 'read', 'write', or 'read_and_write'.", + examples=["read", "write", "read_and_write"] + ) + tags_to_read: Union[List[str], WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND])] = Field( default=[], - description="List of PLC tags to read", - examples=[["tag1", "tag2", "tag3"]] + description="List of PLC tag names to read. Applicable if mode='read' or mode='read_and_write'.", + examples=[["camera_msg", "sku_number"]] ) tags_to_write: Union[Dict[str, Union[int, float, str]], WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND])] = Field( default={}, - description="Dictionary of PLC tags to write and their corresponding values", - examples=[{"class_name": "car", "class_count": 5}] + description="Dictionary of tags and the values to write. Applicable if mode='write' or mode='read_and_write'.", + examples=[{"camera_fault": True, "defect_count": 5}] + ) + + depends_on: Selector() = Field( + description="Reference to the step output this block depends on.", + examples=["$steps.some_previous_step"] ) @classmethod @@ -76,13 +102,14 @@ def get_execution_engine_compatibility(cls) -> Optional[str]: class PLCBlockV1(WorkflowBlock): - """A workflow block for PLC communication. + """A PLC communication workflow block using Ethernet/IP and pylogix. - This block: - - Connects to a PLC using pylogix. - - Reads specified tags from the PLC. - - Writes specified values to the PLC. - - Returns a dictionary containing read results and write confirmations. + Depending on the selected mode: + - 'read': Reads specified tags. + - 'write': Writes provided values to specified tags. + - 'read_and_write': Reads and writes in one go. + + In case of failures, errors are printed to terminal and the corresponding tag entry in the output is set to "ReadFailure" or "WriteFailure". """ @classmethod @@ -92,22 +119,26 @@ def get_manifest(cls) -> Type[WorkflowBlockManifest]: def run( self, plc_ip: str, + mode: str, tags_to_read: List[str], tags_to_write: Dict[str, Union[int, float, str]], + depends_on: any, image: Optional[WorkflowImageData] = None, metadata: Optional[VideoMetadata] = None, ) -> dict: - """Connect to the PLC, read and write tags, and return the results. + """Run PLC read/write operations using pylogix over Ethernet/IP. Args: - plc_ip (str): The PLC IP address. - tags_to_read (List[str]): Tag names to read from PLC. - tags_to_write (Dict[str, Union[int, float, str]]): Key-value pairs of tags and values to write. - image (Optional[WorkflowImageData]): Not required, included for framework compliance. - metadata (Optional[VideoMetadata]): Not required, included for framework compliance. + plc_ip (str): PLC IP address. + mode (str): 'read', 'write', or 'read_and_write'. + tags_to_read (List[str]): Tags to read if applicable. + tags_to_write (Dict[str, Union[int, float, str]]): Tags to write if applicable. + depends_on (any): The step output this block depends on. + image (Optional[WorkflowImageData]): Not required for this block. + metadata (Optional[VideoMetadata]): Not required for this block. Returns: - dict: A dictionary with 'plc_results' containing read and write results. + dict: A dictionary with `plc_results` as a list containing one dictionary. That dictionary has 'read' and/or 'write' keys. """ read_results = {} write_results = {} @@ -115,25 +146,31 @@ def run( with pylogix.PLC() as comm: comm.IPAddress = plc_ip - # Read tags - for tag in tags_to_read: - read_response = comm.Read(tag) - if read_response.Status == "Success": - read_results[tag] = read_response.Value - else: - read_results[tag] = f"ReadError: {read_response.Status}" - - # Write tags - for tag, value in tags_to_write.items(): - write_response = comm.Write(tag, value) - if write_response.Status == "Success": - write_results[tag] = "WriteSuccess" - else: - write_results[tag] = f"WriteError: {write_response.Status}" - - return { - "plc_results": { - "read": read_results, - "write": write_results - } - } + # If mode involves reading + if mode in ["read", "read_and_write"]: + for tag in tags_to_read: + response = comm.Read(tag) + if response.Status == "Success": + read_results[tag] = response.Value + else: + print(f"Error reading tag '{tag}': {response.Status}") + read_results[tag] = "ReadFailure" + + # If mode involves writing + if mode in ["write", "read_and_write"]: + for tag, value in tags_to_write.items(): + response = comm.Write(tag, value) + if response.Status == "Success": + write_results[tag] = "WriteSuccess" + else: + print(f"Error writing tag '{tag}' with value '{value}': {response.Status}") + write_results[tag] = "WriteFailure" + + plc_output = {} + if read_results: + plc_output["read"] = read_results + if write_results: + plc_output["write"] = write_results + + # Return as a list of dicts for the 'plc_results' key + return {"plc_results": [plc_output]} diff --git a/requirements/_requirements.txt b/requirements/_requirements.txt index 68cbf7b791..84554117f7 100644 --- a/requirements/_requirements.txt +++ b/requirements/_requirements.txt @@ -37,3 +37,4 @@ tokenizers>=0.19.0,<=0.20.3 slack-sdk~=3.33.4 twilio~=9.3.7 httpx>=0.25.1,<0.28.0 # must be pinned as bc in 0.28.0 is causing Anthropics to fail +pylogix==1.0.5 From ee1167b491dabb3facf8b1030fda5c900df674a4 Mon Sep 17 00:00:00 2001 From: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:27:35 +0100 Subject: [PATCH 3/6] Move PLCethernetIP to enterprise --- inference/core/workflows/core_steps/loader.py | 4 ---- inference/enterprise/workflows/enterprise_blocks/loader.py | 4 ++++ .../enterprise_blocks}/sinks/PLCethernetIP/__init__.py | 0 .../workflows/enterprise_blocks}/sinks/PLCethernetIP/v1.py | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename inference/{core/workflows/core_steps => enterprise/workflows/enterprise_blocks}/sinks/PLCethernetIP/__init__.py (100%) rename inference/{core/workflows/core_steps => enterprise/workflows/enterprise_blocks}/sinks/PLCethernetIP/v1.py (100%) diff --git a/inference/core/workflows/core_steps/loader.py b/inference/core/workflows/core_steps/loader.py index d2554400fe..a705d23f02 100644 --- a/inference/core/workflows/core_steps/loader.py +++ b/inference/core/workflows/core_steps/loader.py @@ -370,9 +370,6 @@ from inference.core.workflows.core_steps.visualizations.triangle.v1 import ( TriangleVisualizationBlockV1, ) -from inference.core.workflows.core_steps.sinks.PLCethernetIP.v1 import ( - PLCBlockV1, -) from inference.core.workflows.execution_engine.entities.types import ( BAR_CODE_DETECTION_KIND, BOOLEAN_KIND, @@ -585,7 +582,6 @@ def load_blocks() -> List[Type[WorkflowBlock]]: EnvironmentSecretsStoreBlockV1, SlackNotificationBlockV1, TwilioSMSNotificationBlockV1, - PLCBlockV1, ] diff --git a/inference/enterprise/workflows/enterprise_blocks/loader.py b/inference/enterprise/workflows/enterprise_blocks/loader.py index a3895ac15c..7a8a08095e 100644 --- a/inference/enterprise/workflows/enterprise_blocks/loader.py +++ b/inference/enterprise/workflows/enterprise_blocks/loader.py @@ -4,9 +4,13 @@ from inference.enterprise.workflows.enterprise_blocks.sinks.opc_writer.v1 import ( OPCWriterSinkBlockV1, ) +from inference.enterprise.workflows.enterprise_blocks.sinks.PLCethernetIP.v1 import ( + PLCBlockV1, +) def load_enterprise_blocks() -> List[Type[WorkflowBlock]]: return [ OPCWriterSinkBlockV1, + PLCBlockV1, ] diff --git a/inference/core/workflows/core_steps/sinks/PLCethernetIP/__init__.py b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/__init__.py similarity index 100% rename from inference/core/workflows/core_steps/sinks/PLCethernetIP/__init__.py rename to inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/__init__.py diff --git a/inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py similarity index 100% rename from inference/core/workflows/core_steps/sinks/PLCethernetIP/v1.py rename to inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py From 1d8b5ba1d741b5b8a08f0f0314cdaa87cfdef66e Mon Sep 17 00:00:00 2001 From: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:30:26 +0100 Subject: [PATCH 4/6] Update license --- .../workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py index aa9b5876aa..b9bcc48b92 100644 --- a/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py +++ b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py @@ -53,7 +53,7 @@ class PLCBlockManifest(WorkflowBlockManifest): "version": "v1", "short_description": "Generic PLC read/write block using pylogix over Ethernet/IP.", "long_description": LONG_DESCRIPTION, - "license": "Apache-2.0", + "license": "Roboflow Enterprise License", "block_type": "sinks", } ) From e33093c1744d74954c7f30cb02f06622e4253d02 Mon Sep 17 00:00:00 2001 From: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:36:08 +0100 Subject: [PATCH 5/6] Formatting, logging, exceptions handling --- .../sinks/PLCethernetIP/v1.py | 57 +++++++++++++------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py index b9bcc48b92..006568a62e 100644 --- a/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py +++ b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py @@ -19,6 +19,8 @@ WorkflowBlock, WorkflowBlockManifest, ) +from inference.core.logger import logger + LONG_DESCRIPTION = """ This **PLC Communication** block integrates a Roboflow Workflow with a PLC using Ethernet/IP communication. @@ -61,30 +63,34 @@ class PLCBlockManifest(WorkflowBlockManifest): type: Literal["roboflow_core/sinks@v1"] plc_ip: Union[str, WorkflowParameterSelector(kind=[STRING_KIND])] = Field( - description="IP address of the target PLC.", - examples=["192.168.1.10"] + description="IP address of the target PLC.", examples=["192.168.1.10"] ) mode: Literal["read", "write", "read_and_write"] = Field( description="Mode of operation: 'read', 'write', or 'read_and_write'.", - examples=["read", "write", "read_and_write"] + examples=["read", "write", "read_and_write"], ) - tags_to_read: Union[List[str], WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND])] = Field( + tags_to_read: Union[ + List[str], WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND]) + ] = Field( default=[], description="List of PLC tag names to read. Applicable if mode='read' or mode='read_and_write'.", - examples=[["camera_msg", "sku_number"]] + examples=[["camera_msg", "sku_number"]], ) - tags_to_write: Union[Dict[str, Union[int, float, str]], WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND])] = Field( + tags_to_write: Union[ + Dict[str, Union[int, float, str]], + WorkflowParameterSelector(kind=[LIST_OF_VALUES_KIND]), + ] = Field( default={}, description="Dictionary of tags and the values to write. Applicable if mode='write' or mode='read_and_write'.", - examples=[{"camera_fault": True, "defect_count": 5}] + examples=[{"camera_fault": True, "defect_count": 5}], ) depends_on: Selector() = Field( description="Reference to the step output this block depends on.", - examples=["$steps.some_previous_step"] + examples=["$steps.some_previous_step"], ) @classmethod @@ -149,21 +155,36 @@ def run( # If mode involves reading if mode in ["read", "read_and_write"]: for tag in tags_to_read: - response = comm.Read(tag) - if response.Status == "Success": - read_results[tag] = response.Value - else: - print(f"Error reading tag '{tag}': {response.Status}") + try: + response = comm.Read(tag) + if response.Status == "Success": + read_results[tag] = response.Value + else: + logger.error( + f"Error reading tag '%s': %s", tag, response.Status + ) + read_results[tag] = "ReadFailure" + except Exception as e: + logger.error(f"Unhandled error reading tag '%s': %s", tag, e) read_results[tag] = "ReadFailure" # If mode involves writing if mode in ["write", "read_and_write"]: for tag, value in tags_to_write.items(): - response = comm.Write(tag, value) - if response.Status == "Success": - write_results[tag] = "WriteSuccess" - else: - print(f"Error writing tag '{tag}' with value '{value}': {response.Status}") + try: + response = comm.Write(tag, value) + if response.Status == "Success": + write_results[tag] = "WriteSuccess" + else: + logger.error( + "Error writing tag '%s' with value '%s': %s", + tag, + value, + response.Status, + ) + write_results[tag] = "WriteFailure" + except Exception as e: + logger.error(f"Unhandled error writing tag '%s': %s", tag, e) write_results[tag] = "WriteFailure" plc_output = {} From 24328e6ad3267f9a109f50c644eef90b05b51a17 Mon Sep 17 00:00:00 2001 From: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:56:02 +0100 Subject: [PATCH 6/6] minor refactoring --- .../sinks/PLCethernetIP/v1.py | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py index 006568a62e..1e393d13d8 100644 --- a/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py +++ b/inference/enterprise/workflows/enterprise_blocks/sinks/PLCethernetIP/v1.py @@ -1,26 +1,25 @@ from typing import Dict, List, Optional, Type, Union -from pydantic import ConfigDict, Field -from typing_extensions import Literal import pylogix +from pydantic import ConfigDict, Field +from typing_extensions import Literal +from inference.core.logger import logger from inference.core.workflows.execution_engine.entities.base import ( OutputDefinition, - WorkflowImageData, VideoMetadata, + WorkflowImageData, ) from inference.core.workflows.execution_engine.entities.types import ( LIST_OF_VALUES_KIND, STRING_KIND, - WorkflowParameterSelector, Selector, + WorkflowParameterSelector, ) from inference.core.workflows.prototypes.block import ( WorkflowBlock, WorkflowBlockManifest, ) -from inference.core.logger import logger - LONG_DESCRIPTION = """ This **PLC Communication** block integrates a Roboflow Workflow with a PLC using Ethernet/IP communication. @@ -122,6 +121,33 @@ class PLCBlockV1(WorkflowBlock): def get_manifest(cls) -> Type[WorkflowBlockManifest]: return PLCBlockManifest + def _read_single_tag(self, comm, tag): + try: + response = comm.Read(tag) + if response.Status == "Success": + return response.Value + logger.error(f"Error reading tag '%s': %s", tag, response.Status) + return "ReadFailure" + except Exception as e: + logger.error(f"Unhandled error reading tag '%s': %s", tag, e) + return "ReadFailure" + + def _write_single_tag(self, comm, tag, value): + try: + response = comm.Write(tag, value) + if response.Status == "Success": + return "WriteSuccess" + logger.error( + "Error writing tag '%s' with value '%s': %s", + tag, + value, + response.Status, + ) + return "WriteFailure" + except Exception as e: + logger.error(f"Unhandled error writing tag '%s': %s", tag, e) + return "WriteFailure" + def run( self, plc_ip: str, @@ -152,40 +178,16 @@ def run( with pylogix.PLC() as comm: comm.IPAddress = plc_ip - # If mode involves reading if mode in ["read", "read_and_write"]: - for tag in tags_to_read: - try: - response = comm.Read(tag) - if response.Status == "Success": - read_results[tag] = response.Value - else: - logger.error( - f"Error reading tag '%s': %s", tag, response.Status - ) - read_results[tag] = "ReadFailure" - except Exception as e: - logger.error(f"Unhandled error reading tag '%s': %s", tag, e) - read_results[tag] = "ReadFailure" - - # If mode involves writing + read_results = { + tag: self._read_single_tag(comm, tag) for tag in tags_to_read + } + if mode in ["write", "read_and_write"]: - for tag, value in tags_to_write.items(): - try: - response = comm.Write(tag, value) - if response.Status == "Success": - write_results[tag] = "WriteSuccess" - else: - logger.error( - "Error writing tag '%s' with value '%s': %s", - tag, - value, - response.Status, - ) - write_results[tag] = "WriteFailure" - except Exception as e: - logger.error(f"Unhandled error writing tag '%s': %s", tag, e) - write_results[tag] = "WriteFailure" + write_results = { + tag: self._write_single_tag(comm, tag, value) + for tag, value in tags_to_write.items() + } plc_output = {} if read_results: @@ -193,5 +195,4 @@ def run( if write_results: plc_output["write"] = write_results - # Return as a list of dicts for the 'plc_results' key return {"plc_results": [plc_output]}