Skip to content

Commit

Permalink
Merge branch 'stlehmann:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
HectorBailey authored Jan 9, 2025
2 parents ce2fc9b + a9bf970 commit b168eee
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 32 deletions.
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ 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

### Changed

* [#437](https://github.com/stlehmann/pyads/pull/437) Solve issue of too little buffer space allocated to receive for automatic AMS NetID query
* [#438](https://github.com/stlehmann/pyads/pull/438) Fix issue with read list by name using structure defs if more than MAX_SUB_ADS_COMMANDS

## 3.4.2

### Changed

* [#402](https://github.com/stlehmann/pyads/pull/402) Universal DLL path for TwinCat 4026 and 4024

## 3.4.1

### Changed
* [#392](https://github.com/stlehmann/pyads/pull/392) Fixed bug where port left open in Linux if exception during connecting
* [#389](https://github.com/stlehmann/pyads/pull/389) / [#393](https://github.com/stlehmann/pyads/pull/393) Fix for DLL path in TwinCT 4026
* [#369](https://github.com/stlehmann/pyads/pull/304) Add test for [#304](https://github.com/stlehmann/pyads/pull/304) in `tests/test_testserver.py`
* [#304](https://github.com/stlehmann/pyads/pull/304) Implemented try-catch when closing ADS notifications in AdsSymbol destructor
* [#325](https://github.com/stlehmann/pyads/pull/325) Added missing ADS return codes

## 3.4.0

### Added
Expand Down Expand Up @@ -102,7 +127,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Changed
* fixed error with source distribution not containing adslib directory

### Removed

## 3.3.1
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ for communicating with TwinCAT devices. *pyads* uses the C API provided by *TcAd

Documentation: http://pyads.readthedocs.io/en/latest/index.html

Issues: In order to assist with issue management, please keep the issue tracker reserved for bugs and feature requests. For any questions, particularly around usage, route creation and ads error messages when reading or writing variables, please use [Stack Overflow](https://stackoverflow.com/) tagging the question with `twincat-ads` and state you are using the pyads library.
Issues: In order to assist with issue management, please keep the issue tracker reserved for bugs. For any questions or feature requests, please use the [discussions](https://github.com/stlehmann/pyads/discussions) area. Alternatively, questions can be posted to [Stack Overflow](https://stackoverflow.com/) tagged with `twincat-ads` and state you are using the pyads library. Please search around before posting questions, particulary around route creation and ads error messages when reading or writing variables as these are common issues.

# Installation

Expand All @@ -35,7 +35,7 @@ conda install pyads
From source:

```bash
git clone https://github.com/MrLeeh/pyads.git --recursive
git clone https://github.com/stlehmann/pyads.git --recursive
cd pyads
python setup.py install
```
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
2 changes: 1 addition & 1 deletion doc/documentation/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ operating systems in the sections below.

To identify each side of a route we will use the terms *client* and
*target*. The *client* is your computer where pyads runs on. The
*target* is you plc or remote computer which you want to connect to.
*target* is your plc or remote computer which you want to connect to.

Creating routes on Windows
--------------------------
Expand Down
2 changes: 1 addition & 1 deletion pyads/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,4 @@

from .symbol import AdsSymbol

__version__ = '3.4.0'
__version__ = '3.4.2'
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
15 changes: 11 additions & 4 deletions pyads/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
adsSyncAddDeviceNotificationReqEx,
adsSyncDelDeviceNotificationReqEx,
adsSyncSetTimeoutEx,
ADSError,
)
from .structs import (
AmsAddr,
Expand Down Expand Up @@ -195,7 +196,12 @@ def open(self) -> None:
self._port = adsPortOpenEx()

if linux:
adsAddRoute(self._adr.netIdStruct(), self.ip_address)
try:
adsAddRoute(self._adr.netIdStruct(), self.ip_address)
except ADSError:
adsPortCloseEx(self._port)
self._port = None
raise

self._open = True

Expand Down Expand Up @@ -592,8 +598,9 @@ def sum_read(port: int, adr: AmsAddr, data_names: List[str],
list(structure_defs.keys())) # type: ignore

for data_name, structure_def in structure_defs.items(): # type: ignore
result[data_name] = dict_from_bytes(result[data_name],
structure_def) # type: ignore
if data_name in result:
result[data_name] = dict_from_bytes(result[data_name],
structure_def)

return result

Expand Down Expand Up @@ -685,7 +692,7 @@ def write_by_name(
return adsSyncWriteByNameEx(
self._port, self._adr, data_name, value, plc_datatype, handle=handle
)

def write_list_by_name(
self,
data_names_and_values: Dict[str, Any],
Expand Down
36 changes: 19 additions & 17 deletions pyads/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,35 @@
ERROR_CODES = {
0: "no error",
1: "Internal error",
2: "No Rtime",
3: "Allocation locked memory error",
4: "Insert mailbox er",
5: "Wrong receive HMSG",
6: "target port not found ADS Server not started",
7: "target machine not found Missing ADS routes",
2: "No real time",
3: "Allocation locked - memory error",
4: "Mailbox full – the ADS message could not be sent.",
5: "Wrong HMSG",
6: "Target port not found - ADS Server not started",
7: "Target machine not found - Missing ADS routes",
8: "Unknown command ID",
9: "Bad task ID",
9: "Invalid task ID",
10: "No IO",
11: "Unknown ADS command",
11: "Unknown AMS command",
12: "Win 32 error",
13: "Port not connected",
14: "Invalid ADS length",
15: "Invalid ADS Net ID",
16: "Low Installation level",
14: "Invalid AMS length",
15: "Invalid AMS Net ID",
16: "Installation level is too low –TwinCAT 2 license error",
17: "No debug available",
18: "Port disabled",
19: "Port already connected",
20: "ADS Sync Win32 error",
21: "ADS Sync Timeout",
22: "ADS Sync AMS error",
23: "ADS Sync no index map",
24: "Invalid ADS port",
20: "AMS Sync Win32 error",
21: "AMS Sync Timeout",
22: "AMS Sync AMS error",
23: "No index map for AMS Sync available",
24: "Invalid AMS port",
25: "No memory",
26: "TCP send error",
27: "Host unreachable",
28: "Invalid AMS fragm",
28: "Invalid AMS fragment",
29: "TLS send error, secure ADS connection failed",
30: "Access denied, secure ADS access denied",
1280: "ROUTERERR_NOLOCKEDMEMORY",
1281: "ROUTERERR_RESIZEMEMORY",
1282: "ROUTERERR_MAILBOXFULL",
Expand Down
12 changes: 8 additions & 4 deletions pyads/pyads_ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@
# Starting with version 3.8, CPython does not consider the PATH environment
# variable any more when resolving DLL paths. The following works with the default
# installation of the Beckhoff TwinCAT ADS DLL.
dll_path = os.environ["TWINCAT3DIR"] + "\\..\\AdsApi\\TcAdsDll"
dll_path = os.environ["TWINCAT3DIR"] + "\\.."
if platform.architecture()[0] == "64bit":
dll_path += "\\x64"
dll_path += "\\Common64"
else:
dll_path += "\\Common32"
dlldir_handle = os.add_dll_directory(dll_path)
try:
_adsDLL = ctypes.WinDLL("TcAdsDll.dll") # type: ignore
Expand Down Expand Up @@ -392,8 +394,10 @@ def adsGetNetIdForPLC(ip_address: str) -> str:
data_header += struct.pack(">4s", b"\x00\x00\x00\x00") # Block of unknown

data, addr = send_raw_udp_message(
ip_address, data_header, 395
) # PLC response is 395 bytes long
ip_address, data_header, 398
)
# PLC response should be 395 bytes long, but some PLCs running build 4026+
# respond with more bytes, so this takes care of that

rcvd_packet_header = data[
0:12
Expand Down
7 changes: 5 additions & 2 deletions pyads/symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from . import constants # To access all constants, use package notation
from .constants import PLCDataType
from .pyads_ex import adsGetSymbolInfo
from .pyads_ex import adsGetSymbolInfo, ADSError
from .structs import NotificationAttrib

# ads.Connection relies on structs.AdsSymbol (but in type hints only), so use
Expand Down Expand Up @@ -229,7 +229,10 @@ def __repr__(self) -> str:

def __del__(self) -> None:
"""Destructor"""
self.clear_device_notifications()
try:
self.clear_device_notifications()
except ADSError:
pass # Quietly continue, without a connection no cleanup could be done

def add_device_notification(
self,
Expand Down
Loading

0 comments on commit b168eee

Please sign in to comment.