Skip to content

Commit

Permalink
Added feature for processing nested structures (#384)
Browse files Browse the repository at this point in the history
* 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)

---------
  • Loading branch information
msfur authored Aug 30, 2024
1 parent 868df3d commit 7a27561
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions doc/documentation/connection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
16 changes: 16 additions & 0 deletions pyads/ads.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
156 changes: 156 additions & 0 deletions tests/test_ads.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit 7a27561

Please sign in to comment.