Using LogixDriver

Tags and Data Types

When creating the driver it will automatically upload all of the controller scope tag and their data type definitions. These definitions are required for the read() and write() methods to function. Those methods abstract away a lot of the details required for actually implementing the Ethernet/IP protocol. Uploading the tags could take a few seconds depending on the size of program and the network. It was decided this small upfront overhead provided a greater benefit to the user since they would not have to worry about specific implementation details for different types of tags. The init_tags kwarg is True by default, meaning that all of the controller scoped tags will be uploaded. init_program_tags is a separate flag to control whether or not all the program-scoped tags are uploaded as well. By default, init_program_tags is True, set to False to disable and only upload controller-scoped tags.

Below shows how the init tag options are equivalent to calling the get_tag_list() method.

>>> plc1 = LogixDriver('10.20.30.100')
>>> plc2 = LogixDriver('10.20.30.100', init_tags=False)
>>> plc2.get_tag_list()
>>> plc1.tags == plc2.tags
True
>>> plc3 = LogixDriver('10.20.30.100', init_program_tags=True)
>>> plc4 = LogixDriver('10.20.30.100')
>>> plc4.get_tag_list(program='*')  # '*' means all programs
>>> plc3.tags == plc4.tags
True

Tag Structure

Each tag definition is a dict containing all the details retrieved from the PLC. get_tag_list() returns a list of dicts for the tag list while the LogixDriver.tags property stores them as a dict of {tag name: definition}.

Tag Definition Properties:

tag_name

Symbolic name of the tag

instance_id

Internal PLC identifier for the tag. Used for reads/writes on v21+ controllers. Saves space in packet by not requiring the full tag name to be encoded into the request.

tag_type
  • 'atomic' base data types like BOOL, DINT, REAL, etc.

  • 'struct' complex data types like STRING, TIMER, PID, etc as well as UDTs and AOIs.

data_type
  • 'DINT'/'REAL'/etc name of data type for atomic types

  • {data type definition} for structures, detailed in Structure Definitions

data_type_name
  • the string name of the data type: 'DINT'/'REAL'/'TIMER'/'MyCoolUDT'

string

Optional string size if the tag is a STRING type (or custom string)

external_access

'Read/Write'/'Read Only'/'None' matches the External Access tag property in the PLC

dim

number dimensions defined for the tag

  • 0 - not an array

  • 1-3 - a 1 to 3 dimension array tag, e.g. DINT[5] -> 1, DINT[5,5] -> 2, DINT[5,5,5] -> 3

dimensions

length of each dimension defined, 0 if dimension does not exist. [dim0, dim1, dim2]

  • DINT[5] -> [5, 0, 0]

  • DINT[5, 10] -> [5, 10, 0]

  • DINT[5, 10, 15] -> [5, 10, 15]

alias

True/False if the tag is an alias to another.

Note

This is not documented, but an educated guess found through trial and error.

type_class

the DataType that was created for this tag

Structure Definitions

While uploading the tag list, any tags with complex data types will have the full definition of structure uploaded as well. Inside a tag definition, the data_type attribute will be a dict containing the structure definition. The LogixDriver.data_types property also provides access to these definitions as a dict of {data type name: definition}.

Data Type Properties:

name

Name of the data type, UDT, AOI, or builtin structure data types

attributes

List of names for each attribute in the structure. Does not include internal tags not shown in Logix, like the host DINT tag that BOOL attributes are mapped to.

template

dict with template definition. Used internally within LogixDriver, allows reading/writing of full structs and allows the read/write methods to monitor the request/response size.

internal_tags

A dict with each attribute (including internal, not shown in Logix attributes) of the structure containing the definition for the attribute, {attribute: {definition}}.

Definition:

tag_type

Same as Tag Structure

data_type

Same as Tag Structure

data_type_name

Same as Tag Structure

string

Same as Tag Structure

offset

Location/Byte offset of this tag’s data in the response data.

bit

Optional BOOL tags are aliased to internal hidden integer tags, this indicates which bit it is aliased to.

array

Optional Length of the array if this tag is an array, 0 if not an array,

Note

attributes and internal_tags do NOT include InOut parameters.

type_class

The DataType type that was created to represent this structure

Reading/Writing Tags

All reading and writing is handled by the read() and write() methods. The original pycomm and other similar libraries will have different methods for handling different types like strings and arrays, this is not necessary in pycomm3 due to uploading the tag list and creation of a DataType class for each type. Both methods accept any number of tags, they will automatically use the Multiple Service Packet (0x0A) service and track the request/return data size making sure to stay below the connection size. If there is a tag value that cannot fit within the request/reply packet, it will automatically handle that tag independently using the Read Tag Fragmented (0x52) or Write Tag Fragmented (0x53) requests. Users do not have to worry about the number of tags or their size in any single request, this is all handled automatically by the driver.

Program-Scoped Tags

Program-scoped tag names use the format Program:<program>.<tag>. For example, to access a tag named SomeTag in the program MainProgram you would use Program:MainProgram.SomeTag in the request. The tag list uploaded by the driver will also keep this format for the tag names.

Array Tags

To access an index of an array, include the index inside square brackets after the tag name. The format is the same as in Logix, where multiple dimensions are comma separated, e.g. an_array[5] for the 5th element of an_array or array2[1,0] to access the first element of the second dimension of array2. Not specifying an index is equivalent to index 0, i.e array == array[0].

Whether reading or writing, the number of elements needs to be specified. To do so, specify the number of elements inside curly braces at the end of the tag name, e.g. an_array{5} for 5-elements of an_array. If omitted, the number of elements is assumed to be 1, i.e. an_array == an_array[0] == an_array[0]{1}. Only a single element count is used. For 2 and 3 dimensional arrays, the element count is the total number of elements across all dimensions. The tables below show a couple examples of how the element count works for multi-dimension arrays.

array (DINT[3, 2])

array{4}

array[1,1]{3}

array[0, 0]

X

array[0, 1]

X

array[1, 0]

X

array[1, 1]

X

X

array[2, 0]

X

array[2, 1]

X

array (SINT[2, 2, 2])

array{4}

array[0,1,0]{5}

array[0, 0, 0]

X

array[0, 0, 1]

X

array[0, 1, 0]

X

X

array[0, 1, 1]

X

X

array[1, 0, 0]

X

array[1, 0, 1]

X

array[1, 1, 0]

X

array[1, 1, 1]

BOOL Arrays

BOOL arrays work a little differently due them being implemented as DWORD arrays in the PLC. (That is the reason you can only make BOOL arrays in multiples of 32, DWORDs are 32 bits.) The element count in the request ('{#}') represents the number of BOOL elements. To write multiple elements to a BOOL array, you must write the entire underlying DWORD element. This means the list of values must be in multiples of 32 and the starting index must also be multiples of 32, e.g. 'bools{32}', 'bools[32]{64}'. There is no limitation on reading multiple elements or reading and writing a single element.

Reading Tags

LogixDriver.read() accepts any number of tags, all that is required is the tag names.Reading of entire structures is support as long as none of the attributes have an external access of None. To read a structure, just request the base name and the value for the Tag object will a a dict of {attribute: value}

Read an atomic tag

>>> plc.read('dint_tag')
Tag(tag='dint_tag', value=0, type='DINT', error=None)

Read multiple tags

>>> plc.read('tag_1', 'tag_2', 'tag_3')
[Tag(tag='tag_1', value=100, type='INT', error=None), Tag(tag='tag_2', value=True, type='BOOL', error=None), ...]

Read a structure

>>> plc.read('simple_udt')
Tag(tag='simple_udt', value={'attr1': 0, 'attr2': False, 'attr3': 1.234}, type='SimpleUDT', error=None)

Read arrays

>>> plc.read('dint_array{5}')  # starts at index 0
Tag(tag='dint_array', value=[1, 2, 3, 4, 5], type='DINT[5]', error=None)
>>> plc.read('dint_array[20]{3}') # read 3 elements starting at index 20
Tag(tag='dint_array[20]', value=[20, 21, 22], type='DINT[3]', error=None)

Verify all reads were successful

>>> tag_list = ['tag1', 'tag2', ...]
>>> results = plc.read(*tag_list)
>>> if all(results):
...     print('All tags read successfully')
All tags read successfully

Writing Tags

LogixDriver.write() method accepts any number of tag-value pairs of the tag name and value to be written. For writing a single tag, you can do write(<tag name>, <value>), but for multiple tags a sequence of tag-value tuples is required (write((<tag1>, <value1>), (<tag2>, <value2>))). For arrays, the value should be a list of the values to write. A RequestError will be raised if the value list is too short, else it will be truncated if too long. Writing a structure is supported as long as all attributes have Read/Write external access. The value for a struct should be a dict of {<attribute name>: <value>}, nesting as needed. It is not recommended to write full structures for builtin types, like TIMER, PID, etc.

Write a tag

>>> plc.write('dint_tag', 100)
Tag(tag='dint_tag', value=100, type='DINT', error=None)

Write many tags

>>> plc.write(('tag_1', 1), ('tag_2', True), ('tag_3', 1.234))
[Tag(tag='tag_1', value=1, type='INT', error=None), Tag(tag='tag_2', value=True, type='BOOL', error=None), ...]

Write arrays

>>> plc.write('dint_array{10}', list(range(10)))  # starts at index 0
Tag(tag='dint_array', value=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], type='DINT[10]', error=None)
>>> plc.write(('dint_array[10]{3}', [10, 11, 12]))  # write 3 elements starting at index 10
Tag(tag='dint_array[10]', value=[10, 11, 12], type='DINT[3]', error=None)

Write structures

>>> plc.write('my_udt', {'attr1': 100, 'attr2': [1, 2, 3, 4]})
Tag(tag='my_udt', value={'attr1': 100, 'attr2': [1, 2, 3, 4]}, type='MyUDT', error=None)

Check if all writes were successful

>>> tag_values = [('tag1', 10), ('tag2', True), ('tag3', 12.34)]
>>> results = plc.write(*tag_values)
>>> if all(results):
...     print('All tags written successfully')
All tags written successfully

String Tags

Strings are technically structures within the PLC, but are treated as atomic types in this library. There is no need to handle the LEN and DATA attributes, the structure is converted to/from Python str objects transparently. Any structures that contain only a DINT-LEN and a SINT[]-DATA attributes will be automatically treated as string tags. This allows the builtin STRING types plus custom strings to be handled automatically. Strings that are longer than the plc tag will be truncated when writing.

>>> plc.read('string_tag')
Tag(tag='string_tag', value='Hello World!', type='STRING', error=None)
>>> plc.write(('short_string_tag', 'Test Write'))
Tag(tag='short_string_tag', value='Test Write', type='STRING20', error=None)