Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added feature for processing nested structures #384

Merged
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
Loading