├── .github ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── LICENSE.txt ├── README.md ├── docs ├── Contributing.md ├── Documentation.md ├── Emulate.md ├── FAQ.md ├── README.md ├── guides │ ├── Continuous-Reading.md │ ├── Starter-Guide.md │ ├── Working-With-Files.md │ └── Working-With-LogFiles.md ├── mpy │ ├── Development.md │ ├── Guide.md │ └── README.md ├── pics │ ├── Add_Bool_01.PNG │ ├── Edit_Tags.PNG │ ├── Run_Mode.PNG │ ├── Select_Controller_Tags.PNG │ ├── create_bool_arr_01.PNG │ ├── create_bool_arr_02.PNG │ ├── encapsulation1.png │ ├── encapsulation2.png │ ├── progressbar_01.PNG │ └── progressbar_02.PNG └── python_code │ ├── test-01.py │ ├── test-02.py │ └── test-03.py ├── examples ├── 01_micropython_example │ ├── boot.py │ └── main.py ├── 01_read_simple.py ├── 02_read_simple.py ├── 03_read_program_scope.py ├── 04_read_array.py ├── 05_read_multiple_tags.py ├── 06_read_loop.py ├── 07_read_first_instance.py ├── 08_read_faster.py ├── 09_multi_read_faster.py ├── 10_write_simple.py ├── 11_write_simple.py ├── 12_write_program_scope.py ├── 13_write_array.py ├── 14_write_custom_string.py ├── 15_write_faster.py ├── 16_multi_write.py ├── 17_write_sql.py ├── 20_discover_devices.py ├── 21_get_plc_clock.py ├── 22_set_plc_clock.py ├── 23_get_all_tags.py ├── 24_get_controller_tags.py ├── 25_get_program_tags.py ├── 26_save_tags.py ├── 27_get_module_properties.py ├── 28_audit_network.py ├── 30_log_to_txt.py ├── 31_log_to_csv.py ├── 32_log_multiple_to_csv.py ├── 40_read_timer.py ├── 41_routing.py ├── 80_simple_gui.py └── 81_simple_gui.py ├── package.json ├── pylogix ├── __init__.py ├── eip.py ├── lgx_comm.py ├── lgx_device.py ├── lgx_response.py ├── lgx_tag.py ├── lgx_uvendors.py ├── lgx_uvendors.py.bin ├── lgx_vendors.py └── utils.py ├── scripts ├── build_mpy.py └── time_uvendors.bash ├── setup.py ├── tests ├── PylogixTests.py ├── README.md ├── Randomizer.py ├── clx_setup │ ├── pylogix-Controller-Tags.CSV │ ├── pylogix_MainProgram-Tags.CSV │ └── udts │ │ ├── Arrays.L5X │ │ ├── Basic.L5X │ │ └── combined.L5X └── plcConfig.py.template ├── tox.ini └── upylogix ├── __init__.mpy ├── eip.mpy ├── lgx_comm.mpy ├── lgx_device.mpy ├── lgx_response.mpy ├── lgx_tag.mpy ├── lgx_uvendors.mpy ├── lgx_uvendors.mpy.bin └── utils.mpy /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Preflight checks 2 | 3 | Before you post an issue, ensure you have tried the minimal examples within the repo, or tried the pylogix-tester. 4 | 5 | - [ ] Have you tried the [examples](https://github.com/dmroeder/pylogix/tree/master/examples)? 6 | - [ ] Have you tried [pylogix-tester](https://github.com/TheFern2/pylogix-tester)? 7 | - [ ] Have you read previous issues? 8 | - [ ] Is this a supported Rockwell PLC? 9 | 10 | ## Type of issue 11 | 12 | - [ ] Bug 13 | - [ ] Feature Request 14 | - [ ] Question 15 | - [ ] Other 16 | 17 | > Delete items that do not apply below. 18 | 19 | ## Description of issue 20 | 21 | ## Expected behavior 22 | 23 | ## Actual behavior 24 | 25 | ## Code 26 | 27 | Please provide a minimal, reproducible example. 28 | 29 | https://stackoverflow.com/help/minimal-reproducible-example 30 | 31 | ## Screenshots 32 | 33 | ## Stacktrace 34 | 35 | ## Versions 36 | 37 | Include versions to 38 | 39 | - pylogix: 40 | - plc model: 41 | - python: 42 | - OS: 43 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Short description of change 2 | 3 | ## Types of changes 4 | 5 | 6 | 7 | - [ ] Bug fix (non-breaking change which fixes an issue) 8 | - [ ] New feature (non-breaking change which adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 10 | - [ ] I have read the **docs/CONTRIBUTING.md** document. 11 | - [ ] My code follows the code style of this project. 12 | - [ ] My change requires a change to the documentation. 13 | - [ ] I have updated the documentation accordingly. 14 | - [ ] I have read **tests/README.md**. 15 | - [ ] I have added tests to cover my changes. 16 | - [ ] All new and existing tests passed. 17 | 18 | ## What is the change? 19 | 20 | ## What does it fix/add? 21 | 22 | ## Test Configuration 23 | 24 | - PLC Model 25 | - PLC Firmware 26 | - pylogix version 27 | - python version 28 | - OS type and version 29 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # other 107 | .directory 108 | .vscode/ 109 | 110 | # PyCharm 111 | .idea/ 112 | 113 | # pylogix configuration 114 | tests/plcConfig.py 115 | pylogix/testing/ 116 | main.py -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # pylogix 4 | 5 | 6 | ![PyPI](https://img.shields.io/pypi/v/pylogix?label=pypi%20pylogix) 7 | ![PyPI](https://img.shields.io/pypi/l/pylogix) 8 | ![Versions](https://img.shields.io/pypi/pyversions/pylogix) 9 | ![MicroPython](https://img.shields.io/badge/micropython-1.20.0+-red?logo=micropython) 10 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/pylogix) 11 | [![Discord](https://img.shields.io/static/v1?label=discord&message=support&color=738adb&logo=discord)](https://discord.gg/tw8E9EAAnf) 12 | 13 | 14 | **WARNING**: There is a possibility of a scam using the pylogix name. Be careful with anything claiming to be pylogix. https://www.notpylogix.com 15 | 16 | 17 | Pylogix is a communication driver that lets you easily read/write values from tags in Rockwell Automation ControlLogix, CompactLogix, and Micro8xx PLCs over Ethernet I/P using Python. Only PLCs that are programmed with RSLogix5000/Studio5000 or Connected Components Workbench (Micro8xx), models like PLC5, SLC, and MicroLogix are *not* supported. They use a different protocol, which I have no plans to support. You can also connect to RSEmulate, but it may require additional 18 | configuration. See the Emulate [document](https://github.com/dmroeder/pylogix/blob/master/docs/Emulate.md) for more information. 19 | 20 | Many devices support CIP objects that allow for automatic discovery (like RSLinx does), which pylogix can discover but will likely not be able to interact with in any other meaningful way. Pylogix is only intended to talk to the above-mentioned PLCs and is only tested against them. It likely will not communicate with any other brands. 21 | 22 | For general support or questions, I created a [discord](https://discord.gg/tw8E9EAAnf). Feel free to join and ask questions, and I'll do my best to help promptly. 23 | 24 | ## Getting Started 25 | 26 | There are no dependencies, so you can get going quickly without installing other prerequisite packages. Both python2 and python3 are supported. 27 | 28 | ### Installing 29 | 30 | Install pylogix with pip (Latest version): 31 | 32 | ```console 33 | pylogix@pylogix-kde:~$ pip install pylogix 34 | ``` 35 | 36 | To install previous version before major changes (0.3.7): 37 | 38 | ```console 39 | pylogix@pylogix-kde:~$ pip install pylogix==0.3.7 40 | ``` 41 | 42 | To upgrade to the latest version: 43 | 44 | ```console 45 | pylogix@pylogix-kde:~$ pip install pylogix --upgrade 46 | ``` 47 | 48 | Alternatively, you can clone the repo and manually install it: 49 | 50 | ```console 51 | pylogix@pylogix-kde:~$ git clone https://github.com/dmroeder/pylogix.git 52 | pylogix@pylogix-kde:~$ cd pylogix 53 | pylogix@pylogix-kde:~/pylogix$ python setup.py install --user 54 | ``` 55 | 56 | ### Verifying Installation 57 | 58 | To verify the installation on Linux, open the terminal and use the following commands: 59 | 60 | ```console 61 | pylogix@pylogix-kde:~$ python3 62 | Python 3.8.5 (default, Jan 27 2021, 15:41:15) 63 | [GCC 9.3.0] on linux 64 | Type "help", "copyright", "credits" or "license" for more information. 65 | >>> import pylogix 66 | >>> pylogix.__version__ 67 | '0.7.10' 68 | ``` 69 | 70 | ### Your First Script: 71 | 72 | The cloned repository will come with many examples, I'll give one here. We'll read one simple tag and print out the value. All methods will return the Response class, which contains TagName, Value and Status. 73 | 74 | ```python 75 | from pylogix import PLC 76 | with PLC() as comm: 77 | comm.IPAddress = '192.168.1.9' 78 | ret = comm.Read('MyTagName') 79 | print(ret.TagName, ret.Value, ret.Status) 80 | ``` 81 | 82 | NOTE: If your PLC is in a slot other than zero (like can be done with ControLogix), then you can specify the slot with the following: 83 | 84 | ``` 85 | comm.ProcessorSlot = 2 86 | ``` 87 | 88 | NOTE: If you are working with a Micro8xx PLC, you must set the Micro800 flag since the path is different: 89 | 90 | ``` 91 | comm.Micro800 = True 92 | ``` 93 | 94 | Optionally set a specific maximum size for requests/replies. If not specified, defaults to try a Large, then a Small Forward Open (for Implicit, "Connected" sessions). 95 | 96 | ``` 97 | comm.ConnectionSize = 508 98 | ``` 99 | 100 | ## Installing MicroPython 101 | 102 | Checkout [Documentation](https://github.com/dmroeder/pylogix/blob/master/docs/mpy/README.md) 103 | 104 | ### Other Features 105 | 106 | Pylogix has features other than simply reading/writing. See the [documentation](https://github.com/dmroeder/pylogix/blob/master/docs/Documentation.md) for more info, see the examples directory 107 | simple use cases for the various methods. 108 | 109 | ## FAQ 110 | 111 | Here's a list of frequent asked questions. [faq](https://github.com/dmroeder/pylogix/blob/master/docs/FAQ.md) 112 | 113 | ## Authors 114 | * **Burt Peterson** - *Initial work* 115 | * **Dustin Roeder** - *Maintainer* - [dmroeder](https://github.com/dmroeder) 116 | * **Fernando B. (TheFern2)** - *Contributor* - [TheFern2](https://github.com/TheFern2) 117 | * **Joe Ryan** - *Contributor* - [jryan](https://bitbucket.org/jryan/aphytcomm/src/master/) 118 | * **Perry Kundert** - *Contributor* - [pjkundert](https://github.com/pjkundert) 119 | 120 | ## License 121 | 122 | This project is licensed under Apache 2.0 License - see the [LICENSE](https://github.com/dmroeder/pylogix/blob/master/LICENSE.txt) file for details. 123 | 124 | ## Acknowledgments 125 | 126 | * Archie of AdvancedHMI for all kinds pointers and suggestions. 127 | * Thanks to ottowayi for general python and good practice advice. 128 | * Thanks to all of the users that have tested and provided feedback. 129 | * Joe Ryan for Omron testing and feedback 130 | -------------------------------------------------------------------------------- /docs/Contributing.md: -------------------------------------------------------------------------------- 1 | ## Contributing -------------------------------------------------------------------------------- /docs/Documentation.md: -------------------------------------------------------------------------------- 1 | # pylogix API 2 | 3 | Everything with pylogix is done via the PLC module, all the properties and modules will be 4 | discussed beow. Whether you are reading, writing, pulling the tag list, etc. To make things 5 | easy for the user, pylogix will automatically make the necessary connection to the PLC when 6 | you call any of the methods. 7 | 8 | __Properties:__ 9 | - IPAddress (required) 10 | - ProcessorSlot (optional, default=0) 11 | - Micro800 (optional, default=False) 12 | - Route (optional, default=None) 13 | - ConnectionSize (optional, default=4002) 14 | - SocketTimeout (optional, default=5.0) 15 | 16 | __Methods:__ 17 | - [Read](#read)() 18 | - [Write](#write)() 19 | - [GetTagList](#gettaglist)() 20 | - [GetProgramsList](#getprogramslist)() 21 | - [GetProgramTagList](#getprogramtaglist)() 22 | - [GetPLCTime](#getplctime)() 23 | - [SetPLCTime](#setplctime)() 24 | - [Discover](#discover)() 25 | - [GetModuleProperties](#getmoduleproperties)() 26 | - [GetDeviceProperties](#getdeviceproperties)() 27 | - [Message](#message)() 28 | 29 | There are a few options for creating an instance of PLC(), how you do it is a matter of style I 30 | suppose. My preferred method is using contexts, or with statements, but is up to you. 31 | Some options are: 32 | 33 | ```python 34 | comm = PLC() 35 | comm.IPAddress = "192.168.1.10" 36 | # do stuff 37 | comm.Close() 38 | ``` 39 | or 40 | ```python 41 | comm = PLC("192.168.1.10") 42 | # do stuff 43 | comm.Close() 44 | ``` 45 | or 46 | ```python 47 | with PLC() as comm: 48 | comm.IPAddress = "192.168.1.10" 49 | # do stuff 50 | ``` 51 | my preferred 52 | ```python 53 | with PLC("192.168.1.10") as comm: 54 | # do stuff 55 | ``` 56 | 57 | Again, how you do it is up to you, each method works. There are some things to consider: 58 | 59 | 1. Don't call Close() unnecessarily. If the PLC no longer sees a request, it will eventually 60 | flush the connection, after about 90 seconds. So if you are reading/writing more often than 61 | 90 seconds, don't call Close(), just keep reading, call Close() when your program exits 62 | 63 | 2. With statements will automatically call Close() when the indent returns. It's a common 64 | issue where people write their with statement within a loop. Doing this will open and 65 | close the connection with each iteration of the loop. Instead, write the loop inside the 66 | with statement, that way, the driver is declared first, then the loop performs the actions. 67 | 68 | 3. When using with threads, be sure to create an instance for each thread, as opposed to sharing 69 | the instance between threads 70 | 71 | NO: 72 | ```python 73 | while True: 74 | with PLC("192.168.1.10") as comm: 75 | # no no 76 | ``` 77 | YES: 78 | ```python 79 | with PLC("192.168.1.10") as comm: 80 | while True: 81 | # ahhh, much better 82 | ``` 83 | 84 | All pylogix methods will return the data in as the 85 | [Response](https://github.com/dmroeder/pylogix/blob/master/pylogix/lgx_response.py) class, 86 | which has 3 members: TagName, Value and Status. Not all members apply to every method, so there 87 | are occasions where TagName, for example, will be None. Like when getting the PLC time, TagName 88 | doesn't apply. Value's type varies depending which method is being used. For example, Value can 89 | be a single value when reading a single tag, or can be a list of values when reading a list of tags. 90 | Or Value can be the lgx_device type, when requesting device properties or discovering devices on the 91 | network 92 | ```python 93 | ret = comm.Read("MyTag") 94 | print(ret.TagName, ret.Value, ret.Status) 95 | ``` 96 | 97 | __IPAddress__ 98 | Straight forward, this is the PLC's IP Address, as a string. Or, for ControlLogix, the 99 | Ethernet module you will be accessing the PLC at. 100 | >comm.IPAddress = "172.17.130.11" 101 | 102 | __ProcessorSlot__ 103 | Integer for which slot the processor is in. By default, the value is 0, since it is most 104 | common for a processor to be in slot 0. For CompactLogix and Micro8xx, this is always true. 105 | For ControlLogix, the processor can be in any slot. In fact, you can have multiple processors 106 | in one chassis. 107 | >comm.ProcessorSlot = 4 # connect to controller in slot 4 108 | 109 | __Micro800__ 110 | True/False property, False by default. The Micro820/850's support Ethernet I/P, but use a 111 | slightly different path, so the driver needs to know this up front. Set to True when accessing 112 | a Micro800 PLC's. 113 | >comm.Micro800 = True 114 | 115 | Keep in mind, these PLC's are pretty feature limited, so not all pylogix methods will work with 116 | them. 117 | 118 | __Route__ 119 | Route allows you to bridge to other controllers through a ControlLogix backplane or 5380 controller 120 | in dual IP mode. Routes are always in pairs of tuples. An example: 121 | >comm.Route = [(1,4), (2, '10.10.10.9')] 122 | 123 | __NOTE:__ Routing has always been a feature of ControlLogix, so the value of going out of an Ethernet 124 | port has always been 2. That is, until the 5380 CompactLogix and dual IP mode came along. You use 125 | a value of 3 or 4, depending on which port you are going out of. 126 | 127 | __ConnectionSize__ 128 | pylogix, as it currently is, will make an attempt to connect at the maximum possible connection 129 | size, which is 4002 bytes. If unsuccessful, it will attempt at the next common max size, which 130 | is 508 bytes. The user can specify a connection size anywhere in between. The best practice is 131 | to leave this value at default. The main reason for making this configurable is a history 132 | problem. pylogix made this configurable originally. Later, the concept of default to the max 133 | size, then fall back another size was implemented. Making it configurable made sure that there 134 | was no compatibility issues. 135 | 136 | There is no known benefit to reducing the connection size. In fact, you will get the best 137 | performance by leaving it at the max. There seems to be no latency issue with always using the 138 | larger packet size. 139 | 140 | The early controllers and Ethernet modules supported connection sizes of 508 bytes. At around 141 | v18, Rockwell implemented connection sizes of 4002 bytes. 142 | 143 | __SocketTimeout__ 144 | If pylogix cannot connect to a PLC or loses its connection to the PLC, the default timeout is 145 | 5 seconds (5.0). If this time is too long, it can be lowered. Just be sure to not set it lower 146 | than the time it takes the PLC to reply to prevent false timeouts. PLC's typically respond 147 | in a few milliseconds, but that is not guaranteed. 148 | 149 | # Read 150 | Read allows you to pull values from the PLC using tag names. You can perform simple reads using 151 | single tag names, or bundle reads using lists of tags names. Read is only currently capable of 152 | handling the fundamental data types (BOOL, SINT, INT, DINT, LINT, REAL, STRING). While you can 153 | read members of UDT's, it must be at the fundamental data type level. This method will currently 154 | return the raw bytes of the UDT values, which you will have to parse. 155 | 156 | While it is necessary for pylogix to know the data type of the tag being read, to make it simple 157 | for the user, pylogix will discover the data type the very first time a tag is accessed. The data 158 | type is saved in a dict KnownTags so that this only has to happen once per new tag. This does cause 159 | some extra overhead and can impact performance a bit, especially if you are just reading a large number 160 | of unique tags, then closing. To help, you can provide the data type up front, which will skip the 161 | discovery of the data type. 162 | 163 | __NOTE:__ if you want to access program scoped tags, use the following syntax 164 | >Program:ProgramName.TagName 165 | 166 | #### Single tag reads 167 | To read a single tag, provide a tag name and a single instance of the response class will be 168 | returned. 169 | 170 |
Example 171 |

172 | 173 | ```python 174 | from pylogix import PLC 175 | with PLC("192.168.1.9") as comm: 176 | ret = comm.Read("MyDint") 177 | print(ret.TagName, ret.Value, ret.Status) 178 | ``` 179 | result: 180 | ```console 181 | pylogix@pylogix-kde:~$ python3 example.py 182 | MyDint 8675309 Success 183 | ``` 184 |

185 |
186 | 187 | #### Read an array 188 | To read an array, provide a tag name and the number of elements you want to read. Value in 189 | the response will be a list of the values you requested. 190 | 191 |
Example 192 |

193 | 194 | ```python 195 | from pylogix import PLC 196 | with PLC("192.168.1.9") as comm: 197 | ret = comm.Read("MyDintArray[0]", 10) 198 | print(ret.TagName, ret.Value, ret.Status) 199 | ``` 200 | result: 201 | ```console 202 | pylogix@pylogix-kde:~$ python3 example.py 203 | MyDintArray[0] [42, 43, 44, 45, 46, 47, 48, 49, 50, 51] Success 204 | ``` 205 |

206 |
207 | 208 | #### Read a list of tags 209 | The best way to improve performance is to read tags in a list. Reading lists of tags will take 210 | advantage of the mulit-service request, packing many request into a single packet. When reading 211 | lists, a list of the Response class will be returned. 212 | 213 |
Example 214 |

215 | 216 | ```python 217 | from pylogix import PLC 218 | with PLC("192.168.1.9") as comm: 219 | tags = ["MyDint", "MyString", "MyInt"] 220 | ret = comm.Read(tags) 221 | 222 | # print exactly how the data is returned 223 | print("returned data", ret) 224 | 225 | # print each item returned 226 | for r in ret: 227 | print(r.TagName, r.Value, r.Status) 228 | ``` 229 | result: 230 | ```console 231 | pylogix@pylogix-kde:~$ python3 example.py 232 | returned data [Response(TagName=MyDint, Value=8675309, Status=Success), Response(TagName=MyString, Value=I am a string, Status=Success), Response(TagName=MyInt, Value=90, Status=Success)] 233 | MyDint 8675309 Success 234 | MyString I am a string Success 235 | MyInt 90 Success 236 | ``` 237 |

238 |
239 | 240 | #### Skip the data type discovery 241 | While I prefer to keep things simple and let pylogix get the data type for me, you can bypass this 242 | feature to get a little better performance. The data type discovery only happens once per tag, so 243 | if you are reading in a loop, this doesn't have much benefit. But of you are reading a large list of 244 | tags, it can speed things up. 245 | 246 |
Example 247 |

248 | 249 | ```python 250 | from pylogix import PLC 251 | with PLC("192.168.1.9") as comm: 252 | ret = comm.Read("MyDint", datatype=0xc4) 253 | print(ret.TagName, ret.Value, ret.Status) 254 | ``` 255 | result: 256 | ```console 257 | pylogix@pylogix-kde:~$ python3 example.py 258 | MyDint 8675309 Success 259 | ``` 260 |

261 |
262 | 263 | 264 | # Write 265 | Use Write() to write values to PLC tags. You can write a value to a single tag, a list of values to an 266 | array tag or write a list of values to a list of tags. Write will return the Response class, which is 267 | mainly useful for the status to see if your write was successful or not. 268 | 269 | #### Write a single value 270 | To write a single value to a single tag, pass a tag name and a value. Be mindful of the data type, for 271 | integers (SINT/INT/DINT) use the int type for the value, for REAL, use the float type and for strings, 272 | use the str type. 273 | 274 |
Example 275 |

276 | 277 | ```python 278 | from pylogix import PLC 279 | with PLC("192.168.1.9") as comm: 280 | ret = comm.Write("MyDint", 10) 281 | print(ret.TagName, ret.Value, ret.Status) 282 | ``` 283 | result: 284 | ```console 285 | pylogix@pylogix-kde:~$ python3 example.py 286 | MyDint 10 Success 287 | ``` 288 |

289 | 290 |

291 | 292 | ```python 293 | from pylogix import PLC 294 | with PLC("192.168.1.9") as comm: 295 | ret = comm.Write("MyString", "I am a string") 296 | print(ret.TagName, ret.Value, ret.Status) 297 | ``` 298 | result: 299 | ```console 300 | pylogix@pylogix-kde:~$ python3 example.py 301 | MyString I am a string Success 302 | ``` 303 |

304 | 305 |
306 | 307 | 308 | #### Write list of values 309 | You can write a list of values to an array by passing a list of values. You don't have to specify the 310 | length, pylogix will simply use the number of values in the list. 311 | 312 |
Example 313 |

314 | 315 | ```python 316 | from pylogix import PLC 317 | with PLC("192.168.1.9") as comm: 318 | values = [123, 456, 789] 319 | ret = comm.Write("MyDintArray[0]", values) 320 | print(ret.TagName, ret.Value, ret.Status) 321 | ``` 322 | result: 323 | ```console 324 | pylogix@pylogix-kde:~$ python3 example.py 325 | MyDintArray[0] [123, 456, 789] Success 326 | ``` 327 |

328 |
329 | 330 | 331 | #### Write multiple tags at once 332 | Similar to Read, you can write multiple tags in one request. Pylogix will use the multi-service request 333 | and pack the requests into the minimum number of packets. You make a list, where each write is a tuple 334 | containing the tag name and the value. 335 | 336 |
Example 337 |

338 | 339 | ```python 340 | from pylogix import PLC 341 | with PLC("192.168.1.9") as comm: 342 | request = [("MyDint", 10), ("MyInt", 3), ("MyString", "hello world")] 343 | ret = comm.Write(request) 344 | for r in ret: 345 | print(r.TagName, r.Value, r.Status) 346 | ``` 347 | result: 348 | ```console 349 | pylogix@pylogix-kde:~$ python3 example.py 350 | MyDint 10 Success 351 | MyInt 3 Success 352 | MyString hello world Success 353 | ``` 354 |

355 |
356 | 357 | 358 | # GetTagList 359 | Retreives the controllers tag list, including program scoped tags (default). Returns the Response class, 360 | where the Value will be a list of [Tag](https://github.com/dmroeder/pylogix/blob/master/pylogix/lgx_tag.py) 361 | class. pylogix also saves this list internally in TagList. Along with the tag list, pylogix also retrieves 362 | the UDT definitions, which are stored as a dict in UDT. 363 | 364 |
Example 365 |

366 | 367 | ```python 368 | from pylogix import PLC 369 | with PLC("192.168.1.9") as comm: 370 | tags = comm.GetTagList() 371 | for t in tags.Value: 372 | print("Tag:", t.TagName, t.DataType) 373 | ``` 374 | result: 375 | ```console 376 | pylogix@pylogix-kde:~$ python3 example.py 377 | Tag: Program:MainProgram 378 | Tag: MyDint DINT 379 | Tag: MyDintArray DINT 380 | Tag: MyString STRING 381 | Tag: MyInt INT 382 | Tag: MyUDT Pylogix 383 | ``` 384 |

385 |
386 | 387 | # GetProgramsList 388 | Retrieves only a list of the program names. This will automatically call GetTagList in order to get the list 389 | of program names. Only a list of the program names will be returned. This can be useful if you want to only 390 | get a list of a particular programs tag list. 391 | 392 |
Example 393 |

394 | 395 | ```python 396 | from pylogix import PLC 397 | with PLC("192.168.1.9") as comm: 398 | programs = comm.GetProgramsList() 399 | print(programs.Value) 400 | ``` 401 | result: 402 | ```console 403 | pylogix@pylogix-kde:~$ python3 example.py 404 | ['Program:WidgetProgram', 'Program:ThingyProgram'] 405 | ``` 406 |

407 |
408 | 409 | 410 | # GetProgramTagList 411 | Retrieves a list of a particular program. Requires a program name to be provided. Returns the Response class 412 | where the Value will be a list of [Tag](https://github.com/dmroeder/pylogix/blob/master/pylogix/lgx_tag.py) class. 413 | 414 |
Example 415 |

416 | 417 | ```python 418 | from pylogix import PLC 419 | with PLC("192.168.1.9") as comm: 420 | program_tags = comm.GetProgramTagList("Program:WidgetProgram") 421 | for t in program_tags.Value: 422 | print("Tag:", t.TagName, t.DataType) 423 | ``` 424 | result: 425 | ```console 426 | pylogix@pylogix-kde:~$ python3 example.py 427 | ('Tag:', 'Program:WidgetProgram.WidgetDint', 'DINT') 428 | ``` 429 |

430 |
431 | 432 | 433 | # GetPLCTime 434 | Reads the PLC clock, returns the Response class, by default, Value will be the datetime class. Optionally, 435 | if you set raw=True, the raw microseconds will be returned. 436 | 437 |
Example 438 |

439 | 440 | ```python 441 | from pylogix import PLC 442 | with PLC("192.168.1.9") as comm: 443 | time = comm.GetPLCTime() 444 | print("PLC Time:", time.Value) 445 | 446 | raw_time = comm.GetPLCTime(True) 447 | print("Raw Time:", raw_time.Value) 448 | ``` 449 | result: 450 | ```console 451 | pylogix@pylogix-kde:~$ python3 example.py 452 | PLC Time: 2021-04-20 15:41:22.964380 453 | Raw Time: 1618933282970171 454 | ``` 455 |

456 |
457 | 458 | 459 | # SetPLCTime 460 | Synchronizes the PLC clock with your computers time. This is similar to what happens when you ae online with 461 | a controller and click Set Date, Time and Zone from Workstation, in Controller Properties of RSLogix5000 or 462 | Studio5000 Logix Designer. Returns the Response class, which is mainly useful for the status. 463 | 464 | Optionally, you can override the daylight savings time by setting dst=True or False 465 | 466 |
Example 467 |

468 | 469 | ```python 470 | from pylogix import PLC 471 | with PLC("192.168.1.9") as comm: 472 | ret = comm.SetPLCTime() 473 | print(ret.Value, ret.Status) 474 | ``` 475 | result: 476 | ```console 477 | pylogix@pylogix-kde:~$ python3 example.py 478 | 1618958684216474 Success 479 | ``` 480 |

481 |
482 | 483 | # Discover 484 | Sends a broadcast request out on the network, which all Ethernet I/P devices listen for and respond with basic 485 | information about themselves. All Ethernet I/P devices are required to support this feature. This is what 486 | RSLinx uses in its Ethernet I/P driver to discover devices on the network. Returns the Response class, 487 | Value will be the [Device](https://github.com/dmroeder/pylogix/blob/master/pylogix/lgx_device.py) class. 488 | 489 | __NOTE:__ Because all Ethernet I/P devices are designed to respond to this, many think that pylogix will be 490 | able to communicate with 3rd party devices in some meaningful way. The CIP objects targeted by pylogix are 491 | Rockwell specific objects, not part of the ODVA spec. The CIP spec allows for vendor specific object. While 492 | there may be devices out there that support the same objects, I only have access to Rockwell PLC's, so I have 493 | no way to support them. I can offer no support for them. 494 | 495 |
Example 496 |

497 | 498 | ```python 499 | from pylogix import PLC 500 | with PLC("192.168.1.9") as comm: 501 | devices = comm.Discover() 502 | for device in devices.Value: 503 | print(device.ProductName, device.Revision) 504 | ``` 505 | result: 506 | ```console 507 | pylogix@pylogix-kde:~$ python3 example.py 508 | 1769-L30ER/A LOGIX5330ER 30.11 509 | 5069-L310ER/A 32.12 510 | ``` 511 |

512 |
513 | 514 | # GetModuleProperties 515 | Requests properties of a specific module. Requires a slot to be specified. This method is useful for querying 516 | devices that are in a chassis. Like local I/O in a CompactLogix chassis, or even modules in a Point I/O chassis. 517 | Returns the Response class, where Value is the [Device](https://github.com/dmroeder/pylogix/blob/master/pylogix/lgx_device.py) class. 518 | 519 |
Example 520 |

521 | 522 | ```python 523 | from pylogix import PLC 524 | with PLC("192.168.1.9") as comm: 525 | device = comm.GetModuleProperties(3).Value 526 | print(device.ProductName, device.Revision) 527 | ``` 528 | result: 529 | ```console 530 | pylogix@pylogix-kde:~$ python3 example.py 531 | 1734-IB8 8 PT 24VDC SINK IN 3.31 532 | ``` 533 |

534 |
535 | 536 | # GetDeviceProperties 537 | Similar to GetModuleProperties, this queries a device at an IP address. This is useful for querying things that 538 | are not part of a chassis, like PowerFlex drives, or maybe a barcode reader that supports Ethernet I/P. Returns 539 | the Response class, where Value is the [Device](https://github.com/dmroeder/pylogix/blob/master/pylogix/lgx_device.py) class. 540 | 541 |
Example 542 |

543 | 544 | ```python 545 | from pylogix import PLC 546 | with PLC("192.168.1.9") as comm: 547 | device = comm.GetDeviceProperties().Value 548 | print(device.ProductName, device.Revision) 549 | ``` 550 | result: 551 | ```console 552 | pylogix@pylogix-kde:~$ python3 example.py 553 | 1769-L30ER/A LOGIX5330ER 30.11 554 | ``` 555 |

556 |
557 | 558 | # Message 559 | Send a custom CIP object, similar to the Logix MSG instruction. Provide a CIP service/class/instance/attribute, 560 | the Message function will return the raw bytes of the object. It is up to you to know how the data is 561 | organized. When providing multiple attributes, they should be in a list. Data provided to Message should be in 562 | bytes (use struct.pack). 563 | 564 | Please do not open issues asking for help with finding CIP objects, there are just too many out there. Ask in the 565 | discord server as well. There are many users, so the pool of knowledge is larger. 566 | 567 |
Exammple - Get Major/Minor Fault Code 568 |

569 | 570 | ```python 571 | import pylogix 572 | from struct import unpack_from 573 | 574 | with pylogix.PLC("192.168.1.10") as comm: 575 | ret = comm.Message(cip_service=0x01, cip_class=0x73, cip_instance=0x01) 576 | 577 | if ret.Status == "Success": 578 | data = ret.Value[44:] 579 | major = unpack_from(" 585 |

586 | 587 | # Additional information 588 | 589 | When reading/writing, pylogix keeps a dict called KnownTags, this is used to store the tag name 590 | and data type for the purpose of not having to request this each time a tag is read or written 591 | to. There are users who read a list of 1000 or more unique tag names, retrieving the data type 592 | essentially doubles the time that it would take to read the values. While the performance has 593 | increased drastically in this case by utilizing the mult-service request to retrieve the data 594 | type. Still, there ae users that want maximum performance, so Read/Write allows providing the 595 | data type up front. Doing this adds the tag name and data type to the KnownTags dict up front, 596 | then skips the initial exchange. You can see the atomic data type values by printing the CIPTypes 597 | dict: 598 | >print(comm.CIPTypes) 599 | 600 | When requesting the PLC's tag list, there is also a dict that is saved of the UDT definitions 601 | called UDT. This will be the Tag type, which contains a lot of properties. After reading the 602 | tag list, you can print this dict: 603 | >print(comm.UDT) 604 | -------------------------------------------------------------------------------- /docs/Emulate.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Communicating with Emulate 4 | 5 | 6 | It is possible to communicate with RSEmulate using pylogix. You must be running pylogix on the computer that is running Emulate. If you receive the response ***Unknown error [WinError 10061]No connection could be made because the target machine actively refused it*** there are additional steps you may need to take. RSLinx Enterprise has an option to "Listen on Ethernet I/P encapsulation ports" that needs to be turned on. The easiest way for me was to do it 7 | through FactoryTalk View Studio ME, though there may be other ways to enable this feature: 8 | 9 |
10 | 11 | * From FTVSME, go to the very bottom of the Explorer, switch to the Communication tab 12 | * Right click on the Ethernet driver, select Properties 13 | * Switch to the Advanced tab 14 | * Check the Encapsulation check box 15 | 16 |
17 | 18 | ![](pics/encapsulation1.png) 19 | 20 |
21 | 22 | ![](pics/encapsulation2.png) 23 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequent Asked Questions 2 | 3 | 1. Does pylogix work with PLC5, SLC, MicroLogix? 4 | No 5 | 6 | 2. Does pylogix work with emulate? Yes, locally only. Ensure ProcessorSlot is set to 2 7 | 8 | 3. Does pylogix work with softlogix? Yes, locally and remote. Ensure ProcessorSlot is set to 2 9 | 10 | 4. Does pylogix work with Micro8xx, and CCW Simulator? Yes 11 | 12 | 5. Can pylogix read Micro8xx program tags? No, it can only read global tags. 13 | 14 | 6. Does pylogix works with other brand PLCs, such as Omron, Siemens, etc? No 15 | 16 | 7. Does pylogix support custom path routing? Yes 17 | 18 | 8. Does pylogix support reading from multiple PLCs? Yes, create multiple PLC object instances 19 | 20 | 9. What PLC models does pylogix support? CompactLogix, ControlLogix, Micro8xx 21 | 22 | 10. How can I install pylogix? Installation can be done primarily with pip, or local install. Instructions on README 23 | 24 | 11. Does pylogix support UDT's structure read? Yes, and no. You can get a read in raw bytes which you'll then need to know how to unpack yourself example of that [here](https://github.com/dmroeder/pylogix/blob/master/examples/40_read_timer.py). There is no way to read the structure of the UDT at the moment discussion [here](https://github.com/dmroeder/pylogix/issues/67). There's also no current way to write raw to UDT's. 25 | 26 | 12. pylogix .Status says "Forward Open Failed": This is typically caused by port 44818 being blocked by a firewall. 27 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Documentation Contents 2 | 3 | - [Starter Guide](./guides/Starter-Guide.md) 4 | - [Continuous Reading](./guides/Continuous-Reading.md) 5 | - [Working with Files](./guides/Working-With-Files.md) 6 | - [Working with LogFiles](./guides/Working-With-LogFiles.md) 7 | - [API](Documentation.md) 8 | - [Contributing](Contributing.md) TODO 9 | - [Unit testing](../tests/README.md) 10 | - [MicroPython](mpy/README.md) -------------------------------------------------------------------------------- /docs/guides/Continuous-Reading.md: -------------------------------------------------------------------------------- 1 | ## Continuous Reading 2 | 3 | This example just uses a while loop, to keep reading a tag. 4 | 5 | Run [test-03.py](../python_code/test-03.py) to see how this works. -------------------------------------------------------------------------------- /docs/guides/Starter-Guide.md: -------------------------------------------------------------------------------- 1 | # Starter Guide 2 | 3 | This guide makes some knowledge assumptions, experience with AB plc programs and some familiarity with python. 4 | 5 | > Danger: As we all know PLCs control dangerous equipment so ensure you are not writing tags in live systems unless you know what you are doing, and do so at your own risk. 6 | 7 | ## Initial Setup 8 | 9 | If you don't have python installed, download and install [Python Download](https://www.python.org/downloads/). Ensure path checkbox is checked, or set path for your system accordingly for `/Python37/Scripts`. 10 | 11 | The best thing to do with python installed is to use pip which is a python package manager, otherwise you have to copy and paste libraries to where your script is located. 12 | 13 | Install pylogix: 14 | 15 | ``` 16 | pip install git+https://github.com/dmroeder/pylogix 17 | ``` 18 | 19 | > In the near future pylogix will be available from pypi 20 | 21 | ## RSLogix5000 Project 22 | 23 | If you have an existing project then skip this section. You want to test this library with a very minimal code like [test-01.py](../python_code/test-01.py), this will ensure you have connection to the PLC before writing complex code. I am using softlogix 5800, but this applies to any Contrologix, and Compactlogix. If you already have existing code, then go to controller tags, and pick a boolean tag, and replace it on test-01.py line 25. 24 | 25 | > Note: Ethernet Protocol does not work with emulator. 26 | 27 | Create a new project, select your controller type, and once the project is done, add the whatever ethernet module you have in your rack, and configure the IP settings. 28 | 29 | Save, and download program to the plc. If you can go online with the PLC, then we have good connection. If you don't check the below: 30 | 31 | ![Run_Mode](../pics/Run_Mode.PNG) 32 | 33 | - ping plc 34 | - If you can't ping it, check network cables 35 | - Ensure your PC is on the same subnet, i.e. plc: 192.168.1.97, PC: 192.168.1.95 36 | - Ensure project slots are the same as physical layout. 37 | 38 | ### Adding tags to the plc project 39 | 40 | In the controller organizer pane, select controller tags: 41 | 42 | ![Controller_Tags](../pics/Select_Controller_Tags.PNG) 43 | 44 | Select Edit Tags: 45 | 46 | ![Edit_Tags](../pics/Edit_Tags.PNG) 47 | 48 | Add a boolean tag: 49 | 50 | ![bool_tag](../pics/Add_Bool_01.PNG) 51 | 52 | ### Test the boolean tag 53 | 54 | Run [test-01.py](../python_code/test-01.py), you can open the file in python idle, or in the command line: 55 | 56 | ``` 57 | python test-01.py 58 | ``` 59 | 60 | On Windows: 61 | 62 | ``` 63 | py -3.7 test-01.py 64 | ``` 65 | 66 | Output: 67 | 68 | ``` 69 | bool_01 True Success 70 | ``` 71 | 72 | If the tag name is wrong, and doesn't exists, you'll get a value of None, and an error 73 | 74 | ``` 75 | bool_01 None Path segment error 76 | ``` 77 | 78 | If you're able to read that boolean you are good to go with pylogix. If not see possible issues. 79 | 80 | ### Test a boolean tag in a program 81 | 82 | Let's use the default program MainProgram, and double click in Program Tags. In the same fashion as before, click on Edit Tags, and add `bool_01`. Run [test-02.py](/python_code/test-02.py). Remember controller tags are global scope you can use in any program, and program are local scope. Even when we used the same name bool_01 those are two different tags. 83 | 84 | ## Possible Issues 85 | 86 | There are quite a few issues that can arise. 87 | 88 | - If you can't go online with rslogix: 89 | 90 | - ping controller 91 | - check ethernet cable 92 | - check ethernet ip in the IO configuration 93 | - check ip of your pc 94 | 95 | - If you are having import errors: 96 | - ensure pylogix is installed 97 | 98 | ## Report Issues 99 | 100 | https://github.com/dmroeder/pylogix/issues 101 | 102 | Before posting a usage issue, ensure you have ran through test-01.py. If you can't get test-01.py to run or to read the boolean tag, post the following in the issue: 103 | 104 | - Post whatever traceback errors you are getting 105 | - Which slot is the plc in? 106 | - A screenshot of the configuration of the ethernet module 107 | - Run ipconfig or ifconfig on linux, post screenshot 108 | - Plc model, OS system, python version, plc firmware 109 | 110 | The more information you post, the easier, and faster you'll get a response. We are giving free help, on a free repository so be mindful of your responses we can't read your mind. 111 | -------------------------------------------------------------------------------- /docs/guides/Working-With-Files.md: -------------------------------------------------------------------------------- 1 | ## Working with Files 2 | 3 | It is pretty useful to have configuration files with given tags for read/write. 4 | 5 | ### To read all lines from a file: 6 | 7 | tags.txt 8 | 9 | ``` 10 | tag_01 11 | tag_02 12 | tag_03 13 | ... 14 | ``` 15 | 16 | ```python 17 | 18 | file_extension = txt 19 | 20 | with open(path + "\\" + file + "." + file_extension) as f: 21 | all_lines = f.readlines() 22 | ``` 23 | 24 | ### To write tags to a file: 25 | 26 | saved_tags.txt 27 | 28 | First append tags to a list: 29 | 30 | ```python 31 | # read online value 32 | 33 | for index in range(len(all_lines)): 34 | ret = Read(all_lines[index]) 35 | 36 | # could have a sanity check here if ret.Value is None 37 | 38 | put_string = ret.TagName + "|" + str(ret.Value) 39 | 40 | # append to list 41 | tags_list.append(put_string) 42 | 43 | ``` 44 | 45 | Then save to a file: 46 | 47 | ```python 48 | with open(path + "\\" + file + "_Save." + file_extension, "w") as dp_save_file: 49 | dp_save_file.writelines(tags_list) 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/guides/Working-With-LogFiles.md: -------------------------------------------------------------------------------- 1 | # Working with Log Files 2 | 3 | Logging is useful to figure out what is going wrong. Using our files example, we'd probably want to log the error instead of printing it. 4 | 5 | Time of error is very important, I am using the datetime library for that. 6 | 7 | ```python 8 | import datetime 9 | 10 | now = datetime.datetime.now() 11 | log = open("log.txt", "a+") 12 | check_error_log = False 13 | 14 | # read online value 15 | ret = Read(plc_tag) 16 | put_string = ret.TagName + "|" + str(ret.Value) 17 | 18 | # Neccesary sanity check, because there are no exceptions with pylogix 19 | if ret.Status == "Success": 20 | # append to list 21 | tags_list.append(put_string) 22 | 23 | if ret.Status != "Success": 24 | log.write("%s Save Error: %s tag %s\n" % (now.strftime("%c"), ret.TagName, ret.Status)) 25 | log.flush() # this ensures it logs to the file in the case of a crash 26 | check_error_log = True # flag to alert user there are errors logged 27 | ``` 28 | 29 | Remember to close the file at the very end of your application. 30 | 31 | ``` 32 | log.close() 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/mpy/Development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | It is important to ensure any pylogix development is compatible with all python version, and micropython. A quick way to check is to run tox, and then run micropython unittests explained in the tests documentation. 4 | 5 | # MicroPython quick guide 6 | 7 | MicroPython is built for efficiency and thus many sugar functions and entire modules are really short. I would say almost everything can be made to be compatible with MicroPython. 8 | 9 | # Setup 10 | 11 | I would encourage linux for dev setup, it is a bit more streamlined. You can easily install MicroPython from package manager, or build latest. 12 | 13 | My current setup is: 14 | - pycharm with micropython plugin 15 | - it has integrated repl 16 | - can upload files from ide 17 | - esptool installed with python2 for flashing esp32 firmware 18 | - screen installed from package manager 19 | - micropython installed from package manager -------------------------------------------------------------------------------- /docs/mpy/Guide.md: -------------------------------------------------------------------------------- 1 | # Install Pylogix to a board running MicroPython 2 | 3 | Before you can use pylogix on your board, you have to flash it with the correct firmware. For development purposes I went with ESP32, and raspberry pico since they are widely available and relatively low cost. I suggest to stick to these two at least for now, use other models at your own risk. 4 | 5 | There are dozens if not hundreds of supported boards, some will have too little system resources to support pylogix so be aware of those limitation. 6 | 7 | Checkout the official documentation to install MicroPython 1.20+, once you get a repl, return to this document. If your board has wifi onboard ensure you install the correct firmware, some boards have different firmware based on the hardware to make firmware smaller. 8 | 9 | https://docs.micropython.org/en/latest/ 10 | https://docs.micropython.org/en/latest/esp32/tutorial/intro.html#esp32-intro 11 | 12 | > NB: esptool only works with python 2.7, so ensure python 2.7 is installed on your system in order to flash the binary to the esp32. 13 | 14 | https://www.raspberrypi.com/documentation/microcontrollers/micropython.html 15 | 16 | > NB: We will not support issues with getting your board flashed with micropython, so please seek help in the respective board forums. 17 | 18 | # Running py files 19 | 20 | You can run commands, and scripts with [pyboard.py](https://docs.micropython.org/en/latest/reference/pyboard.py.html). I also recommend using mu, or thonny editors if you are a beginner they both support micropython. I use jetbrains pycharm which has a plugin for micropython, although in latest editions the repl seems to be broken. 21 | 22 | In short micropython has two files which are treated special, boot.py and main.py. If these files exist, boot loads when the device boots, here in the example I created some utility functions to either connect to LAN or WAN, and at the bottom of the file you can specify which functions run automatically. The main.py file runs after boot, and will continue to run until you enter a REPL. 23 | 24 | > NB: There are tons of micropython tutorials out there, same as before micropython support is only scoped to pylogix for issues running files and uploading seek help in the respective forums. 25 | 26 | # Installing pylogix with internet boards 27 | 28 | Once you have a repl, and know how to upload files to your board checkout the directory under examples called 01_micropython_example. If you intend to use wifi, enter your ssid and password in the connect_wan() function. In the main.py file, change the ip to your PLC, and configure your tags accordingly. Then upload both files to the board. Launch the repl and type the following lines. 29 | 30 | ```python 31 | import mip 32 | mip.install("github:dmroeder/pylogix") 33 | ``` 34 | 35 | # If there is a dev branch 36 | 37 | ```python 38 | mip.install("github:dmroeder/pylogix", version="micropython/patch1") 39 | ``` 40 | 41 | In the repl, you can run ls() which is a function I've loaded in the boot.py 42 | 43 | ``` 44 | MicroPython v1.20.0 on 2023-04-26; Raspberry Pi Pico W with RP2040 45 | Type "help()" for more information. 46 | >>> ls 47 | 48 | >>> ls() 49 | ['boot.py', 'lib', 'main.py'] 50 | >>> ls("lib") 51 | ['pylogix'] 52 | >>> ls("lib/pylogix") 53 | ['__init__.mpy', 'eip.mpy', 'lgx_comm.mpy', 'lgx_device.mpy', 'lgx_response.mpy', 'lgx_tag.mpy', 'lgx_uvendors.mpy.bin', 'lgx_uvendors.mpy', 'utils.mpy', 'vendors.bin'] 54 | ``` 55 | 56 | # Installing pylogix without internet 57 | 58 | In a pycharm project, create a directory called lib, then inside lib, another called pylogix. `[root]/lib/pylogix`. Inside the pylogix dir, copy all files from upylogix from the repository. Then flash lib dir to the device. Then flash main.py and boot.py, when the devices reboot, it should change whatever tags you've configured in the PLC. -------------------------------------------------------------------------------- /docs/mpy/README.md: -------------------------------------------------------------------------------- 1 | ## MicroPython Docs 2 | 3 | - [Guide](Guide.md) 4 | - [Development](Development.md) -------------------------------------------------------------------------------- /docs/pics/Add_Bool_01.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/Add_Bool_01.PNG -------------------------------------------------------------------------------- /docs/pics/Edit_Tags.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/Edit_Tags.PNG -------------------------------------------------------------------------------- /docs/pics/Run_Mode.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/Run_Mode.PNG -------------------------------------------------------------------------------- /docs/pics/Select_Controller_Tags.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/Select_Controller_Tags.PNG -------------------------------------------------------------------------------- /docs/pics/create_bool_arr_01.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/create_bool_arr_01.PNG -------------------------------------------------------------------------------- /docs/pics/create_bool_arr_02.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/create_bool_arr_02.PNG -------------------------------------------------------------------------------- /docs/pics/encapsulation1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/encapsulation1.png -------------------------------------------------------------------------------- /docs/pics/encapsulation2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/encapsulation2.png -------------------------------------------------------------------------------- /docs/pics/progressbar_01.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/progressbar_01.PNG -------------------------------------------------------------------------------- /docs/pics/progressbar_02.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/docs/pics/progressbar_02.PNG -------------------------------------------------------------------------------- /docs/python_code/test-01.py: -------------------------------------------------------------------------------- 1 | from pylogix import PLC 2 | 3 | # Setup the PLC object with initial parameters 4 | # Change to your plc ip address, and slot, default is 0, shown for clarity 5 | comm = PLC('192.168.1.207', 0) 6 | 7 | # Read returns Response class (.TagName, .Value, .Status) 8 | ret = comm.Read('bool_01') 9 | print(ret.TagName, ret.Value, ret.Status) 10 | 11 | # Close Open Connection to the PLC 12 | comm.Close() 13 | -------------------------------------------------------------------------------- /docs/python_code/test-02.py: -------------------------------------------------------------------------------- 1 | from pylogix import PLC 2 | 3 | # Setup the PLC object with initial parameters 4 | # Change to your plc ip address, and slot, default is 0, shown for clarity 5 | comm = PLC('192.168.1.207', 0) 6 | 7 | # Read returns Response class (.TagName, .Value, .Status) 8 | ret = comm.Read('Program:MainProgram.bool_01') 9 | print(ret.TagName, ret.Value, ret.Status) 10 | 11 | # Close Open Connection to the PLC 12 | comm.Close() 13 | -------------------------------------------------------------------------------- /docs/python_code/test-03.py: -------------------------------------------------------------------------------- 1 | from pylogix import PLC 2 | import time 3 | 4 | # Setup the PLC object with initial parameters 5 | # Change to your plc ip address, and slot, default is 0, shown for clarity 6 | comm = PLC('192.168.1.207', 0) 7 | 8 | # try to read a tag, else print error 9 | while True: 10 | ret = comm.Read('bool_01') 11 | time.sleep(1) # Change seconds here 12 | print(ret.Value) # Do Ctrl + C to interrupt process 13 | 14 | comm.Close() 15 | -------------------------------------------------------------------------------- /examples/01_micropython_example/boot.py: -------------------------------------------------------------------------------- 1 | def connect_wan(): 2 | import network 3 | sta_if = network.WLAN(network.STA_IF) 4 | if not sta_if.isconnected(): 5 | print('connecting to network...') 6 | sta_if.active(True) 7 | sta_if.connect('SSID_NAME', 'Password') 8 | while not sta_if.isconnected(): 9 | pass 10 | print('network wan config:', sta_if.ifconfig()) 11 | 12 | 13 | def disconnect_wan(): 14 | import network 15 | sta_if = network.WLAN(network.STA_IF) 16 | if sta_if.isconnected(): 17 | print('disconnecting wan network...') 18 | sta_if.active(False) 19 | 20 | 21 | def connect_lan(): 22 | import network 23 | import machine as m 24 | 25 | lan = network.LAN(mdc=m.Pin(23), mdio=m.Pin(18), power=m.Pin(16), id=0, phy_addr=1, phy_type=network.PHY_LAN8720) 26 | if not lan.isconnected(): 27 | lan.ifconfig(('192.168.5.2', '255.255.255.0', '192.168.5.1', '192.168.5.1')) 28 | lan.active(True) 29 | print('network lan config:', lan.ifconfig()) 30 | 31 | 32 | def lan_status(): 33 | import network 34 | network.AbstractNIC() 35 | lan = network.LAN(0) 36 | if lan.isconnected(): 37 | print('network lan config:', lan.ifconfig()) 38 | else: 39 | print('network disable') 40 | 41 | 42 | def disconnect_lan(): 43 | import network 44 | import machine as m 45 | 46 | lan = network.LAN(mdc=m.Pin(23), mdio=m.Pin(18), power=m.Pin(16), id=0, phy_addr=1, phy_type=network.PHY_LAN8720) 47 | if lan.isconnected(): 48 | print('disconnecting lan network...') 49 | lan.active(False) 50 | 51 | 52 | def ls(path=""): 53 | import os 54 | print(os.listdir(path)) 55 | 56 | 57 | connect_wan() 58 | # connect_lan() 59 | -------------------------------------------------------------------------------- /examples/01_micropython_example/main.py: -------------------------------------------------------------------------------- 1 | from pylogix import PLC 2 | import time 3 | 4 | comm = PLC('192.168.0.89') 5 | tag = 'BaseBOOL' 6 | 7 | while True: 8 | ret = comm.Read(tag) 9 | print(ret.Value) 10 | comm.Write(tag, True) 11 | time.sleep(2) 12 | ret = comm.Read(tag) 13 | print(ret.Value) 14 | comm.Write(tag, False) 15 | time.sleep(2) 16 | -------------------------------------------------------------------------------- /examples/01_read_simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | The simplest example of reading a tag from a PLC 3 | 4 | NOTE: You only need to call .Close() after you are done exchanging 5 | data with the PLC. If you were going to read in a loop or read 6 | more tags, you wouldn't want to call .Close() every time. 7 | """ 8 | from pylogix import PLC 9 | 10 | comm = PLC() 11 | comm.IPAddress = '192.168.1.9' 12 | ret = comm.Read('CurrentScreen') 13 | print(ret.TagName, ret.Value, ret.Status) 14 | comm.Close() 15 | -------------------------------------------------------------------------------- /examples/02_read_simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple single read using a with statement. 3 | 4 | One advantage of using a with statement is that 5 | you don't have to call .Close() when you are done, 6 | this is handled automatically. 7 | """ 8 | from pylogix import PLC 9 | 10 | with PLC() as comm: 11 | comm.IPAddress = '192.168.1.9' 12 | ret = comm.Read('CurrentScreen') 13 | print(ret.TagName, ret.Value, ret.Status) 14 | -------------------------------------------------------------------------------- /examples/03_read_program_scope.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read a program scoped tag 3 | 4 | I have a program named "MiscHMI" in my main task. 5 | In MiscHMI, the tag I'm reading will be TimeArray[0] 6 | You have to specify that the tag will be program-scoped 7 | by appending the tag name with "Program" and the beginning, 8 | then add the program name, finally the tag name. So our 9 | example will look like this: 10 | 11 | Program:MiscHMI.TimeArray[0] 12 | """ 13 | from pylogix import PLC 14 | 15 | with PLC() as comm: 16 | comm.IPAddress = '192.168.1.9' 17 | ret = comm.Read('Program:MiscHMI.TimeArray[0]') 18 | print(ret.TagName, ret.Value, ret.Status) 19 | -------------------------------------------------------------------------------- /examples/04_read_array.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read an array of values 3 | 4 | I have a tag called "LargeArray", which is DINT[10000] 5 | We can read as many of them as we'd like, which makes 6 | reading arrays the most efficient way to read data. 7 | Read will handle multi-packet replies. 8 | 9 | We're going to pass Read() the tag and the number 10 | to read. 11 | """ 12 | from pylogix import PLC 13 | 14 | with PLC() as comm: 15 | comm.IPAddress = '192.168.1.9' 16 | ret = comm.Read('LargeArray[0]', 500) 17 | print(ret.TagName, ret.Value, ret.Status) 18 | -------------------------------------------------------------------------------- /examples/05_read_multiple_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read a list of tags at once 3 | 4 | Reading lists and arrays is much more efficient than 5 | reading them individually. You can create a list of tags 6 | and pass it to .Read() to read them all in one packet. 7 | The values returned will be in the same order as the tags 8 | you passed to Read() 9 | 10 | NOTE: Packets have a ~500 byte limit, so you have to be cautions 11 | about not exceeding that or the read will fail. It's a little 12 | difficult to predict how many bytes your reads will take up because 13 | the packet will depend on the length of the tag name and the 14 | reply will depend on the data type. Strings are a lot longer than 15 | DINT's for example. 16 | 17 | I'll usually read no more than 5 strings at once, or 10 DINTs 18 | """ 19 | from pylogix import PLC 20 | 21 | tag_list = ['Zone1ASpeed', 'Zone1BSpeed', 'Zone2ASpeed', 'Zone2BSpeed', 'Zone3ASpeed', 'Zone3BSpeed', 22 | 'Zone4ASpeed', 'ZOne4BSpeed', 'Zone1Case', 'Zone2Case'] 23 | 24 | with PLC() as comm: 25 | comm.IPAddress = '192.168.1.9' 26 | ret = comm.Read(tag_list) 27 | for r in ret: 28 | print(r.TagName, r.Value, r.Status) 29 | -------------------------------------------------------------------------------- /examples/06_read_loop.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read a tag in a loop 3 | 4 | We'll use read loop as long as it's True. When 5 | the user presses CTRL+C on the keyboard, we'll 6 | catch the KeyboardInterrupt, which will stop the 7 | loop. The time sleep interval is 1 second, 8 | so we'll be reading every 1 second. 9 | """ 10 | import time 11 | from pylogix import PLC 12 | 13 | with PLC() as comm: 14 | comm.IPAddress = '192.168.1.9' 15 | read = True 16 | while read: 17 | try: 18 | ret = comm.Read('LargeArray[0]') 19 | print(ret.TagName, ret.Value, ret.Status) 20 | time.sleep(1) 21 | except KeyboardInterrupt: 22 | print('exiting') 23 | read = False 24 | -------------------------------------------------------------------------------- /examples/07_read_first_instance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Monitor a tag, when we see a certain value, 3 | call another function once. 4 | 5 | I often find myself needing to trigger some 6 | other code the first time a BOOL goes true, for 7 | example. I only want the function to fire once, 8 | so we'll have to make sure it goes false before 9 | firing again. 10 | 11 | We'll start reading in a loop, in my case, I'm 12 | reading a tag called PE040, which is a BOOL. 13 | Once we see that the value is True, we'll call 14 | FaultHappened(), then we enter another loop that 15 | will keep reading until the value goes False. 16 | This will make sure that we only call the function 17 | once per True result. 18 | """ 19 | import time 20 | from pylogix import PLC 21 | 22 | 23 | def fault_happened(): 24 | # this should get called once. 25 | print('we had a fault') 26 | 27 | 28 | with PLC() as comm: 29 | comm.IPAddress = '192.168.1.9' 30 | 31 | read = True 32 | while read: 33 | try: 34 | ret = comm.Read('PE040') 35 | time.sleep(1) 36 | if ret.Value: 37 | fault_happened() 38 | while ret.Value: 39 | ret = comm.Read('PE040') 40 | time.sleep(1) 41 | except KeyboardInterrupt: 42 | print('exiting') 43 | read = False 44 | -------------------------------------------------------------------------------- /examples/08_read_faster.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read a little faster by providing the data type 3 | up front 4 | 5 | This only really makes sense to do if you have to 6 | read a lot of unique tags. Typically, when you read a 7 | tag, it has to fetch the data type first. This only 8 | happens the first time you read a tag for the first time. 9 | Once we have read a tag, we remember the type. 10 | 11 | If you have, for example, 1000 tags to read which are 12 | all unique, you would have to fetch the data type, 13 | then the value, which is quite a bit of overhead. 14 | 15 | If you pass the data type up front, it will skip that 16 | initial read... 17 | """ 18 | from pylogix import PLC 19 | 20 | with PLC() as comm: 21 | comm.IPAddress = '192.168.1.9' 22 | ret = comm.Read('CurrentScreen', datatype=196) 23 | print(ret.TagName, ret.Value, ret.Status) 24 | -------------------------------------------------------------------------------- /examples/09_multi_read_faster.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read a little faster by providing the data type 3 | up front 4 | 5 | This only really makes sense to do if you have to 6 | read a lot of unique tags. Typically, when you read a 7 | tag, it has to fetch the data type first. This only 8 | happens the first time you read a tag for the first time. 9 | Once we have read a tag, we remember the type. 10 | 11 | If you have, for example, 1000 tags to read which are 12 | all unique, you would have to fetch the data type, 13 | then the value, which is quite a bit of overhead. 14 | 15 | If you pass the data type up front, it will skip that 16 | initial read... 17 | """ 18 | from pylogix import PLC 19 | 20 | with PLC() as comm: 21 | comm.IPAddress = '192.168.1.9' 22 | tags = ['BaseINT', ['BaseDINT', 196], ('BaseBOOL', 193), ['BaseSTRING', 160]] 23 | ret = comm.Read(tags) 24 | for r in ret: 25 | print(r.TagName, r.Value, r.Status) 26 | -------------------------------------------------------------------------------- /examples/10_write_simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | The simplest example of writing a tag from a PLC 3 | 4 | NOTE: You only need to call .Close() after you are done exchanging 5 | data with the PLC. If you were going to read/write in a loop or read/write 6 | more tags, you wouldn't want to call .Close() every time. 7 | """ 8 | from pylogix import PLC 9 | comm = PLC() 10 | comm.IPAddress = '192.168.1.9' 11 | ret = comm.Write('CurrentScreen', 10) 12 | print(ret.Status) 13 | comm.Close() 14 | -------------------------------------------------------------------------------- /examples/11_write_simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple single write using a with statement. 3 | 4 | One advantage of using a with statement is that 5 | you don't have to call .Close() when you are done, 6 | this is handled automatically. 7 | """ 8 | from pylogix import PLC 9 | 10 | with PLC() as comm: 11 | comm.IPAddress = '192.168.1.9' 12 | ret = comm.Write('CurrentScreen', 10) 13 | print(ret.Status) 14 | -------------------------------------------------------------------------------- /examples/12_write_program_scope.py: -------------------------------------------------------------------------------- 1 | """ 2 | Write a program scoped tag 3 | 4 | I have a program named "MiscHMI" in my main task. 5 | In MiscHMI, the tag I'm reading will be TimeArray[0] 6 | You have to specify that the tag will be program-scoped 7 | by appending the tag name with "Program" and the beginning, 8 | then add the program name, finally the tag name. So our 9 | example will look like this: 10 | 11 | Program:MiscHMI.TimeArray[0] 12 | """ 13 | from pylogix import PLC 14 | 15 | with PLC() as comm: 16 | comm.IPAddress = '192.168.1.9' 17 | ret = comm.Write('Program:MiscHMI.TimeArray[0]', 2019) 18 | print(ret.Status) 19 | -------------------------------------------------------------------------------- /examples/13_write_array.py: -------------------------------------------------------------------------------- 1 | """ 2 | Write an array of values 3 | 4 | I have a tag called "LargeArray", which is DINT[10000] 5 | We can write a list of values all at once to be more efficient. 6 | You should be careful not to exceed the ~500 byte limit of 7 | the packet. You can pack quite a few values into 500 bytes. 8 | """ 9 | from pylogix import PLC 10 | 11 | values = [8, 6, 7, 5, 3, 0, 9] 12 | 13 | with PLC() as comm: 14 | comm.IPAddress = '192.168.1.9' 15 | ret = comm.Write('LargeArray[10]', values) 16 | print(ret.Status) 17 | -------------------------------------------------------------------------------- /examples/14_write_custom_string.py: -------------------------------------------------------------------------------- 1 | """ 2 | Write to a custom size string (not intended for the default STRING) 3 | 4 | WHen you create a custom size string, it is essentially 5 | a UDT. We cannot write to them in the same way that we 6 | can write to a standard size string. 7 | 8 | In this case, we're going to write some text to the tag 9 | String20, which is a custom string STRING20. We not only 10 | have to write the data, we have to also write the length. 11 | """ 12 | from pylogix import PLC 13 | 14 | with PLC() as comm: 15 | comm.IPAddress = '192.168.1.9' 16 | string_size = 20 17 | text = 'This is some text' 18 | values = [ord(c) for c in text] + [0] * (string_size - len(text)) 19 | comm.Write('String20.LEN', len(text)) 20 | comm.Write('String20.DATA[0]', values) 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/15_write_faster.py: -------------------------------------------------------------------------------- 1 | """ 2 | Write a little faster by providing the data type 3 | up front 4 | 5 | This only really makes sense to do if you have to 6 | write a lot of unique tags. Typically, when you write a 7 | tag, it has to fetch the data type first. This only 8 | happens the first time you read/write a tag for the first time. 9 | 10 | If you have, for example, 1000 tags to write which are 11 | all unique, you would have to fetch the data type, 12 | then write the value, which is extra overhead. 13 | 14 | If you pass the data type up front, it will skip that 15 | initial read... 16 | """ 17 | from pylogix import PLC 18 | 19 | with PLC() as comm: 20 | comm.IPAddress = '192.168.1.9' 21 | ret = comm.Write('Zone1Case', 10, datatype=196) 22 | print(ret.Status) 23 | -------------------------------------------------------------------------------- /examples/16_multi_write.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to utilize the multi write service 3 | 4 | If you pass Write a list of tuples (tag, value), it will 5 | utilize multi write service to send your request in a 6 | single packet. 7 | """ 8 | from pylogix import PLC 9 | 10 | with PLC() as comm: 11 | comm.IPAddress = '192.168.1.9' 12 | 13 | write_data = [('tag1', 100), 14 | ('tag2', 6.45), 15 | ('tag3', True)] 16 | 17 | # write the values 18 | ret = comm.Write(write_data) 19 | 20 | # print the status of the writes 21 | for r in ret: 22 | print(r.TagName, r.Status) 23 | -------------------------------------------------------------------------------- /examples/17_write_sql.py: -------------------------------------------------------------------------------- 1 | 2 | import pylogix 3 | import time 4 | 5 | import mysql.connector 6 | 7 | """ 8 | I have a database named pylogix_db, table named recipes 9 | with columns name, number. Every 5 seconds, we'll read 10 | the tags and update the database with the values/ 11 | """ 12 | 13 | db_connection = mysql.connector.connect( 14 | host="localhost", 15 | user="root", 16 | passwd="password", 17 | database="pylogix_db") 18 | 19 | my_cursor = db_connection.cursor() 20 | tags = ["recipe_name", "recipe_value"] 21 | comm = pylogix.PLC("192.168.1.10") 22 | 23 | 24 | def write_to_db(values): 25 | """ 26 | Write our values to the database 27 | """ 28 | cmd = "INSERT INTO recipes (name, number) VALUES (%s, %s)" 29 | val = (values[0], values[1]) 30 | my_cursor.execute(cmd, val) 31 | db_connection.commit() 32 | 33 | 34 | def read_from_plc(): 35 | """ 36 | Read the defined tags. Return the values as a list 37 | so tha they are easy to pass to the database. 38 | """ 39 | ret = comm.Read(tags) 40 | 41 | return [r.Value for r in ret] 42 | 43 | 44 | run = True 45 | while run: 46 | try: 47 | v = read_from_plc() 48 | write_to_db(v) 49 | time.sleep(5) 50 | 51 | except KeyboardInterrupt: 52 | run = False 53 | -------------------------------------------------------------------------------- /examples/20_discover_devices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Discover Ethernet I/P Devices 3 | 4 | This will send a broadcast packet out, all Ethernet I/P 5 | devices will respond with the following information about 6 | themselves: 7 | 8 | EncapsulationVersion 9 | IPAddress 10 | VendorID 11 | Vendor 12 | DeviceID 13 | DeviceType 14 | ProductCode 15 | Revision 16 | Status 17 | SerialNumber 18 | ProductNameLength 19 | ProductName 20 | State 21 | """ 22 | from pylogix import PLC 23 | 24 | with PLC() as comm: 25 | devices = comm.Discover() 26 | for device in devices.Value: 27 | print(device.IPAddress) 28 | print(' Product Code: ' + device.ProductName + " " + str(device.ProductCode)) 29 | print(' Vendor/Device ID:' + device.Vendor + " " + str(device.DeviceID)) 30 | print(' Revision/Serial:' + device.Revision + " " + device.SerialNumber) 31 | print('') 32 | -------------------------------------------------------------------------------- /examples/21_get_plc_clock.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the PLC time 3 | 4 | returns datetime.datetime type 5 | """ 6 | from pylogix import PLC 7 | 8 | with PLC() as comm: 9 | comm.IPAddress = '192.168.1.9' 10 | ret = comm.GetPLCTime() 11 | time_value = ret.Value 12 | 13 | # print the Response value 14 | print(ret) 15 | 16 | # print the whole value 17 | print(time_value) 18 | 19 | # print each piece of time 20 | print(time_value.year, 21 | time_value.month, 22 | time_value.day, 23 | time_value.hour, 24 | time_value.minute, 25 | time_value.second, 26 | time_value.microsecond) 27 | -------------------------------------------------------------------------------- /examples/22_set_plc_clock.py: -------------------------------------------------------------------------------- 1 | """ 2 | Set the PLC clock 3 | 4 | Sets the PLC clock to the same time as your computer 5 | """ 6 | from pylogix import PLC 7 | 8 | with PLC() as comm: 9 | comm.IPAddress = '192.168.1.9' 10 | comm.SetPLCTime() 11 | -------------------------------------------------------------------------------- /examples/23_get_all_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the tag list from the PLC 3 | 4 | This will fetch all the controller and program 5 | scoped tags from the PLC. In the case of 6 | Structs (UDTs), it will not give you the makeup 7 | of each tag, just main tag names. 8 | """ 9 | from pylogix import PLC 10 | 11 | with PLC() as comm: 12 | comm.IPAddress = '192.168.1.9' 13 | tags = comm.GetTagList() 14 | 15 | for t in tags.Value: 16 | print(t.TagName, t.DataType) 17 | -------------------------------------------------------------------------------- /examples/24_get_controller_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get controller scoped tags from the PLC 3 | 4 | This will fetch all the controller scoped tags 5 | from the PLC. In the case of Structs (UDTs), 6 | it will not give you the makeup of each tag, 7 | just main tag names. 8 | """ 9 | from pylogix import PLC 10 | 11 | with PLC() as comm: 12 | comm.IPAddress = '192.168.1.9' 13 | tags = comm.GetTagList(False) 14 | 15 | for t in tags.Value: 16 | print(t.TagName, t.DataType) 17 | -------------------------------------------------------------------------------- /examples/25_get_program_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get tag list from specific program 3 | 4 | In this case, I had a program named MiscHMI, 5 | this retrieves the program scoped tags from 6 | just that program 7 | 8 | NOTE: This actually reads all tags from the 9 | PLC, it returns only the list of tags from the 10 | program you specified. 11 | """ 12 | from pylogix import PLC 13 | 14 | with PLC() as comm: 15 | comm.IPAddress = '192.168.1.9' 16 | tags = comm.GetProgramTagList('Program:MiscHMI') 17 | 18 | for t in tags.Value: 19 | print(t.TagName, t.DataType) 20 | -------------------------------------------------------------------------------- /examples/26_save_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the tag list from the PLC, save them to a file 3 | 4 | In this case, we'll get all tags from the 5 | PLC, then save them to a text file 6 | """ 7 | from pylogix import PLC 8 | 9 | with PLC() as comm: 10 | comm.IPAddress = '192.168.1.9' 11 | tags = comm.GetTagList() 12 | 13 | with open('tag_list.txt', 'w') as f: 14 | for t in tags.Value: 15 | f.write('%s %d \n'.format(t.TagName, t.DataType)) 16 | -------------------------------------------------------------------------------- /examples/27_get_module_properties.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the properties of a module in the specified slot 3 | 4 | In this example, we're getting the slot 0 module 5 | properties 6 | """ 7 | from pylogix import PLC 8 | 9 | with PLC() as comm: 10 | comm.IPAddress = '192.168.1.9' 11 | prop = comm.GetModuleProperties(0) 12 | print(prop.Value.ProductName, prop.Value.Revision) 13 | -------------------------------------------------------------------------------- /examples/28_audit_network.py: -------------------------------------------------------------------------------- 1 | from pylogix import PLC 2 | 3 | 4 | def get_devices(): 5 | """ 6 | Get all the devices on the network 7 | """ 8 | with PLC() as comm: 9 | return comm.Discover() 10 | 11 | 12 | def audit_controllers(): 13 | """ 14 | Parse out any devices that are a PLC, then audit 15 | the rack for all the modules 16 | """ 17 | for d in devices.Value: 18 | if d.DeviceID == 14: 19 | audit_rack(d) 20 | else: 21 | f.write('%s %s\n' % (d.ProductName, d.Revision)) 22 | 23 | 24 | def audit_rack(plc): 25 | """ 26 | Query each slot for a module 27 | """ 28 | with PLC() as c: 29 | c.IPAddress = plc.IPAddress 30 | f.write('%s - %s\n' % (plc.IPAddress, plc.ProductName)) 31 | for i in range(17): 32 | x = c.GetModuleProperties(i) 33 | f.write('\tSlot %d:%s rev:%s\n' % (i, x.ProductName, x.Revision)) 34 | f.write('') 35 | 36 | 37 | devices = get_devices() 38 | with open('network_audit.txt', 'w') as f: 39 | audit_controllers() 40 | -------------------------------------------------------------------------------- /examples/30_log_to_txt.py: -------------------------------------------------------------------------------- 1 | """ 2 | We're going to log a tag value 10 3 | times to a text file 4 | """ 5 | from pylogix import PLC 6 | import time 7 | 8 | with PLC() as comm: 9 | comm.IPAddress = '192.168.1.9' 10 | 11 | with open('30_log.txt', 'w') as txt_file: 12 | for i in range(10): 13 | ret = comm.Read('LargeArray[50]') 14 | txt_file.write(str(ret.Value)+'\n') 15 | time.sleep(1) 16 | -------------------------------------------------------------------------------- /examples/31_log_to_csv.py: -------------------------------------------------------------------------------- 1 | """ 2 | We're going to log a tag value 10 3 | times to a text file 4 | """ 5 | import csv 6 | from pylogix import PLC 7 | import time 8 | 9 | with PLC() as comm: 10 | comm.IPAddress = '192.168.1.9' 11 | 12 | with open('31_log.csv', 'w') as csv_file: 13 | csv_file = csv.writer(csv_file, delimiter=',', quotechar='/', quoting=csv.QUOTE_MINIMAL) 14 | for i in range(10): 15 | ret = comm.Read('LargeArray[5]') 16 | csv_file.writerow([ret.Value]) 17 | time.sleep(1) 18 | -------------------------------------------------------------------------------- /examples/32_log_multiple_to_csv.py: -------------------------------------------------------------------------------- 1 | """ 2 | We're going to log a few tag values 10 3 | times to a CSV file 4 | 5 | For the first row, we'll write tag names, 6 | then log each set of values with each read 7 | """ 8 | import csv 9 | import time 10 | from pylogix import PLC 11 | 12 | with PLC() as comm: 13 | comm.IPAddress = '192.168.1.9' 14 | tags = ['Zone1ASpeed', 'Zone1BSpeed', 'Zone2ASpeed', 'Zone2BSpeed'] 15 | with open('32_log.csv', 'w') as csv_file: 16 | csv_file = csv.writer(csv_file, delimiter=',', lineterminator='\n', quotechar='/', quoting=csv.QUOTE_MINIMAL) 17 | csv_file.writerow(tags) 18 | for i in range(10): 19 | ret = comm.Read(tags) 20 | row = [x.Value for x in ret] 21 | csv_file.writerow(row) 22 | time.sleep(1) 23 | -------------------------------------------------------------------------------- /examples/40_read_timer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reading a UDT will return the raw byte data, it is up to you as the user 3 | to understand how it is packed and how to unpack it. Looking at the data 4 | type in L5K format can be helpful. 5 | 6 | This example is reading the entire structure of a timer in 1 read. 7 | For a timer, bytes 2-5 contain the .EN, .TT packed at the end of the word. 8 | Bytes 6-9 contain the preset and bytes 10-13 contain the acc value 9 | """ 10 | from pylogix import PLC 11 | from struct import pack, unpack_from 12 | 13 | 14 | class Timer(object): 15 | 16 | def __init__(self, data): 17 | 18 | self.PRE = unpack_from('> 13 106 | t.Struct = (val & 0x8000) >> 15 107 | 108 | if t.Array: 109 | t.Size = unpack_from(' 12, 'Invalid vendors.bin file' 37 | lo, hi = 1, 1 + span 38 | 39 | # Read vendor ID plus colon of next (first data) record 40 | lread = vendor_file.read(12) 41 | 42 | # Check that vendor ID, return if vendorID argument matches 43 | if lread.startswith(vc): 44 | # Read balance of line, reconstruct vendor data 45 | return (lread[colonpos + 1:] 46 | + vendor_file.read(fixedlength - 13) 47 | ).rstrip().decode('UTF-8') 48 | 49 | elif lread[colonpos:colonpos + 1] == b':' and lread > vc: 50 | # Short-circuit the binary search if invariant fails 51 | span = 0 52 | elif lread.index(b':') > colonpos: 53 | span = 0 54 | 55 | # Binary search 56 | # - Invariant is (vendor ID of record [lo]) < vendorID 57 | while span > 1: 58 | 59 | # Read vendor ID of record ~halfway between lo and hi 60 | mid = lo + (span >> 1) 61 | vendor_file.seek(mid * fixedlength, 0) 62 | lread = vendor_file.read(12) 63 | 64 | # Check that vendor ID, return if vendorID arg matches 65 | if lread.startswith(vc): 66 | # Read balance of line, reconstruct vendor data 67 | return (lread[colonpos + 1:] 68 | + vendor_file.read(fixedlength - 13) 69 | ).rstrip().decode('UTF-8') 70 | 71 | # Maintain invariant by assigning mid to either lo or hi 72 | if lread[colonpos:colonpos + 1] == b':' and lread < vc: 73 | lo = mid 74 | elif lread.index(b':') < colonpos: 75 | lo = mid 76 | else: 77 | hi = mid 78 | 79 | # Re-calculate magnitude of remaining hi:lo range 80 | span = hi - lo 81 | 82 | # No match found, return vendor data indicating same 83 | return 'Unknown' 84 | 85 | # Obsolete O(N) lookup, replaced by O(logN) lookup above 86 | def getitem_O_N(self, vendorID): 87 | sID = str(vendorID) 88 | with open(__file__ + ".txt") as vendor_data: 89 | for line in vendor_data: 90 | # i) Split line into colon-separated tokens 91 | # ii) If first token does not match vendorID, continue 92 | # iii) Else Re-join remaining tokens and return 93 | tokens = line.split(':') 94 | if sID != tokens.pop(0): continue 95 | return ':'.join(tokens) 96 | # No tokens matched vendorID, return unknown result 97 | return 'Unknown' 98 | 99 | # Do-nothing on class initialization 100 | def __init__(self): 101 | pass 102 | 103 | # Test if vendorID is in file of vendor IDs 104 | # .__contains__ method is called when executing `id in uvendors` 105 | # - use .__getitem__ method above 106 | def __contains__(self, vendorID): 107 | return self[vendorID] != 'Unknown' 108 | 109 | 110 | uvendors = Uvendors() 111 | -------------------------------------------------------------------------------- /pylogix/lgx_uvendors.py.bin: -------------------------------------------------------------------------------- 1 | ../upylogix/lgx_uvendors.mpy.bin -------------------------------------------------------------------------------- /pylogix/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2022 Dustin Roeder (dmroeder@gmail.com) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | import sys 18 | 19 | 20 | def is_micropython(): 21 | if hasattr(sys, 'implementation'): 22 | if sys.implementation.name == 'micropython': 23 | return True 24 | return False 25 | return False 26 | 27 | 28 | def is_python3(): 29 | if hasattr(sys.version_info, 'major'): 30 | if sys.version_info.major == 3: 31 | return True 32 | return False 33 | return False 34 | 35 | 36 | def is_python2(): 37 | if hasattr(sys.version_info, 'major'): 38 | if sys.version_info.major == 2: 39 | return True 40 | return False 41 | return False 42 | -------------------------------------------------------------------------------- /scripts/build_mpy.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # scripts/build_mpy.py 3 | # - Cross-compile .py=>.mpy files for micropython 4 | # - Build upylogix/lgx_uvendors.mpy.bin file from pylogix/lgx_vendors.py 5 | # - Generate package.json 6 | ######################################################################## 7 | 8 | # This should never be loaded as a module 9 | assert "__main__" == __name__,"Don't use [' + __file__ + '] as a module" 10 | 11 | import os 12 | from mpy_cross import run 13 | 14 | # CHDIR to root of repository 15 | script_dir = os.path.dirname(__file__) 16 | toproot_dir = os.path.join(script_dir,'..') 17 | os.chdir(toproot_dir) 18 | 19 | # Compile pylogix/*.py (except lgx_vendors.py) to upylogix/*.mpy 20 | for modyule in '__init__ eip lgx_comm lgx_device lgx_response lgx_tag utils lgx_uvendors'.split(): 21 | argv = list() 22 | argv.append(os.path.join('pylogix','{0}.py'.format(modyule))) 23 | argv.append('-o') 24 | argv.append(os.path.join('upylogix','{0}.mpy'.format(modyule))) 25 | run(*argv) 26 | 27 | ######################################################################## 28 | build_vendors_bin_doc = """ 29 | Build script to write vendor names bin for micropython environments 30 | - File vendors.bin is read by app module [u]pylogix/lgx_uvendors.[m]py 31 | 32 | Usage 33 | ===== 34 | 35 | python build_vendors_bin.py 36 | 37 | This script is not part of the library. It only needs to be run when 38 | the vendors dict change in script pylogix/lgx_uvendors.py,, but is 39 | called by build_mpy.sh when the upylogix/*.mpy files are rebuilt 40 | 41 | Background 42 | ========== 43 | 44 | Memory is typically limited in [micropython] environments, and the 45 | vendors dict in pylogix/lgx_vendors.py uses a large chunk of RAM, such 46 | that the pylogix library takes too much memory on some hardware devices 47 | to be useful. 48 | 49 | This current solution writes the vendors data to a file* with sorted, 50 | fixed-length records, upylogix/vendors.bin, which data file can be read 51 | by the code in pylogix/lgx_uvendors.[m]py to emulate the vendors dict. 52 | 53 | * The assumption is that the hardware device has separate file-based 54 | memory, which memory does affect the available RAM space 55 | 56 | """ 57 | 58 | # Load vendors dict, and create sorted list of keys; set max name length 59 | import sys 60 | sys.path.insert(0,'.') 61 | from pylogix.lgx_vendors import vendors as vdict 62 | ks = sorted(vdict.keys()) 63 | maxL = 0 64 | 65 | # Convert vendors to "key:vendor" byte-strings in key-sorted list 66 | # - Also store length of longest encoded byte-string in maxL 67 | vlist = list() 68 | for k in ks: 69 | assert isinstance(k,int) 70 | assert k > -1 71 | v = vdict[k] 72 | assert isinstance(v,str) 73 | vencoded = "{0}:{1}".format(k,v).encode('UTF-8') 74 | vlist.append(vencoded) 75 | L = len(vencoded) 76 | if L > maxL: maxL = L 77 | 78 | # Lambda pads byte-string to fixed length with spaces and a newline 79 | oneline = lambda v: (v + (b' '*maxL))[:maxL] + b'\n' 80 | 81 | # Write fixed-length records to vendors.bin file for fast lookups 82 | with open('upylogix/lgx_uvendors.mpy.bin','wb') as fbin: 83 | 84 | # Write an initial header record containing the vendor count 85 | headerstring = "{0}:{1}".format(len(ks),"Count of vendors") 86 | fbin.write(oneline(headerstring.encode('UTF-8'))) 87 | 88 | # Write vendor data records 89 | for key_vendor in vlist: fbin.write(oneline(key_vendor)) 90 | 91 | ######################################################################## 92 | # Generate package.json 93 | import os 94 | import glob 95 | import json 96 | 97 | from pylogix import __version__ 98 | 99 | directory_path = f'{toproot_dir}/pylogix/' 100 | py_files_fullpath = glob.glob(os.path.join(directory_path, '*.py')) 101 | py_files = [] 102 | # exclude lgx_vendors.py 103 | for py_file in py_files_fullpath: 104 | if "lgx_vendors.py" not in py_file: 105 | py_files.append(os.path.basename(py_file)) 106 | 107 | # manually add lgx_uvendors.mpy.bin 108 | py_files.append("lgx_uvendors.mpy.bin") 109 | py_files = sorted(py_files) 110 | package_json_dict = {} 111 | list_of_package_urls = [] 112 | for py_file in py_files: 113 | repo_url = "github:dmroeder/pylogix/upylogix" 114 | py_file = py_file.replace(".py", ".mpy") 115 | list_to_add = [f"pylogix/{py_file}", f"{repo_url}/{py_file}"] 116 | list_of_package_urls.append(list_to_add) 117 | 118 | package_json_dict["urls"] = list_of_package_urls 119 | package_json_dict["version"] = __version__ 120 | with open("package.json", "w") as fp: 121 | json.dump(package_json_dict, fp, indent=4) 122 | -------------------------------------------------------------------------------- /scripts/time_uvendors.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | time micropython -c 'import pylogix.lgx_vendors as v;import pylogix.lgx_uvendors as u;print([None for k in v.vendors if "Unknown" is v.vendors[k]])' 4 | #time micropython -c 'import pylogix.lgx_vendors as v;import pylogix.lgx_uvendors as u;print([None for k in v.vendors if "Unknown" is u.uvendors.getitem_O_N(k)])' 5 | time micropython -c 'import pylogix.lgx_vendors as v;import pylogix.lgx_uvendors as u;print([None for k in v.vendors if "Unknown" is u.uvendors.getitem_O_logN(k)])' 6 | time micropython -c 'import pylogix.lgx_vendors as v;import pylogix.lgx_uvendors as u;print([None for k in v.vendors if "Unknown" is u.uvendors[k]])' 7 | micropython -c 'import pylogix.lgx_vendors as v;import pylogix.lgx_uvendors as u;assert not [None for k in v.vendors if v.vendors[k] != u.uvendors[k]];assert "Unknown" == u.uvendors[-1];assert "Unknown" == u.uvendors[1048576];print("\nSuccess")' 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import pylogix 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setuptools.setup( 8 | name="pylogix", 9 | version=pylogix.__version__, 10 | author="Dustin Roeder", 11 | author_email="dmroeder@gmail.com", 12 | description="Read/Write Rockwell Automation Logix based PLC's", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | license="Apache License 2.0", 16 | url="https://github.com/dmroeder/pylogix", 17 | packages=setuptools.find_packages(), 18 | package_data={'': ['lgx_uvendors.py.bin']}, 19 | include_package_data=True, 20 | classifiers=[ 21 | "Programming Language :: Python :: 2.7", 22 | "Programming Language :: Python :: 3.6", 23 | "License :: OSI Approved :: Apache Software License", 24 | "Operating System :: OS Independent", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /tests/PylogixTests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Originally created by Burt Peterson 3 | Updated and maintained by Dustin Roeder (dmroeder@gmail.com) 4 | 5 | Copyright 2019 Dustin Roeder 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import plcConfig # Info: tests\README.md - Setup test configuration file 21 | import pylogix 22 | import time 23 | import unittest 24 | 25 | from pylogix.lgx_response import Response 26 | from pylogix.lgx_tag import Tag # Need Classes for type checking 27 | from Randomizer import Randomizer 28 | from pylogix.utils import is_micropython, is_python2 29 | 30 | 31 | class PylogixTests(unittest.TestCase): 32 | 33 | def __init__(self, *args, **kwargs): 34 | unittest.TestCase.__init__(self, *args, **kwargs) 35 | self.comm = pylogix.PLC() 36 | self.r = Randomizer() 37 | 38 | def compare_bool(self, tag): 39 | # write false 40 | self.comm.Write(tag, 0) 41 | response = self.comm.Read(tag) 42 | self.assertEqual(response.Value, False, response.Status) 43 | # write true 44 | self.comm.Write(tag, 1) 45 | response = self.comm.Read(tag) 46 | self.assertEqual(response.Value, True, response.Status) 47 | 48 | def compare_tag(self, tag, value): 49 | self.comm.Write(tag, value) 50 | response = self.comm.Read(tag) 51 | self.assertEqual(response.Value, value, response.Status) 52 | 53 | def array_result(self, tagname, arraylen): 54 | response = self.comm.Read(tagname, arraylen) 55 | self.assertGreaterEqual(len(response.Value), arraylen, response.Status) 56 | 57 | def basic_fixture(self, prefix=''): 58 | self.compare_bool(prefix + 'BaseBool') 59 | self.compare_bool(prefix + 'BaseBits.0') 60 | self.compare_bool(prefix + 'BaseBits.31') 61 | self.compare_tag(prefix + 'BaseSINT', self.r.Sint()) 62 | self.compare_tag(prefix + 'BaseINT', self.r.Int()) 63 | self.compare_tag(prefix + 'BaseDINT', self.r.Dint()) 64 | self.compare_tag(prefix + 'BaseLINT', self.r.Dint()) 65 | self.compare_tag(prefix + 'BaseReal', self.r.Sint()) 66 | self.compare_tag(prefix + 'BaseSTRING', self.r.String()) 67 | self.compare_tag(prefix + 'BaseTimer.PRE', abs(self.r.Int())) 68 | 69 | def basic_array_fixture(self, prefix=''): 70 | self.compare_bool(prefix + 'BaseBoolArray[0]') 71 | self.compare_bool(prefix + 'BaseBoolArray[31]') 72 | self.compare_bool(prefix + 'BaseBITSArray[0].0') 73 | self.compare_bool(prefix + 'BaseBITSArray[0].31') 74 | self.compare_bool(prefix + 'BaseBITSArray[31].0') 75 | self.compare_bool(prefix + 'BaseBITSArray[31].31') 76 | self.compare_tag(prefix + 'BaseSINTArray[0]', self.r.Sint()) 77 | self.compare_tag(prefix + 'BaseSINTArray[31]', self.r.Sint()) 78 | self.compare_tag(prefix + 'BaseINTArray[0]', self.r.Int()) 79 | self.compare_tag(prefix + 'BaseINTArray[31]', self.r.Int()) 80 | self.compare_tag(prefix + 'BaseDINTArray[0]', self.r.Dint()) 81 | self.compare_tag(prefix + 'BaseDINTArray[31]', self.r.Dint()) 82 | self.compare_tag(prefix + 'BaseLINTArray[0]', self.r.Dint()) 83 | self.compare_tag(prefix + 'BaseLINTArray[31]', self.r.Dint()) 84 | self.compare_tag(prefix + 'BaseREALArray[0]', self.r.Sint()) 85 | self.compare_tag(prefix + 'BaseREALArray[31]', self.r.Sint()) 86 | self.compare_tag(prefix + 'BaseSTRINGArray[0]', self.r.String()) 87 | self.compare_tag(prefix + 'BaseSTRINGArray[31]', self.r.String()) 88 | self.compare_tag(prefix + 'BaseTimerArray[0].PRE', abs(self.r.Int())) 89 | self.compare_tag(prefix + 'BaseTimerArray[31].PRE', abs(self.r.Int())) 90 | self.compare_tag(prefix + 'MultiDim[1,1,1]', self.r.Dint()) 91 | self.compare_tag(prefix + 'MultiString[1,1,1].LEN', self.r.Sint()) 92 | 93 | def udt_basic_fixture(self, prefix=''): 94 | self.compare_bool(prefix + 'UDTBasic.b_BOOL') 95 | self.compare_bool(prefix + 'UDTBasic.b_BITS.0') 96 | self.compare_bool(prefix + 'UDTBasic.b_BITS.31') 97 | self.compare_tag(prefix + 'UDTBasic.b_SINT', self.r.Sint()) 98 | self.compare_tag(prefix + 'UDTBasic.b_INT', self.r.Int()) 99 | self.compare_tag(prefix + 'UDTBasic.b_DINT', self.r.Dint()) 100 | self.compare_tag(prefix + 'UDTBasic.b_LINT', self.r.Dint()) 101 | self.compare_tag(prefix + 'UDTBasic.b_REAL', self.r.Sint()) 102 | self.compare_tag(prefix + 'UDTBasic.b_STRING', self.r.String()) 103 | self.compare_tag('UDTBasic.b_Timer.PRE', abs(self.r.Int())) 104 | 105 | def udt_array_fixture_01(self, prefix=''): 106 | self.compare_bool(prefix + 'UDTArray.b_BOOL[0]') 107 | self.compare_bool(prefix + 'UDTArray.b_BOOL[31]') 108 | self.compare_bool(prefix + 'UDTArray.b_BITS[0].0') 109 | self.compare_bool(prefix + 'UDTArray.b_BITS[0].31') 110 | self.compare_bool(prefix + 'UDTArray.b_BITS[31].0') 111 | self.compare_bool(prefix + 'UDTArray.b_BITS[31].31') 112 | self.compare_tag(prefix + 'UDTArray.b_SINT[0]', self.r.Sint()) 113 | self.compare_tag(prefix + 'UDTArray.b_SINT[31]', self.r.Sint()) 114 | self.compare_tag(prefix + 'UDTArray.b_INT[0]', self.r.Int()) 115 | self.compare_tag(prefix + 'UDTArray.b_INT[31]', self.r.Int()) 116 | self.compare_tag(prefix + 'UDTArray.b_DINT[0]', self.r.Dint()) 117 | self.compare_tag(prefix + 'UDTArray.b_DINT[31]', self.r.Dint()) 118 | self.compare_tag(prefix + 'UDTArray.b_LINT[0]', self.r.Dint()) 119 | self.compare_tag(prefix + 'UDTArray.b_LINT[31]', self.r.Dint()) 120 | self.compare_tag(prefix + 'UDTArray.b_REAL[0]', self.r.Sint()) 121 | self.compare_tag(prefix + 'UDTArray.b_REAL[31]', self.r.Sint()) 122 | self.compare_tag(prefix + 'UDTArray.b_STRING[0]', self.r.String()) 123 | self.compare_tag(prefix + 'UDTArray.b_STRING[31]', self.r.String()) 124 | self.compare_tag(prefix + 'UDTArray.b_Timer[0].PRE', abs(self.r.Int())) 125 | self.compare_tag(prefix + 'UDTArray.b_Timer[31].PRE', abs(self.r.Int())) 126 | 127 | def udt_array_fixture_02(self, prefix=''): 128 | self.compare_bool(prefix + 'UDTArray2[0].b_BOOL[0]') 129 | self.compare_bool(prefix + 'UDTArray2[0].b_BOOL[31]') 130 | self.compare_bool(prefix + 'UDTArray2[0].b_BITS[0].0') 131 | self.compare_bool(prefix + 'UDTArray2[0].b_BITS[0].31') 132 | self.compare_bool(prefix + 'UDTArray2[0].b_BITS[31].0') 133 | self.compare_bool(prefix + 'UDTArray2[0].b_BITS[31].31') 134 | self.compare_tag(prefix + 'UDTArray2[0].b_SINT[0]', self.r.Sint()) 135 | self.compare_tag(prefix + 'UDTArray2[0].b_SINT[31]', self.r.Sint()) 136 | self.compare_tag(prefix + 'UDTArray2[0].b_INT[0]', self.r.Int()) 137 | self.compare_tag(prefix + 'UDTArray2[0].b_INT[31]', self.r.Int()) 138 | self.compare_tag(prefix + 'UDTArray2[0].b_DINT[0]', self.r.Dint()) 139 | self.compare_tag(prefix + 'UDTArray2[0].b_DINT[31]', self.r.Dint()) 140 | self.compare_tag(prefix + 'UDTArray2[0].b_LINT[0]', self.r.Dint()) 141 | self.compare_tag(prefix + 'UDTArray2[0].b_LINT[31]', self.r.Dint()) 142 | self.compare_tag(prefix + 'UDTArray2[0].b_REAL[0]', self.r.Sint()) 143 | self.compare_tag(prefix + 'UDTArray2[0].b_REAL[31]', self.r.Sint()) 144 | self.compare_tag(prefix + 'UDTArray2[0].b_STRING[0]', self.r.String()) 145 | self.compare_tag(prefix + 'UDTArray2[0].b_STRING[31]', self.r.String()) 146 | self.compare_tag(prefix + 'UDTArray2[0].b_Timer[0].PRE', abs(self.r.Int())) 147 | self.compare_tag(prefix + 'UDTArray2[0].b_Timer[31].PRE', abs(self.r.Int())) 148 | self.compare_bool(prefix + 'UDTArray2[31].b_BOOL[0]') 149 | self.compare_bool(prefix + 'UDTArray2[31].b_BOOL[31]') 150 | self.compare_bool(prefix + 'UDTArray2[31].b_BITS[0].0') 151 | self.compare_bool(prefix + 'UDTArray2[31].b_BITS[0].31') 152 | self.compare_bool(prefix + 'UDTArray2[31].b_BITS[31].0') 153 | self.compare_bool(prefix + 'UDTArray2[31].b_BITS[31].31') 154 | self.compare_tag(prefix + 'UDTArray2[31].b_SINT[0]', self.r.Sint()) 155 | self.compare_tag(prefix + 'UDTArray2[31].b_SINT[31]', self.r.Sint()) 156 | self.compare_tag(prefix + 'UDTArray2[31].b_INT[0]', self.r.Int()) 157 | self.compare_tag(prefix + 'UDTArray2[31].b_INT[31]', self.r.Int()) 158 | self.compare_tag(prefix + 'UDTArray2[31].b_DINT[0]', self.r.Dint()) 159 | self.compare_tag(prefix + 'UDTArray2[31].b_DINT[31]', self.r.Dint()) 160 | self.compare_tag(prefix + 'UDTArray2[31].b_LINT[0]', self.r.Dint()) 161 | self.compare_tag(prefix + 'UDTArray2[31].b_LINT[31]', self.r.Dint()) 162 | self.compare_tag(prefix + 'UDTArray2[31].b_REAL[0]', self.r.Sint()) 163 | self.compare_tag(prefix + 'UDTArray2[31].b_REAL[31]', self.r.Sint()) 164 | self.compare_tag(prefix + 'UDTArray2[31].b_STRING[0]', self.r.String()) 165 | self.compare_tag(prefix + 'UDTArray2[31].b_STRING[31]', self.r.String()) 166 | self.compare_tag(prefix + 'UDTArray2[31].b_Timer[0].PRE', abs(self.r.Int())) 167 | self.compare_tag( 168 | prefix + 'UDTArray2[31].b_Timer[31].PRE', abs(self.r.Int())) 169 | 170 | def udt_combined_fixture(self, prefix=''): 171 | self.compare_bool(prefix + 'UDTCombined.c_Array.b_BOOL[0]') 172 | self.compare_bool(prefix + 'UDTCombined.c_Array.b_BOOL[31]') 173 | self.compare_bool(prefix + 'UDTCombined.c_Array.b_BITS[0].0') 174 | self.compare_bool(prefix + 'UDTCombined.c_Array.b_BITS[0].31') 175 | self.compare_bool(prefix + 'UDTCombined.c_Array.b_BITS[31].0') 176 | self.compare_bool(prefix + 'UDTCombined.c_Array.b_BITS[31].31') 177 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_SINT[0]', self.r.Sint()) 178 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_SINT[31]', self.r.Sint()) 179 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_INT[0]', self.r.Int()) 180 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_INT[31]', self.r.Int()) 181 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_DINT[0]', self.r.Dint()) 182 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_DINT[31]', self.r.Dint()) 183 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_LINT[0]', self.r.Dint()) 184 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_LINT[31]', self.r.Dint()) 185 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_REAL[0]', self.r.Sint()) 186 | self.compare_tag(prefix + 'UDTCombined.c_Array.b_REAL[31]', self.r.Sint()) 187 | self.compare_tag( 188 | prefix + 'UDTCombined.c_Array.b_STRING[0]', self.r.String()) 189 | self.compare_tag( 190 | prefix + 'UDTCombined.c_Array.b_STRING[31]', self.r.String()) 191 | self.compare_tag( 192 | prefix + 'UDTCombined.c_Array.b_Timer[0].PRE', abs(self.r.Int())) 193 | self.compare_tag( 194 | prefix + 'UDTCombined.c_Array.b_Timer[31].PRE', abs(self.r.Int())) 195 | 196 | def udt_combined_array_fixture(self, prefix=''): 197 | self.compare_bool(prefix + 'UDTCombinedArray[0].c_Array.b_BOOL[0]') 198 | self.compare_bool(prefix + 'UDTCombinedArray[0].c_Array.b_BOOL[31]') 199 | self.compare_bool(prefix + 'UDTCombinedArray[0].c_Array.b_BITS[0].0') 200 | self.compare_bool(prefix + 'UDTCombinedArray[0].c_Array.b_BITS[0].31') 201 | self.compare_bool(prefix + 'UDTCombinedArray[0].c_Array.b_BITS[31].0') 202 | self.compare_bool(prefix + 'UDTCombinedArray[0].c_Array.b_BITS[31].31') 203 | self.compare_tag( 204 | prefix + 'UDTCombinedArray[0].c_Array.b_SINT[0]', self.r.Sint()) 205 | self.compare_tag( 206 | prefix + 'UDTCombinedArray[0].c_Array.b_SINT[31]', self.r.Sint()) 207 | self.compare_tag( 208 | prefix + 'UDTCombinedArray[0].c_Array.b_INT[0]', self.r.Int()) 209 | self.compare_tag( 210 | prefix + 'UDTCombinedArray[0].c_Array.b_INT[31]', self.r.Int()) 211 | self.compare_tag( 212 | prefix + 'UDTCombinedArray[0].c_Array.b_DINT[0]', self.r.Dint()) 213 | self.compare_tag( 214 | prefix + 'UDTCombinedArray[0].c_Array.b_DINT[31]', self.r.Dint()) 215 | self.compare_tag( 216 | prefix + 'UDTCombinedArray[0].c_Array.b_LINT[0]', self.r.Dint()) 217 | self.compare_tag( 218 | prefix + 'UDTCombinedArray[0].c_Array.b_LINT[31]', self.r.Dint()) 219 | self.compare_tag( 220 | prefix + 'UDTCombinedArray[0].c_Array.b_REAL[0]', self.r.Sint()) 221 | self.compare_tag( 222 | prefix + 'UDTCombinedArray[0].c_Array.b_REAL[31]', self.r.Sint()) 223 | self.compare_tag( 224 | prefix + 'UDTCombinedArray[0].c_Array.b_STRING[0]', self.r.String()) 225 | self.compare_tag( 226 | prefix + 'UDTCombinedArray[0].c_Array.b_STRING[31]', self.r.String()) 227 | self.compare_tag( 228 | prefix + 'UDTCombinedArray[0].c_Array.b_Timer[0].PRE', 229 | abs(self.r.Int())) 230 | self.compare_tag( 231 | prefix + 'UDTCombinedArray[0].c_Array.b_Timer[31].PRE', 232 | abs(self.r.Int())) 233 | self.compare_bool(prefix + 'UDTCombinedArray[9].c_Array.b_BOOL[0]') 234 | self.compare_bool(prefix + 'UDTCombinedArray[9].c_Array.b_BOOL[31]') 235 | self.compare_bool(prefix + 'UDTCombinedArray[9].c_Array.b_BITS[0].0') 236 | self.compare_bool(prefix + 'UDTCombinedArray[9].c_Array.b_BITS[0].31') 237 | self.compare_bool(prefix + 'UDTCombinedArray[9].c_Array.b_BITS[31].0') 238 | self.compare_bool(prefix + 'UDTCombinedArray[9].c_Array.b_BITS[31].31') 239 | self.compare_tag( 240 | prefix + 'UDTCombinedArray[9].c_Array.b_SINT[0]', self.r.Sint()) 241 | self.compare_tag( 242 | prefix + 'UDTCombinedArray[9].c_Array.b_SINT[31]', self.r.Sint()) 243 | self.compare_tag( 244 | prefix + 'UDTCombinedArray[9].c_Array.b_INT[0]', self.r.Int()) 245 | self.compare_tag( 246 | prefix + 'UDTCombinedArray[9].c_Array.b_INT[31]', self.r.Int()) 247 | self.compare_tag( 248 | prefix + 'UDTCombinedArray[9].c_Array.b_DINT[0]', self.r.Dint()) 249 | self.compare_tag( 250 | prefix + 'UDTCombinedArray[9].c_Array.b_DINT[31]', self.r.Dint()) 251 | self.compare_tag( 252 | prefix + 'UDTCombinedArray[9].c_Array.b_LINT[0]', self.r.Dint()) 253 | self.compare_tag( 254 | prefix + 'UDTCombinedArray[9].c_Array.b_LINT[31]', self.r.Dint()) 255 | self.compare_tag( 256 | prefix + 'UDTCombinedArray[9].c_Array.b_REAL[0]', self.r.Sint()) 257 | self.compare_tag( 258 | prefix + 'UDTCombinedArray[9].c_Array.b_REAL[31]', self.r.Sint()) 259 | self.compare_tag( 260 | prefix + 'UDTCombinedArray[9].c_Array.b_STRING[0]', self.r.String()) 261 | self.compare_tag( 262 | prefix + 'UDTCombinedArray[9].c_Array.b_STRING[31]', self.r.String()) 263 | self.compare_tag( 264 | prefix + 'UDTCombinedArray[9].c_Array.b_Timer[0].PRE', 265 | abs(self.r.Int())) 266 | self.compare_tag( 267 | prefix + 'UDTCombinedArray[9].c_Array.b_Timer[31].PRE', 268 | abs(self.r.Int())) 269 | 270 | def read_array_fixture(self, prefix=''): 271 | self.array_result('BaseBOOLArray[0]', 10) 272 | self.array_result('BaseSINTArray[0]', 10) 273 | self.array_result('BaseINTArray[0]', 10) 274 | self.array_result('BaseDINTArray[0]', 10) 275 | self.array_result('BaseLINTArray[0]', 10) 276 | self.array_result('BaseREALArray[0]', 10) 277 | self.array_result('BaseSTRINGArray[0]', 10) 278 | self.array_result('BaseBOOLArray[10]', 10) 279 | self.array_result('BaseSINTArray[10]', 10) 280 | self.array_result('BaseINTArray[10]', 10) 281 | self.array_result('BaseDINTArray[10]', 10) 282 | self.array_result('BaseLINTArray[10]', 10) 283 | self.array_result('BaseREALArray[10]', 10) 284 | self.array_result('BaseSTRINGArray[10]', 10) 285 | 286 | def multi_read_fixture(self, tags): 287 | response = self.comm.Read(tags) 288 | self.assertEqual(len(response), len( 289 | tags), 'Unable to read multiple tags!') 290 | 291 | for i in range(len(response)): 292 | self.assertEqual('Success', response[i].Status) 293 | 294 | def multi_write_fixture(self): 295 | tags = [("BaseSINTArray[10]", self.r.Sint()), 296 | ("BaseINTArray[10]", self.r.Int()), 297 | ("BaseDINTArray[10]", self.r.Dint()), 298 | ("BaseSTRINGArray[10]", self.r.String())] 299 | expected_values = [v[1] for v in tags] 300 | 301 | response = self.comm.Write(tags) 302 | 303 | tag_names = [tag[0] for tag in tags] 304 | response = self.comm.Read(tag_names) 305 | 306 | values = [r.Value for r in response] 307 | 308 | for i in range(len(values)): 309 | self.assertEqual(values[i], expected_values[i], 310 | "Multi-write failed") 311 | 312 | 313 | def bool_list_fixture(self, length=8): 314 | bool_list = [] 315 | for i in range(length): 316 | bool_list.append('BaseBOOLArray[{}]'.format(i)) 317 | return bool_list 318 | 319 | def nemesis_fixture(self, tag, length): 320 | # test write BOOL array 321 | true_val = [1 for i in range(length)] 322 | false_val = [0 for i in range(length)] 323 | 324 | # write array to 0 325 | self.comm.Write(tag, false_val) 326 | ret = self.comm.Read(tag, length).Value 327 | self.assertEqual(ret, false_val, "Failed to write nemesis to 0") 328 | 329 | # write array to 1 330 | self.comm.Write(tag, true_val) 331 | ret = self.comm.Read(tag, length).Value 332 | self.assertEqual(ret, true_val, "Failed to write nemesis to 1") 333 | 334 | def large_list_fixture(self): 335 | length = 50 336 | tags = ["Str{}".format(i) for i in range(length)] 337 | vals = [self.r.String() for i in range(length)] 338 | 339 | req = [[tags[i], vals[i]] for i in range(length)] 340 | self.comm.Write(req) 341 | 342 | ret = self.comm.Read(tags) 343 | read_vals = [r.Value for r in ret] 344 | 345 | self.assertEqual(vals, read_vals, "Failed to write large list") 346 | 347 | def test_with_datatype(self): 348 | """ 349 | Try a few variations of reads when including data type 350 | """ 351 | self.comm.KnownTags = {} 352 | value = self.r.Dint() 353 | self.comm.Write("BaseDINT", value, 0xc4) 354 | self.comm.KnownTags = {} 355 | ret = self.comm.Read("BaseDINT", 1, 0xc4).Value 356 | self.assertEqual(value, ret, "Failed when including data type") 357 | 358 | # try with a list/tuple, but only one instance 359 | value = self.r.Int() 360 | self.comm.KnownTags = {} 361 | write_request = [("BaseINT", value, 0xc3)] 362 | self.comm.Write(write_request) 363 | 364 | read_request = [("BaseINT", 1, 0xc3)] 365 | self.comm.KnownTags = {} 366 | ret = self.comm.Read(read_request) 367 | 368 | self.assertEqual(value, ret[0].Value, "Failed read list of one with data type") 369 | 370 | # try a list with multiple values and data type 371 | values = [self.r.Sint() for i in range(10)] 372 | write_request = [("BaseSINTArray[{}]".format(i), values[i], 0xc2) for i in range(10)] 373 | read_request = [("BaseSINTArray[{}]".format(i), 1, 0xc2) for i in range(10)] 374 | self.KnownTags = {} 375 | self.comm.Write(write_request) 376 | 377 | self.comm.KnownTags = {} 378 | ret = self.comm.Read(read_request) 379 | return_values = [r.Value for r in ret] 380 | 381 | self.assertEqual(values, return_values, "Failed reading a list with data type") 382 | 383 | def write_array_fixture(self): 384 | 385 | # clear the values before trying to write 386 | for i in range(6): 387 | self.comm.Write("BaseSINTArray[{}]".format(i), 0) 388 | self.comm.Write("BaseINTArray[{}]".format(i), 0) 389 | self.comm.Write("BaseDINTArray[{}]".format(i), 0) 390 | self.comm.Write("BaseLINTArray[{}]".format(i), 0) 391 | self.comm.Write("BaseREALArray[{}]".format(i), 0) 392 | self.comm.Write("BaseSTRINGArray[{}]".format(i), "") 393 | 394 | values = [i for i in range(100)] 395 | self.comm.Write("BaseSINTArray[0]", values) 396 | return_values = self.comm.Read("BaseSINTArray[0]", len(values)).Value 397 | self.assertEqual(values, return_values, "Failed to write array of SINT values") 398 | 399 | values = [i for i in range(100)] 400 | self.comm.Write("BaseINTArray[0]", values) 401 | return_values = self.comm.Read("BaseINTArray[0]", len(values)).Value 402 | self.assertEqual(values, return_values, "Failed to write array of INT values") 403 | 404 | values = [i for i in range(100)] 405 | self.comm.Write("BaseDINTArray[0]", values) 406 | return_values = self.comm.Read("BaseDINTArray[0]", len(values)).Value 407 | self.assertEqual(values, return_values, "Failed to write array of DINT values") 408 | 409 | values = [i for i in range(100)] 410 | self.comm.Write("BaseLINTArray[0]", values) 411 | return_values = self.comm.Read("BaseLINTArray[0]", len(values)).Value 412 | self.assertEqual(values, return_values, "Failed to write array of LINT values") 413 | 414 | values = [i for i in range(100)] 415 | self.comm.Write("BaseREALArray[0]", values) 416 | return_values = self.comm.Read("BaseREALArray[0]", len(values)).Value 417 | self.assertEqual(values, return_values, "Failed to write array of REAL values") 418 | 419 | values = ["String{}".format(i) for i in range(100)] 420 | self.comm.Write("BaseSTRINGArray[0]", values) 421 | return_values = self.comm.Read("BaseSTRINGArray[0]", len(values)).Value 422 | self.assertEqual(values, return_values, "Failed to write array of STRING values") 423 | 424 | def message_fixture(self): 425 | return_values = self.comm.Message(0x01, 0x73, 0x01) 426 | self.assertEqual(return_values.Status, "Success", "Failed to send Message: {}".format(return_values.Status)) 427 | 428 | def setUp(self): 429 | self.comm.IPAddress = plcConfig.plc_ip 430 | self.comm.ProcessorSlot = plcConfig.plc_slot 431 | self.comm.Micro800 = plcConfig.isMicro800 432 | 433 | @unittest.skipIf(plcConfig.isMicro800,'for Micro800') 434 | def test_basic(self): 435 | self.basic_fixture() 436 | self.basic_array_fixture() 437 | self.basic_fixture('PROGRAM:MainProgram.p') 438 | self.basic_array_fixture('PROGRAM:MainProgram.p') 439 | 440 | @unittest.skipIf(plcConfig.isMicro800,'for Micro800') 441 | def test_udt(self): 442 | self.udt_basic_fixture() 443 | self.udt_array_fixture_01() 444 | self.udt_array_fixture_02() 445 | self.udt_basic_fixture('PROGRAM:MainProgram.p') 446 | self.udt_array_fixture_01('PROGRAM:MainProgram.p') 447 | self.udt_array_fixture_02('PROGRAM:MainProgram.p') 448 | 449 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 450 | def test_combined(self): 451 | self.udt_combined_fixture() 452 | self.udt_combined_array_fixture() 453 | self.udt_combined_fixture('PROGRAM:MainProgram.p') 454 | self.udt_combined_array_fixture('PROGRAM:MainProgram.p') 455 | 456 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 457 | def test_array(self): 458 | self.read_array_fixture() 459 | self.read_array_fixture('PROGRAM:MainProgram.p') 460 | 461 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 462 | def test_multi_read(self): 463 | tags = ['BaseDINT', 'BaseINT', 'BaseSTRING'] 464 | self.multi_read_fixture(tags) 465 | 466 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 467 | def test_multi_write(self): 468 | self.multi_write_fixture() 469 | 470 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 471 | def test_bool_list(self): 472 | tags = self.bool_list_fixture(128) 473 | self.multi_read_fixture(tags) 474 | 475 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 476 | def test_nemesis_write(self): 477 | self.nemesis_fixture("Nemesis[0]", 64) 478 | 479 | def test_array_write(self): 480 | self.write_array_fixture() 481 | 482 | def test_message(self): 483 | self.message_fixture() 484 | 485 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 486 | def test_large_list(self): 487 | self.large_list_fixture() 488 | 489 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 490 | def test_large_list(self): 491 | self.test_with_datatype() 492 | 493 | @unittest.skipIf(is_micropython(), 'No gethostname in micropython socket module') 494 | def test_discover(self): 495 | devices = self.comm.Discover() 496 | self.assertEqual(devices.Status, 'Success', devices.Status) 497 | 498 | @unittest.skipIf(plcConfig.isMicro800, 'for Micro800') 499 | @unittest.skipIf(is_micropython(), 'No daylight in micropython time module') 500 | def test_time(self): 501 | self.comm.SetPLCTime() 502 | time = self.comm.GetPLCTime() 503 | self.assertEqual(time.Status, 'Success', time.Status) 504 | 505 | def test_get_tags(self): 506 | tags = self.comm.GetTagList() 507 | self.assertEqual(tags.Status, 'Success', tags.Status) 508 | 509 | def test_unexistent_tags(self): 510 | expected_msg = (plcConfig.isMicro800 511 | and 'Path destination unknown' 512 | or 'Path segment error' 513 | ) 514 | response = self.comm.Read('DumbTag') 515 | self.assertEqual( 516 | response.Status, expected_msg, response.Status) 517 | write_response = self.comm.Write('DumbTag', 10) 518 | self.assertEqual( 519 | write_response.Status, expected_msg, write_response.Status) 520 | 521 | def test_lgx_tag_class(self): 522 | tags = self.comm.GetTagList() 523 | self.assertEqual( 524 | isinstance( 525 | tags.Value[0], Tag), True, "LgxTag not found in GetTagList") 526 | 527 | def test_response_class(self): 528 | one_bool = self.comm.Read('BaseBool') 529 | self.assertEqual( 530 | isinstance(one_bool, Response), 531 | True, "Response class not found in Read") 532 | bool_tags = ['BaseBool', 'BaseBits.0', 'BaseBits.31'] 533 | booleans = self.comm.Read(bool_tags) 534 | self.assertEqual( 535 | isinstance(booleans[0], Response), 536 | True, "Response class not found in Multi Read") 537 | bool_write = self.comm.Write('BaseBool', 1) 538 | self.assertEqual( 539 | isinstance(bool_write, Response), 540 | True, "Response class not found in Write") 541 | 542 | @unittest.skipIf(plcConfig.isMicro800,'for Micro800') 543 | def test_program_list(self): 544 | programs = self.comm.GetProgramsList() 545 | self.assertEqual(programs.Status, 'Success', programs.Status) 546 | self.assertEqual( 547 | isinstance(programs, Response), 548 | True, "Response class not found in GetProgramsList") 549 | 550 | @unittest.skipIf(plcConfig.isMicro800,'for Micro800') 551 | def test_program_tag_list(self): 552 | program_tags = self.comm.GetProgramTagList('Program:MainProgram') 553 | self.assertEqual(program_tags.Status, 'Success', program_tags.Status) 554 | self.assertEqual( 555 | isinstance(program_tags, Response), 556 | True, "Response class not found in GetProgramTagList") 557 | self.assertEqual( 558 | isinstance(program_tags.Value[0], Tag), 559 | True, "LgxTag class not found in GetProgramTagList Value") 560 | 561 | def test_micro_800_init(self): 562 | self.assertFalse(pylogix.PLC().Micro800) 563 | self.assertFalse(pylogix.PLC(Micro800=False).Micro800) 564 | self.assertTrue(pylogix.PLC(Micro800=True).Micro800) 565 | 566 | @unittest.skipIf(is_micropython(), 'Not loading vendors dict into micropython') 567 | def test_all_uvendors(self): 568 | from pylogix.lgx_uvendors import uvendors 569 | vendors = pylogix.lgx_vendors.vendors 570 | for k in vendors: 571 | if is_python2(): 572 | self.assertEqual(uvendors[k].encode('utf-8'), vendors[k].strip(), "Mismatch vendors/uvendors") 573 | else: 574 | self.assertEqual(uvendors[k], vendors[k].strip(), "Mismatch vendors/uvendors") 575 | 576 | @unittest.skipIf(not is_micropython(), 'Not testing uvendors for python') 577 | def test_known_uvendors(self): 578 | from pylogix.lgx_uvendors import uvendors 579 | self.assertEqual(uvendors[26], 'Festo SE & Co KG', "Festo SE & Co KG uvendor not found") 580 | self.assertEqual(uvendors[1], 'Rockwell Automation/Allen-Bradley', "Rockwell uvendor not found") 581 | self.assertEqual(uvendors[-1], 'Unknown', "Unknown uvendor not returned") 582 | self.assertEqual(uvendors[(1<<32)-1], 'Unknown', "Unknown uvendor not returned") 583 | 584 | def test_unknown_attribute_in_plc(self): 585 | with self.assertRaises(AttributeError): 586 | getattr(self.comm, 'undefined_attribute') 587 | 588 | def tearDown(self): 589 | self.comm.Close() 590 | 591 | 592 | if __name__ == "__main__": 593 | unittest.main() 594 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | ## Unit Test for Pylogix (CLX, CompactLogix) 2 | 3 | In order to ensure code quality, every PR/Commit should ensure that all tests are passed. If the code change is covered by tests that are already written then just ensure everything passes and paste results within PR to increase approval. 4 | 5 | Output sample: 6 | 7 | ``` 8 | test_array (PylogixTests.PylogixTests) ... ok 9 | test_basic (PylogixTests.PylogixTests) ... ok 10 | test_combined (PylogixTests.PylogixTests) ... ok 11 | test_discover (PylogixTests.PylogixTests) ... ok 12 | test_get_tags (PylogixTests.PylogixTests) ... ok 13 | test_multi_read (PylogixTests.PylogixTests) ... ok 14 | test_time (PylogixTests.PylogixTests) ... ok 15 | test_udt (PylogixTests.PylogixTests) ... ok 16 | 17 | ---------------------------------------------------------------------- 18 | Ran 8 tests in 5.691s 19 | 20 | OK 21 | ``` 22 | 23 | ## Video Demo 24 | 25 | [![Demo](https://img.youtube.com/vi/RCHo5xJQIlg/0.jpg)](https://www.youtube.com/watch?v=RCHo5xJQIlg) 26 | 27 | ## Adding new tests 28 | 29 | The followings paths should guide you when deciding to create new tests: 30 | 31 | - Fix a bug, for an existing test (Use PylogixTests.py) 32 | - Fix a bug, for a non existing test, with a basic PLC setup. (Use PylogixTests.py) 33 | - Adding a new feature, with a basic PLC setup (Use PylogixTests.py) 34 | - Adding a new feature, with a difficult PLC setup. i.e. not easy to reproduce. (See folder structure below) 35 | 36 | It is recommended to create a fixture when the test is easily repeatable by different arguments following (DRY) concepts, then call that fixture from a test. If is something that doesn't need to be repeated more than once, then just do a test. 37 | 38 | If is something that is hard to setup then do a complete unittest class, and add a Results.txt with passed tests. This will ensure the code works, but at the same time not add extra tests to the basic tests PylogixTests.py. In the end most code should be easy to test, but is understandable to add custom code once proven it works. 39 | 40 | Sample new feature folder tree with difficult setup: 41 | 42 | ``` 43 | tests/ 44 | | 45 | -- NewFeatureNameTests/ 46 | |__ NewFeatureNameTests.py 47 | |__ NewFeatureNameResults.txt 48 | 49 | ``` 50 | 51 | Sample fixture: 52 | 53 | ``` 54 | def udt_array_fixture(self, prefix=""): 55 | # test code 56 | # assertion 57 | ... 58 | ``` 59 | 60 | Sample test: 61 | 62 | ``` 63 | def test_array(self, prefix=""): 64 | self.udt_array_fixture() 65 | self.udt_array_fixture('Program:MainProgram.p') 66 | ... 67 | ``` 68 | 69 | In addition to fixtures, and tests, you can add helper methods, if is one or two then just add within the PylogixTests class just don't put keyword `test` within the function name, otherwise separate into is own class, see Randomizer example within this Tests folder. 70 | 71 | ## setUp and tearDown 72 | 73 | These are default functions from unittest, they will run before each test. 74 | 75 | ## Setup test configuration 76 | 77 | I've added a `.gitignore` entry for plc configurations in order to avoid having to keep discarding ip, slot changes inside pylogixTests.py. 78 | 79 | Inside the tests folder create a file `plcConfig.py`, then copy and paste below variables: 80 | 81 | ``` 82 | plc_ip = '192.168.0.26' 83 | plc_slot = 1 84 | isMicro800 = False 85 | ``` 86 | 87 | See the sample file plcConfig.py.template. 88 | 89 | ## Setting up an RSLogix 5000 Project 90 | 91 | Due to the versioning system of Rockwell it is difficult to provide one project for all CLX, and Compactlogix controllers. (Will need someone with a Micro800 to do a repeatable test setup as I don't have one of those laying around) 92 | 93 | (Files are in the clx_setup folder) 94 | 95 | - Import all 3 UDT's (Click on Data Types/User-Defined -> Import) 96 | - Import tags (Tools -> Import -> Tags) (Import controller, then program tags) 97 | - Setup your I/O configuration as per your rack configuration 98 | - Save in a safe spot for next time 99 | - Download test project to PLC, and run unittest. 100 | 101 | If for whatever reason your PR is testing something super crazy, then add UDT, and tags but make sure to export new UDTs, and re-export program tags, and controller tags by right clicking in the controller organizer and selecting export tags or Tools/export. Keep in mind the current tags have just about everything so be mindful about adding new tags/udts unless is necessary. As always ask if unsure. 102 | 103 | ## Tox Integration 104 | 105 | The project also leverages tox testing since it supports py2, and every change must be checked against py2 and py3. 106 | 107 | To run tox, install tox in your preferred python version, then run tox: 108 | 109 | ``` 110 | pip install tox 111 | tox 112 | ``` 113 | 114 | ## Micropython testing 115 | 116 | Install micropython either on your board or local machine. Specific boards is out of scope for this library. 117 | Linux distros also have micropython on package managers, but might be outdated. 118 | https://docs.micropython.org/en/latest/develop/gettingstarted.html 119 | 120 | Install dependencies, this is tested on the unix/port of micropython: 121 | ```python 122 | micropython -m mip install unittest 123 | micropython -m mip install unittest-discover 124 | ``` 125 | 126 | Run tests: 127 | ```python 128 | micropython -m unittest tests/PylogixTests.py 129 | ``` 130 | 131 | ## TODO 132 | 133 | - Create a micro800 project, and export tags 134 | - Conform `pylogixTests.py` to micro800 based on functions that work with that plc 135 | - Have the community run the test on micro800 136 | -------------------------------------------------------------------------------- /tests/Randomizer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Originally created by Burt Peterson 3 | Updated and maintained by Dustin Roeder (dmroeder@gmail.com) 4 | 5 | Copyright 2019 Dustin Roeder 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | import random 21 | 22 | 23 | class Randomizer: 24 | ''' 25 | Collectin of random values 26 | ''' 27 | 28 | def Sint(self): 29 | ''' 30 | Get a random 8 bit integer 31 | ''' 32 | return random.randint(-128, 127) 33 | 34 | def Int(self): 35 | ''' 36 | Get a random 16 bit integer 37 | ''' 38 | return random.randint(-32768, 32767) 39 | 40 | def Dint(self): 41 | ''' 42 | Get a random 32 bit integer 43 | ''' 44 | return random.randint(-2147483647, 2147483647) 45 | 46 | def String(self): 47 | ''' 48 | Get a random 8 bit integer 49 | ''' 50 | integer = random.randint(1, 82) 51 | letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 52 | return ''.join( 53 | random.choice(letters) for i in range(integer)) 54 | -------------------------------------------------------------------------------- /tests/clx_setup/pylogix-Controller-Tags.CSV: -------------------------------------------------------------------------------- 1 | remark,"CSV-Import-Export" 2 | remark,"Date = Tue Sep 24 01:52:35 2019" 3 | remark,"Version = RSLogix 5000 v20.01" 4 | remark,"Owner = CanrigAdmin" 5 | remark,"Company = Nabors Corporate Services" 6 | 0.3 7 | TYPE,SCOPE,NAME,DESCRIPTION,DATATYPE,SPECIFIER,ATTRIBUTES 8 | TAG,,BaseBITS,,DINT,,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 9 | TAG,,BaseBITSArray,,DINT[128],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 10 | TAG,,BaseBOOL,,BOOL,,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 11 | TAG,,BaseBOOLArray,,BOOL[128],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 12 | TAG,,BaseCOUNTER,,COUNTER,,"(Constant := false, ExternalAccess := Read/Write)" 13 | TAG,,BaseDINT,,DINT,,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 14 | TAG,,BaseDINTArray,,DINT[128],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 15 | TAG,,BaseINT,,INT,,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 16 | TAG,,BaseINTArray,,INT[128],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 17 | TAG,,BaseLINT,,LINT,,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 18 | TAG,,BaseLINTArray,,LINT[128],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 19 | TAG,,BaseREAL,,REAL,,"(RADIX := Float, Constant := false, ExternalAccess := Read/Write)" 20 | TAG,,BaseREALArray,,REAL[128],,"(RADIX := Float, Constant := false, ExternalAccess := Read/Write)" 21 | TAG,,BaseSINT,,SINT,,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 22 | TAG,,BaseSINTArray,,SINT[128],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 23 | TAG,,BaseSTRING,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 24 | TAG,,BaseSTRINGArray,,STRING[128],,"(Constant := false, ExternalAccess := Read/Write)" 25 | TAG,,BaseTimer,,TIMER,,"(Constant := false, ExternalAccess := Read/Write)" 26 | TAG,,BaseTimerArray,,TIMER[32],,"(Constant := false, ExternalAccess := Read/Write)" 27 | TAG,,ReadOnly,,DINT,,"(RADIX := Decimal, Constant := false, ExternalAccess := Read Only)" 28 | TAG,,UDTArray,,Arrays,,"(Constant := false, ExternalAccess := Read/Write)" 29 | TAG,,UDTArray2,,Arrays[32],,"(Constant := false, ExternalAccess := Read/Write)" 30 | TAG,,UDTBasic,,Basic,,"(Constant := false, ExternalAccess := Read/Write)" 31 | TAG,,UDTBasicArray,,Basic[5],,"(Constant := false, ExternalAccess := Read/Write)" 32 | TAG,,UDTCombined,,combined,,"(Constant := false, ExternalAccess := Read/Write)" 33 | TAG,,UDTCombinedArray,,combined[32],,"(Constant := false, ExternalAccess := Read/Write)" 34 | TAG,,YugeArray,,DINT[60000],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 35 | TAG,,Nemesis,,DINT[64],,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 36 | TAG,,MultiDim,"","DINT[5,5,5]","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 37 | TAG,,MultiString,"","STRING[2,2,2]","","(Constant := false, ExternalAccess := Read/Write)" 38 | TAG,,Str0,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 39 | TAG,,Str1,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 40 | TAG,,Str10,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 41 | TAG,,Str11,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 42 | TAG,,Str12,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 43 | TAG,,Str13,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 44 | TAG,,Str14,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 45 | TAG,,Str15,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 46 | TAG,,Str16,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 47 | TAG,,Str17,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 48 | TAG,,Str18,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 49 | TAG,,Str19,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 50 | TAG,,Str2,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 51 | TAG,,Str20,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 52 | TAG,,Str21,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 53 | TAG,,Str22,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 54 | TAG,,Str23,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 55 | TAG,,Str24,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 56 | TAG,,Str25,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 57 | TAG,,Str26,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 58 | TAG,,Str27,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 59 | TAG,,Str28,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 60 | TAG,,Str29,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 61 | TAG,,Str3,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 62 | TAG,,Str30,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 63 | TAG,,Str31,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 64 | TAG,,Str32,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 65 | TAG,,Str33,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 66 | TAG,,Str34,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 67 | TAG,,Str35,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 68 | TAG,,Str36,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 69 | TAG,,Str37,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 70 | TAG,,Str38,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 71 | TAG,,Str39,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 72 | TAG,,Str4,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 73 | TAG,,Str40,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 74 | TAG,,Str41,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 75 | TAG,,Str42,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 76 | TAG,,Str43,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 77 | TAG,,Str44,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 78 | TAG,,Str45,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 79 | TAG,,Str46,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 80 | TAG,,Str47,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 81 | TAG,,Str48,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 82 | TAG,,Str49,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 83 | TAG,,Str5,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 84 | TAG,,Str50,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 85 | TAG,,Str6,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 86 | TAG,,Str7,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 87 | TAG,,Str8,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 88 | TAG,,Str9,,STRING,,"(Constant := false, ExternalAccess := Read/Write)" 89 | -------------------------------------------------------------------------------- /tests/clx_setup/pylogix_MainProgram-Tags.CSV: -------------------------------------------------------------------------------- 1 | remark,"CSV-Import-Export" 2 | remark,"Date = Tue Sep 24 01:52:21 2019" 3 | remark,"Version = RSLogix 5000 v20.01" 4 | remark,"Owner = CanrigAdmin" 5 | remark,"Company = Nabors Corporate Services" 6 | 0.3 7 | TYPE,SCOPE,NAME,DESCRIPTION,DATATYPE,SPECIFIER,ATTRIBUTES 8 | TAG,MainProgram,pBaseBITS,"","DINT","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 9 | TAG,MainProgram,pBaseBITSArray,"","DINT[32]","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 10 | TAG,MainProgram,pBaseBOOL,"","BOOL","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 11 | TAG,MainProgram,pBaseBOOLArray,"","BOOL[32]","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 12 | TAG,MainProgram,pBaseDINT,"","DINT","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 13 | TAG,MainProgram,pBaseDINTArray,"","DINT[32]","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 14 | TAG,MainProgram,pBaseINT,"","INT","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 15 | TAG,MainProgram,pBaseINTArray,"","INT[32]","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 16 | TAG,MainProgram,pBaseLINT,"","LINT","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 17 | TAG,MainProgram,pBaseLINTArray,"","LINT[32]","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 18 | TAG,MainProgram,pBaseREAL,"","REAL","","(RADIX := Float, Constant := false, ExternalAccess := Read/Write)" 19 | TAG,MainProgram,pBaseREALArray,"","REAL[32]","","(RADIX := Float, Constant := false, ExternalAccess := Read/Write)" 20 | TAG,MainProgram,pBaseSINT,"","SINT","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 21 | TAG,MainProgram,pBaseSINTArray,"","SINT[32]","","(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 22 | TAG,MainProgram,pBaseSTRING,"","STRING","","(Constant := false, ExternalAccess := Read/Write)" 23 | TAG,MainProgram,pBaseSTRINGArray,"","STRING[32]","","(Constant := false, ExternalAccess := Read/Write)" 24 | TAG,MainProgram,pBaseTimer,"","TIMER","","(Constant := false, ExternalAccess := Read/Write)" 25 | TAG,MainProgram,pBaseTimerArray,"","TIMER[32]","","(Constant := false, ExternalAccess := Read/Write)" 26 | TAG,MainProgram,pUDTArray,"","Arrays","","(Constant := false, ExternalAccess := Read/Write)" 27 | TAG,MainProgram,pUDTArray2,"","Arrays[32]","","(Constant := false, ExternalAccess := Read/Write)" 28 | TAG,MainProgram,pUDTBasic,"","Basic","","(Constant := false, ExternalAccess := Read/Write)" 29 | TAG,MainProgram,pUDTBasicArray,"","Basic[32]","","(Constant := false, ExternalAccess := Read/Write)" 30 | TAG,MainProgram,pUDTCombined,"","combined","","(Constant := false, ExternalAccess := Read/Write)" 31 | TAG,MainProgram,pUDTCombinedArray,"","combined[32]","","(Constant := false, ExternalAccess := Read/Write)" 32 | TAG,MainProgram,pMultiDim,,"DINT[5,5,5]",,"(RADIX := Decimal, Constant := false, ExternalAccess := Read/Write)" 33 | TAG,MainProgram,pMultiString,"","STRING[2,2,2]","","(Constant := false, ExternalAccess := Read/Write)" 34 | -------------------------------------------------------------------------------- /tests/clx_setup/udts/Arrays.L5X: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/clx_setup/udts/Basic.L5X: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/clx_setup/udts/combined.L5X: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/plcConfig.py.template: -------------------------------------------------------------------------------- 1 | plc_ip = '192.168.1.10' 2 | plc_slot = 0 3 | isMicro800 = False 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | # https://tox.wiki/en/4.11.3/faq.html#testing-end-of-life-python-versions 6 | 7 | [tox] 8 | requires = virtualenv<20.22.0 9 | envlist = py27, py34, py35, py37, py38, py310, py312 10 | 11 | [testenv] 12 | deps = 13 | pylogix 14 | commands = 15 | python tests/PylogixTests.py 16 | -------------------------------------------------------------------------------- /upylogix/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/__init__.mpy -------------------------------------------------------------------------------- /upylogix/eip.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/eip.mpy -------------------------------------------------------------------------------- /upylogix/lgx_comm.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/lgx_comm.mpy -------------------------------------------------------------------------------- /upylogix/lgx_device.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/lgx_device.mpy -------------------------------------------------------------------------------- /upylogix/lgx_response.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/lgx_response.mpy -------------------------------------------------------------------------------- /upylogix/lgx_tag.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/lgx_tag.mpy -------------------------------------------------------------------------------- /upylogix/lgx_uvendors.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/lgx_uvendors.mpy -------------------------------------------------------------------------------- /upylogix/utils.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmroeder/pylogix/5ac063a804c269a017ed8b31e7f10c595dadc613/upylogix/utils.mpy --------------------------------------------------------------------------------