diff --git a/plc4py/plc4py/api/value/PlcValue.py b/plc4py/plc4py/api/value/PlcValue.py index 972e6896326..0e25102868e 100644 --- a/plc4py/plc4py/api/value/PlcValue.py +++ b/plc4py/plc4py/api/value/PlcValue.py @@ -51,6 +51,13 @@ def __len__(self): return len(self.value) return 1 + def __eq__(self, other): + """Compare PlcValue with another or a native data type""" + if isinstance(other, PlcValue): + return (self.value == other.value) and (self.__class__.__name__ == other.__class__.__name__) + else: + return other == self.value + class PlcResponseCode(Enum): OK = auto() diff --git a/plc4py/plc4py/drivers/modbus/ModbusConfiguration.py b/plc4py/plc4py/drivers/modbus/ModbusConfiguration.py index e76707ad859..4910cdcc901 100644 --- a/plc4py/plc4py/drivers/modbus/ModbusConfiguration.py +++ b/plc4py/plc4py/drivers/modbus/ModbusConfiguration.py @@ -18,6 +18,8 @@ # from plc4py.spi.configuration.PlcConfiguration import PlcConfiguration +from plc4py.utils.GenericTypes import ByteOrder + class ModbusConfiguration(PlcConfiguration): """ @@ -39,3 +41,5 @@ def __init__(self, url): self.port = self.port or 502 # Get the unit identifier from the parameters, default to 1 self.unit_identifier = self.parameters.get("unit_identifier", 1) + # Specifies the byte/word order of the payload + self.byte_order = ByteOrder[self.parameters.get("byte_order", "BIG_ENDIAN")] diff --git a/plc4py/plc4py/drivers/modbus/ModbusConnection.py b/plc4py/plc4py/drivers/modbus/ModbusConnection.py index 81656baa757..d12b437f6a3 100644 --- a/plc4py/plc4py/drivers/modbus/ModbusConnection.py +++ b/plc4py/plc4py/drivers/modbus/ModbusConnection.py @@ -26,6 +26,7 @@ PlcRequest, PlcWriteRequest, ReadRequestBuilder, + WriteRequestBuilder, ) from plc4py.api.messages.PlcResponse import ( PlcResponse, @@ -39,7 +40,10 @@ from plc4py.drivers.modbus.ModbusTag import ModbusTagBuilder from plc4py.drivers.PlcDriverLoader import PlcDriverLoader from plc4py.spi.messages.PlcReader import DefaultPlcReader -from plc4py.spi.messages.PlcRequest import DefaultReadRequestBuilder +from plc4py.spi.messages.PlcRequest import ( + DefaultReadRequestBuilder, + DefaultWriteRequestBuilder, +) from plc4py.spi.messages.PlcWriter import DefaultPlcWriter from plc4py.spi.transport.Plc4xBaseTransport import Plc4xBaseTransport from plc4py.spi.transport.TCPTransport import TCPTransport @@ -136,6 +140,12 @@ def read_request_builder(self) -> ReadRequestBuilder: """ return DefaultReadRequestBuilder(ModbusTagBuilder) + def write_request_builder(self) -> WriteRequestBuilder: + """ + :return: write request builder. + """ + return DefaultWriteRequestBuilder(ModbusTagBuilder) + class ModbusDriver(PlcDriver): def __init__(self): diff --git a/plc4py/plc4py/drivers/modbus/ModbusDevice.py b/plc4py/plc4py/drivers/modbus/ModbusDevice.py index 08b9e7a8e88..b52993027ad 100644 --- a/plc4py/plc4py/drivers/modbus/ModbusDevice.py +++ b/plc4py/plc4py/drivers/modbus/ModbusDevice.py @@ -20,6 +20,7 @@ import logging from asyncio import Transport from dataclasses import dataclass, field +from math import ceil from typing import Dict, List from bitarray import bitarray @@ -53,9 +54,19 @@ from plc4py.spi.generation.ReadBuffer import ReadBuffer, ReadBufferByteBased from plc4py.spi.generation.WriteBuffer import WriteBufferByteBased from plc4py.spi.messages.utils.ResponseItem import ResponseItem -from plc4py.spi.values.PlcValues import PlcList, PlcNull +from plc4py.spi.values.PlcValues import PlcList, PlcNull, PlcBOOL from plc4py.utils.GenericTypes import AtomicInteger, ByteOrder +from plc4py.protocols.modbus.readwrite.ModbusPDUWriteMultipleCoilsRequest import ( + ModbusPDUWriteMultipleCoilsRequest, +) +from plc4py.protocols.modbus.readwrite.ModbusPDUWriteMultipleHoldingRegistersRequest import ( + ModbusPDUWriteMultipleHoldingRegistersRequestBuilder, +) + +from plc4py.drivers.modbus.ModbusTag import ModbusTag +from plc4py.protocols.modbus.readwrite.ModbusDataType import ModbusDataType + @dataclass class ModbusDevice: @@ -85,13 +96,25 @@ async def read( message_future = loop.create_future() if isinstance(tag, ModbusTagCoil): + if tag.data_type.value != ModbusDataType.BOOL.value: + raise NotImplementedError( + f"Only BOOL data types can be used with the coil register area" + ) pdu = ModbusPDUReadCoilsRequest(tag.address, tag.quantity) elif isinstance(tag, ModbusTagDiscreteInput): + if tag.data_type.value != ModbusDataType.BOOL.value: + raise NotImplementedError( + f"Only BOOL data types can be used with the digital input register area" + ) pdu = ModbusPDUReadDiscreteInputsRequest(tag.address, tag.quantity) elif isinstance(tag, ModbusTagInputRegister): - pdu = ModbusPDUReadInputRegistersRequest(tag.address, tag.quantity) + number_of_registers_per_item = tag.data_type.data_type_size / 2 + number_of_registers = ceil(tag.quantity * number_of_registers_per_item) + pdu = ModbusPDUReadInputRegistersRequest(tag.address, number_of_registers) elif isinstance(tag, ModbusTagHoldingRegister): - pdu = ModbusPDUReadHoldingRegistersRequest(tag.address, tag.quantity) + number_of_registers_per_item = tag.data_type.data_type_size / 2 + number_of_registers = ceil(tag.quantity * number_of_registers_per_item) + pdu = ModbusPDUReadHoldingRegistersRequest(tag.address, number_of_registers) else: raise NotImplementedError( "Modbus tag type not implemented " + str(tag.__class__) @@ -128,20 +151,36 @@ async def read( return response if isinstance(tag, ModbusTagCoil) or isinstance(tag, ModbusTagDiscreteInput): + # As we need to do some funky stuff with the ordering of bits when reading bits + # We aren't using the DataItem code for it + + # Normalize the array so we can just read the bits in one by one a = bitarray() a.frombytes(bytearray(result.value)) a.bytereverse() - read_buffer = ReadBufferByteBased(bytearray(a), ByteOrder.BIG_ENDIAN) + read_buffer = ReadBufferByteBased( + bytearray(a), self._configuration.byte_order + ) + + # If it's an array we need to wrap it in a PlcList + quantity = request.tags[request.tag_names[0]].quantity + if quantity == 1: + returned_value = PlcBOOL(read_buffer.read_bit("")) + else: + returned_array = [] + for _ in range(quantity): + returned_array.append(PlcBOOL(read_buffer.read_bit(""))) + returned_value = PlcList(returned_array) else: read_buffer = ReadBufferByteBased( - bytearray(result.value), ByteOrder.BIG_ENDIAN + bytearray(result.value), self._configuration.byte_order + ) + returned_value = DataItem.static_parse( + read_buffer, + request.tags[request.tag_names[0]].data_type, + request.tags[request.tag_names[0]].quantity, + True, ) - returned_value = DataItem.static_parse( - read_buffer, - request.tags[request.tag_names[0]].data_type, - request.tags[request.tag_names[0]].quantity, - True, - ) response_item = ResponseItem(PlcResponseCode.OK, returned_value) @@ -156,4 +195,78 @@ async def write( """ Writes one field from the Modbus Device """ - pass + if len(request.tags) > 1: + raise NotImplementedError( + "The Modbus driver only supports writing single tags at once" + ) + if len(request.tags) == 0: + raise PlcRuntimeException("No tags have been specified to write") + tag = request.tags[request.tag_names[0]] + logging.debug(f"Writing tag {str(tag)} from Modbus Device") + + # Create future to be returned when a value is returned + loop = asyncio.get_running_loop() + message_future = loop.create_future() + values = request.values[request.tag_names[0]] + if isinstance(tag, ModbusTagCoil): + pdu = ModbusPDUWriteMultipleCoilsRequest(tag.address, tag.quantity, values) + elif isinstance(tag, ModbusTagDiscreteInput): + raise PlcRuntimeException( + "Modbus doesn't support writing to discrete inputs" + ) + elif isinstance(tag, ModbusTagInputRegister): + raise PlcRuntimeException( + "Modbus doesn't support writing to input registers" + ) + elif isinstance(tag, ModbusTagHoldingRegister): + values = self._serialize_data_items(tag, values) + quantity = tag.quantity * (tag.data_type.data_type_size / 2) + pdu = ModbusPDUWriteMultipleHoldingRegistersRequestBuilder( + tag.address, quantity, values + ).build() + else: + raise NotImplementedError( + "Modbus tag type not implemented " + str(tag.__class__) + ) + + adu = ModbusTcpADU( + False, + self._transaction_generator.increment(), + self._configuration.unit_identifier, + pdu, + ) + write_buffer = WriteBufferByteBased(adu.length_in_bytes(), ByteOrder.BIG_ENDIAN) + adu.serialize(write_buffer) + + protocol = transport.protocol + protocol.write_wait_for_response( + write_buffer.get_bytes(), + transport, + adu.transaction_identifier, + message_future, + ) + + await message_future + result = message_future.result() + if isinstance(result, ModbusPDUError): + response_item = ResponseItem(PlcResponseCode.INVALID_ADDRESS, None) + else: + response_item = ResponseItem(PlcResponseCode.OK, None) + write_response = PlcWriteResponse( + PlcResponseCode.OK, {request.tag_names[0]: response_item} + ) + return write_response + + def _serialize_data_items(self, tag: ModbusTag, values: PlcValue) -> List[int]: + length = tag.quantity * tag.data_type.data_type_size + write_buffer = WriteBufferByteBased(length, self._configuration.byte_order) + + DataItem.static_serialize( + write_buffer, + values, + tag.data_type, + tag.quantity, + True, + self._configuration.byte_order, + ) + return list(write_buffer.get_bytes().tobytes()) diff --git a/plc4py/plc4py/drivers/modbus/ModbusTag.py b/plc4py/plc4py/drivers/modbus/ModbusTag.py index a67c4197846..4ad6c6d50ba 100644 --- a/plc4py/plc4py/drivers/modbus/ModbusTag.py +++ b/plc4py/plc4py/drivers/modbus/ModbusTag.py @@ -102,7 +102,7 @@ def create(cls, address_string): ) data_type = ( - ModbusDataType(matcher.group("datatype")) + ModbusDataType[matcher.group("datatype")] if "datatype" in matcher.groupdict() and matcher.group("datatype") is not None else cls._DEFAULT_DATA_TYPE diff --git a/plc4py/plc4py/drivers/umas/UmasVariables.py b/plc4py/plc4py/drivers/umas/UmasVariables.py index 265efc6b49e..31b8da6b266 100644 --- a/plc4py/plc4py/drivers/umas/UmasVariables.py +++ b/plc4py/plc4py/drivers/umas/UmasVariables.py @@ -186,7 +186,7 @@ def build(self) -> UmasVariable: variable = UmasCustomVariable( self.tag_name, data_type, - self.tag_reference.block, + self.block, self.tag_reference.offset, children, ) diff --git a/plc4py/plc4py/protocols/umas/readwrite/UmasPDUErrorResponse.py b/plc4py/plc4py/protocols/umas/readwrite/UmasPDUErrorResponse.py new file mode 100644 index 00000000000..f50fcc4c2fd --- /dev/null +++ b/plc4py/plc4py/protocols/umas/readwrite/UmasPDUErrorResponse.py @@ -0,0 +1,123 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from dataclasses import dataclass + +from plc4py.api.exceptions.exceptions import PlcRuntimeException +from plc4py.api.exceptions.exceptions import SerializationException +from plc4py.api.messages.PlcMessage import PlcMessage +from plc4py.protocols.umas.readwrite.UmasPDUItem import UmasPDUItem +from plc4py.spi.generation.ReadBuffer import ReadBuffer +from plc4py.spi.generation.WriteBuffer import WriteBuffer +from plc4py.utils.GenericTypes import ByteOrder +from typing import Any +from typing import ClassVar +from typing import List +import math + + +@dataclass +class UmasPDUErrorResponse(UmasPDUItem): + block: List[int] + # Arguments. + byte_length: int + # Accessors for discriminator values. + umas_function_key: ClassVar[int] = 0xFD + umas_request_function_key: ClassVar[int] = 0 + + def serialize_umas_pduitem_child(self, write_buffer: WriteBuffer): + write_buffer.push_context("UmasPDUErrorResponse") + + # Array Field (block) + write_buffer.write_simple_array( + self.block, write_buffer.write_unsigned_byte, logical_name="block" + ) + + write_buffer.pop_context("UmasPDUErrorResponse") + + def length_in_bytes(self) -> int: + return int(math.ceil(float(self.length_in_bits() / 8.0))) + + def length_in_bits(self) -> int: + length_in_bits: int = super().length_in_bits() + _value: UmasPDUErrorResponse = self + + # Array field + if self.block is not None: + length_in_bits += 8 * len(self.block) + + return length_in_bits + + @staticmethod + def static_parse_builder( + read_buffer: ReadBuffer, umas_request_function_key: int, byte_length: int + ): + read_buffer.push_context("UmasPDUErrorResponse") + + if isinstance(umas_request_function_key, str): + umas_request_function_key = int(umas_request_function_key) + if isinstance(byte_length, str): + byte_length = int(byte_length) + + block: List[Any] = read_buffer.read_array_field( + logical_name="block", + read_function=read_buffer.read_unsigned_byte, + count=byte_length - int(2), + byte_order=ByteOrder.LITTLE_ENDIAN, + umas_request_function_key=umas_request_function_key, + byte_length=byte_length, + ) + + read_buffer.pop_context("UmasPDUErrorResponse") + # Create the instance + return UmasPDUErrorResponseBuilder(block) + + def equals(self, o: object) -> bool: + if self == o: + return True + + if not isinstance(o, UmasPDUErrorResponse): + return False + + that: UmasPDUErrorResponse = UmasPDUErrorResponse(o) + return (self.block == that.block) and super().equals(that) and True + + def hash_code(self) -> int: + return hash(self) + + def __str__(self) -> str: + pass + # write_buffer_box_based: WriteBufferBoxBased = WriteBufferBoxBased(True, True) + # try: + # write_buffer_box_based.writeSerializable(self) + # except SerializationException as e: + # raise PlcRuntimeException(e) + + # return "\n" + str(write_buffer_box_based.get_box()) + "\n" + + +@dataclass +class UmasPDUErrorResponseBuilder: + block: List[int] + + def build(self, byte_length: int, pairing_key) -> UmasPDUErrorResponse: + umas_pduerror_response: UmasPDUErrorResponse = UmasPDUErrorResponse( + byte_length, pairing_key, self.block + ) + return umas_pduerror_response diff --git a/plc4py/plc4py/protocols/umas/readwrite/UmasPDUItem.py b/plc4py/plc4py/protocols/umas/readwrite/UmasPDUItem.py index d5963a04ff6..dc9cf9dad7b 100644 --- a/plc4py/plc4py/protocols/umas/readwrite/UmasPDUItem.py +++ b/plc4py/plc4py/protocols/umas/readwrite/UmasPDUItem.py @@ -225,6 +225,15 @@ def static_parse_context( builder = UmasPDUReadUnlocatedVariableNamesRequest.static_parse_builder( read_buffer, umas_request_function_key, byte_length ) + from plc4py.protocols.umas.readwrite.UmasPDUErrorResponse import ( + UmasPDUErrorResponse, + ) + + if umas_function_key == int(0xFD): + + builder = UmasPDUErrorResponse.static_parse_builder( + read_buffer, umas_request_function_key, byte_length + ) from plc4py.protocols.umas.readwrite.UmasInitCommsResponse import ( UmasInitCommsResponse, ) diff --git a/plc4py/plc4py/spi/configuration/PlcConfiguration.py b/plc4py/plc4py/spi/configuration/PlcConfiguration.py index e67fd849528..a009467d5b0 100644 --- a/plc4py/plc4py/spi/configuration/PlcConfiguration.py +++ b/plc4py/plc4py/spi/configuration/PlcConfiguration.py @@ -39,7 +39,7 @@ def _parse_configuration(self, url): + r"(:(?P[\w]*))?" + r":\/\/(?P[\w+.]*)" + r"(:(?P\d+))?" - + r"(?P(&{1}([^&=]*={1}[^&=]*))*)" + + r"/?(?P(([^&=]*={1}[^&=]*){1}&*)*)" ) matches = re.search(regex, url) diff --git a/plc4py/plc4py/spi/generation/ReadBuffer.py b/plc4py/plc4py/spi/generation/ReadBuffer.py index 62b110243d0..f250582b979 100644 --- a/plc4py/plc4py/spi/generation/ReadBuffer.py +++ b/plc4py/plc4py/spi/generation/ReadBuffer.py @@ -324,6 +324,11 @@ def read_int(self, bit_length: int = 32, logical_name: str = "", **kwargs) -> in padded = (32 - bit_length) * bitarray("0") + bitarray( self.bb[self.position : self.position + bit_length] ) + if ( + byte_order == ByteOrder.BIG_ENDIAN_BYTE_SWAP + or byte_order == ByteOrder.LITTLE_ENDIAN_BYTE_SWAP + ): + padded = padded[16:] + padded[0:16] result: int = struct.unpack(endian_string + "i", padded)[0] self.position += bit_length return result @@ -342,6 +347,11 @@ def read_long(self, bit_length: int = 64, logical_name: str = "", **kwargs) -> i padded = (64 - bit_length) * bitarray("0") + bitarray( self.bb[self.position : self.position + bit_length] ) + if ( + byte_order == ByteOrder.BIG_ENDIAN_BYTE_SWAP + or byte_order == ByteOrder.LITTLE_ENDIAN_BYTE_SWAP + ): + padded = padded[16:32] + padded[0:16] + padded[48:] + padded[32:48] result: int = struct.unpack(endian_string + "q", padded)[0] self.position += bit_length return result @@ -356,9 +366,15 @@ def read_float( endianness: str = ">" if byte_order == ByteOrder.LITTLE_ENDIAN: endianness = "<" + buffer = self.bb[self.position : self.position + bit_length] + if ( + byte_order == ByteOrder.BIG_ENDIAN_BYTE_SWAP + or byte_order == ByteOrder.LITTLE_ENDIAN_BYTE_SWAP + ): + buffer = buffer[16:] + buffer[0:16] result: float = struct.unpack( endianness + "f", - self.bb[self.position : self.position + bit_length], + buffer, )[0] self.position += bit_length return result @@ -373,9 +389,15 @@ def read_double( endianness: str = ">" if byte_order == ByteOrder.LITTLE_ENDIAN: endianness = "<" + buffer = self.bb[self.position : self.position + bit_length] + if ( + byte_order == ByteOrder.BIG_ENDIAN_BYTE_SWAP + or byte_order == ByteOrder.LITTLE_ENDIAN_BYTE_SWAP + ): + buffer = buffer[16:32] + buffer[0:16] + buffer[48:] + buffer[32:48] result: float = struct.unpack( endianness + "d", - self.bb[self.position : self.position + bit_length], + buffer, )[0] self.position += bit_length return result diff --git a/plc4py/plc4py/spi/generation/WriteBuffer.py b/plc4py/plc4py/spi/generation/WriteBuffer.py index 6f0321a40be..ac98cd32c4b 100644 --- a/plc4py/plc4py/spi/generation/WriteBuffer.py +++ b/plc4py/plc4py/spi/generation/WriteBuffer.py @@ -381,6 +381,14 @@ def _handle_numeric_encoding(self, value, bit_length: int, **kwargs): value, ) src.frombytes(result) + if ( + byte_order == ByteOrder.BIG_ENDIAN_BYTE_SWAP + or byte_order == ByteOrder.LITTLE_ENDIAN_BYTE_SWAP + ): + if 32 < bit_length <= 64: + src = src[16:32] + src[0:16] + src[48:] + src[32:48] + elif 16 < bit_length <= 32: + src = src[16:] + src[0:16] if bit_length < 8: self.bb[self.position : self.position + bit_length] = src[-bit_length:] else: diff --git a/plc4py/plc4py/spi/messages/PlcReader.py b/plc4py/plc4py/spi/messages/PlcReader.py index 4731adc2c5e..0de97da74cc 100644 --- a/plc4py/plc4py/spi/messages/PlcReader.py +++ b/plc4py/plc4py/spi/messages/PlcReader.py @@ -87,9 +87,9 @@ async def _read(self, request: PlcReadRequest) -> PlcReadResponse: self._device.read(request, self._transport), 10 ) return response - except Exception: + except Exception as e: # TODO:- This exception is very general and probably should be replaced - return PlcReadResponse(PlcResponseCode.INTERNAL_ERROR, {}) + raise e def is_read_supported(self) -> bool: """ diff --git a/plc4py/plc4py/spi/messages/PlcRequest.py b/plc4py/plc4py/spi/messages/PlcRequest.py index 73769d72995..b15d46a1a95 100644 --- a/plc4py/plc4py/spi/messages/PlcRequest.py +++ b/plc4py/plc4py/spi/messages/PlcRequest.py @@ -16,6 +16,8 @@ # specific language governing permissions and limitations # under the License. # +from typing import List, Any + from plc4py.api.messages.PlcField import PlcTag from plc4py.api.messages.PlcRequest import ( BrowseRequestBuilder, diff --git a/plc4py/plc4py/spi/messages/PlcWriter.py b/plc4py/plc4py/spi/messages/PlcWriter.py index fb807210d78..f3d943c5fd7 100644 --- a/plc4py/plc4py/spi/messages/PlcWriter.py +++ b/plc4py/plc4py/spi/messages/PlcWriter.py @@ -90,11 +90,12 @@ async def _write(self, request: PlcWriteRequest) -> PlcWriteResponse: ) # Return the response return response - except Exception: + except Exception as e: # If an error occurs during the execution of the write request, return a response with + # Still haven't found a nice way to return an error # the INTERNAL_ERROR code. This exception is very general and probably should be replaced. # TODO:- This exception is very general and probably should be replaced - return PlcWriteResponse(PlcResponseCode.INTERNAL_ERROR, {}) + raise e def is_write_supported(self) -> bool: """ diff --git a/plc4py/plc4py/spi/messages/utils/ResponseItem.py b/plc4py/plc4py/spi/messages/utils/ResponseItem.py index a26cfdb2095..f62912773cf 100644 --- a/plc4py/plc4py/spi/messages/utils/ResponseItem.py +++ b/plc4py/plc4py/spi/messages/utils/ResponseItem.py @@ -18,12 +18,12 @@ # from abc import ABC from dataclasses import dataclass -from typing import Generic, TypeVar +from typing import Generic, TypeVar, Union from plc4py.api.messages.PlcResponse import PlcResponseCode from plc4py.api.value.PlcValue import PlcValue -T = TypeVar("T", bound=PlcValue) +T = TypeVar("T", bound=Union[PlcValue, None]) @dataclass diff --git a/plc4py/plc4py/spi/values/PlcValues.py b/plc4py/plc4py/spi/values/PlcValues.py index d2c44e78c2d..437c6a795f2 100644 --- a/plc4py/plc4py/spi/values/PlcValues.py +++ b/plc4py/plc4py/spi/values/PlcValues.py @@ -19,164 +19,132 @@ from dataclasses import dataclass from typing import Any, Dict, List -from plc4py.api.value.PlcValue import PlcValue +from ...api.value.PlcValue import PlcValue -@dataclass class PlcINT(PlcValue[int]): pass -@dataclass class PlcBYTE(PlcValue[int]): pass -@dataclass class PlcCHAR(PlcValue[str]): pass -@dataclass class PlcDATE(PlcValue[int]): pass -@dataclass class PlcDATE_AND_TIME(PlcValue[int]): pass -@dataclass class PlcDINT(PlcValue[int]): pass -@dataclass class PlcDWORD(PlcValue[int]): pass -@dataclass class PlcLDATE(PlcValue[int]): pass -@dataclass class PlcLDATE_AND_TIME(PlcValue[int]): pass -@dataclass class PlcLINT(PlcValue[int]): pass -@dataclass class PlcList(PlcValue[List[Any]]): pass -@dataclass class PlcLREAL(PlcValue[float]): pass -@dataclass class PlcLTIME(PlcValue[int]): pass -@dataclass class PlcLTIME_OF_DAY(PlcValue[int]): pass -@dataclass class PlcLWORD(PlcValue[int]): pass -@dataclass class PlcNull(PlcValue[None]): pass -@dataclass class PlcRawByteArray(List[PlcValue[Any]]): pass -@dataclass class PlcREAL(PlcValue[float]): pass -@dataclass class PlcSINT(PlcValue[int]): pass -@dataclass class PlcSTRING(PlcValue[str]): pass -@dataclass class PlcStruct(PlcValue[Dict[str, PlcValue[str]]]): pass -@dataclass class PlcTIME(PlcValue[int]): pass -@dataclass class PlcTIME_OF_DAY(PlcValue[int]): pass -@dataclass class PlcUBINT(PlcValue[int]): pass -@dataclass class PlcUDINT(PlcValue[int]): pass -@dataclass class PlcUINT(PlcValue[int]): pass -@dataclass class PlcULINT(PlcValue[int]): pass -@dataclass class PlcUSINT(PlcValue[int]): pass -@dataclass class PlcWCHAR(PlcValue[str]): pass -@dataclass class PlcWORD(PlcValue[int]): pass -@dataclass class PlcWSTRING(PlcValue[str]): pass -@dataclass class PlcBOOL(PlcValue[bool]): pass diff --git a/plc4py/plc4py/utils/GenericTypes.py b/plc4py/plc4py/utils/GenericTypes.py index 582ead53c84..90406e8147b 100644 --- a/plc4py/plc4py/utils/GenericTypes.py +++ b/plc4py/plc4py/utils/GenericTypes.py @@ -46,6 +46,8 @@ class ByteOrder(Enum): LITTLE_ENDIAN = auto() BIG_ENDIAN = auto() + LITTLE_ENDIAN_BYTE_SWAP = auto() + BIG_ENDIAN_BYTE_SWAP = auto() def __new__(cls, value): obj = object.__new__(cls) @@ -54,9 +56,12 @@ def __new__(cls, value): @staticmethod def get_short_name(order): - if order == ByteOrder.LITTLE_ENDIAN: + if ( + order == ByteOrder.LITTLE_ENDIAN + or order == ByteOrder.LITTLE_ENDIAN_BYTE_SWAP + ): return "little" - elif order == ByteOrder.BIG_ENDIAN: + elif order == ByteOrder.BIG_ENDIAN or order == ByteOrder.BIG_ENDIAN_BYTE_SWAP: return "big" diff --git a/plc4py/setup.py b/plc4py/setup.py index f245afe23fd..253ccd7a30a 100644 --- a/plc4py/setup.py +++ b/plc4py/setup.py @@ -21,7 +21,7 @@ setup( name="plc4py", - version="0.11a0", + version="0.13", description="Plc4py The Python Industrial IOT Adapter", classifiers=[ "Development Status :: 3 - Alpha", diff --git a/plc4py/tests/unit/plc4py/drivers/modbus/test_modbus_connection.py b/plc4py/tests/unit/plc4py/drivers/modbus/test_modbus_connection.py index 1fd9c49e6c3..5fa77516e03 100644 --- a/plc4py/tests/unit/plc4py/drivers/modbus/test_modbus_connection.py +++ b/plc4py/tests/unit/plc4py/drivers/modbus/test_modbus_connection.py @@ -17,6 +17,7 @@ # under the License. # import time +from unittest import TestCase import pytest @@ -24,10 +25,14 @@ from plc4py.api.value.PlcValue import PlcResponseCode import logging +from plc4py.spi.values.PlcValues import PlcINT, PlcREAL, PlcList + logger = logging.getLogger("testing") +TEST_SERVER_IP = "192.168.190.174" @pytest.mark.asyncio +@pytest.mark.xfail async def manual_test_plc_driver_modbus_connect(): """ Test the connection to a Modbus PLC using PlcDriverManager. @@ -36,7 +41,7 @@ async def manual_test_plc_driver_modbus_connect(): driver_manager = PlcDriverManager() # Establish a connection to the Modbus PLC - async with driver_manager.connection("modbus://1") as connection: + async with driver_manager.connection(f"modbus://{TEST_SERVER_IP}") as connection: # Check if the connection is successful assert connection.is_connected() @@ -46,7 +51,214 @@ async def manual_test_plc_driver_modbus_connect(): @pytest.mark.asyncio @pytest.mark.xfail -async def test_plc_driver_modbus_read(): +async def test_plc_driver_modbus_read_coil(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "0x00001") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == True + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_coil_non_bool(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "0x00001:REAL") + request = builder.build() + TestCase.assertRaises( + await connection.execute(request), NotImplementedError + ) + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_coil_array(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "0x00001[2]") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == [True, False] + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_contacts(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "1x00001") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == True + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_contact_array(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "1x00001[2]") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == [True, False] + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_input_register(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "3x00001") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == 333 + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_input_register_array(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "3x00001[2]") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == [333, 0] + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_holding(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "4x00001") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == 874 + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_holding(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "4x00001[2]") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == [874, 0] + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_holding_real(): """ Test reading data from a Modbus PLC. """ @@ -56,17 +268,127 @@ async def test_plc_driver_modbus_read(): driver_manager = PlcDriverManager() # Establish a connection to the Modbus PLC - async with driver_manager.connection("modbus://127.0.0.1:5020") as connection: + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502?byte_order=BIG_ENDIAN_BYTE_SWAP" + ) as connection: with connection.read_request_builder() as builder: - builder.add_item("Random Tag", "4x00001[10]") + builder.add_item("Random Tag", "4x00011:REAL[2]") request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == [PlcREAL(value=874), PlcREAL(value=0.0)] + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_holding_string_even(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "4x00041:CHAR[6]") + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"].value + assert value == [b"F", b"A", b"F", b"B", b"C", b"B"] - # Execute the read request - for _ in range(100): - future = connection.execute(request) - response = await future +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_read_holding_string_odd(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.read_request_builder() as builder: + builder.add_item("Random Tag", "4x00041:CHAR[5]") + request = builder.build() + response = await connection.execute(request) value = response.tags["Random Tag"].value - log.info("Read tag 4x00001[10] - %s", value) + assert value == [b"F", b"A", b"F", b"B", b"C"] + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_write_holding_int(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.write_request_builder() as builder: + builder.add_item("Random Tag", "4x00001", PlcINT(874)) + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"] + assert value.response_code == PlcResponseCode.OK - pass + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_write_holding_int_array(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502" + ) as connection: + with connection.write_request_builder() as builder: + builder.add_item("Random Tag", "4x00001[5]", PlcList([PlcINT(874), PlcINT(0), PlcINT(3), PlcINT(4), PlcINT(5)])) + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"] + assert value.response_code == PlcResponseCode.OK + + +@pytest.mark.asyncio +@pytest.mark.xfail +async def test_plc_driver_modbus_write_holding_real(): + """ + Test reading data from a Modbus PLC. + """ + log = logging.getLogger(__name__) + + # Initialize the PlcDriverManager + driver_manager = PlcDriverManager() + + # Establish a connection to the Modbus PLC + async with driver_manager.connection( + f"modbus://{TEST_SERVER_IP}:502?byte_order=BIG_ENDIAN_BYTE_SWAP" + ) as connection: + with connection.write_request_builder() as builder: + builder.add_item("Random Tag", "4x00011:REAL", PlcREAL(874)) + request = builder.build() + response = await connection.execute(request) + value = response.tags["Random Tag"] + assert value.response_code == PlcResponseCode.OK diff --git a/plc4py/tests/unit/plc4py/drivers/umas/test_umas_connection.py b/plc4py/tests/unit/plc4py/drivers/umas/test_umas_connection.py index c1d6fc57a15..a46badcf7a5 100644 --- a/plc4py/tests/unit/plc4py/drivers/umas/test_umas_connection.py +++ b/plc4py/tests/unit/plc4py/drivers/umas/test_umas_connection.py @@ -40,23 +40,21 @@ async def test_plc_driver_umas_read(): log = logging.getLogger(__name__) driver_manager = PlcDriverManager() - async with driver_manager.connection("umas://192.168.1.177:502") as connection: + async with driver_manager.connection("umas://192.168.190.174:502") as connection: with connection.read_request_builder() as builder: - builder.add_item(f"Random Tag {1}", "testing:DINT") + builder.add_item(f"Random Tag {1}", "blurbe:REAL") request = builder.build() future = connection.execute(request) - await future - response = future.result() + response = await future value = response.tags["Random Tag 1"].value log.error(f"Read tag test_REAL - {value}") - await asyncio.sleep(1) - pass + assert value == 0.0 @pytest.mark.asyncio @pytest.mark.xfail -async def manual_test_plc_driver_umas_browse(): +async def test_plc_driver_umas_browse(): driver_manager = PlcDriverManager() async with driver_manager.connection("umas://192.168.1.174:502") as connection: with connection.browse_request_builder() as builder: diff --git a/plc4py/tests/unit/plc4py/spi/configuration/test_configuration.py b/plc4py/tests/unit/plc4py/spi/configuration/test_configuration.py index f9bbadeee6f..8e2c6d2dfcd 100644 --- a/plc4py/tests/unit/plc4py/spi/configuration/test_configuration.py +++ b/plc4py/tests/unit/plc4py/spi/configuration/test_configuration.py @@ -21,7 +21,7 @@ def test_configuration_standard_raw_ip(): config = PlcConfiguration( - "profibus:raw://127.0.0.1:4664&host=localhost&mac=01:02:03:04:05:06" + "profibus:raw://127.0.0.1:4664?host=localhost&mac=01:02:03:04:05:06" ) assert config.protocol == "profibus" assert config.transport == "raw" @@ -32,7 +32,7 @@ def test_configuration_standard_raw_ip(): def test_configuration_standard_tcp_localhost(): config = PlcConfiguration( - "profibus:tcp://localhost:4664&host=localhost&mac=01:02:03:04:05:06" + "profibus:tcp://localhost:4664?host=localhost&mac=01:02:03:04:05:06" ) assert config.protocol == "profibus" assert config.transport == "tcp" @@ -43,7 +43,7 @@ def test_configuration_standard_tcp_localhost(): def test_configuration_standard_no_transport(): config = PlcConfiguration( - "profibus://localhost:4664&host=localhost&mac=01:02:03:04:05:06" + "profibus://localhost:4664?host=localhost&mac=01:02:03:04:05:06" ) assert config.protocol == "profibus" assert config.transport == None @@ -54,7 +54,7 @@ def test_configuration_standard_no_transport(): def test_configuration_standard_second_parameter(): config = PlcConfiguration( - "profibus://localhost:4664&host=localhost&mac=01:02:03:04:05:06" + "profibus://localhost:4664?host=localhost&mac=01:02:03:04:05:06" ) assert config.protocol == "profibus" assert config.transport == None @@ -66,7 +66,7 @@ def test_configuration_standard_second_parameter(): def test_configuration_standard_no_port(): config = PlcConfiguration( - "profibus://localhost&host=localhost&mac=01:02:03:04:05:06" + "profibus://localhost?host=localhost&mac=01:02:03:04:05:06" ) assert config.protocol == "profibus" assert config.transport == None @@ -86,7 +86,7 @@ def test_configuration_standard_no_parameters(): def test_configuration_standard_no_parameters(): - config = PlcConfiguration("eip://127.0.0.1&test=plc4x") + config = PlcConfiguration("eip://127.0.0.1?test=plc4x") assert config.protocol == "eip" assert config.transport == None assert config.host == "127.0.0.1" diff --git a/protocols/umas/src/main/resources/protocols/umas/umas.mspec b/protocols/umas/src/main/resources/protocols/umas/umas.mspec index e724fbb8c3a..1acd5594039 100644 --- a/protocols/umas/src/main/resources/protocols/umas/umas.mspec +++ b/protocols/umas/src/main/resources/protocols/umas/umas.mspec @@ -99,6 +99,9 @@ [simple uint 16 offset] [const uint 16 blank 0x00] ] + ['0xFD' UmasPDUErrorResponse + [array uint 8 block count 'byteLength - 2'] + ] ['0xFE', '0x01' UmasInitCommsResponse [simple uint 16 maxFrameSize] [simple uint 16 firmwareVersion]