This is an automated email from the ASF dual-hosted git repository.
hutcheb pushed a commit to branch feat/umas_write
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/feat/umas_write by this push:
new 6f959f4a73 fix(plc4py/umas): Start testing all the data types.
6f959f4a73 is described below
commit 6f959f4a733998ddc9af3692611450e4a43e6db2
Author: hutcheb <[email protected]>
AuthorDate: Thu Oct 24 22:42:36 2024 +0800
fix(plc4py/umas): Start testing all the data types.
---
.../python/PythonLanguageTemplateHelper.java | 14 +-
.../templates/python/data-io-template.python.ftlh | 32 +-
plc4py/plc4py/drivers/umas/UmasDevice.py | 56 +++-
plc4py/plc4py/drivers/umas/UmasTag.py | 2 +-
plc4py/plc4py/drivers/umas/UmasVariables.py | 2 +-
.../plc4py/protocols/modbus/readwrite/DataItem.py | 14 +-
.../protocols/simulated/readwrite/DataItem.py | 18 +-
plc4py/plc4py/protocols/umas/readwrite/DataItem.py | 216 ++++++++++++-
.../protocols/umas/readwrite/UmasDataType.py | 2 +-
plc4py/plc4py/spi/generation/ReadBuffer.py | 107 +++++--
plc4py/plc4py/spi/values/PlcValues.py | 5 +-
.../drivers/modbus/test_modbus_connection.py | 2 +-
.../plc4py/drivers/umas/test_umas_connection.py | 335 ++++++++++++++++++---
.../src/main/resources/protocols/umas/umas.mspec | 33 +-
src/site/asciidoc/users/protocols/index.adoc | 18 ++
src/site/asciidoc/users/protocols/umas.adoc | 52 ++--
src/site/site.xml | 1 +
17 files changed, 764 insertions(+), 145 deletions(-)
diff --git
a/code-generation/language-python/src/main/java/org/apache/plc4x/language/python/PythonLanguageTemplateHelper.java
b/code-generation/language-python/src/main/java/org/apache/plc4x/language/python/PythonLanguageTemplateHelper.java
index bd2370721b..c4054c61ce 100644
---
a/code-generation/language-python/src/main/java/org/apache/plc4x/language/python/PythonLanguageTemplateHelper.java
+++
b/code-generation/language-python/src/main/java/org/apache/plc4x/language/python/PythonLanguageTemplateHelper.java
@@ -579,7 +579,7 @@ public class PythonLanguageTemplateHelper extends
BaseFreemarkerLanguageTemplate
case BYTE:
// Byte values are represented as signed integers in PLC4Py
emitRequiredImport("from plc4py.spi.values.PlcValues import
PlcSINT");
- return "PlcSINT";
+ return "PlcBYTE";
case UINT:
IntegerTypeReference unsignedIntegerTypeReference =
(IntegerTypeReference) simpleTypeReference;
if (unsignedIntegerTypeReference.getSizeInBits() <= 4) {
@@ -807,10 +807,10 @@ public class PythonLanguageTemplateHelper extends
BaseFreemarkerLanguageTemplate
switch (simpleTypeReference.getBaseType()) {
case BIT:
String bitType = "bit";
- return "read_buffer.read_" + bitType + "(\"" + logicalName +
"\")";
+ return "read_buffer.read_" + bitType + "(\"" + logicalName +
"\"";
case BYTE:
String byteType = "byte";
- return "read_buffer.read_" + byteType + "(\"" + logicalName +
"\")";
+ return "read_buffer.read_" + byteType + "(\"" + logicalName +
"\"";
case UINT:
String unsignedIntegerType;
IntegerTypeReference unsignedIntegerTypeReference =
(IntegerTypeReference) simpleTypeReference;
@@ -825,7 +825,7 @@ public class PythonLanguageTemplateHelper extends
BaseFreemarkerLanguageTemplate
} else {
unsignedIntegerType = "unsigned_long";
}
- return "read_buffer.read_" + unsignedIntegerType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\")";
+ return "read_buffer.read_" + unsignedIntegerType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\"";
case INT:
String integerType;
if (simpleTypeReference.getSizeInBits() <= 8) {
@@ -839,10 +839,10 @@ public class PythonLanguageTemplateHelper extends
BaseFreemarkerLanguageTemplate
} else {
integerType = "long";
}
- return "read_buffer.read_" + integerType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\")";
+ return "read_buffer.read_" + integerType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\"";
case FLOAT:
String floatType = (simpleTypeReference.getSizeInBits() <= 32)
? "float" : "double";
- return "read_buffer.read_" + floatType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\")";
+ return "read_buffer.read_" + floatType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\"";
case STRING:
case VSTRING:
String stringType = "str";
@@ -856,7 +856,7 @@ public class PythonLanguageTemplateHelper extends
BaseFreemarkerLanguageTemplate
VstringTypeReference vstringTypeReference =
(VstringTypeReference) simpleTypeReference;
length = toParseExpression(field, INT_TYPE_REFERENCE,
vstringTypeReference.getLengthExpression(), null);
}
- return "read_buffer.read_" + stringType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\",
encoding=" + "\"\")";
+ return "read_buffer.read_" + stringType + "(" +
simpleTypeReference.getSizeInBits() + ", logical_name=\"" + logicalName + "\"";
default:
return "";
diff --git
a/code-generation/language-python/src/main/resources/templates/python/data-io-template.python.ftlh
b/code-generation/language-python/src/main/resources/templates/python/data-io-template.python.ftlh
index 029c485408..19ad41c98a 100644
---
a/code-generation/language-python/src/main/resources/templates/python/data-io-template.python.ftlh
+++
b/code-generation/language-python/src/main/resources/templates/python/data-io-template.python.ftlh
@@ -89,6 +89,7 @@ class ${type.name}:
<#switch field.typeName>
<#case "array">
<#assign arrayField =
field.asArrayField().orElseThrow()>
+ <#assign typedField =
field.asTypedField().orElseThrow()>
<#assign
elementTypeReference=arrayField.type.elementTypeReference>
# Array field (${helper.camelCaseToSnakeCase(arrayField.name)})
<#-- Only update curPos if the length expression uses it -->
@@ -103,7 +104,7 @@ class ${type.name}:
<@emitImport import="from plc4py.api.value.PlcValue import
PlcValue" />
${helper.camelCaseToSnakeCase(arrayField.name)}: List[PlcValue] =
[]
for _ in range(item_count):
-
${helper.camelCaseToSnakeCase(arrayField.name)}.append(${helper.getPlcValueTypeForTypeReference(elementTypeReference)}(${helper.getLanguageTypeNameForTypeReference(elementTypeReference,
false)}(<#if
elementTypeReference.isSimpleTypeReference()>${helper.getReadBufferReadMethodCall(elementTypeReference.asSimpleTypeReference().orElseThrow(),
"",
arrayField)})<#else>${elementTypeReference.asComplexTypeReference().orElseThrow().name}IO.static_parse(read_buffer<#if
elementTypeR [...]
+
${helper.camelCaseToSnakeCase(arrayField.name)}.append(${helper.getPlcValueTypeForTypeReference(elementTypeReference)}(${helper.getLanguageTypeNameForTypeReference(elementTypeReference,
false)}(<#if
elementTypeReference.isSimpleTypeReference()>${helper.getReadBufferReadMethodCall(elementTypeReference.asSimpleTypeReference().orElseThrow(),
"", arrayField)}${helper.getFieldOptions(typedField,
parserArguments)}))<#else>${elementTypeReference.asComplexTypeReference().orElseTh
[...]
<#-- In all other cases do we have to work with a list, that is
later converted to an array -->
<#else>
@@ -118,7 +119,7 @@ class ${type.name}:
while read_buffer.get_pos() <
${helper.camelCaseToSnakeCase(arrayField.name)}_end_pos):
value.append(<@compress single_line=true>
<#if elementTypeReference.isSimpleTypeReference()>
-
${helper.getPlcValueTypeForTypeReference(elementTypeReference)}(${helper.getReadBufferReadMethodCall(elementTypeReference.asSimpleTypeReference().orElseThrow(),
"", arrayField)})
+
${helper.getPlcValueTypeForTypeReference(elementTypeReference)}(${helper.getReadBufferReadMethodCall(elementTypeReference.asSimpleTypeReference().orElseThrow(),
"", arrayField)}${helper.getFieldOptions(typedField, parserArguments)}))
<#else>${elementTypeReference.asNonSimpleTypeReference().orElseThrow().name}IO.static_parse(readBuffer
<#if elementTypeReference.params.isPresent()>,
<#list elementTypeReference.params.orElseThrow() as
parserArgument>
@@ -136,7 +137,7 @@ class ${type.name}:
# Terminated array
${arrayField.name}:
${helper.getNonPrimitiveLanguageTypeNameForField(arrayField)} = new
LinkedList<>()
while not
bool(${helper.camelCaseToSnakeCase(helper.toParseExpression(arrayField,
helper.boolTypeReference, arrayField.loopExpression,parserArguments))})):
- ${helper.camelCaseToSnakeCase(arrayField.name)}.append(<#if
elementTypeReference.isSimpleTypeReference()>${helper.getReadBufferReadMethodCall(elementTypeReference.asSimpleTypeReference().orElseThrow(),
"",
arrayField)}<#else>${elementTypeReference.asComplexTypeReference().orElseThrow().name}IO.static_parse(readBuffer<#if
arrayField.params.isPresent()>, <#list arrayField.params.orElseThrow() as
parserArgument>(${helper.getLanguageTypeNameForTypeReference(helper.getArgument
[...]
+ ${helper.camelCaseToSnakeCase(arrayField.name)}.append(<#if
elementTypeReference.isSimpleTypeReference()>${helper.getReadBufferReadMethodCall(elementTypeReference.asSimpleTypeReference().orElseThrow(),
"", arrayField)}${helper.getFieldOptions(typedField,
parserArguments)})<#else>${elementTypeReference.asComplexTypeReference().orElseThrow().name}IO.static_parse(readBuffer<#if
arrayField.params.isPresent()>, <#list arrayField.params.orElseThrow() as
parserArgument>(${helper [...]
<#-- After parsing, update the current position, but only if
it's needed -->
<#if
arrayField.loopExpression.contains("curPos")>
@@ -151,9 +152,10 @@ class ${type.name}:
<#break>
<#case "const">
<#assign constField=field.asConstField().orElseThrow()>
+ <#assign typedField =
field.asTypedField().orElseThrow()>
# Const Field (${helper.camelCaseToSnakeCase(constField.name)})
- ${helper.camelCaseToSnakeCase(constField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(constField)} =
${helper.getReadBufferReadMethodCall(constField.type.asSimpleTypeReference().orElseThrow(),
"", constField)}
+ ${helper.camelCaseToSnakeCase(constField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(constField)} =
${helper.getReadBufferReadMethodCall(constField.type.asSimpleTypeReference().orElseThrow(),
"", constField)}${helper.getFieldOptions(typedField, parserArguments)})
if ${helper.camelCaseToSnakeCase(constField.name)} !=
${dataIoTypeDefinition.name}.${constField.name?upper_case}):
<@emitImport import="from plc4py.api.exceptions.exceptions
import ParseException" />
raise ParseException("Expected constant value " +
${dataIoTypeDefinition.name}.${constField.name?upper_case} + " but got " +
${helper.camelCaseToSnakeCase(constField.name)})
@@ -164,9 +166,10 @@ class ${type.name}:
<#break>
<#case "enum">
<#assign enumField=field.asEnumField().orElseThrow()>
+ <#assign typedField =
field.asTypedField().orElseThrow()>
# Enum field (${helper.camelCaseToSnakeCase(enumField.name)})
- ${helper.camelCaseToSnakeCase(enumField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(enumField)} =
${helper.getNonPrimitiveLanguageTypeNameForField(enumField)}.enum_for_value(${helper.getReadBufferReadMethodCall(helper.getEnumBaseTypeReference(enumField.type.asSimpleTypeReference().orElseThrow()),
"", enumField)})
+ ${helper.camelCaseToSnakeCase(enumField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(enumField)} =
${helper.getNonPrimitiveLanguageTypeNameForField(enumField)}.enum_for_value(${helper.getReadBufferReadMethodCall(helper.getEnumBaseTypeReference(enumField.type.asSimpleTypeReference().orElseThrow()),
"", enumField)}${helper.getFieldOptions(typedField, parserArguments)}))
<#if enumField.name == "value">
<#assign valueDefined=true>
</#if>
@@ -182,22 +185,24 @@ class ${type.name}:
<#break>
<#case "reserved">
<#assign
reservedField=field.asReservedField().orElseThrow()>
+ <#assign typedField =
field.asTypedField().orElseThrow()>
# Reserved Field (Compartmentalized so the "reserved" variable
can't leak)
- reserved: ${helper.getLanguageTypeNameForField(field)} =
${helper.getReadBufferReadMethodCall(reservedField.type.asSimpleTypeReference().orElseThrow(),
"", reservedField)}
+ reserved: ${helper.getLanguageTypeNameForField(field)} =
${helper.getReadBufferReadMethodCall(reservedField.type.asSimpleTypeReference().orElseThrow(),
"", reservedField)}${helper.getFieldOptions(typedField, parserArguments)})
if reserved != ${helper.getReservedValue(reservedField)}:
<@emitImport import="import logging" />
logging.warning("Expected constant value " +
str(${reservedField.referenceValue}) + " but got " + str(reserved) + " for
reserved field.")
<#break>
<#case "simple">
<#assign
simpleField=field.asSimpleField().orElseThrow()>
+ <#assign typedField =
field.asTypedField().orElseThrow()>
<#if helper.isEnumField(simpleField)>
# Enum field (${simpleField.name})
- ${helper.camelCaseToSnakeCase(simpleField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(simpleField)} =
${helper.getNonPrimitiveLanguageTypeNameForField(simpleField)}(${helper.getReadBufferReadMethodCall(helper.getEnumBaseTypeReference(simpleField.type),
"", simpleField)})
+ ${helper.camelCaseToSnakeCase(simpleField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(simpleField)} =
${helper.getNonPrimitiveLanguageTypeNameForField(simpleField)}(${helper.getReadBufferReadMethodCall(helper.getEnumBaseTypeReference(simpleField.type),
"", simpleField)}${helper.getFieldOptions(typedField, parserArguments)}))
<#else>
# Simple Field (${simpleField.name})
- ${helper.camelCaseToSnakeCase(simpleField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(simpleField)} = <#if
simpleField.type.isSimpleTypeReference()>${helper.getReadBufferReadMethodCall(simpleField.type.asSimpleTypeReference().orElseThrow(),
"",
simpleField)}<#else>${simpleField.type.asComplexTypeReference().orElseThrow().name}IO.staticParse(readBuffer<#if
simpleField.params.isPresent()>, <#list field.params.orElseThrow() as
parserArgument>${helper.getLanguageType [...]
+ ${helper.camelCaseToSnakeCase(simpleField.name)}:
${helper.getNonPrimitiveLanguageTypeNameForField(simpleField)} = <#if
simpleField.type.isSimpleTypeReference()>${helper.getReadBufferReadMethodCall(simpleField.type.asSimpleTypeReference().orElseThrow(),
"", simpleField)}${helper.getFieldOptions(typedField,
parserArguments)})<#else>${simpleField.type.asComplexTypeReference().orElseThrow().name}IO.staticParse(readBuffer<#if
simpleField.params.isPresent()>, <#list field.params.o [...]
</#if>
<#if case.name == "Struct" ||
((case.name == "DATE_AND_TIME") &&
((simpleField.name == "year") || (simpleField.name == "month") ||
(simpleField.name == "day") || (simpleField.name == "hour") ||
(simpleField.name == "minutes") || (simpleField.name == "seconds"))) ||
@@ -289,7 +294,8 @@ class ${type.name}:
<#break>
<#case "DATE">
<#if helper.hasFieldsWithNames(case.fields, "year",
"month", "day")>
- value: LocalDate = LocalDate(int(year), (month == 0) ? 1 :
int(month), (day == 0) ? 1 : int(day))
+ <@emitImport import="import datetime" />
+ value: datetime = datetime.datetime(int(year), int(month),
int(day))
</#if>
<@emitImport import="from plc4py.spi.values.PlcValues import
PlcDATE" />
return PlcDATE(value)
@@ -304,12 +310,10 @@ class ${type.name}:
return PlcTIME_OF_DAY(value)
<#break>
<#case "DATE_AND_TIME">
- <#if helper.hasFieldsWithNames(case.fields, "year",
"month", "day", "hour", "minutes", "seconds", "nanos")>
- value: LocalDateTime = LocalDateTime(int(year), (month == 0) ? 1 :
int(month), (day == 0) ? 1 : int(day), int(hour), int(minutes), int(seconds),
int(nanos))
+ <#if helper.hasFieldsWithNames(case.fields, "year",
"month", "day", "hour", "minutes", "seconds", "microseconds")>
+ value: datetime = datetime.datetime(int(year), int(month),
int(day), int(hour), int(minutes), int(seconds), int(microseconds))
<#elseif helper.hasFieldsWithNames(case.fields, "year",
"month", "day", "hour", "minutes", "seconds")>
- value: LocalDateTime = LocalDateTime(int(year), (month == 0) ? 1 :
int(month), (day == 0) ? 1 : int(day), int(hour), int(minutes), int(seconds))
- <#elseif helper.hasFieldsWithNames(case.fields,
"secondsSinceEpoch")>
- value: LocalDateTime =
LocalDateTime.ofEpochSecond(secondsSinceEpoch, 0, ZoneOffset.UTC)
+ value: datetime = datetime.datetime(int(year), int(month),
int(day), int(hour), int(minutes), int(seconds))
</#if>
<@emitImport import="from plc4py.spi.values.PlcValues import
PlcDATE_AND_TIME" />
return PlcDATE_AND_TIME(value)
diff --git a/plc4py/plc4py/drivers/umas/UmasDevice.py
b/plc4py/plc4py/drivers/umas/UmasDevice.py
index e5a135886a..593a4e8cd7 100644
--- a/plc4py/plc4py/drivers/umas/UmasDevice.py
+++ b/plc4py/plc4py/drivers/umas/UmasDevice.py
@@ -21,6 +21,7 @@ from asyncio import AbstractEventLoop, Transport
from dataclasses import dataclass, field
from typing import Dict, List, Tuple, cast
+from plc4py.api.exceptions.exceptions import PlcFieldParseException
from plc4py.api.messages.PlcRequest import (
PlcBrowseRequest,
PlcReadRequest,
@@ -34,7 +35,11 @@ from plc4py.api.messages.PlcResponse import (
from plc4py.api.value.PlcValue import PlcResponseCode, PlcValue
from plc4py.drivers.umas.UmasConfiguration import UmasConfiguration
from plc4py.drivers.umas.UmasTag import UmasTag
-from plc4py.drivers.umas.UmasVariables import UmasVariable, UmasVariableBuilder
+from plc4py.drivers.umas.UmasVariables import (
+ UmasVariable,
+ UmasVariableBuilder,
+ UmasElementryVariable,
+)
from plc4py.protocols.umas.readwrite import (
UmasPDUReadUnlocatedVariableResponse,
)
@@ -110,6 +115,7 @@ from
plc4py.protocols.umas.readwrite.VariableWriteRequestReference import (
VariableWriteRequestReference,
)
from plc4py.spi.values.PlcValues import PlcNull
+from plc4py.protocols.umas.readwrite.UmasDataType import UmasDataType
@dataclass
@@ -413,9 +419,17 @@ class UmasDevice:
else:
quantity = 1
+ if request_tag.data_type == None:
+ request_tag.data_type = UmasDataType(
+ self.variables[request_tag.tag_name].data_type
+ )
+
+ value = DataItem.static_parse(
+ read_buffer, request_tag.data_type, request_tag.quantity
+ )
response_item = ResponseItem(
PlcResponseCode.OK,
- DataItem.static_parse(read_buffer, request_tag.data_type,
quantity),
+ value,
)
values[key] = response_item
@@ -469,6 +483,32 @@ class UmasDevice:
response = PlcReadResponse(PlcResponseCode.OK, values)
return response
+ def _get_internal_words(self, tag) -> UmasElementryVariable:
+ system_word_block = 0x002B
+ area = tag[1:3].upper()
+ if area == "SW":
+ area_offset = 80
+ word_number = (int(tag[3:]) * 2) + area_offset
+ data_type = UmasDataType.INT.value
+ base_offset = word_number // 0x100
+ offset = word_number % 0x100
+ return UmasElementryVariable(
+ tag, data_type, system_word_block, base_offset, offset
+ )
+
+ area = tag[1:2].upper()
+ if area == "S":
+ area_offset = 50
+ word_number = (int(tag[3:]) * 2) + area_offset
+ data_type = UmasDataType.BOOL.value
+ base_offset = word_number // 0x100
+ offset = word_number % 0x100
+ return UmasElementryVariable(
+ tag, data_type, system_word_block, base_offset, offset
+ )
+ else:
+ raise PlcFieldParseException("Unable to read non system addresses")
+
def _sort_tags_based_on_memory_address(self, request):
tag_list: List[List[Tuple[str, VariableReadRequestReference]]] = [[]]
current_list_index = 0
@@ -477,7 +517,10 @@ class UmasDevice:
for kea, tag in request.tags.items():
umas_tag = cast(UmasTag, tag)
base_tag_name = umas_tag.tag_name.split(".")[0]
- variable = self.variables[base_tag_name]
+ if base_tag_name[:1] != "%":
+ variable = self.variables[base_tag_name]
+ else:
+ variable = self._get_internal_words(base_tag_name)
if byte_count + variable.get_byte_length() > self.max_frame_size:
current_list_index += 1
@@ -510,14 +553,17 @@ class UmasDevice:
for kea, tag in request.tags.items():
umas_tag = cast(UmasTag, tag)
base_tag_name = umas_tag.tag_name.split(".")[0]
- variable = self.variables[base_tag_name]
+ if base_tag_name[:1] != "%":
+ variable = self.variables[base_tag_name]
+ else:
+ variable = self._get_internal_words(base_tag_name)
if byte_count + variable.get_byte_length() > self.max_frame_size:
current_list_index += 1
tag_list.append([])
current_list = tag_list[current_list_index]
byte_count = 0
- byte_count += variable.get_byte_length()
+ byte_count += variable.get_byte_length() + variable.data_type
current_list.append(
(
kea,
diff --git a/plc4py/plc4py/drivers/umas/UmasTag.py
b/plc4py/plc4py/drivers/umas/UmasTag.py
index 480a41dc85..c6f48fdf92 100644
--- a/plc4py/plc4py/drivers/umas/UmasTag.py
+++ b/plc4py/plc4py/drivers/umas/UmasTag.py
@@ -64,7 +64,7 @@ class UmasTag(PlcTag):
if "dataType" in matcher.groupdict()
and matcher.group("dataType") is not None
and len(matcher.group("dataType")) is not 0
- else cls._DEFAULT_DATA_TYPE
+ else None
)
return cls(tag_name, quantity, data_type)
diff --git a/plc4py/plc4py/drivers/umas/UmasVariables.py
b/plc4py/plc4py/drivers/umas/UmasVariables.py
index e730cbff16..92eb364b02 100644
--- a/plc4py/plc4py/drivers/umas/UmasVariables.py
+++ b/plc4py/plc4py/drivers/umas/UmasVariables.py
@@ -153,7 +153,7 @@ class UmasCustomVariable(UmasVariable):
if len(split_tag_address) > 1:
child_index = split_tag_address[1]
return self.children[child_index].get_write_variable_reference(
- ".".join(split_tag_address[1:])
+ ".".join(split_tag_address[1:]), value
)
else:
raise NotImplementedError("Unable to write structures of UDT's")
diff --git a/plc4py/plc4py/protocols/modbus/readwrite/DataItem.py
b/plc4py/plc4py/protocols/modbus/readwrite/DataItem.py
index 2eec738436..9d76786554 100644
--- a/plc4py/plc4py/protocols/modbus/readwrite/DataItem.py
+++ b/plc4py/plc4py/protocols/modbus/readwrite/DataItem.py
@@ -425,7 +425,7 @@ class DataItem:
if data_type == ModbusDataType.CHAR and number_of_values == int(1): #
CHAR
# Simple Field (value)
- value: str = read_buffer.read_str(8, logical_name="", encoding="")
+ value: str = read_buffer.read_str(8, logical_name="",
encoding='"UTF-8"')
return PlcCHAR(value)
if data_type == ModbusDataType.CHAR: # List
@@ -436,7 +436,9 @@ class DataItem:
for _ in range(item_count):
value.append(
PlcSTRING(
- str(read_buffer.read_str(8, logical_name="",
encoding=""))
+ str(
+ read_buffer.read_str(8, logical_name="",
encoding='"UTF-8"')
+ )
)
)
@@ -444,7 +446,7 @@ class DataItem:
if data_type == ModbusDataType.WCHAR and number_of_values == int(1):
# WCHAR
# Simple Field (value)
- value: str = read_buffer.read_str(16, logical_name="", encoding="")
+ value: str = read_buffer.read_str(16, logical_name="",
encoding='"UTF-16"')
return PlcWCHAR(value)
if data_type == ModbusDataType.WCHAR: # List
@@ -455,7 +457,11 @@ class DataItem:
for _ in range(item_count):
value.append(
PlcSTRING(
- str(read_buffer.read_str(16, logical_name="",
encoding=""))
+ str(
+ read_buffer.read_str(
+ 16, logical_name="", encoding='"UTF-16"'
+ )
+ )
)
)
diff --git a/plc4py/plc4py/protocols/simulated/readwrite/DataItem.py
b/plc4py/plc4py/protocols/simulated/readwrite/DataItem.py
index f5a2dc2bbf..02af3dea89 100644
--- a/plc4py/plc4py/protocols/simulated/readwrite/DataItem.py
+++ b/plc4py/plc4py/protocols/simulated/readwrite/DataItem.py
@@ -298,7 +298,7 @@ class DataItem:
if data_type == "CHAR" and number_of_values == int(1): # CHAR
# Simple Field (value)
- value: str = read_buffer.read_str(8, logical_name="", encoding="")
+ value: str = read_buffer.read_str(8, logical_name="",
encoding='"UTF-8"')
return PlcCHAR(value)
if data_type == "CHAR": # List
@@ -309,7 +309,9 @@ class DataItem:
for _ in range(item_count):
value.append(
PlcSTRING(
- str(read_buffer.read_str(8, logical_name="",
encoding=""))
+ str(
+ read_buffer.read_str(8, logical_name="",
encoding='"UTF-8"')
+ )
)
)
@@ -317,7 +319,7 @@ class DataItem:
if data_type == "WCHAR" and number_of_values == int(1): # WCHAR
# Simple Field (value)
- value: str = read_buffer.read_str(16, logical_name="", encoding="")
+ value: str = read_buffer.read_str(16, logical_name="",
encoding='"UTF-16"')
return PlcWCHAR(value)
if data_type == "WCHAR": # List
@@ -328,7 +330,11 @@ class DataItem:
for _ in range(item_count):
value.append(
PlcSTRING(
- str(read_buffer.read_str(16, logical_name="",
encoding=""))
+ str(
+ read_buffer.read_str(
+ 16, logical_name="", encoding='"UTF-16"'
+ )
+ )
)
)
@@ -336,13 +342,13 @@ class DataItem:
if data_type == "STRING": # STRING
# Simple Field (value)
- value: str = read_buffer.read_str(255, logical_name="",
encoding="")
+ value: str = read_buffer.read_str(255, logical_name="",
encoding='"UTF-8"')
return PlcSTRING(value)
if data_type == "WSTRING": # STRING
# Simple Field (value)
- value: str = read_buffer.read_str(255, logical_name="",
encoding="")
+ value: str = read_buffer.read_str(255, logical_name="",
encoding='"UTF-16"')
return PlcSTRING(value)
return None
diff --git a/plc4py/plc4py/protocols/umas/readwrite/DataItem.py
b/plc4py/plc4py/protocols/umas/readwrite/DataItem.py
index 83b54d806d..8e75e9bf42 100644
--- a/plc4py/plc4py/protocols/umas/readwrite/DataItem.py
+++ b/plc4py/plc4py/protocols/umas/readwrite/DataItem.py
@@ -24,12 +24,17 @@ from plc4py.spi.generation.ReadBuffer import ReadBuffer
from plc4py.spi.generation.WriteBuffer import WriteBuffer
from plc4py.spi.values.PlcValues import PlcBOOL
from plc4py.spi.values.PlcValues import PlcBYTE
+from plc4py.spi.values.PlcValues import PlcDATE
+from plc4py.spi.values.PlcValues import PlcDATE_AND_TIME
from plc4py.spi.values.PlcValues import PlcDINT
from plc4py.spi.values.PlcValues import PlcDWORD
from plc4py.spi.values.PlcValues import PlcINT
from plc4py.spi.values.PlcValues import PlcList
from plc4py.spi.values.PlcValues import PlcREAL
+from plc4py.spi.values.PlcValues import PlcSINT
from plc4py.spi.values.PlcValues import PlcSTRING
+from plc4py.spi.values.PlcValues import PlcTIME
+from plc4py.spi.values.PlcValues import PlcTIME_OF_DAY
from plc4py.spi.values.PlcValues import PlcUDINT
from plc4py.spi.values.PlcValues import PlcUINT
from plc4py.spi.values.PlcValues import PlcULINT
@@ -37,6 +42,7 @@ from plc4py.spi.values.PlcValues import PlcWORD
from plc4py.utils.GenericTypes import ByteOrder
from typing import List
from typing import cast
+import datetime
import logging
import math
@@ -101,7 +107,7 @@ class DataItem:
if data_type == UmasDataType.BYTE and number_of_values == int(1): #
BYTE
# Simple Field (value)
- value: int = read_buffer.read_unsigned_short(8, logical_name="")
+ value: int = read_buffer.read_byte("")
return PlcBYTE(value)
if data_type == UmasDataType.BYTE: # List
@@ -110,7 +116,7 @@ class DataItem:
item_count: int = int(number_of_values * int(8))
value: List[PlcValue] = []
for _ in range(item_count):
- value.append(PlcBOOL(bool(read_buffer.read_bit(""))))
+ value.append(PlcBYTE(int(read_buffer.read_byte(""))))
return PlcList(value)
if data_type == UmasDataType.WORD: # WORD
@@ -226,6 +232,98 @@ class DataItem:
)
return PlcList(value)
+ if data_type == UmasDataType.TIME and number_of_values == int(1): #
TIME
+
+ # Simple Field (value)
+ value: int = read_buffer.read_unsigned_long(32, logical_name="")
+
+ return PlcTIME(value)
+ if data_type == UmasDataType.TIME: # List
+ # Array field (value)
+ # Count array
+ item_count: int = int(number_of_values)
+ value: List[PlcValue] = []
+ for _ in range(item_count):
+ value.append(
+ PlcULINT(int(read_buffer.read_unsigned_long(32,
logical_name="")))
+ )
+
+ return PlcList(value)
+ if data_type == UmasDataType.DATE and number_of_values == int(1): #
DATE
+
+ # Simple Field (day)
+ day: int = read_buffer.read_unsigned_short(
+ 8, logical_name="", encoding="BCD"
+ )
+
+ # Simple Field (month)
+ month: int = read_buffer.read_unsigned_short(
+ 8, logical_name="", encoding="BCD"
+ )
+
+ # Simple Field (year)
+ year: int = read_buffer.read_unsigned_int(
+ 16, logical_name="", encoding="BCD"
+ )
+
+ value: datetime = datetime.datetime(int(year), int(month),
int(day))
+ return PlcDATE(value)
+ if data_type == UmasDataType.TOD and number_of_values == int(1): #
TIME_OF_DAY
+
+ # Simple Field (value)
+ value: int = read_buffer.read_unsigned_long(32, logical_name="")
+
+ return PlcTIME_OF_DAY(value)
+ if data_type == UmasDataType.TOD: # List
+ # Array field (value)
+ # Count array
+ item_count: int = int(number_of_values)
+ value: List[PlcValue] = []
+ for _ in range(item_count):
+ value.append(
+ PlcULINT(int(read_buffer.read_unsigned_long(32,
logical_name="")))
+ )
+
+ return PlcList(value)
+ if data_type == UmasDataType.DT and number_of_values == int(1): #
DATE_AND_TIME
+
+ # Simple Field (unused)
+ unused: int = read_buffer.read_unsigned_short(8, logical_name="")
+
+ # Simple Field (seconds)
+ seconds: int = read_buffer.read_unsigned_short(
+ 8, logical_name="", encoding="BCD"
+ )
+
+ # Simple Field (minutes)
+ minutes: int = read_buffer.read_unsigned_short(
+ 8, logical_name="", encoding="BCD"
+ )
+
+ # Simple Field (hour)
+ hour: int = read_buffer.read_unsigned_short(
+ 8, logical_name="", encoding="BCD"
+ )
+
+ # Simple Field (day)
+ day: int = read_buffer.read_unsigned_short(
+ 8, logical_name="", encoding="BCD"
+ )
+
+ # Simple Field (month)
+ month: int = read_buffer.read_unsigned_short(
+ 8, logical_name="", encoding="BCD"
+ )
+
+ # Simple Field (year)
+ year: int = read_buffer.read_unsigned_int(
+ 16, logical_name="", encoding="BCD"
+ )
+
+ value: datetime = datetime.datetime(
+ int(year), int(month), int(day), int(hour), int(minutes),
int(seconds)
+ )
+ return PlcDATE_AND_TIME(value)
return None
@staticmethod
@@ -265,13 +363,13 @@ class DataItem:
elif data_type == UmasDataType.BYTE and number_of_values == int(1): #
BYTE
# Simple Field (value)
value: int = _value.get_int()
- write_buffer.write_byte((value), 8, "value")
+ write_buffer.write_byte((value), "value")
elif data_type == UmasDataType.BYTE: # List
values: PlcList = cast(PlcList, _value)
for val in values.get_list():
- value: bool = val.get_bool()
- write_buffer.write_bit((value), "value")
+ value: List[int] = val.get_raw()
+ write_buffer.write_byte_array("", value)
elif data_type == UmasDataType.WORD: # WORD
# Simple Field (value)
@@ -347,6 +445,74 @@ class DataItem:
value: float = val.get_float()
write_buffer.write_float((value), 32, "value")
+ elif data_type == UmasDataType.TIME and number_of_values == int(1): #
TIME
+ # Simple Field (value)
+ value: int = _value.get_int()
+ write_buffer.write_unsigned_int((value), 32, "value")
+
+ elif data_type == UmasDataType.TIME: # List
+ values: PlcList = cast(PlcList, _value)
+ for val in values.get_list():
+ value: int = val.get_int()
+ write_buffer.write_unsigned_int((value), 32, "value")
+
+ elif data_type == UmasDataType.DATE and number_of_values == int(1): #
DATE
+ # Simple Field (day)
+ day: int = 0
+ write_buffer.write_byte((day), 8, "day")
+
+ # Simple Field (month)
+ month: int = 0
+ write_buffer.write_byte((month), 8, "month")
+
+ # Simple Field (year)
+ year: int = 0
+ write_buffer.write_unsigned_short((year), 16, "year")
+
+ elif data_type == UmasDataType.TOD and number_of_values == int(
+ 1
+ ): # TIME_OF_DAY
+ # Simple Field (value)
+ value: int = _value.get_int()
+ write_buffer.write_unsigned_int((value), 32, "value")
+
+ elif data_type == UmasDataType.TOD: # List
+ values: PlcList = cast(PlcList, _value)
+ for val in values.get_list():
+ value: int = val.get_int()
+ write_buffer.write_unsigned_int((value), 32, "value")
+
+ elif data_type == UmasDataType.DT and number_of_values == int(
+ 1
+ ): # DATE_AND_TIME
+ # Simple Field (unused)
+ unused: int = 0
+ write_buffer.write_byte((unused), 8, "unused")
+
+ # Simple Field (seconds)
+ seconds: int = 0
+ write_buffer.write_byte((seconds), 8, "seconds")
+
+ # Simple Field (minutes)
+ minutes: int = 0
+ write_buffer.write_byte((minutes), 8, "minutes")
+
+ # Simple Field (hour)
+ hour: int = 0
+ write_buffer.write_byte((hour), 8, "hour")
+
+ # Simple Field (day)
+ day: int = 0
+ write_buffer.write_byte((day), 8, "day")
+
+ # Simple Field (month)
+ month: int = 0
+ write_buffer.write_byte((month), 8, "month")
+
+ # Simple Field (year)
+ year: int = 0
+ write_buffer.write_unsigned_short((year), 16, "year")
+
@staticmethod
def get_length_in_bytes(
_value: PlcValue, data_type: UmasDataType, number_of_values: int
@@ -384,7 +550,7 @@ class DataItem:
size_in_bits += 8
elif data_type == UmasDataType.BYTE: # List
values: PlcList = cast(PlcList, _value)
- size_in_bits += len(values.get_list()) * 1
+ size_in_bits += len(values.get_list()) * 8
elif data_type == UmasDataType.WORD: # WORD
# Simple Field (value)
size_in_bits += 16
@@ -427,5 +593,43 @@ class DataItem:
elif data_type == UmasDataType.STRING: # List
values: PlcList = cast(PlcList, _value)
size_in_bits += len(values.get_list()) * 32
+ elif data_type == UmasDataType.TIME and number_of_values == int(1): #
TIME
+ # Simple Field (value)
+ size_in_bits += 32
+ elif data_type == UmasDataType.TIME: # List
+ values: PlcList = cast(PlcList, _value)
+ size_in_bits += len(values.get_list()) * 32
+ elif data_type == UmasDataType.DATE and number_of_values == int(1): #
DATE
+ # Simple Field (day)
+ size_in_bits += 8
+ # Simple Field (month)
+ size_in_bits += 8
+ # Simple Field (year)
+ size_in_bits += 16
+ elif data_type == UmasDataType.TOD and number_of_values == int(
+ 1
+ ): # TIME_OF_DAY
+ # Simple Field (value)
+ size_in_bits += 32
+ elif data_type == UmasDataType.TOD: # List
+ values: PlcList = cast(PlcList, _value)
+ size_in_bits += len(values.get_list()) * 32
+ elif data_type == UmasDataType.DT and number_of_values == int(
+ 1
+ ): # DATE_AND_TIME
+ # Simple Field (unused)
+ size_in_bits += 8
+ # Simple Field (seconds)
+ size_in_bits += 8
+ # Simple Field (minutes)
+ size_in_bits += 8
+ # Simple Field (hour)
+ size_in_bits += 8
+ # Simple Field (day)
+ size_in_bits += 8
+ # Simple Field (month)
+ size_in_bits += 8
+ # Simple Field (year)
+ size_in_bits += 16
return size_in_bits
diff --git a/plc4py/plc4py/protocols/umas/readwrite/UmasDataType.py
b/plc4py/plc4py/protocols/umas/readwrite/UmasDataType.py
index 69fcc90f31..061afb668b 100644
--- a/plc4py/plc4py/protocols/umas/readwrite/UmasDataType.py
+++ b/plc4py/plc4py/protocols/umas/readwrite/UmasDataType.py
@@ -38,7 +38,7 @@ class UmasDataType(AutoNumberEnum):
UNKNOWN13 = (13, int(1), int(1))
DATE = (14, int(3), int(4))
TOD = (15, int(3), int(4))
- DT = (16, int(3), int(4))
+ DT = (16, int(4), int(8))
UNKNOWN17 = (17, int(1), int(1))
UNKNOWN18 = (18, int(1), int(1))
UNKNOWN19 = (19, int(1), int(1))
diff --git a/plc4py/plc4py/spi/generation/ReadBuffer.py
b/plc4py/plc4py/spi/generation/ReadBuffer.py
index f250582b97..2a985cd7ba 100644
--- a/plc4py/plc4py/spi/generation/ReadBuffer.py
+++ b/plc4py/plc4py/spi/generation/ReadBuffer.py
@@ -24,7 +24,7 @@ import aenum
from bitarray import bitarray
from bitarray.util import ba2base, ba2int, zeros
-from plc4py.api.exceptions.exceptions import SerializationException
+from plc4py.api.exceptions.exceptions import SerializationException,
ParseException
from plc4py.api.messages.PlcMessage import PlcMessage
from plc4py.utils.GenericTypes import ByteOrder, ByteOrderAware
@@ -220,6 +220,7 @@ class ReadBufferByteBased(ReadBuffer):
self, bit_length: int = 16, logical_name: str = "", **kwargs
) -> int:
byte_order = kwargs.get("byte_order", self.byte_order)
+ encoding = kwargs.get("encoding", "")
if bit_length <= 0:
raise SerializationException("unsigned short must contain at least
1 bit")
elif bit_length > 16:
@@ -227,19 +228,41 @@ class ReadBufferByteBased(ReadBuffer):
else:
if byte_order == ByteOrder.LITTLE_ENDIAN:
endian_string = "<"
+ padded = bitarray(
+ self.bb[self.position : self.position + bit_length]
+ ) + (16 - bit_length) * bitarray("0")
else:
endian_string = ">"
- padded = (16 - bit_length) * bitarray("0") + bitarray(
- self.bb[self.position : self.position + bit_length]
- )
- result: int = struct.unpack(endian_string + "H", padded)[0]
- self.position += bit_length
- return result
+ padded = (16 - bit_length) * bitarray("0") + bitarray(
+ self.bb[self.position : self.position + bit_length]
+ )
+
+ if encoding == "BCD":
+ if bit_length % 4 != 0:
+ raise ParseException(
+ "'BCD' encoded fields must have a length that is a
multiple of 4 bits long"
+ )
+ result: int = 0
+ for i in range(0, bit_length, 4):
+ digit: int = ba2int(padded[i : i + 4])
+ if digit > 9:
+ raise ParseException(
+ "'BCD' encoded value is not a correctly encoded
BCD value"
+ )
+ multiplier = 10 ** ((int(bit_length - i) / 4) - 1)
+ result += multiplier * digit
+ self.position += bit_length
+ return result
+ else:
+ result: int = struct.unpack(endian_string + "H", padded)[0]
+ self.position += bit_length
+ return result
def read_unsigned_int(
self, bit_length: int = 32, logical_name: str = "", **kwargs
) -> int:
byte_order = kwargs.get("byte_order", self.byte_order)
+ encoding = kwargs.get("encoding", "")
if bit_length <= 0:
raise SerializationException("unsigned int must contain at least 1
bit")
elif bit_length > 32:
@@ -247,14 +270,36 @@ class ReadBufferByteBased(ReadBuffer):
else:
if byte_order == ByteOrder.LITTLE_ENDIAN:
endian_string = "<"
+ padded = bitarray(
+ self.bb[self.position : self.position + bit_length]
+ ) + (32 - bit_length) * bitarray("0")
else:
endian_string = ">"
- padded = (32 - bit_length) * bitarray("0") + bitarray(
- self.bb[self.position : self.position + bit_length]
- )
- result: int = struct.unpack(endian_string + "I", padded)[0]
- self.position += bit_length
- return result
+ padded = (32 - bit_length) * bitarray("0") + bitarray(
+ self.bb[self.position : self.position + bit_length]
+ )
+ if encoding == "BCD":
+ if bit_length % 4 != 0:
+ raise ParseException(
+ "'BCD' encoded fields must have a length that is a
multiple of 4 bits long"
+ )
+ if byte_order == ByteOrder.LITTLE_ENDIAN:
+ padded = padded[8:16] + padded[:8] + padded[24:32] +
padded[16:24]
+ result: int = 0
+ for i in range(0, bit_length, 4):
+ digit: int = ba2int(padded[i : i + 4])
+ if digit > 9:
+ raise ParseException(
+ "'BCD' encoded value is not a correctly encoded
BCD value"
+ )
+ multiplier = 10 ** ((int(bit_length - i) / 4) - 1)
+ result += multiplier * digit
+ self.position += bit_length
+ return result
+ else:
+ result: int = struct.unpack(endian_string + "I", padded)[0]
+ self.position += bit_length
+ return result
def read_unsigned_long(
self, bit_length: int = 64, logical_name: str = "", **kwargs
@@ -267,11 +312,14 @@ class ReadBufferByteBased(ReadBuffer):
else:
if byte_order == ByteOrder.LITTLE_ENDIAN:
endian_string = "<"
+ padded = bitarray(
+ self.bb[self.position : self.position + bit_length]
+ ) + (64 - bit_length) * bitarray("0")
else:
endian_string = ">"
- padded = (64 - bit_length) * bitarray("0") + bitarray(
- self.bb[self.position : self.position + bit_length]
- )
+ padded = (64 - bit_length) * bitarray("0") + bitarray(
+ self.bb[self.position : self.position + bit_length]
+ )
result: int = struct.unpack(endian_string + "Q", padded)[0]
self.position += bit_length
return result
@@ -301,11 +349,14 @@ class ReadBufferByteBased(ReadBuffer):
else:
if byte_order == ByteOrder.LITTLE_ENDIAN:
endian_string = "<"
+ padded = bitarray(
+ self.bb[self.position : self.position + bit_length]
+ ) + (16 - bit_length) * bitarray("0")
else:
endian_string = ">"
- padded = (16 - bit_length) * bitarray("0") + bitarray(
- self.bb[self.position : self.position + bit_length]
- )
+ padded = (16 - bit_length) * bitarray("0") + bitarray(
+ self.bb[self.position : self.position + bit_length]
+ )
result: int = struct.unpack(endian_string + "h", padded)[0]
self.position += bit_length
return result
@@ -319,11 +370,14 @@ class ReadBufferByteBased(ReadBuffer):
else:
if byte_order == ByteOrder.LITTLE_ENDIAN:
endian_string = "<"
+ padded = bitarray(
+ self.bb[self.position : self.position + bit_length]
+ ) + (32 - bit_length) * bitarray("0")
else:
endian_string = ">"
- padded = (32 - bit_length) * bitarray("0") + bitarray(
- self.bb[self.position : self.position + bit_length]
- )
+ 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
@@ -342,11 +396,14 @@ class ReadBufferByteBased(ReadBuffer):
else:
if byte_order == ByteOrder.LITTLE_ENDIAN:
endian_string = "<"
+ padded = bitarray(
+ self.bb[self.position : self.position + bit_length]
+ ) + (64 - bit_length) * bitarray("0")
else:
endian_string = ">"
- padded = (64 - bit_length) * bitarray("0") + bitarray(
- self.bb[self.position : self.position + bit_length]
- )
+ 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
diff --git a/plc4py/plc4py/spi/values/PlcValues.py
b/plc4py/plc4py/spi/values/PlcValues.py
index 3c2e5ca884..ed207cfb74 100644
--- a/plc4py/plc4py/spi/values/PlcValues.py
+++ b/plc4py/plc4py/spi/values/PlcValues.py
@@ -17,6 +17,7 @@
# under the License.
#
from dataclasses import dataclass
+from datetime import datetime
from typing import Any, Dict, List
from plc4py.api.value.PlcValue import PlcValue
@@ -34,11 +35,11 @@ class PlcCHAR(PlcValue[str]):
pass
-class PlcDATE(PlcValue[int]):
+class PlcDATE(PlcValue[datetime]):
pass
-class PlcDATE_AND_TIME(PlcValue[int]):
+class PlcDATE_AND_TIME(PlcValue[datetime]):
pass
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 f3d2ea13ad..edf295d8e9 100644
--- a/plc4py/tests/unit/plc4py/drivers/modbus/test_modbus_connection.py
+++ b/plc4py/tests/unit/plc4py/drivers/modbus/test_modbus_connection.py
@@ -28,7 +28,7 @@ import logging
from plc4py.spi.values.PlcValues import PlcINT, PlcREAL, PlcList
logger = logging.getLogger("testing")
-TEST_SERVER_IP = "192.168.190.174"
+TEST_SERVER_IP = "192.168.190.152"
@pytest.mark.asyncio
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 932456db1a..a7a37fb4d0 100644
--- a/plc4py/tests/unit/plc4py/drivers/umas/test_umas_connection.py
+++ b/plc4py/tests/unit/plc4py/drivers/umas/test_umas_connection.py
@@ -17,74 +17,329 @@
# under the License.
#
import asyncio
+import datetime
import logging
import time
+from typing import AsyncGenerator
import pytest
+import pytest_asyncio
+from plc4py.api.PlcConnection import PlcConnection
from plc4py.api.value.PlcValue import PlcResponseCode
from plc4py.PlcDriverManager import PlcDriverManager
from plc4py.spi.values.PlcValues import PlcBOOL, PlcINT, PlcREAL
+@pytest_asyncio.fixture
+async def connection() -> AsyncGenerator[PlcConnection, None]:
+ driver_manager = PlcDriverManager()
+ async with driver_manager.connection("umas://192.168.190.152:502") as
connection:
+ yield connection
+
+
@pytest.mark.asyncio
@pytest.mark.xfail
-async def manual_test_plc_driver_umas_connect():
- driver_manager = PlcDriverManager()
- async with driver_manager.connection("umas://127.0.0.1:5555") as
connection:
- assert connection.is_connected()
- assert not connection.is_connected()
+async def test_plc_driver_umas_connect(connection):
+ assert connection.is_connected
@pytest.mark.asyncio
@pytest.mark.xfail
-async def test_plc_driver_umas_read():
- log = logging.getLogger(__name__)
+async def test_plc_driver_umas_read_boolean(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == True
+ assert response_code == PlcResponseCode.OK
- driver_manager = PlcDriverManager()
- async with driver_manager.connection("umas://192.168.190.152:502") as
connection:
- with connection.read_request_builder() as builder:
- #builder.add_item(f"Random Tag {1}", "TESTING_10:BOOL")
- builder.add_item(f"Random Tag {2}", "TESTING_REAL:REAL")
- request = builder.build()
[email protected]
[email protected]
+async def test_plc_driver_umas_read_boolean_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING:BOOL"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == True
+ assert response_code == PlcResponseCode.OK
+
- future = connection.execute(request)
- response = await future
- value = response.tags["Random Tag 1"].value
- assert value == 0.0
[email protected]
[email protected]
+async def test_plc_driver_umas_read_int(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_INT"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 99
+ assert response_code == PlcResponseCode.OK
@pytest.mark.asyncio
@pytest.mark.xfail
-async def test_plc_driver_umas_write():
- log = logging.getLogger(__name__)
+async def test_plc_driver_umas_read_int_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_INT:INT"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 99
+ assert response_code == PlcResponseCode.OK
- driver_manager = PlcDriverManager()
- async with driver_manager.connection("umas://192.168.190.152:502") as
connection:
- with connection.write_request_builder() as builder:
- # builder.add_item(f"Random Tag {1}", "TESTING_10:BOOL",
PlcBOOL(True))
- # builder.add_item(f"Random Tag {2}", "TESTING_INT:INT",
PlcINT(10))
- # builder.add_item(f"Random Tag {3}", "TESTING_EBOOL:BOOL",
PlcBOOL(True))
- builder.add_item(f"Random Tag {4}", "TESTING_REAL:REAL",
PlcREAL(3.18))
- request = builder.build()
- future = connection.execute(request)
- response = await future
- value = response.tags["Random Tag 1"].response_code
- assert value == PlcResponseCode.OK
[email protected]
[email protected]
+async def test_plc_driver_umas_read_dint(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_DINT"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 763539
+ assert response_code == PlcResponseCode.OK
@pytest.mark.asyncio
@pytest.mark.xfail
-async def test_plc_driver_umas_browse():
- driver_manager = PlcDriverManager()
- async with driver_manager.connection("umas://192.168.190.174:502") as
connection:
- with connection.browse_request_builder() as builder:
- builder.add_query("All Tags", "*")
- request = builder.build()
+async def test_plc_driver_umas_read_dint_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_DINT:DINT"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 763539
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_ebool(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_EBOOL"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == True
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_ebool_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_EBOOL:BOOL"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == True
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_string(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_STRING"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == "Hello World"
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_string_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_STRING:STRING"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == "Hello World"
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_time(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_TIME"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 200000
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_time_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_TIME:TIME"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 200000
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_byte(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_BYTE"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 253
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_byte_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_BYTE:BYTE"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == 253
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_date(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_DATE"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == datetime.datetime(2024, 10, 25)
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_time_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_DATE:DATE"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == datetime.datetime(2024, 10, 25)
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_dt(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_DT"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == datetime.datetime(2000, 1, 10, 0, 40)
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_read_dt_with_data_type(connection):
+ tag_alias = "Random Tag"
+ tag_name = "TESTING_DT:DATE_AND_TIME"
+ with connection.read_request_builder() as builder:
+ builder.add_item(tag_alias, tag_name)
+ request = builder.build()
+ future = connection.execute(request)
+ response = await future
+ value = response.tags[tag_alias].value
+ response_code = response.tags[tag_alias].response_code
+ assert value == datetime.datetime(2000, 1, 10, 0, 40)
+ assert response_code == PlcResponseCode.OK
+
+
[email protected]
[email protected]
+async def test_plc_driver_umas_browse(connection):
+ with connection.browse_request_builder() as builder:
+ builder.add_query("All Tags", "*")
+ request = builder.build()
- future = connection.execute(request)
- response = await future
+ future = connection.execute(request)
+ response = await future
- pass
+ pass
diff --git a/protocols/umas/src/main/resources/protocols/umas/umas.mspec
b/protocols/umas/src/main/resources/protocols/umas/umas.mspec
index 878f64117c..de9079130a 100644
--- a/protocols/umas/src/main/resources/protocols/umas/umas.mspec
+++ b/protocols/umas/src/main/resources/protocols/umas/umas.mspec
@@ -257,11 +257,10 @@
[array bit value count 'numberOfValues' ]
]
['BYTE','1' BYTE
- [simple uint 8 value]
+ [simple byte value]
]
['BYTE' List
- // TODO: If the number of values is odd, add a reserved byte
- [array bit value count 'numberOfValues * 8' ]
+ [array byte value count 'numberOfValues * 8' ]
]
['WORD' WORD
[simple uint 16 value]
@@ -305,6 +304,32 @@
['STRING' List
[array float 32 value count 'numberOfValues']
]
+ ['TIME','1' TIME
+ [simple uint 32 value]
+ ]
+ ['TIME' List
+ [array uint 32 value count 'numberOfValues']
+ ]
+ ['DATE','1' DATE
+ [simple uint 8 day encoding='BCD']
+ [simple uint 8 month encoding='BCD']
+ [simple uint 16 year encoding='BCD']
+ ]
+ ['TOD','1' TIME_OF_DAY
+ [simple uint 32 value]
+ ]
+ ['TOD' List
+ [array uint 32 value count 'numberOfValues']
+ ]
+ ['DT','1' DATE_AND_TIME
+ [simple uint 8 unused]
+ [simple uint 8 seconds encoding='BCD']
+ [simple uint 8 minutes encoding='BCD']
+ [simple uint 8 hour encoding='BCD']
+ [simple uint 8 day encoding='BCD']
+ [simple uint 8 month encoding='BCD']
+ [simple uint 16 year encoding='BCD']
+ ]
]
]
@@ -324,7 +349,7 @@
['13' UNKNOWN13 ['1','1']]
['14' DATE ['4','3']]
['15' TOD ['4','3']]
- ['16' DT ['4','3']]
+ ['16' DT ['8','4']]
['17' UNKNOWN17 ['1','1']]
['18' UNKNOWN18 ['1','1']]
['19' UNKNOWN19 ['1','1']]
diff --git a/src/site/asciidoc/users/protocols/index.adoc
b/src/site/asciidoc/users/protocols/index.adoc
index 5ad3536941..4bb5e16e54 100644
--- a/src/site/asciidoc/users/protocols/index.adoc
+++ b/src/site/asciidoc/users/protocols/index.adoc
@@ -153,6 +153,13 @@
|icon:check[role="green"]
|icon:times[role="red"]
+|UMAS
+|icon:times[role="red"]
+|icon:times[role="red"]
+|icon:times[role="red"]
+|icon:times[role="red"]
+|icon:check[role="green"]
+
|===
Legend:
@@ -379,6 +386,17 @@ The following table contains a list of operations and the
protocols that support
|icon:question[role="red"]
|icon:question[role="red"]
+|UMAS
+|icon:question[role="red"]
+|icon:question[role="red"]
+|icon:check[role="green"]
+|icon:check[role="green"]
+|icon:check[role="green"]
+|icon:question[role="red"]
+|icon:question[role="red"]
+|icon:question[role="red"]
+|icon:question[role="red"]
+
|===
Legend:
diff --git a/src/site/asciidoc/users/protocols/umas.adoc
b/src/site/asciidoc/users/protocols/umas.adoc
index 18c2d00098..534ba8f08e 100644
--- a/src/site/asciidoc/users/protocols/umas.adoc
+++ b/src/site/asciidoc/users/protocols/umas.adoc
@@ -21,18 +21,20 @@
=== Connection String Options
-==== Modbus TCP
-
-include::../../../plc4j/drivers/all/src/site/generated/modbus-tcp.adoc[]
-
-==== Modbus RTU
-
-include::../../../plc4j/drivers/all/src/site/generated/modbus-rtu.adoc[]
-
-==== Modbus ASCII
-
-include::../../../plc4j/drivers/all/src/site/generated/modbus-ascii.adoc[]
+[cols="2,2a,2a,2a,4a"]
+|===
+|Name |Type |Default Value |Required |Description
+|Name 4+|UMAS
+|Code 4+|`umas`
+|Default Transport 4+|`tcp`
+|Supported Transports 4+|
+ - `tcp`
+5+|Config options:
+|`request-timeout` |INT |5000| |Default timeout for all types of requests.
+|`default-unit-identifier` |INT |1| |Unit-identifier or slave-id that
identifies the target PLC (On RS485 multiple Modbus Devices can be listening).
Defaults to 1.
++++
+|===
=== Supported Operations
[cols="2,2a,5a"]
@@ -46,19 +48,22 @@
include::../../../plc4j/drivers/all/src/site/generated/modbus-ascii.adoc[]
|
2+| `write`
+
+|
+2+| `browse`
|===
=== Individual Resource Address Format
==== Connection String
-Modbus has the following connection string format:-
+UMAS has the following connection string format:-
----
-modbus-tcp:{transport}://{ip-address}:{port}?{options}
+umas:{transport}://{ip-address}:{port}?{options}
----
An example connection string would look like:-
----
-modbus-tcp:tcp://127.0.0.1:502
+umas:tcp://127.0.0.1:502
----
Note the transport, port and option fields are optional.
@@ -68,23 +73,14 @@ Note the transport, port and option fields are optional.
In general all Modbus addresses have this format:
----
-{memory-Area}{start-address}:{data-type}[{array-size}]:{name-value-tag-options}
+{tag-name}.{child-name}.{child-name}:{data-type}[{array-size}]
----
+Depending on the type of tag the child-name parameters are optional.
+e.g. A tag with a BOOL data type could be 'TESTING_BOOL_1' whereas
+if it is a UDT the tag name is followed by the child 'TESTING_UDT_1.START'
which in itself could be a BOOL.
If the array-size part is omitted, the size-default of `1` is assumed.
-If the data-type part is omitted, it defaults to BOOL for Coils and Discrete
Inputs and INT for input, holding and extended registers.
-If the name-value-tag-options part is omitted, simply no configuration
fine-tuning is applied.
-
-Additionally address can contain tag configuration:
-----
-{unit-id: 123}
-----
-Specifying this value overrides value of `default-unit-id` parameter specified
at the connection string.
-
-----
-{byte-order: 'LITTLE_ENDIAN'}
-----
-With this, can the default byte-order be overridden on a per-tag basis. If not
provided the default-byte-order from the connection string is used, or
BIG_ENDIAN, if this is also not provided.
+If the data-type part is omitted, it defaults to the data type of the tag read
from the PLC.
==== Memory Areas
diff --git a/src/site/site.xml b/src/site/site.xml
index 0e4d6cd09a..8f37821fd3 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -87,6 +87,7 @@
<item name="PROFINET" href="users/protocols/profinet.html"/>
<item name="S7 (Step7)" href="users/protocols/s7.html"/>
<item name="Simulated" href="users/protocols/simulated.html"/>
+ <item name="UMAS" href="users/protocols/umas.html"/>
</item>
<item name="Transports" href="users/transports/index.html">
<item name="TCP" href="users/transports/tcp.html"/>