├── .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 | 
7 | 
8 | 
9 | 
10 | 
11 | [](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 | 
19 |
20 |
21 |
22 | 
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 | 
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 | 
43 |
44 | Select Edit Tags:
45 |
46 | 
47 |
48 | Add a boolean tag:
49 |
50 | 
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 | [](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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/clx_setup/udts/Basic.L5X:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/clx_setup/udts/combined.L5X:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
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
--------------------------------------------------------------------------------