├── requirements.txt ├── tests ├── __init__.py └── test_arrays.py ├── counsyl_pyads ├── version.py ├── constants.py ├── adssymbol.py ├── adsstate.py ├── __init__.py ├── adsutils.py ├── adsconnection.py ├── binaryparser.py ├── adsexception.py ├── amspacket.py ├── adsconstants.py ├── adscommands.py ├── adsclient.py └── adsdatatypes.py ├── requirements-dev.txt ├── .gitignore ├── setup.py ├── LICENSE ├── bin └── twincat_plc_info.py ├── Makefile └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /counsyl_pyads/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | -------------------------------------------------------------------------------- /counsyl_pyads/constants.py: -------------------------------------------------------------------------------- 1 | """Library constants. 2 | 3 | This file contains constant used in this library that are not documented by 4 | Beckhoff. Beckhoff documented constants that describe the ADS protocol are in 5 | adsconstant.py. 6 | """ 7 | 8 | PYADS_ENCODING = 'windows-1252' 9 | -------------------------------------------------------------------------------- /counsyl_pyads/adssymbol.py: -------------------------------------------------------------------------------- 1 | class AdsSymbol(object): 2 | def __init__( 3 | self, index_group, index_offset, name, symtype, comment): 4 | self.index_group = index_group 5 | self.index_offset = index_offset 6 | self.name = name 7 | self.symtype = symtype 8 | self.comment = comment 9 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # pytest and dependencies 2 | pytest==2.8.5 3 | py==1.4.30 4 | pytest-cov==2.2.0 5 | coverage==4.0.3 6 | 7 | # flake8 and its requirements 8 | flake8==2.5.1 9 | mccabe==0.3.1 10 | pep8==1.5.7 11 | pyflakes==1.0.0 12 | 13 | # mock and its requirements 14 | mock==1.3.0 15 | funcsigs==0.4 16 | pbr==1.8.1 17 | six==1.10.0 18 | 19 | # radon and its requirements 20 | radon==1.4.0 21 | mando==0.3.3 22 | colorama==0.3.7 23 | -------------------------------------------------------------------------------- /counsyl_pyads/adsstate.py: -------------------------------------------------------------------------------- 1 | 2 | class AdsState(): 3 | 4 | def __init__(self): 5 | pass 6 | 7 | Invalid = 0 8 | Idle = 1 9 | Reset = 2 10 | Init = 3 11 | Start = 4 12 | Run = 5 13 | Stop = 6 14 | SaveConfig = 7 15 | LoadConfig = 8 16 | PowerFailure = 9 17 | PowerGood = 10 18 | Error = 11 19 | Shutdown = 12 20 | Suspend = 13 21 | Resume = 14 22 | Config = 15 23 | ReConfig = 16 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .coverage 34 | .coverage.* 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | 39 | # Dev environment leftovers 40 | .env/ 41 | .venv/ 42 | -------------------------------------------------------------------------------- /counsyl_pyads/__init__.py: -------------------------------------------------------------------------------- 1 | from .adsclient import AdsClient 2 | from .adsconnection import AdsConnection 3 | from .adsdatatypes import AdsDatatype 4 | from .adsexception import PyadsException 5 | from .adsexception import AdsException 6 | from .adsexception import PyadsTypeError 7 | from .adsstate import AdsState 8 | from .adssymbol import AdsSymbol 9 | from .amspacket import AmsPacket 10 | from .binaryparser import BinaryParser 11 | from .adsutils import HexBlock 12 | from .version import __version__ 13 | 14 | 15 | __all__ = [ 16 | "AdsClient", 17 | "AdsConnection", 18 | "AdsDatatype", 19 | "PyadsException", 20 | "AdsException", 21 | "PyadsTypeError", 22 | "AdsState", 23 | "AdsSymbol", 24 | "AmsPacket", 25 | "BinaryParser", 26 | "HexBlock", 27 | "__version__", 28 | ] 29 | -------------------------------------------------------------------------------- /counsyl_pyads/adsutils.py: -------------------------------------------------------------------------------- 1 | def HexBlock(data, width=8): 2 | i = 0 3 | result = '' 4 | currentHexLine = '' 5 | currentChrLine = '' 6 | 7 | for byte in data: 8 | # next line, if required 9 | if (i == width): 10 | result += '%s %s\n' % (currentHexLine, currentChrLine) 11 | currentHexLine = '' 12 | currentChrLine = '' 13 | i = 0 14 | 15 | # python2 / python3 - normalize to numeric byte 16 | char = ord(byte) if isinstance(byte, str) else byte 17 | 18 | # append to lines 19 | currentHexLine += '%02x ' % char 20 | currentChrLine += '.' if (char < 32 or char > 126) else chr(char) 21 | i += 1 22 | 23 | # append last line 24 | result += '%s %s' % (currentHexLine, currentChrLine) 25 | return result 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | exec(open('counsyl_pyads/version.py').read()) 4 | 5 | setup( 6 | name='counsyl-pyads', 7 | version=__version__, 8 | packages=find_packages(), 9 | scripts=['bin/twincat_plc_info.py'], 10 | include_package_data=True, 11 | zip_safe=False, 12 | author='Counsyl Inc.', 13 | author_email='opensource@counsyl.com', 14 | description='A library for directly interacting with a Twincat PLC.', 15 | url='https://github.com/counsyl/counsyl-pyads/', 16 | classifiers=[ 17 | 'Development Status :: 5 - Production/Stable', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Programming Language :: Python', 20 | 'Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator', 21 | 'Topic :: Software Development :: Libraries :: Python Modules', 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Christoph Wiedemann for portions from project pyads 4 | Copyright (c) 2014 - 2017 Counsyl Inc. and contributors for all other portions 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_arrays.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from counsyl_pyads.adsdatatypes import AdsArrayDatatype 4 | from counsyl_pyads.adsdatatypes import INT 5 | 6 | 7 | @pytest.fixture 8 | def dims_1dim(): 9 | return [(1, 3)] 10 | 11 | 12 | @pytest.fixture 13 | def flat_1dim(): 14 | return (9, 8, 7) 15 | 16 | 17 | @pytest.fixture 18 | def dict_1dim(): 19 | return {1: 9, 2: 8, 3: 7} 20 | 21 | 22 | @pytest.fixture 23 | def dims_2dim(): 24 | return [(1, 2), (3, 4)] 25 | 26 | 27 | @pytest.fixture 28 | def flat_2dim(): 29 | return (1, 2, 3, 5) 30 | 31 | 32 | @pytest.fixture 33 | def dict_2dim(): 34 | return {1: {3: 1, 4: 2}, 2: {3: 3, 4: 5}} 35 | 36 | 37 | class TestArray(object): 38 | 39 | def test_flatten_1dim(self, dims_1dim, flat_1dim, dict_1dim): 40 | arr = AdsArrayDatatype(INT, dims_1dim) 41 | dict_ = dict_1dim 42 | flat = arr._dict_to_flat_list(dict_) 43 | assert(flat == flat_1dim) 44 | 45 | def test_flatten_2dim(self, dims_2dim, flat_2dim, dict_2dim): 46 | arr = AdsArrayDatatype(INT, dims_2dim) 47 | dict_ = dict_2dim 48 | flat = arr._dict_to_flat_list(dict_) 49 | assert(flat == flat_2dim) 50 | 51 | def test_unflatten_1dim(self, dims_1dim, flat_1dim, dict_1dim): 52 | arr = AdsArrayDatatype(INT, dims_1dim) 53 | flat = flat_1dim 54 | dict_ = arr._flat_list_to_dict(flat) 55 | assert(dict_ == dict_1dim) 56 | 57 | def test_unflatten_2dim(self, dims_2dim, flat_2dim, dict_2dim): 58 | arr = AdsArrayDatatype(INT, dims_2dim) 59 | flat = flat_2dim 60 | dict_ = arr._flat_list_to_dict(flat) 61 | assert(dict_ == dict_2dim) 62 | -------------------------------------------------------------------------------- /bin/twincat_plc_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | import pprint 6 | 7 | from counsyl_pyads.adsconnection import AdsConnection 8 | from counsyl_pyads.adsclient import AdsClient 9 | 10 | 11 | def main(): 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument( 14 | 'target_ams', help='The Ams ID of the PLC.') 15 | parser.add_argument( 16 | 'target_host', help='The IP address or hostname of the PLC.') 17 | parser.add_argument( 18 | 'target_port', type=int, default=801, 19 | help='The port of the Twincat system on the PLC.') 20 | parser.add_argument( 21 | 'source_ams', 22 | help='The (arbitrary) Ams ID of the relay server.') 23 | args = parser.parse_args() 24 | 25 | logging.basicConfig( 26 | level=logging.DEBUG, 27 | format="[%(asctime)s][%(process)d:%(threadName)s]" 28 | "[%(filename)s/%(funcName)s][%(levelname)s] %(message)s") 29 | 30 | ads_conn = AdsConnection( 31 | target_ams=args.target_ams, 32 | target_ip=args.target_host, 33 | target_port=args.target_port, 34 | source_ams=args.source_ams, 35 | ) 36 | 37 | print("Target AMS: %s" % args.target_ams) 38 | print("Target host: %s:%s" % (args.target_host, args.target_port)) 39 | 40 | with AdsClient(ads_conn) as device: 41 | print "" 42 | print "DEVICE INFO" 43 | print "" 44 | pprint.pprint(device.read_device_info().__dict__) 45 | print "" 46 | print "SYMBOLS" 47 | print "" 48 | for sym in device.get_symbols(): 49 | pprint.pprint(sym.__dict__) 50 | 51 | 52 | if __name__ == '__main__': 53 | main() 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME?=counsyl_pyads 2 | TEST_OUTPUT_DIR?=test-output 3 | TEST_OUTPUT_XML?=nosetests.xml 4 | COVERAGE_DIR?=htmlcov 5 | COVERAGE_DATA?=.coverage 6 | 7 | VENV_DIR?=.venv 8 | ifneq ($(VIRTUAL_ENV),) 9 | VENV_DIR=$(VIRTUAL_ENV) 10 | endif 11 | VENV_ACTIVATE=$(VENV_DIR)/bin/activate 12 | WITH_VENV=. $(VENV_ACTIVATE); 13 | 14 | $(VENV_ACTIVATE): requirements.txt requirements-dev.txt 15 | test -f $@ || virtualenv --python=python2.7 $(VENV_DIR) 16 | $(WITH_VENV) pip install --no-deps -r requirements.txt 17 | $(WITH_VENV) pip install --no-deps -r requirements-dev.txt 18 | touch $@ 19 | 20 | default: 21 | python setup.py check build 22 | 23 | .PHONY: venv 24 | venv: $(VENV_ACTIVATE) 25 | 26 | .PHONY: setup 27 | setup: venv 28 | 29 | .PHONY: develop 30 | develop: venv 31 | $(WITH_VENV) python setup.py develop 32 | 33 | .PHONY: clean 34 | clean: 35 | python setup.py clean 36 | rm -rf build/ 37 | rm -rf dist/ 38 | rm -rf *.egg*/ 39 | rm -rf __pycache__/ 40 | rm -f MANIFEST 41 | rm -rf $(TEST_OUTPUT_DIR) 42 | rm -rf $(COVERAGE_DIR) 43 | rm -f $(COVERAGE_DATA) 44 | find $(PACKAGE_NAME) -type f -name '*.pyc' -delete 45 | 46 | .PHONY: teardown 47 | teardown: 48 | rm -rf $(VENV_DIR)/ 49 | 50 | .PHONY: lint 51 | lint: venv 52 | $(WITH_VENV) flake8 -v $(PACKAGE_NAME)/ $(LINT_ARGS) 53 | 54 | .PHONY: quality 55 | quality: venv 56 | $(WITH_VENV) radon cc -s $(PACKAGE_NAME)/ 57 | $(WITH_VENV) radon mi $(PACKAGE_NAME)/ 58 | 59 | .PHONY: test 60 | test: develop 61 | $(WITH_VENV) py.test -v \ 62 | --doctest-modules \ 63 | --ignore=setup.py \ 64 | --ignore=$(VENV_DIR) \ 65 | --junit-xml=$(TEST_OUTPUT_DIR)/$(TEST_OUTPUT_XML) \ 66 | --cov=${PACKAGE_NAME} \ 67 | --cov-report=html $(TEST_ARGS) 68 | 69 | .PHONY: sdist 70 | sdist: 71 | python setup.py sdist 72 | -------------------------------------------------------------------------------- /counsyl_pyads/adsconnection.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class AdsConnection(object): 5 | """Container for parameters necessary to establish an Ads connection.""" 6 | def __init__( 7 | self, target_ams, source_ams, target_ip=None, target_port=None): 8 | """ 9 | target_ams, source_ams must be of format 0.0.0.0.0.0:0 10 | target_ip, target_port are derived from target_ams if not provided 11 | """ 12 | target_ams_info = self.parse_ams(target_ams) 13 | 14 | self.target_ams_id = target_ams_info[0] 15 | self.target_ip = target_ip or target_ams_info[1] 16 | self.target_ams_port = target_port or target_ams_info[2] 17 | 18 | source_ams_info = self.parse_ams(source_ams) 19 | self.source_ams_id = source_ams_info[0] 20 | self.source_ams_port = source_ams_info[2] 21 | 22 | def parse_ams(self, ams_address): 23 | """Parses a full AMS address into AMS ID, IP and port. 24 | 25 | A full AMS address has the format 192.168.1.17.1.1:801. Splitting at 26 | the ':' results in AMS ID and port. The IP is the first four sections 27 | of the IP. 28 | 29 | This method returns a tuple ordered (AMS ID, IP, port). 30 | """ 31 | pattern = '([0-9\.]+):([0-9]+){1}' 32 | match = re.match(pattern, ams_address) 33 | if (match is None): 34 | raise Exception( 35 | "AmsId port format not valid. Expected format is " 36 | "'192.168.1.17.1.1:801'") 37 | 38 | ams_id = str(match.group(1)) 39 | tcp_ip = '.'.join(x for x in ams_id.split('.')[:4]) 40 | ams_port = int(match.group(2)) 41 | 42 | return (ams_id, tcp_ip, ams_port) 43 | 44 | def __str__(self): 45 | return "%s:%s --> %s:%s" % ( 46 | self.source_ams_id, 47 | self.source_ams_port, 48 | self.target_ams_id, 49 | self.target_ams_port, 50 | ) 51 | -------------------------------------------------------------------------------- /counsyl_pyads/binaryparser.py: -------------------------------------------------------------------------------- 1 | from struct import pack 2 | from struct import unpack_from 3 | from struct import calcsize 4 | 5 | 6 | class BinaryParser: 7 | 8 | def __init__(self, byteData=b''): 9 | self.ByteData = byteData 10 | self.Position = 0 11 | 12 | def Append(self, fmt, value): 13 | self.ByteData = self.ByteData + pack(fmt, value) 14 | 15 | def Unpack(self, fmt): 16 | result = unpack_from(fmt, self.ByteData, self.Position) 17 | self.Position = self.Position + calcsize(fmt) 18 | 19 | return result[0] 20 | 21 | def ReadBytes(self, length): 22 | result = '' 23 | for i in range(length): 24 | result = result + chr(self.ReadByte()) 25 | 26 | return result 27 | 28 | def WriteBytes(self, byteList): 29 | if (isinstance(byteList, str)): 30 | self.ByteData = self.ByteData + byteList 31 | return 32 | 33 | for b in byteList: 34 | self.WriteByte(b) 35 | 36 | def ReadUInt8(self): 37 | return self.ReadByte() 38 | 39 | def ReadByte(self): 40 | return self.Unpack('B') 41 | 42 | def WriteUInt8(self, value): 43 | self.WriteByte(value) 44 | 45 | def WriteByte(self, value): 46 | self.Append('B', value) 47 | 48 | def ReadInt8(self): 49 | return self.Unpack('b') 50 | 51 | def WriteInt8(self, value): 52 | self.Append('b', value) 53 | 54 | def ReadInt16(self): 55 | return self.Unpack('h') 56 | 57 | def WriteInt16(self, value): 58 | self.Append('h', value) 59 | 60 | def ReadUInt16(self): 61 | return self.Unpack('H') 62 | 63 | def WriteUInt16(self, value): 64 | self.Append('H', value) 65 | 66 | def ReadInt32(self): 67 | return self.Unpack('i') 68 | 69 | def WriteInt32(self, value): 70 | self.Append('i', value) 71 | 72 | def ReadUInt32(self): 73 | return self.Unpack('I') 74 | 75 | def WriteUInt32(self, value): 76 | self.Append('I', value) 77 | 78 | def ReadInt64(self): 79 | return self.Unpack('q') 80 | 81 | def WriteInt64(self, value): 82 | self.Append('q', value) 83 | 84 | def ReadUInt64(self): 85 | return self.Unpack('Q') 86 | 87 | def WriteUInt64(self, value): 88 | self.Append('Q', value) 89 | 90 | def ReadDouble(self): 91 | return self.Unpack('d') 92 | 93 | def WriteDouble(self, value): 94 | self.Append('d', value) 95 | 96 | def ReadFloat(self): 97 | return self.Unpack('f') 98 | 99 | def WriteFloat(self, value): 100 | self.Append('f', value) 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | counsyl-pyads 2 | ============= 3 | 4 | This Python package contains a Python-only implementation of the AMS/ADS (Automation Device Specification) protocol for communicating directly with Beckhoff PLCs. 5 | No router is included or required. 6 | It is a fork of the [pyads library by Christoph Wiedemann](https://github.com/chwiede/pyads), with support for more data types (including arrays). 7 | 8 | 9 | ### Installation 10 | 11 | ``` 12 | git clone git@github.com:counsyl/counsyl-pyads.git 13 | cd counsyl-pyads 14 | # consider making yourself a virtualenv 15 | pip install -r requirements.txt 16 | python setup.py install 17 | ``` 18 | 19 | ### PLC setup 20 | 21 | Beckhoff PLCs won't accept connections from just anyone. The Twincat System Manager on the PLC contains a list of "routes" that define allowed clients. Perform the following steps to add the host where you plan to use `counsyl_pyads` to the list of routes: 22 | 23 | 1. Find your local IP address (`ifconfig | grep inet` should help), e.g. `192.168.4.13` 24 | * Dream up an Ams ID for your local machine. The Ams ID is a program identifier used by the ADS protocol. It's often `IP address + .1.1` but any six byte sequence works, for example`192.168.192.168.1.1`. See the [Beckoff Documentation](https://infosys.beckhoff.com/english.php?content=../content/1033/tcadscommon/html/tcadscommon_remoteconnection.htm&id=) for more information. 25 | * Connect to the PLC computer and log in 26 | * Open the System Manager (right click Twincat tray icon, then select System Manager) 27 | * Navigate to `SYSTEM - Configuration`, `Route Settings`, then open the `Static Routes` tab 28 | * Click `Add`. In the `Add Route Dialog` window, you need to fill out the bottom half of the form 29 | * `Route Name (Target)`: Something descriptive to describe this route, consider including your name 30 | * `AmsNetId`: The Ams ID you dreamed up above 31 | * `Transport Type`: `TCP/IP` 32 | * `Address Info`: Your IP address 33 | * Select `IP Address` 34 | * `Connection Timeout`: 5 35 | * `Target Route`: Static 36 | * `Remote Route`: None 37 | 38 | 39 | ### Usage 40 | 41 | The script `bin/twincat_plc_info.py` should get you started with the basics. You can use it to query system information and a list of all varibles on a PLC. 42 | 43 | ```bash 44 | twincat_plc_info.py 5.21.172.208.1.1:801 10.1.0.99 801 192.168.192.168.1.1:5555 45 | ``` 46 | 47 | This assumes that you have a PLC with Ams ID `5.21.172.208.1.1` available at IP `10.1.0.99` that is set up to accept connections from you (see PLC setup section above). Port `801` is default. `192.168.192.168.1.1:5555` is your arbitrary local Ams ID including a port that isn't used for anything. 48 | 49 | 50 | ### Related Links 51 | 52 | * [AMS/ADS Protocol Overview](http://infosys.beckhoff.com/content/1033/bk9000/html/bt_ethernet%20ads%20potocols.htm?id=2222) 53 | * [ADS Command Documentation](http://infosys.beckhoff.com/english.php?content=../content/1033/TcAdsAmsSpec/HTML/TcAdsAmsSpec_Intro.htm) 54 | * [pyads library by Christoph Wiedemann](https://github.com/chwiede/pyads) 55 | -------------------------------------------------------------------------------- /counsyl_pyads/adsexception.py: -------------------------------------------------------------------------------- 1 | """Exceptions representing errors in the ADS/AMS protocol or data conversion. 2 | 3 | All exceptions raised by the pyads library are sub-classed from the 4 | PyadsException. 5 | """ 6 | 7 | 8 | class PyadsException(Exception): 9 | """Base exception class for the pyads library.""" 10 | pass 11 | 12 | 13 | class AdsException(PyadsException): 14 | """Represents error codes specified in the ADS protocol as Python exception 15 | """ 16 | def __init__(self, code): 17 | self.code = code 18 | 19 | def __str__(self): 20 | if(self.code in self.AdsCodeNumbers): 21 | return repr(self.AdsCodeNumbers[self.code]) 22 | else: 23 | return repr("Error Code #%s" % self.code) 24 | 25 | AdsCodeNumbers = { 26 | 0x1: "Internal error", 27 | 0x2: "No Runtime", 28 | 0x3: "Allocation locked memory error", 29 | 0x4: "Insert mailbox error. Reduce the number of ADS calls", 30 | 0x5: "Wrong receive HMSG", 31 | 0x6: "target port not found", 32 | 0x7: "target machine not found", 33 | 0x8: "Unknown command ID", 34 | 0x9: "Bad task ID", 35 | 0xA: "No IO", 36 | 0xB: "Unknown ADS command", 37 | 0xC: "Win 32 error", 38 | 0xD: "Port not connected", 39 | 0xE: "Invalid ADS length", 40 | 0xF: "Invalid ADS Net ID", 41 | 0x10: "Low Installation level", 42 | 0x11: "No debug available", 43 | 0x12: "Port disabled", 44 | 0x13: "Port already connected", 45 | 0x14: "ADS Sync Win32 error", 46 | 0x15: "ADS Sync Timeout", 47 | 0x16: "ADS Sync AMS error", 48 | 0x17: "ADS Sync no index map", 49 | 0x18: "Invalid ADS port", 50 | 0x19: "No memory", 51 | 0x1A: "TCP send error", 52 | 0x1B: "Host unreachable", 53 | 0x700: "error class ", 54 | 0x701: "Service is not supported by server", 55 | 0x702: "invalid index group", 56 | 0x703: "invalid index offset", 57 | 0x704: "reading/writing not permitted", 58 | 0x705: "parameter size not correct", 59 | 0x706: "invalid parameter value(s)", 60 | 0x707: "device is not in a ready state", 61 | 0x708: "device is busy", 62 | 0x709: "invalid context (must be in Windows)", 63 | 0x70A: "out of memory", 64 | 0x70B: "invalid parameter value(s)", 65 | 0x70C: "not found (files, ...)", 66 | 0x70D: "syntax error in command or file", 67 | 0x70E: "objects do not match", 68 | 0x70F: "object already exists", 69 | 0x710: "symbol not found", 70 | 0x711: "symbol version invalid", 71 | 0x712: "server is in invalid state", 72 | 0x713: "AdsTransMode not supported", 73 | 0x714: "Notification handle is invalid", 74 | 0x715: "Notification client not registered", 75 | 0x716: "no more notification handles", 76 | 0x717: "size for watch too big", 77 | 0x718: "device not initialized", 78 | 0x719: "device has a timeout", 79 | 0x71A: "query interface failed", 80 | 0x71B: "wrong interface required", 81 | 0x71C: "class ID is invalid", 82 | 0x71D: "object ID is invalid", 83 | 0x71E: "request is pending", 84 | 0x71F: "request is aborted", 85 | 0x720: "signal warning", 86 | 0x721: "invalid array index", 87 | 0x722: "symbol not active", 88 | 0x723: "access denied", 89 | 0x724: "missing license", 90 | 0x72c: "exception occured during system start", 91 | 0x740: "Error class ", 92 | 0x741: "invalid parameter at service", 93 | 0x742: "polling list is empty", 94 | 0x743: "var connection already in use", 95 | 0x744: "invoke ID in use", 96 | 0x745: "timeout elapsed", 97 | 0x746: "error in win32 subsystem", 98 | 0x747: "Invalid client timeout value", 99 | 0x748: "ads-port not opened", 100 | 0x750: "internal error in ads sync", 101 | 0x751: "hash table overflow", 102 | 0x752: "key not found in hash", 103 | 0x753: "no more symbols in cache", 104 | 0x754: "invalid response received", 105 | 0x755: "sync port is locked", 106 | } 107 | 108 | 109 | class PyadsTypeError(PyadsException, TypeError): 110 | """Raised when a supplied value cannot be converted to the data type of a 111 | PLC variable.""" 112 | pass 113 | -------------------------------------------------------------------------------- /counsyl_pyads/amspacket.py: -------------------------------------------------------------------------------- 1 | from .adsconnection import AdsConnection 2 | from .binaryparser import BinaryParser 3 | from .adsutils import HexBlock 4 | 5 | 6 | class AmsPacket(object): 7 | """An incoming or outgoing communications packet in the Ams protocol""" 8 | 9 | def __init__(self, connection): 10 | """Constructor for packet. Because the header information is specific 11 | to an connection, the connection argument must be an instance of 12 | AdsConnection. 13 | """ 14 | assert(isinstance(connection, AdsConnection)) 15 | # the ams-net-id of the destination device (6 bytes) 16 | self.target_ams_id = connection.target_ams_id 17 | # the ams-port to use (2 bytes, UInt16) 18 | self.target_ams_port = connection.target_ams_port 19 | # the ams-net-id of the sender device (6 bytes) 20 | self.source_ams_id = connection.source_ams_id 21 | # the ams-port of the sender (2 bytes, UInt16) 22 | self.source_ams_port = connection.source_ams_port 23 | 24 | # command-id (2 bytes, UInt16) 25 | self.command_id = 0 26 | # state flags, i.e. 0x0004 for request. (2 bytes, UInt16) 27 | self.state_flags = 0 28 | # length of data (4 bytes, UInt32) 29 | self.length = 0 30 | # error code of ads-response (4 bytes, UInt32) 31 | self.error_code = 0 32 | # arbitrary number to identify request<->response (4 bytes, UInt32) 33 | self.invoke_id = 0 34 | # the ADS-data to transmit as payload of the AMS packet 35 | self.data = b'' 36 | 37 | @staticmethod 38 | def ams_id_to_bytes(dotted_decimal): 39 | return map(int, dotted_decimal.split('.')) 40 | 41 | @staticmethod 42 | def ams_id_from_bytes(byteList): 43 | words = [] 44 | 45 | for bt in byteList: 46 | words.append("%s" % ord(bt)) 47 | 48 | return ".".join(words) 49 | 50 | def GetBinaryData(self): 51 | binary = BinaryParser() 52 | 53 | # ams-target id & port 54 | binary.WriteBytes(AmsPacket.ams_id_to_bytes(self.target_ams_id)) 55 | binary.WriteUInt16(self.target_ams_port) 56 | 57 | # ams-source id & port 58 | binary.WriteBytes(AmsPacket.ams_id_to_bytes(self.source_ams_id)) 59 | binary.WriteUInt16(self.source_ams_port) 60 | 61 | # command id, state flags & data length 62 | binary.WriteUInt16(self.command_id) 63 | binary.WriteUInt16(self.state_flags) 64 | binary.WriteUInt32(len(self.data)) 65 | 66 | # error code & invoke id 67 | binary.WriteUInt32(self.error_code) 68 | binary.WriteUInt32(self.invoke_id) 69 | 70 | # last but not least - the data 71 | binary.WriteBytes(self.data) 72 | 73 | # return byte buffer 74 | return binary.ByteData 75 | 76 | @staticmethod 77 | def from_binary_data(data=''): 78 | binary = BinaryParser(data) 79 | 80 | # ams target & source 81 | target_ams_id = AmsPacket.ams_id_from_bytes(binary.ReadBytes(6)) 82 | target_ams_port = binary.ReadUInt16() 83 | source_ams_id = AmsPacket.ams_id_from_bytes(binary.ReadBytes(6)) 84 | source_ams_port = binary.ReadUInt16() 85 | 86 | ads_conn = AdsConnection( 87 | target_ams="%s:%s" % (target_ams_id, target_ams_port), 88 | source_ams="%s:%s" % (source_ams_id, source_ams_port), 89 | ) 90 | 91 | packet = AmsPacket(ads_conn) 92 | 93 | packet.command_id = binary.ReadUInt16() 94 | packet.state_flags = binary.ReadUInt16() 95 | packet.length = binary.ReadUInt32() 96 | packet.error_code = binary.ReadUInt32() 97 | packet.invoke_id = binary.ReadUInt32() 98 | packet.data = binary.ByteData[32:] 99 | 100 | return packet 101 | 102 | def __str__(self): 103 | result = "%s:%s --> " % (self.source_ams_id, self.source_ams_port) 104 | result += "%s:%s\n" % (self.target_ams_id, self.target_ams_port) 105 | result += "Command ID: %s\n" % self.command_id 106 | result += "Invoke ID: %s\n" % self.invoke_id 107 | result += "State Flags: %s\n" % self.state_flags 108 | result += "Data Length: %s\n" % self.length 109 | result += "Error: %s\n" % self.error_code 110 | 111 | if (len(self.data) == 0): 112 | result += "Packet contains no data.\n" 113 | else: 114 | result += "Data:\n%s\n" % HexBlock(self.data) 115 | 116 | return result 117 | -------------------------------------------------------------------------------- /counsyl_pyads/adsconstants.py: -------------------------------------------------------------------------------- 1 | """Collection of all documented ADS constants. Only a small subset of these 2 | are used by code in this library. 3 | 4 | Source: http://infosys.beckhoff.com/english.php?content=../content/1033/tcplclibsystem/html/tcplclibsys_constants.htm&id= # nopep8 5 | """ 6 | 7 | 8 | """Port numbers""" 9 | # Port number of the standard loggers. 10 | AMSPORT_LOGGER = 100 11 | # Port number of the TwinCAT Eventloggers. 12 | AMSPORT_EVENTLOG = 110 13 | # Port number of the TwinCAT Realtime Servers. 14 | AMSPORT_R0_RTIME = 200 15 | # Port number of the TwinCAT I/O Servers. 16 | AMSPORT_R0_IO = 300 17 | # Port number of the TwinCAT NC Servers. 18 | AMSPORT_R0_NC = 500 19 | # Port number of the TwinCAT NC Servers (Task SAF). 20 | AMSPORT_R0_NCSAF = 501 21 | # Port number of the TwinCAT NC Servers (Task SVB). 22 | AMSPORT_R0_NCSVB = 511 23 | # internal 24 | AMSPORT_R0_ISG = 550 25 | # Port number of the TwinCAT NC I Servers. 26 | AMSPORT_R0_CNC = 600 27 | # internal 28 | AMSPORT_R0_LINE = 700 29 | # Port number of the TwinCAT PLC Servers (only at the Buscontroller). 30 | AMSPORT_R0_PLC = 800 31 | # Port number of the TwinCAT PLC Servers in the runtime 1. 32 | AMSPORT_R0_PLC_RTS1 = 801 33 | # Port number of the TwinCAT PLC Servers in the runtime 2. 34 | AMSPORT_R0_PLC_RTS2 = 811 35 | # Port number of the TwinCAT PLC Servers in the runtime 3. 36 | AMSPORT_R0_PLC_RTS3 = 821 37 | # Port number of the TwinCAT PLC Servers in the runtime 4. 38 | AMSPORT_R0_PLC_RTS4 = 831 39 | # Port number of the TwinCAT CAM Server. 40 | AMSPORT_R0_CAM = 900 41 | # Port number of the TwinCAT CAMTOOL Server. 42 | AMSPORT_R0_CAMTOOL = 950 43 | # Port number of the TwinCAT System Service. 44 | AMSPORT_R3_SYSSERV = 10000 45 | # Port number of the TwinCAT Scope Servers (since Lib. V2.0.12) 46 | AMSPORT_R3_SCOPESERVER = 27110 47 | 48 | 49 | """ADS States""" 50 | ADSSTATE_INVALID = 0 # ADS Status: invalid 51 | ADSSTATE_IDLE = 1 # ADS Status: idle 52 | ADSSTATE_RESET = 2 # ADS Status: reset. 53 | ADSSTATE_INIT = 3 # ADS Status: init 54 | ADSSTATE_START = 4 # ADS Status: start 55 | ADSSTATE_RUN = 5 # ADS Status: run 56 | ADSSTATE_STOP = 6 # ADS Status: stop 57 | ADSSTATE_SAVECFG = 7 # ADS Status: save configuration 58 | ADSSTATE_LOADCFG = 8 # ADS Status: load configuration 59 | ADSSTATE_POWERFAILURE = 9 # ADS Status: Power failure 60 | ADSSTATE_POWERGOOD = 10 # ADS Status: Power good 61 | ADSSTATE_ERROR = 11 # ADS Status: Error 62 | ADSSTATE_SHUTDOWN = 12 # ADS Status: Shutdown 63 | ADSSTATE_SUSPEND = 13 # ADS Status: Suspend 64 | ADSSTATE_RESUME = 14 # ADS Status: Resume 65 | ADSSTATE_CONFIG = 15 # ADS Status: Configuration 66 | ADSSTATE_RECONFIG = 16 # ADS Status: Reconfiguration 67 | ADSSTATE_MAXSTATES = 17 68 | 69 | 70 | """Reserved Index Groups""" 71 | ADSIGRP_SYMTAB = 0xF000 72 | ADSIGRP_SYMNAME = 0xF001 73 | ADSIGRP_SYMVAL = 0xF002 74 | ADSIGRP_SYM_HNDBYNAME = 0xF003 75 | ADSIGRP_SYM_VALBYNAME = 0xF004 76 | ADSIGRP_SYM_VALBYHND = 0xF005 77 | ADSIGRP_SYM_RELEASEHND = 0xF006 78 | ADSIGRP_SYM_INFOBYNAME = 0xF007 79 | ADSIGRP_SYM_VERSION = 0xF008 80 | ADSIGRP_SYM_INFOBYNAMEEX = 0xF009 81 | ADSIGRP_SYM_DOWNLOAD = 0xF00A 82 | ADSIGRP_SYM_UPLOAD = 0xF00B 83 | ADSIGRP_SYM_UPLOADINFO = 0xF00C 84 | ADSIGRP_SYMNOTE = 0xF010 85 | ADSIGRP_IOIMAGE_RWIB = 0xF020 86 | ADSIGRP_IOIMAGE_RWIX = 0xF021 87 | ADSIGRP_IOIMAGE_RISIZE = 0xF025 88 | ADSIGRP_IOIMAGE_RWOB = 0xF030 89 | ADSIGRP_IOIMAGE_RWOX = 0xF031 90 | ADSIGRP_IOIMAGE_RWOSIZE = 0xF035 91 | ADSIGRP_IOIMAGE_CLEARI = 0xF040 92 | ADSIGRP_IOIMAGE_CLEARO = 0xF050 93 | ADSIGRP_IOIMAGE_RWIOB = 0xF060 94 | ADSIGRP_DEVICE_DATA = 0xF100 95 | ADSIOFFS_DEVDATA_ADSSTATE = 0x0000 96 | ADSIOFFS_DEVDATA_DEVSTATE = 0x0002 97 | 98 | 99 | """System Service Index Groups""" 100 | SYSTEMSERVICE_OPENCREATE = 100 101 | SYSTEMSERVICE_OPENREAD = 101 102 | SYSTEMSERVICE_OPENWRITE = 102 103 | SYSTEMSERVICE_CREATEFILE = 110 104 | SYSTEMSERVICE_CLOSEHANDLE = 111 105 | SYSTEMSERVICE_FOPEN = 120 106 | SYSTEMSERVICE_FCLOSE = 121 107 | SYSTEMSERVICE_FREAD = 122 108 | SYSTEMSERVICE_FWRITE = 123 109 | SYSTEMSERVICE_FSEEK = 124 110 | SYSTEMSERVICE_FTELL = 125 111 | SYSTEMSERVICE_FGETS = 126 112 | SYSTEMSERVICE_FPUTS = 127 113 | SYSTEMSERVICE_FSCANF = 128 114 | SYSTEMSERVICE_FPRINTF = 129 115 | SYSTEMSERVICE_FEOF = 130 116 | SYSTEMSERVICE_FDELETE = 131 117 | SYSTEMSERVICE_FRENAME = 132 118 | SYSTEMSERVICE_REG_HKEYLOCALMACHINE = 200 119 | SYSTEMSERVICE_SENDEMAIL = 300 120 | SYSTEMSERVICE_TIMESERVICES = 400 121 | SYSTEMSERVICE_STARTPROCESS = 500 122 | SYSTEMSERVICE_CHANGENETID = 600 123 | 124 | 125 | """System Service Index Offsets (Timeservices)""" 126 | TIMESERVICE_DATEANDTIME = 1 127 | TIMESERVICE_SYSTEMTIMES = 2 128 | TIMESERVICE_RTCTIMEDIFF = 3 129 | TIMESERVICE_ADJUSTTIMETORTC = 4 130 | 131 | 132 | """Masks for Log output""" 133 | ADSLOG_MSGTYPE_HINT = 0x01 134 | ADSLOG_MSGTYPE_WARN = 0x02 135 | ADSLOG_MSGTYPE_ERROR = 0x04 136 | ADSLOG_MSGTYPE_LOG = 0x10 137 | ADSLOG_MSGTYPE_MSGBOX = 0x20 138 | ADSLOG_MSGTYPE_RESOURCE = 0x40 139 | ADSLOG_MSGTYPE_STRING = 0x80 140 | 141 | 142 | """Masks for Bootdata-Flagsx""" 143 | BOOTDATAFLAGS_RETAIN_LOADED = 0x01 144 | BOOTDATAFLAGS_RETAIN_INVALID = 0x02 145 | BOOTDATAFLAGS_RETAIN_REQUESTED = 0x04 146 | BOOTDATAFLAGS_PERSISTENT_LOADED = 0x10 147 | BOOTDATAFLAGS_PERSISTENT_INVALID = 0x20 148 | 149 | 150 | """Masks for BSOD-Flags""" 151 | SYSTEMSTATEFLAGS_BSOD = 0x01 # BSOD: Blue Screen of Death 152 | SYSTEMSTATEFLAGS_RTVIOLATION = 0x02 # Realtime violation, latency time overrun 153 | 154 | 155 | """Masks for File output""" 156 | # 'r': Opens file for reading 157 | FOPEN_MODEREAD = 0x0001 158 | # 'w': Opens file for writing, (possible) existing files were overwritten. 159 | FOPEN_MODEWRITE = 0x0002 160 | # 'a': Opens file for writing, is attached to (possible) exisiting files. If no 161 | # file exists, it will be created. 162 | FOPEN_MODEAPPEND = 0x0004 163 | # '+': Opens a file for reading and writing. 164 | FOPEN_MODEPLUS = 0x0008 165 | # 'b': Opens a file for binary reading and writing. 166 | FOPEN_MODEBINARY = 0x0010 167 | # 't': Opens a file for textual reading and writing. 168 | FOPEN_MODETEXT = 0x0020 169 | 170 | 171 | """Masks for Eventlogger Flags""" 172 | # Class and priority are defined by the formatter. 173 | TCEVENTFLAG_PRIOCLASS = 0x0010 174 | # The formatting information comes with the event 175 | TCEVENTFLAG_FMTSELF = 0x0020 176 | # Logg. 177 | TCEVENTFLAG_LOG = 0x0040 178 | # Show message box . 179 | TCEVENTFLAG_MSGBOX = 0x0080 180 | # Use Source-Id instead of Source name. 181 | TCEVENTFLAG_SRCID = 0x0100 182 | 183 | 184 | """TwinCAT Eventlogger Status messages""" 185 | # Not valid, occurs also if the event was not reported. 186 | TCEVENTSTATE_INVALID = 0x0000 187 | # Event is reported, but neither signed off nor acknowledged. 188 | TCEVENTSTATE_SIGNALED = 0x0001 189 | # Event is signed off ('gone'). 190 | TCEVENTSTATE_RESET = 0x0002 191 | # Event is acknowledged. 192 | TCEVENTSTATE_CONFIRMED = 0x0010 193 | # Event is signed off and acknowledged. 194 | TCEVENTSTATE_RESETCON = 0x0012 195 | 196 | 197 | """TwinCAT Eventlogger Status messages""" 198 | TCEVENT_SRCNAMESIZE = 15 # Max. Length for the Source name. 199 | TCEVENT_FMTPRGSIZE = 31 # Max. Length for the name of the formatters. 200 | 201 | 202 | """Other""" 203 | PI = 3.1415926535897932384626433832795 # Pi number 204 | DEFAULT_ADS_TIMEOUT = 5 # (seconds) Default ADS timeout 205 | MAX_STRING_LENGTH = 255 # The max. string length of T_MaxString data type 206 | -------------------------------------------------------------------------------- /counsyl_pyads/adscommands.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import struct 3 | 4 | from .constants import PYADS_ENCODING 5 | from .adsutils import HexBlock 6 | from .amspacket import AmsPacket 7 | from .adsexception import AdsException 8 | 9 | 10 | class AdsCommand(object): 11 | def __init__(self): 12 | self.command_id = 0 13 | 14 | def CreateRequest(self): 15 | raise NotImplementedError() 16 | 17 | def CreateResponse(self, responsePacket): 18 | raise NotImplementedError() 19 | 20 | def to_ams_packet(self, adsConnection): 21 | packet = AmsPacket(adsConnection) 22 | packet.command_id = self.command_id 23 | packet.state_flags = 0x0004 24 | packet.data = self.CreateRequest() 25 | 26 | return packet 27 | 28 | 29 | class AdsResponse(object): 30 | def __init__(self, responseAmsPacket): 31 | self.Error = struct.unpack_from('I', responseAmsPacket.data)[0] 32 | 33 | if (self.Error > 0): 34 | raise AdsException(self.Error) 35 | 36 | 37 | class DeviceInfoCommand(AdsCommand): 38 | def __init__(self): 39 | super(DeviceInfoCommand, self).__init__() 40 | self.command_id = 0x0001 41 | 42 | def CreateRequest(self): 43 | return b'' 44 | 45 | def CreateResponse(self, responsePacket): 46 | return DeviceInfoResponse(responsePacket) 47 | 48 | 49 | class DeviceInfoResponse(AdsResponse): 50 | def __init__(self, responseAmsPacket): 51 | super(DeviceInfoResponse, self).__init__(responseAmsPacket) 52 | 53 | self.MajorVersion = struct.unpack_from( 54 | 'B', responseAmsPacket.data, 4)[0] 55 | self.MinorVersion = struct.unpack_from( 56 | 'B', responseAmsPacket.data, 5)[0] 57 | self.Build = struct.unpack_from( 58 | 'H', responseAmsPacket.data, 6)[0] 59 | 60 | deviceNameEnd = 16 61 | for i in range(8, 24): 62 | if ord(responseAmsPacket.data[i]) == 0: 63 | deviceNameEnd = i 64 | break 65 | 66 | deviceNameRaw = responseAmsPacket.data[8:deviceNameEnd] 67 | self.DeviceName = deviceNameRaw.decode( 68 | PYADS_ENCODING).strip(' \t\n\r\0') 69 | 70 | @property 71 | def Version(self): 72 | return "%s.%s.%s" % (self.MajorVersion, self.MinorVersion, self.Build) 73 | 74 | def __str__(self): 75 | return unicode(self).encode('utf-8') 76 | 77 | def __unicode__(self): 78 | return u"%s (Version %s)" % (self.DeviceName, self.Version) 79 | 80 | 81 | class ReadCommand(AdsCommand): 82 | def __init__(self, indexGroup, indexOffset, length): 83 | if not isinstance(indexOffset, int): 84 | raise TypeError('indexOffset argument must be integer') 85 | if not isinstance(length, int): 86 | raise TypeError('length argument must be integer') 87 | super(ReadCommand, self).__init__() 88 | self.command_id = 0x0002 89 | self.index_group = indexGroup 90 | self.index_offset = indexOffset 91 | self.length = length 92 | 93 | def CreateRequest(self): 94 | return struct.pack( 95 | ' 0): 140 | raise AdsException(responsePacket.error_code) 141 | 142 | # return response object 143 | result = command.CreateResponse(responsePacket) 144 | if (result.Error > 0): 145 | raise AdsException(result.Error) 146 | 147 | return result 148 | 149 | def read_device_info(self): 150 | cmd = DeviceInfoCommand() 151 | return self.execute(cmd) 152 | 153 | def read(self, indexGroup, indexOffset, length): 154 | cmd = ReadCommand(indexGroup, indexOffset, length) 155 | return self.execute(cmd) 156 | 157 | def write(self, indexGroup, indexOffset, data): 158 | cmd = WriteCommand(indexGroup, indexOffset, data) 159 | return self.execute(cmd) 160 | 161 | def read_state(self): 162 | cmd = ReadStateCommand() 163 | return self.execute(cmd) 164 | 165 | def write_control(self, adsState, deviceState, data=''): 166 | cmd = WriteControlCommand(adsState, deviceState, data) 167 | return self.execute(cmd) 168 | 169 | def read_write(self, indexGroup, indexOffset, readLen, dataToWrite=''): 170 | cmd = ReadWriteCommand(indexGroup, indexOffset, readLen, dataToWrite) 171 | return self.execute(cmd) 172 | 173 | # END Read/Write Methods 174 | 175 | # BEGIN variable access methods 176 | 177 | def get_handle_by_name(self, var_name): 178 | """Retrieves the internal handle of a symbol identified by symbol name. 179 | 180 | var_name: is of type unicode (or str if only ASCII characters are used) 181 | Both fully qualified PLC symbol names (e.g. including leading "." 182 | for global variables) or PLC variable names (the name used in the 183 | PLC program) are accepted. Names are NOT case-sensitive because the 184 | PLC converts all variables to all-uppercase internally. 185 | """ 186 | # convert unicode or ascii input to the Windows-1252 encoding used by 187 | # the plc 188 | var_name_enc = var_name.encode(PYADS_ENCODING) 189 | symbol = self.read_write( 190 | indexGroup=ADSIGRP_SYM_HNDBYNAME, 191 | indexOffset=0x0000, 192 | readLen=4, 193 | dataToWrite=var_name_enc + '\x00') 194 | return struct.unpack("I", symbol.data)[0] 195 | 196 | def get_info_by_name(self, var_name): 197 | """Retrieves extended symbol information including data type and 198 | comment for a symbol identified by symbol name. 199 | 200 | var_name: is of type unicode (or str if only ASCII characters are used) 201 | Both fully qualified PLC symbol names (e.g. including leading "." 202 | for global variables) or PLC variable names (the name used in the 203 | PLC program) are accepted. Names are NoT case-sensitive because the 204 | PLC converts all variables to all-uppercase internally. 205 | """ 206 | var_name_enc = var_name.encode(PYADS_ENCODING) 207 | # Note: The length of the output varies based on the length of the data 208 | # type description and length of the comment (which are specified in 209 | # the first few bytes of the returned byte string. readLen is therefore 210 | # set to the maximal value that does not result in an error. It's 211 | # practical meaning seems to be more of a maxReadLen anyway, because 212 | # the returned string is only as long as necessary to describe the 213 | # symbol (instead of being zero-padded, for example). 214 | resp = self.read_write( 215 | indexGroup=ADSIGRP_SYM_INFOBYNAMEEX, 216 | indexOffset=0x0000, 217 | readLen=0xFFFF, 218 | dataToWrite=var_name_enc + '\x00') 219 | 220 | # First four bytes are the full length of the variable definition, 221 | # which in Twincat3 includes a non-constant number of bytes of 222 | # undocumented purpose. Commenting this out because it's not useful 223 | # when not reading those undocumented bytes, but keeping around as 224 | # a reminder that this information exists. 225 | # read_length = struct.unpack("I", resp.data[0:4])[0] 226 | index_group = struct.unpack("I", resp.data[4:8])[0] 227 | index_offset = struct.unpack("I", resp.data[8:12])[0] 228 | name_length = struct.unpack("H", resp.data[24:26])[0] 229 | type_length = struct.unpack("H", resp.data[26:28])[0] 230 | comment_length = struct.unpack("H", resp.data[28:30])[0] 231 | 232 | name_start_ptr = 30 233 | name_end_ptr = name_start_ptr + name_length 234 | type_start_ptr = name_end_ptr + 1 235 | type_end_ptr = type_start_ptr + type_length 236 | comment_start_ptr = type_end_ptr + 1 237 | comment_end_ptr = comment_start_ptr + comment_length 238 | 239 | name = resp.data[name_start_ptr:name_end_ptr].decode( 240 | PYADS_ENCODING).strip(' \t\n\r\0') 241 | symtype = resp.data[type_start_ptr:type_end_ptr] 242 | comment = resp.data[comment_start_ptr:comment_end_ptr].decode( 243 | PYADS_ENCODING).strip(' \t\n\r\0') 244 | 245 | return AdsSymbol( 246 | index_group, index_offset, name, symtype, comment) 247 | 248 | def read_by_handle(self, symbolHandle, ads_data_type): 249 | """Retrieves the current value of a symbol identified by its handle. 250 | 251 | ads_data_type: The data type of the symbol must be specified as 252 | AdsDatatype object. 253 | """ 254 | assert(isinstance(ads_data_type, AdsDatatype)) 255 | response = self.read( 256 | indexGroup=ADSIGRP_SYM_VALBYHND, 257 | indexOffset=symbolHandle, 258 | length=ads_data_type.byte_count) 259 | data = response.data 260 | return ads_data_type.unpack(data) 261 | 262 | def read_by_name(self, var_name, ads_data_type): 263 | """Retrieves the current value of a symbol identified by symbol name. 264 | 265 | var_name: is of type unicode (or str if only ASCII characters are used) 266 | Both fully qualified PLC symbol names (e.g. including leading "." 267 | for global variables) or PLC variable names (the name used in the 268 | PLC program) are accepted. Names are NoT case-sensitive because the 269 | PLC converts all variables to all-uppercase internally. 270 | ads_data_type: The data type of the symbol must be specified as 271 | AdsDatatype object. 272 | """ 273 | assert(isinstance(ads_data_type, AdsDatatype)) 274 | var_name_enc = var_name.encode(PYADS_ENCODING) 275 | response = self.read_write( 276 | indexGroup=ADSIGRP_SYM_VALBYNAME, 277 | indexOffset=0x0000, 278 | readLen=ads_data_type.byte_count, 279 | dataToWrite=var_name_enc + '\x00') 280 | data = response.data 281 | return ads_data_type.unpack(data) 282 | 283 | def write_by_handle(self, symbolHandle, ads_data_type, value): 284 | """Retrieves the current value of a symbol identified by its handle. 285 | 286 | ads_data_type: The data type of the symbol must be specified as 287 | AdsDatatype object. 288 | value: must meet the requirements of the ads_data_type. For example, 289 | integer datatypes will require a number to be passed, etc. 290 | """ 291 | assert(isinstance(ads_data_type, AdsDatatype)) 292 | value_raw = ads_data_type.pack(value) 293 | self.write( 294 | indexGroup=ADSIGRP_SYM_VALBYHND, 295 | indexOffset=symbolHandle, 296 | data=value_raw) 297 | 298 | def write_by_name(self, var_name, ads_data_type, value): 299 | """Sets the current value of a symbol identified by symbol name. 300 | 301 | This simply calls get_handle_by_name() first and then uses the handle 302 | to call write_by_handle(). 303 | 304 | var_name: must meet the same requirements as in get_handle_by_name, 305 | i.e. be unicode or an ASCII-only str. 306 | ads_data_type: must meet the same requirements as in write_by_handle. 307 | value: must meet the requirements of the ads_data_type. For example, 308 | integer datatypes will require a number to be passed, etc. 309 | """ 310 | symbol_handle = self.get_handle_by_name(var_name) 311 | self.write_by_handle(symbol_handle, ads_data_type, value) 312 | 313 | def get_symbols(self): 314 | # Figure out the length of the symbol table first 315 | resp1 = self.read( 316 | indexGroup=0xF00F, # Not a documented constant 317 | indexOffset=0x0000, 318 | length=24) 319 | sym_count = struct.unpack("I", resp1.data[0:4])[0] 320 | sym_list_length = struct.unpack("I", resp1.data[4:8])[0] 321 | 322 | # Get the symbol table 323 | resp2 = self.read( 324 | indexGroup=ADSIGRP_SYM_UPLOAD, 325 | indexOffset=0x0000, 326 | length=sym_list_length) 327 | 328 | ptr = 0 329 | symbols = [] 330 | for idx in xrange(sym_count): 331 | read_length = struct.unpack("I", resp2.data[ptr+0:ptr+4])[0] 332 | index_group = struct.unpack("I", resp2.data[ptr+4:ptr+8])[0] 333 | index_offset = struct.unpack("I", resp2.data[ptr+8:ptr+12])[0] 334 | name_length = struct.unpack("H", resp2.data[ptr+24:ptr+26])[0] 335 | type_length = struct.unpack("H", resp2.data[ptr+26:ptr+28])[0] 336 | comment_length = struct.unpack("H", resp2.data[ptr+28:ptr+30])[0] 337 | 338 | name_start_ptr = ptr + 30 339 | name_end_ptr = name_start_ptr + name_length 340 | type_start_ptr = name_end_ptr + 1 341 | type_end_ptr = type_start_ptr + type_length 342 | comment_start_ptr = type_end_ptr + 1 343 | comment_end_ptr = comment_start_ptr + comment_length 344 | 345 | name = resp2.data[name_start_ptr:name_end_ptr].decode( 346 | PYADS_ENCODING).strip(' \t\n\r\0') 347 | symtype = resp2.data[type_start_ptr:type_end_ptr] 348 | comment = resp2.data[comment_start_ptr:comment_end_ptr].decode( 349 | PYADS_ENCODING).strip(' \t\n\r\0') 350 | 351 | ptr = ptr + read_length 352 | 353 | symbol = AdsSymbol( 354 | index_group, index_offset, name, symtype, comment) 355 | 356 | symbols.append(symbol) 357 | 358 | return symbols 359 | 360 | # END variable access methods 361 | 362 | def read_ams_packet_from_socket(self): 363 | # read default buffer 364 | response = self.socket.recv(ADS_CHUNK_SIZE_DEFAULT) 365 | # ensure correct beckhoff tcp header 366 | if(len(response) < 6): 367 | return None 368 | # first two bits must be 0 369 | if (response[0:2] != b'\x00\x00'): 370 | return None 371 | # read whole data length 372 | dataLen = struct.unpack('I', response[2:6])[0] + 6 373 | # read rest of data, if any 374 | while (len(response) < dataLen): 375 | nextReadLen = min(ADS_CHUNK_SIZE_DEFAULT, dataLen - len(response)) 376 | response += self.socket.recv(nextReadLen) 377 | # cut off tcp-header and return response amspacket 378 | return AmsPacket.from_binary_data(response[6:]) 379 | 380 | def get_tcp_header(self, amsData): 381 | # pack 2 bytes (reserved) and 4 bytes (length) 382 | # format _must_ be little endian! 383 | return struct.pack('>> sending ams-packet:") 417 | logger.debug(amspacket) 418 | 419 | def await_command_invoke(self): 420 | # unfortunately threading.event is slower than this oldschool poll :-( 421 | timeout = 0 422 | while (self._current_packet is None): 423 | timeout += 0.001 424 | time.sleep(0.001) 425 | if (timeout > 10): 426 | raise AdsException("Timout: Did not receive ADS Answer!") 427 | if self.debug: 428 | logger.debug("<<< received ams-packet:") 429 | logger.debug(self._current_packet) 430 | return self._current_packet 431 | -------------------------------------------------------------------------------- /counsyl_pyads/adsdatatypes.py: -------------------------------------------------------------------------------- 1 | """Collection of utility functions for data types available in Twincat. 2 | 3 | A documentation of Twincat data types is available at 4 | http://infosys.beckhoff.com/content/1033/tcplccontrol/html/tcplcctrl_plc_data_types_overview.htm?id=20295 # nopep8 5 | """ 6 | from collections import OrderedDict 7 | from collections import Sequence 8 | from copy import copy 9 | import datetime 10 | from functools import reduce 11 | import struct 12 | 13 | from .constants import PYADS_ENCODING 14 | from .adsexception import PyadsTypeError 15 | 16 | 17 | class AdsDatatype(object): 18 | """Represents a simple data type with a fixed byte count.""" 19 | def __init__(self, byte_count, pack_format): 20 | self.byte_count = int(byte_count) 21 | self.pack_format = str(pack_format) 22 | 23 | def pack(self, values_list): 24 | """Pack a value using Python's struct.pack()""" 25 | assert(self.pack_format is not None) 26 | return struct.pack(self.pack_format, *values_list) 27 | 28 | def pack_into_buffer(self, byte_buffer, offset, values_list): 29 | assert(self.pack_format is not None) 30 | struct.pack_into(self.pack_format, byte_buffer, offset, *values_list) 31 | 32 | def unpack(self, value): 33 | """Unpack a value using Python's struct.unpack()""" 34 | assert(self.pack_format is not None) 35 | # Note: "The result is a tuple even if it contains exactly one item." 36 | # (https://docs.python.org/2/library/struct.html#struct.unpack) 37 | # For single-valued data types, use AdsSingleValuedDatatype to get the 38 | # first (and only) entry of the tuple after unpacking. 39 | return struct.unpack(self.pack_format, value) 40 | 41 | def unpack_from_buffer(self, byte_buffer, offset): 42 | assert(self.pack_format is not None) 43 | return struct.unpack_from(self.pack_format, byte_buffer, offset) 44 | 45 | 46 | class AdsSingleValuedDatatype(AdsDatatype): 47 | """Represents Twincat's variable types that are NOT arrays.""" 48 | def pack(self, value): 49 | return super(AdsSingleValuedDatatype, self).pack([value]) 50 | 51 | def pack_into_buffer(self, byte_buffer, offset, value): 52 | return super(AdsSingleValuedDatatype, self).pack( 53 | byte_buffer, offset, [value]) 54 | 55 | def unpack(self, value): 56 | unpacked_tuple = super(AdsSingleValuedDatatype, self).unpack(value) 57 | return unpacked_tuple[0] 58 | 59 | def unpack_from_buffer(self, byte_buffer, offset): 60 | unpacked_tuple = super( 61 | AdsSingleValuedDatatype, self).unpack(byte_buffer, offset) 62 | return unpacked_tuple[0] 63 | 64 | 65 | class AdsStringDatatype(AdsSingleValuedDatatype): 66 | """Represents Twincat's variable length STRING data type.""" 67 | def __init__(self, str_length=80): 68 | super(AdsStringDatatype, self).__init__( 69 | byte_count=str_length, pack_format='%ss' % str_length) 70 | 71 | def byte_str_to_decoded_str(self, byte_str): 72 | return byte_str.split('\x00', 1)[0].decode(PYADS_ENCODING) 73 | 74 | def pack(self, value): 75 | # encode in Windows-1252 encoding 76 | value = value.encode(PYADS_ENCODING) 77 | return super(AdsStringDatatype, self).pack(value) 78 | 79 | def pack_into_buffer(self, byte_buffer, offset, value): 80 | # encode in Windows-1252 encoding 81 | value = value.encode(PYADS_ENCODING) 82 | super(AdsStringDatatype, self).pack_into_buffer( 83 | byte_buffer, offset, value) 84 | 85 | def unpack(self, value): 86 | """Unpacks the value into a byte string of str_length, then 87 | drops all bytes after and including the first NULL character. 88 | """ 89 | value = super(AdsStringDatatype, self).unpack(value) 90 | return self.byte_str_to_decoded_str(value) 91 | 92 | def unpack_from_buffer(self, byte_buffer, offset): 93 | """c.f. unpack()""" 94 | value = super(AdsStringDatatype, self).unpack_from_buffer( 95 | byte_buffer, offset) 96 | return self.byte_str_to_decoded_str(value) 97 | 98 | 99 | class AdsTimeDatatype(AdsSingleValuedDatatype): 100 | """Represents Twincat's TIME data type.""" 101 | def __init__(self): 102 | # DATE, TIME, and DATE_AND_TIME are all handled as WORD by Twincat 103 | super(AdsTimeDatatype, self).__init__(byte_count=4, pack_format='I') 104 | 105 | def time_to_milliseconds_integer(self, value): 106 | """Converts a Python datetime.time object to an integer. 107 | 108 | The output represents the number of milliseconds since 109 | datetime.time(0). Any time zone information is ignored. 110 | """ 111 | assert(isinstance(value, datetime.time)) 112 | return ( 113 | ((value.hours * 60 + value.minutes) * 60 + value.seconds) * 1000 + 114 | int(value.microseconds / 1000)) 115 | 116 | def milliseconds_integer_to_time(self, value): 117 | """Converts an integer into a Python datetime.time object. 118 | 119 | The input is assumed to represent the number of milliseconds since 120 | datetime.time(0). Any time zone information is ignored. 121 | """ 122 | assert(isinstance(value, int)) 123 | # pretend this is a timestamp in millisecond resolution and get the 124 | # datetime ignoring timezones, then discard the date component 125 | dt = datetime.datetime.utcfromtimestamp(value / 1000.0) 126 | return dt.time() 127 | 128 | def pack(self, value): 129 | value = self.time_to_milliseconds_integer(value) 130 | return super(AdsTimeDatatype, self).pack(value) 131 | 132 | def pack_into_buffer(self, byte_buffer, offset, value): 133 | value = self.time_to_milliseconds_integer(value) 134 | super(AdsTimeDatatype, self).pack_into_buffer( 135 | byte_buffer, offset, value) 136 | 137 | def unpack(self, value): 138 | value = super(AdsTimeDatatype, self).unpack(value) 139 | return self.milliseconds_integer_to_time(value) 140 | 141 | def unpack_from_buffer(self, byte_buffer, offset): 142 | value = super(AdsTimeDatatype, self).unpack_from_buffer( 143 | byte_buffer, offset) 144 | return self.milliseconds_integer_to_time(value) 145 | 146 | 147 | class AdsDateDatatype(AdsSingleValuedDatatype): 148 | def __init__(self): 149 | # DATE, TIME, and DATE_AND_TIME are all handled as WORD by Twincat 150 | super(AdsDateDatatype, self).__init__(byte_count=4, pack_format='I') 151 | 152 | # contrary to what the docs say the resolution of the DATE datatype is 153 | # one day, not one second 154 | def time_to_days_integer(self, value): 155 | assert(isinstance(value, datetime.date)) 156 | dt1970 = datetime.date(1970, 1, 1) 157 | tdelta = value - dt1970 158 | return tdelta.days 159 | 160 | def days_integer_to_time(self, value): 161 | assert(isinstance(value, int)) 162 | dt1970 = datetime.date(1970, 1, 1) 163 | return dt1970 + datetime.date(days=value) 164 | 165 | def pack(self, value): 166 | value = self.time_to_days_integer(value) 167 | return super(AdsTimeDatatype, self).pack(value) 168 | 169 | def pack_into_buffer(self, byte_buffer, offset, value): 170 | value = self.time_to_days_integer(value) 171 | super(AdsTimeDatatype, self).pack_into_buffer( 172 | byte_buffer, offset, value) 173 | 174 | def unpack(self, value): 175 | value = super(AdsTimeDatatype, self).unpack(value) 176 | return self.days_integer_to_time(value) 177 | 178 | def unpack_from_buffer(self, byte_buffer, offset): 179 | value = super(AdsTimeDatatype, self).unpack_from_buffer( 180 | byte_buffer, offset) 181 | return self.days_integer_to_time(value) 182 | 183 | 184 | # TODO 185 | class AdsDateAndTimeDatatype(AdsSingleValuedDatatype): 186 | def __init__(self): 187 | # DATE, TIME, and DATE_AND_TIME are all handled as WORD by Twincat 188 | super(AdsDateAndTimeDatatype, self).__init__( 189 | byte_count=4, pack_format='I') 190 | 191 | def pack(self, value): 192 | pass 193 | 194 | def pack_into_buffer(self, byte_buffer, offset, value): 195 | pass 196 | 197 | def unpack(self, value): 198 | pass 199 | 200 | def unpack_from_buffer(self, byte_buffer, offset): 201 | pass 202 | 203 | 204 | class AdsArrayDatatype(AdsDatatype): 205 | """Factory for data types represented as arrays in PLC code: 206 | 'ARRAY [0..3,1..4] OF UINT'. 207 | """ 208 | def __init__(self, data_type, dimensions=None): 209 | """Creates data type capable of packing and unpacking an array of 210 | elements of a single-valued data type. The Python representation of 211 | the array is as a dict because PLC arrays are arbitrarily indexed. 212 | Multidimensional PLC arrays are represented as nested dicts. 213 | 214 | data_type must be of type AdsSingleValuedDatatype 215 | dimensions is either the total number of elements in the array as 216 | integer or a list of tuple of (inclusive) start and end indices in 217 | the same order as they appear in the array definition in PLC code 218 | """ 219 | assert(isinstance(data_type, AdsSingleValuedDatatype)) 220 | 221 | # if the array is 1-dimensional and zero-indexed the dimensions 222 | # argument could be an integer 223 | if isinstance(dimensions, int): 224 | self.dimensions = [(0, dimensions - 1)] # 0..n => n+1 elements! 225 | elif isinstance(dimensions, list): 226 | self.dimensions = dimensions 227 | else: 228 | raise TypeError( 229 | "The dimensions parameter must be either int or a list of " 230 | "tuples. %s was given." % type(dimensions)) 231 | 232 | # calculate the total number of elements in the array, keeping in mind 233 | # that it could be multidimensional 234 | self.total_element_count = reduce( 235 | lambda x, y: x * (y[1] - y[0] + 1), # 1..4 => 4 elements! 236 | dimensions, 1) 237 | 238 | total_byte_count = self.total_element_count * data_type.byte_count 239 | super(AdsArrayDatatype, self).__init__( 240 | byte_count=total_byte_count, 241 | pack_format='{cnt}{fmt}'.format( 242 | cnt=self.total_element_count, 243 | fmt=data_type.pack_format, 244 | )) 245 | 246 | def _dict_to_flat_list(self, dict_, dims=None): 247 | """Recursively builds a flat list from a dict while checking if the 248 | dict's keys match the array specification. The returned data type is 249 | tuple. 250 | 251 | For example, an integer array specified as [(0, 2), (7,9)] is correctly 252 | represented by a dict of this structure: 253 | {0: {7: a, 8: b, 9: c}, 1: {7: d, 8: e, 9: f}, 2: {7: g, 8: h, 9: i}} 254 | or this list/tuple: [a, b, c, d, e, f, g, h, i] 255 | 256 | If dims is not provided, self.dimensions is used. When the function 257 | calls itself, it passes a truncated copy its own version of dims. 258 | """ 259 | # initialize the flattened list as empty 260 | flat = [] 261 | # operate on a local copy of dims list to not modify the version 262 | # used by the calling function (which in many cases will be another 263 | # branch of the recursive tree) 264 | dims = copy(dims or self.dimensions) 265 | # pop from the left to get the index bounds of the dimension of the 266 | # array we are currently validating, while shortening dims for 267 | # validation of the next dimension 268 | try: 269 | cur_dims = dims.pop(0) 270 | except (AttributeError, KeyError): 271 | raise PyadsTypeError( 272 | "Failed to pop the first entry off the list of array " 273 | "dimensions.") 274 | try: 275 | indices = sorted(dict_.keys()) 276 | except AttributeError: 277 | raise PyadsTypeError( 278 | "Failed to find array keys from dict representation.") 279 | # perform validation for current dimension 280 | if min(indices) != cur_dims[0]: 281 | raise PyadsTypeError( 282 | "Expected lowest index %d but found %d." % 283 | (cur_dims[0], min(indices))) 284 | if max(indices) != cur_dims[1]: 285 | raise PyadsTypeError( 286 | "Expected highgest index %d but found %d." % 287 | (cur_dims[1], max(indices))) 288 | if len(indices) != max(indices) - min(indices) + 1: 289 | raise PyadsTypeError( 290 | "All indices between and including {mn} and {mx} must be " 291 | "present but only {lst} are.".format( 292 | mn=min(indices), 293 | mx=max(indices), 294 | lst=','.join(map(str, indices)))) 295 | # can't iterate over dict_.values(), they might not be in order, 296 | # iterate over sorted indices instead 297 | for idx in indices: 298 | if len(dims) > 0: 299 | flat += self._dict_to_flat_list(dict_[idx], dims) 300 | else: 301 | flat.append(dict_[idx]) 302 | return tuple(flat) 303 | 304 | def _flat_list_to_dict(self, flat, dims=None): 305 | """Inverse of _dict_to_flat_list: Recursively builds a dict from a flat 306 | list using the array spec. 307 | 308 | If dims is not provided, self.dimensions is used. When the function 309 | calls itself, it passes a truncated copy its own version of dims. 310 | """ 311 | if not isinstance(flat, Sequence): 312 | raise PyadsTypeError( 313 | "Array data must be a sequence (list, tuple, string), but %s " 314 | "was given." % type(flat)) 315 | # For recursion to work on multi-dimensional arrays, all nodes of the 316 | # recursion tree must work on the same mutable sequence (list). For 317 | # that to work, convert to list iff flat is not a list. Always calling 318 | # list() would result in each call to this function operating on a 319 | # different copy of the input sequence. This conversion is generally 320 | # useful because struct.unpack(), which is where the input to this 321 | # function usually originates, returns a tuple. 322 | if not isinstance(flat, list): 323 | flat = list(flat) 324 | # operate on a local copy of dims list to not modify the version 325 | # used by the calling function (which in many cases will be another 326 | # branch of the recursive tree) 327 | dims = copy(dims or self.dimensions) 328 | # pop from the left to get the index bounds of the array dimension we 329 | # are currently building, while shortening dims for the next dimension 330 | cur_dims = dims.pop(0) 331 | # Recursively step through the array specification (in dims) and pop 332 | # elements from the flat list into the dict. 333 | assert(cur_dims[0] <= cur_dims[1]) 334 | dict_ = OrderedDict() 335 | for idx in xrange(cur_dims[0], cur_dims[1] + 1): 336 | if len(dims) > 0: 337 | dict_[idx] = self._flat_list_to_dict(flat, dims) 338 | else: 339 | # As opposed to the dims array, here we actually want to modify 340 | # the list globally across all branches of the recursion. 341 | try: 342 | dict_[idx] = flat.pop(0) 343 | except IndexError: 344 | raise PyadsTypeError( 345 | 'The array data from the PLC has fewer elements than ' 346 | 'required by the array specification.') 347 | return dict_ 348 | 349 | def pack(self, value): 350 | """Packs the Python representation of the array into a binary string. 351 | 352 | As a convenience, both the dict representation returned by unpack() and 353 | a flattened list are accepted as inputs. 354 | """ 355 | # The exception message for incorrect arguments can get complex here, 356 | # pre-assemble a base message first, then modify it for each specific 357 | # exception. 358 | dims = len(self.dimensions) 359 | exception_str = """The Python representation of this PLC array variable 360 | must either be a sequence (tuple, list, string) of length {list_len} or 361 | a {nested} dict with keys {dict_keys}. %s""".format( 362 | list_len=self.total_element_count, 363 | nested="%d-fold nested " % dims if dims > 1 else "", 364 | dict_keys=','.join(["%d..%d" % bnds for bnds in self.dimensions])) 365 | 366 | # Check the value argument for correct type. If it's a dict, check the 367 | # dict keys against the array dimensions. After all checks, convert the 368 | # input value into a flattened array. 369 | if isinstance(value, Sequence): 370 | # Check for correct list length 371 | if len(value) != self.total_element_count: 372 | raise PyadsTypeError( 373 | exception_str % 374 | "The supplied list has %d elements." % 375 | len(value)) 376 | # Nothing else to do in this branch, the array is already a 377 | # flattened list. 378 | flat = value 379 | elif isinstance(value, dict): 380 | # Recursively flatten the dict into a list 381 | try: 382 | flat = self._dict_to_flat_list(value) 383 | except PyadsTypeError as ex: 384 | raise PyadsTypeError(exception_str % ex.message) 385 | else: 386 | raise PyadsTypeError( 387 | exception_str % "The value must be a list or a dict.") 388 | 389 | return super(AdsArrayDatatype, self).pack(flat) 390 | 391 | def unpack(self, value): 392 | flat = super(AdsArrayDatatype, self).unpack(value) 393 | return self._flat_list_to_dict(flat) 394 | 395 | 396 | BOOL = AdsSingleValuedDatatype(byte_count=1, pack_format='?') # Bool 397 | BYTE = AdsSingleValuedDatatype(byte_count=1, pack_format='b') # Int8 398 | WORD = AdsSingleValuedDatatype(byte_count=2, pack_format='H') # UInt16 399 | DWORD = AdsSingleValuedDatatype(byte_count=4, pack_format='I') # UInt32 400 | SINT = AdsSingleValuedDatatype(byte_count=1, pack_format='b') # Int8 (Char) 401 | USINT = AdsSingleValuedDatatype(byte_count=1, pack_format='B') # UInt8 402 | INT = AdsSingleValuedDatatype(byte_count=2, pack_format='h') # Int16 403 | INT16 = INT # Int16 404 | UINT = AdsSingleValuedDatatype(byte_count=2, pack_format='H') # UInt16 405 | UINT16 = UINT # UInt16 406 | DINT = AdsSingleValuedDatatype(byte_count=4, pack_format='i') # Int32 407 | UDINT = AdsSingleValuedDatatype(byte_count=4, pack_format='I') # UInt32 408 | # LINT (64 Bit Integer, not supported by TwinCAT) 409 | # ULINT (Unsigned 64 Bit Integer, not supported by TwinCAT) 410 | REAL = AdsSingleValuedDatatype(byte_count=4, pack_format='f') # float 411 | LREAL = AdsSingleValuedDatatype(byte_count=8, pack_format='d') # double 412 | STRING = AdsStringDatatype 413 | # Duration time. The most siginificant digit is one millisecond. The data type 414 | # is handled internally like DWORD. 415 | TIME = AdsTimeDatatype() 416 | TIME_OF_DAY = TIME # only semantically different from TIME 417 | TOD = TIME_OF_DAY # alias 418 | DATE = AdsDateDatatype() 419 | DATE_AND_TIME = AdsDateAndTimeDatatype() 420 | DT = DATE_AND_TIME # alias 421 | --------------------------------------------------------------------------------