From 7a27561674226abba7e475b671424bf9fc4dda78 Mon Sep 17 00:00:00 2001 From: msfur Date: Fri, 30 Aug 2024 15:31:23 +0200 Subject: [PATCH] Added feature for processing nested structures (#384) * Modification of size_of_structure to be able to process nested structures * Added test for the modified size_of_structure to test the support for processing nested structures * Modification of dict_from_bytes to be able to process nested structures * Added test for the modified dict_from_bytes to test the support for processing nested structures * Modification of bytes_from_dict to be able to process nested structures * Added test for the modified bytes_from_dict to test the support for processing nested structures * Update of the documentation with a section on the use of nested structures * Add changelog entry * fixed some typos in the newly added nested structures section * Update CHANGELOG.md added section 3.5.0 (unreleased to changelog) --------- --- CHANGELOG.md | 5 + doc/documentation/connection.rst | 100 ++++++++++++++++++++ pyads/ads.py | 16 ++++ tests/test_ads.py | 156 +++++++++++++++++++++++++++++++ 4 files changed, 277 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57859947..60d77d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 3.5.0 (Unreleased) + +### Added +* [#384](https://github.com/stlehmann/pyads/pull/384) Enable processing of nested structures + ## 3.4.2 ### Changed diff --git a/doc/documentation/connection.rst b/doc/documentation/connection.rst index d571abbd..5ce4c0d8 100644 --- a/doc/documentation/connection.rst +++ b/doc/documentation/connection.rst @@ -188,6 +188,106 @@ using the OrderedDict type. >>> plc.read_structure_by_name('global.sample_structure', structure_def) OrderedDict([('rVar', 11.1), ('rVar2', 22.2), ('iVar', 3), ('iVar2', [4, 44, 444]), ('sVar', 'abc')]) +Nested Structures +^^^^^^^^^^^^^^^^^ + +**The structures in the PLC must be defined with \`{attribute ‘pack_mode’ +:= ‘1’}.** + +TwinCAT declaration of the sub structure: + +:: + + {attribute 'pack_mode' := '1'} + TYPE sub_sample_structure : + STRUCT + rVar : LREAL; + rVar2 : REAL; + iVar : INT; + iVar2 : ARRAY [1..3] OF DINT; + sVar : STRING; + END_STRUCT + END_TYPE + +TwinCAT declaration of the nested structure: + +:: + + {attribute 'pack_mode' := '1'} + TYPE sample_structure : + STRUCT + rVar : LREAL; + structVar: ARRAY [0..1] OF sub_sample_structure; + END_STRUCT + END_TYPE + +First declare a tuple which defines the PLC structure. This should match +the order as declared in the PLC. + +Declare the tuples either as + +.. code:: python + + >>> substructure_def = ( + ... ('rVar', pyads.PLCTYPE_LREAL, 1), + ... ('rVar2', pyads.PLCTYPE_REAL, 1), + ... ('iVar', pyads.PLCTYPE_INT, 1), + ... ('iVar2', pyads.PLCTYPE_DINT, 3), + ... ('sVar', pyads.PLCTYPE_STRING, 1) + ... ) + + >>> structure_def = ( + ... ('rVar', pyads.PLCTYPE_LREAL, 1), + ... ('structVar', substructure_def, 2) + ... ) + +or as + +.. code:: python + + >>> structure_def = ( + ... ('rVar', pyads.PLCTYPE_LREAL, 1), + ... ('structVar', ( + ... ('rVar', pyads.PLCTYPE_LREAL, 1), + ... ('rVar2', pyads.PLCTYPE_REAL, 1), + ... ('iVar', pyads.PLCTYPE_INT, 1), + ... ('iVar2', pyads.PLCTYPE_DINT, 3), + ... ('sVar', pyads.PLCTYPE_STRING, 1) + ... ), 2) + ... ) + +Information is passed and returned using the OrderedDict type. + +.. code:: python + + >>> from collections import OrderedDict + + >>> vars_to_write = collections.OrderedDict([ + ... ('rVar',0.1), + ... ('structVar', ( + ... OrderedDict([ + ... ('rVar', 11.1), + ... ('rVar2', 22.2), + ... ('iVar', 3), + ... ('iVar2', [4, 44, 444]), + ... ('sVar', 'abc') + ... ]), + ... OrderedDict([ + ... ('rVar', 55.5), + ... ('rVar2', 66.6), + ... ('iVar', 7), + ... ('iVar2', [8, 88, 888]), + ... ('sVar', 'xyz') + ... ])) + ... ) + ... ]) + + >>> plc.write_structure_by_name('GVL.sample_structure', vars_to_write, structure_def) + >>> plc.read_structure_by_name('GVL.sample_structure', structure_def) + ... OrderedDict({'rVar': 0.1, 'structVar': [OrderedDict({'rVar': 11.1, 'rVar2': 22.200000762939453, 'iVar': 3, 'iVar2': + ... [4, 44, 444], 'sVar': 'abc'}), OrderedDict({'rVar': 55.5, 'rVar2': 66.5999984741211, 'iVar': 7, 'iVar2': [8, 88, 888], + ... 'sVar': 'xyz'})]}) + Read and write by handle ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pyads/ads.py b/pyads/ads.py index 9ca4f401..323cdb73 100644 --- a/pyads/ads.py +++ b/pyads/ads.py @@ -268,6 +268,8 @@ def size_of_structure(structure_def: StructureDef) -> int: num_of_bytes += 2 * (str_len + 1) * size # WSTRING uses 2 bytes per character + null-terminator else: num_of_bytes += (PLC_DEFAULT_STRING_SIZE + 1) * 2 * size + elif type(plc_datatype) is tuple: + num_of_bytes += size_of_structure(plc_datatype) * size elif plc_datatype not in DATATYPE_MAP: raise RuntimeError("Datatype not found") else: @@ -334,6 +336,15 @@ def dict_from_bytes( null_idx = find_wstring_null_terminator(a) var_array.append(a[:null_idx].decode("utf-16-le")) index += n_bytes + elif type(plc_datatype) is tuple: + n_bytes = size_of_structure(plc_datatype) + var_array.append( + dict_from_bytes( + byte_list[index : (index + n_bytes)], + structure_def=plc_datatype, + ) + ) + index += n_bytes elif plc_datatype not in DATATYPE_MAP: raise RuntimeError("Datatype not found. Check structure definition") else: @@ -424,6 +435,11 @@ def bytes_from_dict( byte_list += encoded remaining_bytes = 2 * (str_len + 1) - len(encoded) # 2 bytes a character plus null-terminator byte_list.extend(remaining_bytes * [0]) + elif type(plc_datatype) is tuple: + bytecount = bytes_from_dict( + values=var[i], structure_def=plc_datatype + ) + byte_list += bytecount elif plc_datatype not in DATATYPE_MAP: raise RuntimeError("Datatype not found. Check structure definition") else: diff --git a/tests/test_ads.py b/tests/test_ads.py index ec189c5f..068db61d 100644 --- a/tests/test_ads.py +++ b/tests/test_ads.py @@ -180,6 +180,30 @@ def test_size_of_structure(self): ) self.assertEqual(pyads.size_of_structure(structure_def), 46) + # known structure size with defined string + substructure_def = ( + ("rVar", pyads.PLCTYPE_LREAL, 1), + ("sVar", pyads.PLCTYPE_STRING, 2, 35), + ("rVar1", pyads.PLCTYPE_REAL, 4), + ("iVar", pyads.PLCTYPE_DINT, 5), + ("iVar1", pyads.PLCTYPE_INT, 3), + ("ivar2", pyads.PLCTYPE_UDINT, 6), + ("iVar3", pyads.PLCTYPE_UINT, 7), + ("iVar4", pyads.PLCTYPE_BYTE, 1), + ("iVar5", pyads.PLCTYPE_SINT, 1), + ("iVar6", pyads.PLCTYPE_USINT, 1), + ("bVar", pyads.PLCTYPE_BOOL, 4), + ("iVar7", pyads.PLCTYPE_WORD, 1), + ("iVar8", pyads.PLCTYPE_DWORD, 1), + ) + + # test structure with array of nested structure + structure_def = ( + ('iVar9', pyads.PLCTYPE_USINT, 1), + ('structVar', substructure_def, 100), + ) + self.assertEqual(pyads.size_of_structure(structure_def), 17301) + def test_dict_from_bytes(self): # type: () -> None """Test dict_from_bytes function""" @@ -438,6 +462,72 @@ def test_dict_from_bytes(self): with self.assertRaises(TypeError): pyads.dict_from_bytes([], structure_def) + # tests for known values + substructure_def = ( + ("rVar", pyads.PLCTYPE_LREAL, 1), + ("sVar", pyads.PLCTYPE_STRING, 2, 35), + ("wsVar", pyads.PLCTYPE_WSTRING, 2, 10), + ("rVar1", pyads.PLCTYPE_REAL, 4), + ("iVar", pyads.PLCTYPE_DINT, 5), + ("iVar1", pyads.PLCTYPE_INT, 3), + ("ivar2", pyads.PLCTYPE_UDINT, 6), + ("iVar3", pyads.PLCTYPE_UINT, 7), + ("iVar4", pyads.PLCTYPE_BYTE, 1), + ("iVar5", pyads.PLCTYPE_SINT, 1), + ("iVar6", pyads.PLCTYPE_USINT, 1), + ("bVar", pyads.PLCTYPE_BOOL, 4), + ("iVar7", pyads.PLCTYPE_WORD, 1), + ("iVar8", pyads.PLCTYPE_DWORD, 1), + ) + subvalues = OrderedDict( + [ + ("rVar", 1.11), + ("sVar", ["Hello", "World"]), + ("wsVar", ["foo", "bar"]), + ("rVar1", [2.25, 2.25, 2.5, 2.75]), + ("iVar", [3, 4, 5, 6, 7]), + ("iVar1", [8, 9, 10]), + ("ivar2", [11, 12, 13, 14, 15, 16]), + ("iVar3", [17, 18, 19, 20, 21, 22, 23]), + ("iVar4", 24), + ("iVar5", 25), + ("iVar6", 26), + ("bVar", [True, False, True, False]), + ("iVar7", 27), + ("iVar8", 28), + ] + ) + # fmt: off + subbytes_list = [195, 245, 40, 92, 143, 194, 241, 63, 72, 101, 108, 108, 111, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 87, 111, 114, 108, 100, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 102, 0, 111, 0, 111, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 98, 0, 97, 0, 114, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, + 64, 0, 0, 16, 64, 0, 0, 32, 64, 0, 0, 48, 64, 3, 0, 0, 0, 4, + 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 8, 0, 9, 0, 10, + 0, 11, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 15, 0, + 0, 0, 16, 0, 0, 0, 17, 0, 18, 0, 19, 0, 20, 0, 21, 0, 22, 0, + 23, 0, 24, 25, 26, 1, 0, 1, 0, 27, 0, 28, 0, 0, 0] + + # test structure with array of nested structure + structure_def = ( + ('iVar9', pyads.PLCTYPE_USINT, 1), + ('structVar', substructure_def, 2), + ) + values = OrderedDict( + [ + ("iVar9", 29), + ("structVar", [subvalues, subvalues,]), + ] + ) + # fmt: off + bytes_list = [29] + subbytes_list + subbytes_list + + # fmt: on + self.assertEqual(values, pyads.dict_from_bytes(bytes_list, structure_def)) + def test_bytes_from_dict(self) -> None: """Test bytes_from_dict function""" # tests for known values @@ -691,6 +781,72 @@ def test_bytes_from_dict(self) -> None: with self.assertRaises(KeyError): pyads.bytes_from_dict(OrderedDict(), structure_def) + # tests for known values + substructure_def = ( + ("rVar", pyads.PLCTYPE_LREAL, 1), + ("sVar", pyads.PLCTYPE_STRING, 2, 35), + ("wsVar", pyads.PLCTYPE_WSTRING, 2, 10), + ("rVar1", pyads.PLCTYPE_REAL, 4), + ("iVar", pyads.PLCTYPE_DINT, 5), + ("iVar1", pyads.PLCTYPE_INT, 3), + ("ivar2", pyads.PLCTYPE_UDINT, 6), + ("iVar3", pyads.PLCTYPE_UINT, 7), + ("iVar4", pyads.PLCTYPE_BYTE, 1), + ("iVar5", pyads.PLCTYPE_SINT, 1), + ("iVar6", pyads.PLCTYPE_USINT, 1), + ("bVar", pyads.PLCTYPE_BOOL, 4), + ("iVar7", pyads.PLCTYPE_WORD, 1), + ("iVar8", pyads.PLCTYPE_DWORD, 1), + ) + subvalues = OrderedDict( + [ + ("rVar", 1.11), + ("sVar", ["Hello", "World"]), + ("wsVar", ["foo", "bar"]), + ("rVar1", [2.25, 2.25, 2.5, 2.75]), + ("iVar", [3, 4, 5, 6, 7]), + ("iVar1", [8, 9, 10]), + ("ivar2", [11, 12, 13, 14, 15, 16]), + ("iVar3", [17, 18, 19, 20, 21, 22, 23]), + ("iVar4", 24), + ("iVar5", 25), + ("iVar6", 26), + ("bVar", [True, False, True, False]), + ("iVar7", 27), + ("iVar8", 28), + ] + ) + # fmt: off + subbytes_list = [195, 245, 40, 92, 143, 194, 241, 63, 72, 101, 108, 108, 111, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 87, 111, 114, 108, 100, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 102, 0, 111, 0, 111, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 98, 0, 97, 0, 114, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, + 64, 0, 0, 16, 64, 0, 0, 32, 64, 0, 0, 48, 64, 3, 0, 0, 0, 4, + 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 8, 0, 9, 0, 10, + 0, 11, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 15, 0, + 0, 0, 16, 0, 0, 0, 17, 0, 18, 0, 19, 0, 20, 0, 21, 0, 22, 0, + 23, 0, 24, 25, 26, 1, 0, 1, 0, 27, 0, 28, 0, 0, 0] + + # test structure with array of nested structure + structure_def = ( + ('iVar9', pyads.PLCTYPE_USINT, 1), + ('structVar', substructure_def, 2), + ) + values = OrderedDict( + [ + ("iVar9", 29), + ("structVar", [subvalues, subvalues,]), + ] + ) + # fmt: off + bytes_list = [29] + subbytes_list + subbytes_list + + # fmt: on + self.assertEqual(bytes_list, pyads.bytes_from_dict(values, structure_def)) + def test_dict_slice_generator(self): """test _dict_slice_generator function.""" test_dict = {