Connections
Important
Before starting a connection to a target make sure you created proper routes on the client and the target like described in the Routing chapter.
Connect to a remote device
>>> import pyads
>>> plc = pyads.Connection('127.0.0.1.1.1', pyads.PORT_TC3PLC1)
>>> plc.open()
>>> plc.close()
The connection will be closed automatically if the object runs out of scope, making
Connection.close()
optional.
A context notation (using with:
) can be used to open a connection:
>>> import pyads
>>> plc = pyads.Connection('127.0.0.1.1.1', pyads.PORT_TC3PLC1)
>>> with plc:
>>> # ...
The context manager will make sure the connection is closed, either when
the with
clause runs out, or an uncaught error is thrown.
Read and write by name
Values
Reading and writing values from/to variables on the target can be done with Connection.read_by_name()
and
Connection.write_by_name()
. Passing the plc_datatype is optional for both methods. If plc_datatype
is None the datatype will be queried from the target on the first call and cached inside the Connection
object. You can disable symbol-caching by setting the parameter cache_symbol_info to False.
Warning
Querying the datatype only works for basic datatypes.
For structs, lists and lists of structs you need provide proper definitions of the datatype and use
Connection.read_structure_by_name()
or Connection.read_list_by_name()
.
Examples:
>>> import pyads
>>> plc = pyads.Connection('127.0.0.1.1.1', pyads.PORT_TC3PLC1):
>>> plc.open()
>>>
>>> plc.read_by_name('GVL.bool_value') # datatype will be queried and cached
True
>>> plc.read_by_name('GVL.bool_value') # cached datatype will be used
True
>>> plc.read_by_name('GVL.bool_value', cache_symbol_info=False) # datatype will not be cached and queried on each call
True
>>> plc.read_by_name('GVL.int_value', pyads.PLCTYPE_INT) # datatype is provided and will not be queried
0
>>> plc.write_by_name('GVL.int_value', 10) # write to target
>>> plc.read_by_name('GVL.int_value')
10
>>> plc.close()
If the name could not be found an Exception containing the error message and ADS Error number is raised.
>>> plc.read_by_name('GVL.wrong_name', pyads.PLCTYPE_BOOL)
ADSError: ADSError: symbol not found (1808)
For reading strings the maximum buffer length is 1024.
>>> plc.read_by_name('GVL.sample_string', pyads.PLCTYPE_STRING)
'Hello World'
>>> plc.write_by_name('GVL.sample_string', 'abc', pyads.PLCTYPE_STRING)
>>> plc.read_by_name('GVL.sample_string', pyads.PLCTYPE_STRING)
'abc'
Arrays
You can also read/write arrays. For this you simply need to multiply the datatype by the number of elements in the array or structure you want to read/write.
>>> plc.write_by_name('GVL.sample_array', [1, 2, 3], pyads.PLCTYPE_INT * 3)
>>> plc.read_by_name('GVL.sample_array', pyads.PLCTYPE_INT * 3)
[1, 2, 3]
>>> plc.write_by_name('GVL.sample_array[0]', 5, pyads.PLCTYPE_INT)
>>> plc.read_by_name('GVL.sample_array[0]', pyads.PLCTYPE_INT)
5
Structures of the same datatype
TwinCAT declaration:
TYPE sample_structure :
STRUCT
rVar : LREAL;
rVar2 : LREAL;
rVar3 : LREAL;
rVar4 : ARRAY [1..3] OF LREAL;
END_STRUCT
END_TYPE
Python code:
>>> plc.write_by_name('GVL.sample_structure',
[11.1, 22.2, 33.3, 44.4, 55.5, 66.6],
pyads.PLCTYPE_LREAL * 6)
>>> plc.read_by_name('GVL.sample_structure', pyads.PLCTYPE_LREAL * 6)
[11.1, 22.2, 33.3, 44.4, 55.5, 66.6]
>>> plc.write_by_name('GVL.sample_structure.rVar2', 1234.5, pyads.PLCTYPE_LREAL)
>>> plc.read_by_name('GVL.sample_structure.rVar2', pyads.PLCTYPE_LREAL)
1234.5
Structures with multiple datatypes
The structure in the PLC must be defined with `{attribute ‘pack_mode’ := ‘1’}.
TwinCAT declaration:
{attribute 'pack mode' := '1'}
TYPE sample_structure :
STRUCT
rVar : LREAL;
rVar2 : REAL;
iVar : INT;
iVar2 : ARRAY [1..3] OF DINT;
sVar : STRING;
END_STRUCT
END_TYPE
Python code:
First declare a tuple which defines the PLC structure. This should match the order as declared in the PLC. Information is passed and returned using the OrderedDict type.
>>> structure_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)
... )
>>> vars_to_write = OrderedDict([
... ('rVar', 11.1),
... ('rar2', 22.2),
... ('iVar', 3),
... ('iVar2', [4, 44, 444]),
... ('sVar', 'abc')]
... )
>>> plc.write_structure_by_name('global.sample_structure', vars_to_write, structure_def)
>>> 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')])
Read and write by handle
When reading and writing by name, internally pyads is acquiring a handle from the PLC, reading/writing the value using that handle, before releasing the handle. A handle is just a unique identifier that the PLC associates to an address meaning that should an address change, the ADS client does not need to know the new address.
It is possible to manage the acquiring, tracking and releasing of handles yourself, which is advantageous if you plan on reading/writing the value frequently in your program, or wish to speed up the reading/writing by up to three times; as by default when reading/writing by name it makes 3 ADS calls (acquire, read/write, release), where as if you track the handles manually it only makes a single ADS call.
Using the Connection class:
>>> var_handle = plc.get_handle('global.bool_value')
>>> plc.write_by_name('', True, pyads.PLCTYPE_BOOL, handle=var_handle)
>>> plc.read_by_name('', pyads.PLCTYPE_BOOL, handle=var_handle)
True
>>> plc.release_handle(var_handle)
Be aware to release handles before closing the port to the PLC. Leaving handles open reduces the available bandwidth in the ADS router.
Read and write by address
Read and write UDINT variables by address.
>>> import pyads
>>> plc = pyads.Connection('127.0.0.1.1.1', pyads.PORT_TC3PLC1)
>>> plc.open()
>>> # write 65536 to memory byte MDW0
>>> plc.write(INDEXGROUP_MEMORYBYTE, 0, 65536, pyads.PLCTYPE_UDINT)
>>> # write memory byte MDW0
>>> plc.read(INDEXGROUP_MEMORYBYTE, 0, pyads.PLCTYPE_UDINT)
65536
>>> plc.close()
Toggle bitsize variables by address.
>>> # read memory bit MX100.0
>>> data = plc.read(INDEXGROUP_MEMORYBIT, 100*8 + 0, pyads.PLCTYPE_BOOL)
>>> # write inverted value to memory bit MX100.0
>>> plc.write(INDEXGROUP_MEMORYBIT, 100*8 + 0, not data)
Read and write multiple variables with one command
Reading and writing of multiple values can be performed in a single transaction. After the first operation, the symbol info is cached for future use.
>>> import pyads
>>> plc = pyads.Connection('127.0.0.1.1.1', pyads.PORT_TC3PLC1)
>>> var_list = ['MAIN.b_Execute', 'MAIN.str_TestString', 'MAIN.r32_TestReal']
>>> plc.read_list_by_name(var_list)
{'MAIN.b_Execute': True, 'MAIN.str_TestString': 'Hello World', 'MAIN.r32_TestReal': 123.45}
>>> write_dict = {'MAIN.b_Execute': False, 'MAIN.str_TestString': 'Goodbye World', 'MAIN.r32_TestReal': 54.321}
>>> plc.write_list_by_name(write_dict)
{'MAIN.b_Execute': 'no error', 'MAIN.str_TestString': 'no error', 'MAIN.r32_TestReal': 'no error'}
Device Notifications
ADS supports device notifications, meaning you can pass a callback that gets executed if a certain variable changes its state. However as the callback gets called directly from the ADS DLL you need to extract the information you need from the ctypes variables which are passed as arguments to the callback function. A sample for adding a notification for an integer variable can be seen here:
>>> import pyads
>>> from ctypes import sizeof
>>>
>>>
>>> plc = pyads.Connection('127.0.0.1.1.1', pyads.PORT_TC3PLC1)
>>> plc.open()
>>> tags = {"GVL.integer_value": pyads.PLCTYPE_INT}
>>>
>>> # define the callback which extracts the value of the variable
>>> def mycallback(notification, data):
>>> data_type = tags[data]
>>> handle, timestamp, value = plc.parse_notification(notification, data_type)
>>> print(value)
>>>
>>> attr = pyads.NotificationAttrib(sizeof(pyads.PLCTYPE_INT))
>>>
>>> # add_device_notification returns a tuple of notification_handle and
>>> # user_handle which we just store in handles
>>> handles = plc.add_device_notification('GVL.integer_value', attr, mycallback)
>>>
>>> # To remove the device notification use the del_device_notification function.
>>> plc.del_device_notification(handles)
>>> plc.close()
This examples uses the default values for NotificationAttrib
. The
default behaviour is that you get notified when the value of the
variable changes on the server. If you want to change this behaviour you
can set the NotificationAttrib.trans_mode
attribute to one of the
following values:
ADSTRANS_SERVERONCHA
(default)a notification will be sent everytime the value of the specified variable changes
ADSTRANS_SERVERCYCLE
a notification will be sent on a cyclic base, the interval is specified by the
cycle_time
property
ADSTRANS_NOTRANS
no notifications will be sent
For more information about the NotificationAttrib settings have a look at Beckhoffs specification of the AdsNotificationAttrib struct.
Device Notification callback decorator
To make the handling of notifications more pythonic a notification decorator has been introduced in version 2.2.4. This decorator takes care of converting the ctype values transferred via ADS to python datatypes.
>>> import pyads
>>> plc = pyads.Connection('127.0.0.1.1.1', 48898)
>>> plc.open()
>>>
>>> @plc.notification(pyads.PLCTYPE_INT)
>>> def callback(handle, name, timestamp, value):
>>> print(
>>> '{1}: received new notitifiction for variable "{0}", value: {2}'
>>> .format(name, timestamp, value)
>>> )
>>>
>>> plc.add_device_notification('GVL.intvar', pyads.NotificationAttrib(2),
callback)
>>> # Write to the variable to trigger a notification
>>> plc.write_by_name('GVL.intvar', 123, pyads.PLCTYPE_INT)
2017-10-01 10:41:23.640000: received new notitifiction for variable "GVL.intvar", value: abc
Structures can be read in a this way by requesting bytes directly from the PLC. Usage is similar to reading structures by name where you must first declare a tuple defining the PLC structure.
>>> structure_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))
>>>
>>> size_of_struct = pyads.size_of_structure(structure_def)
>>>
>>> @plc.notification(size_of_struct)
>>> def callback(handle, name, timestamp, value):
... values = pyads.dict_from_bytes(value, structure_def)
... print(values)
>>>
>>> attr = pyads.NotificationAttrib(ctypes.sizeof(size_of_struct))
>>> plc.add_device_notification('global.sample_structure', attr, callback)
OrderedDict([('rVar', 11.1), ('rVar2', 22.2), ('iVar', 3), ('iVar2', [4, 44, 444]), ('sVar', 'abc')])
The notification callback works for all basic plc datatypes but not for
arrays. Since version 3.0.5 the ctypes.Structure
datatype is
supported. Find an example below:
>>> class TowerEvent(Structure):
>>> _fields_ = [
>>> ("Category", c_char * 21),
>>> ("Name", c_char * 81),
>>> ("Message", c_char * 81)
>>> ]
>>>
>>> @plc.notification(TowerEvent)
>>> def callback(handle, name, timestamp, value):
>>> print(f'Received new event notification for {name}.Message = {value.Message}')