├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Proxima.png ├── README.md ├── _config.yml ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── conf.py ├── index.rst ├── make.bat ├── pymcprotocol.rst ├── requirements.txt └── runtime.txt ├── netlify.toml ├── pyproject.toml ├── src └── pymcprotocol │ ├── __init__.py │ ├── mcprotocolconst.py │ ├── mcprotocolerror.py │ ├── type3e.py │ └── type4e.py └── tests ├── __init__.py ├── config.ini └── test_pymcprotocol.py /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | poetry.lock 3 | *.pyc 4 | docs/_build/* 5 | *.ipynb 6 | .vscode 7 | .cache/ 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # For contributer 2 | Thank you very much for your contribution. 3 | If you make pull request, please set destination branch to __develop__ branch. 4 | 5 | main branch is for release package. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yohei Osawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Proxima.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senrust/pymcprotocol/aa06b03c0fdc616c3f1916092e8ecc962a3320b4/Proxima.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymcprotocol 2 | MC protocol(MELSEC Communication Protocol) implementation by Python. 3 | MC protocol enables you to operate PLC from computer. 4 | 5 | # Sponsor 6 | pymcptorocol is sponsored by [Proxima Technology Inc.](https://proxima-ai-tech.com/) 7 | 8 | ![Proxima Technology Inc.](./Proxima.png "Proxima Technology Inc.") 9 | 10 | Proxima Technology Inc. is an AI startup company that aims at social implementation of mathematics and as a part of it, aims to privatize model predictive control. Their mission is to change society with the power of mathematical science. 11 | 12 | このプロジェクトはProxima Technology様よりご支援を頂いております。 13 | Proxima Technology(プロキシマテクノロジー)は数学の社会実装を⽬指し、その⼀環としてモデル予測制御の⺠主化を掲げているAIスタートアップ企業です。数理科学の⼒で社会を変えることを企業の使命としています。 14 | 15 | 16 | 17 | ## Installation 18 | ```console 19 | pip install pymcprotocol 20 | ``` 21 | 22 | ## Protocol type 23 | pymcprotocol supports only mcprotocol 3E type and test by QPLC. 24 | 4E type is implemented. But not tested. 25 | 1C~4C type is not suuported. 26 | 27 | ## Support PLC series 28 | - Q Series 29 | - L Series 30 | - QnA Series 31 | - iQ-L Series 32 | - iQ-R Series 33 | 34 | A and FX series are not supportted because they does not support 3E or 4E type. 35 | 36 | ## How to use mc protocol 37 | ### 1. Set up PLC 38 | You need to open PLC's port for mcprotocol by GxWorks2 or GxWorks3 software. 39 | 1. Set IP address for PLC 40 | 2. Open TCP port of PLC 41 | 3. Set the port for mcprotocol. 42 | 4. Restart PLC 43 | 44 | This page will help you. 45 | English: https://www.faweb.net/en/product/opc/plc/melsec/plc 46 | Japanese: https://qiita.com/satosisotas/items/38f64c872d161b612071 47 | 48 | #### Note: 49 | - If you select ascii type communiation, 50 | you also need to set "ascii" mode in setaccessopt method. (default is "bainary" mode) 51 | - If you would like to write data in PLC, you have to enable online change 52 | 53 | ### 2. Connect by Python 54 | ```python 55 | import pymcprotocol 56 | 57 | #If you use Q series PLC 58 | pymc3e = pymcprotocol.Type3E() 59 | #if you use L series PLC, 60 | pymc3e = pymcprotocol.Type3E(plctype="L") 61 | #if you use QnA series PLC, 62 | pymc3e = pymcprotocol.Type3E(plctype="QnA") 63 | #if you use iQ-L series PLC, 64 | pymc3e = pymcprotocol.Type3E(plctype="iQ-L") 65 | #if you use iQ-R series PLC, 66 | pymc3e = pymcprotocol.Type3E(plctype="iQ-R") 67 | 68 | #If you use 4E type 69 | pymc4e = pymcprotocol.Type4E() 70 | 71 | #If you use ascii byte communication, (Default is "binary") 72 | pymc3e.setaccessopt(commtype="ascii") 73 | pymc3e.connect("192.168.1.2", 1025) 74 | 75 | ``` 76 | 77 | ### 3. Send command 78 | ```python 79 | 80 | #read from D100 to D110 81 | wordunits_values = pymc3e.batchread_wordunits(headdevice="D100", readsize=10) 82 | 83 | #read from X10 to X20 84 | bitunits_values = pymc3e.batchread_bitunits(headdevice="X10", readsize=10) 85 | 86 | #write from D10 to D15 87 | pymc3e.batchwrite_wordunits(headdevice="D10", values=[0, 10, 20, 30, 40]) 88 | 89 | #write from Y10 to Y15 90 | pymc3e.batchwrite_bitunits(headdevice="Y10", values=[0, 1, 0, 1, 0]) 91 | 92 | #read "D1000", "D2000" and dword "D3000". 93 | word_values, dword_values = pymc3e.randomread(word_devices=["D1000", "D2000"], dword_devices=["D3000"]) 94 | 95 | #write 1000 to "D1000", 2000 to "D2000" and 655362 todword "D3000" 96 | pymc3e.randomwrite(word_devices=["D1000", "D1002"], word_values=[1000, 2000], 97 | dword_devices=["D1004"], dword_values=[655362]) 98 | 99 | #write 1(ON) to "X0", 0(OFF) to "X10" 100 | pymc3e.randomwrite_bitunits(bit_devices=["X0", "X10"], values=[1, 0]) 101 | 102 | ``` 103 | 104 | ### 4. Unlock and lock PLC 105 | ```python 106 | 107 | #Unlock PLC, 108 | #If you set PLC to locked, you need to unlkock to remote operation 109 | #Except iQ-R, password is 4 character. 110 | pymc3e.remote_unlock(password="1234") 111 | #If you want to hide password from program 112 | #You can enter passwrod directly 113 | pymc3e.remote_unlock(request_input=True) 114 | 115 | #Lock PLC 116 | pymc3e.remote_lock(password="1234") 117 | pymc3e.remote_lock(request_input=True) 118 | ``` 119 | 120 | ### 5. Remote Operation 121 | If you connect to your system by E71 module, Ethernet communication module, 122 | These commands are available. 123 | 124 | If you connect to PLC directly, C059 error returns. 125 | 126 | ```python 127 | 128 | #remote run, clear all device 129 | pymc3e.remote_run(clear_mode=2, force_exec=True) 130 | 131 | #remote stop 132 | pymc3e.remote_stop() 133 | 134 | #remote latch clear. (have to PLC be stopped) 135 | pymc3e.remote_latchclear() 136 | 137 | #remote pause 138 | pymc3e.remote_pause(force_exec=False) 139 | 140 | #remote reset 141 | pymc3e.remote_reset() 142 | 143 | #read PLC type 144 | cpu_type, cpu_code = pymc3e.read_cputype() 145 | 146 | ``` 147 | 148 | ### API Reference 149 | API manual is here. 150 | https://pymcprotocol.netlify.app/ 151 | 152 | ### Lisence 153 | pymcprotocol is Released under the MIT license. 154 | 155 | ### Caution 156 | pymcprotocol does not support entire MC protocol since it is very complicated and troublesome. 157 | If you would like to use unsupported function, please make Github issue. 158 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | google_analytics: UA-197905865-1 3 | plugins: 4 | - jekyll-sitemap -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senrust/pymcprotocol/aa06b03c0fdc616c3f1916092e8ecc962a3320b4/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | 17 | sys.path.insert(0, os.path.abspath('../src')) 18 | sys.path.insert(0, os.path.abspath('../src/pymcprotocol')) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'pymcprotocol' 24 | copyright = '2020, Yohei Osawa' 25 | author = 'Yohei Osawa' 26 | 27 | with open("../src/pymcprotocol/__init__.py") as f: 28 | version_line = f.readline() 29 | version = version_line.strip().split(" ")[-1] 30 | # The full version, including alpha/beta/rc tags 31 | release = version 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.viewcode' 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # List of patterns, relative to source directory, that match files and 46 | # directories to ignore when looking for source files. 47 | # This pattern also affects html_static_path and html_extra_path. 48 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 49 | 50 | 51 | # -- Options for HTML output ------------------------------------------------- 52 | 53 | # The theme to use for HTML and HTML Help pages. See the documentation for 54 | # a list of builtin themes. 55 | # 56 | html_theme = 'sphinx_rtd_theme' 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = ['_static'] 62 | 63 | autodoc_default_options = { 64 | 'member-order': 'bysource', 65 | 'special-members': '__init__', 66 | 'undoc-members': True, 67 | } -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pymcprotocol documentation master file, created by 2 | sphinx-quickstart on Sun Nov 29 18:32:01 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ============================================ 7 | Welcome to pymcprotocol's documentation! 8 | ============================================ 9 | 10 | About pymcprotocol 11 | ================================== 12 | pymcprotocol is MC Protocol(MELSEC Communication Protocol) implementation by Python. 13 | 14 | Installation 15 | ================================== 16 | .. code-block:: shell 17 | 18 | pip install pymcprotocol 19 | 20 | 21 | Protocol type 22 | ================================== 23 | | pymcprotocol supports only mcprotocol 3E type and test by QPLC. 24 | | 4E type is implemented. But not tested. 25 | | 1C~4C type is not suuported. 26 | 27 | Support PLC series 28 | ================================== 29 | - Q Series 30 | - L Series 31 | - QnA Series 32 | - iQ-L Series 33 | - iQ-R Series 34 | 35 | A and FX series are not supportted because they does not support 3E or 4E type. 36 | 37 | How to use pymcprotocol 38 | ================================== 39 | 40 | 1. Set up PLC 41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 42 | | You need to open PLC's port for mcprotocol by GxWorks2 or GxWorks3 software. 43 | | 1. Set IP address for PLC 44 | | 2. Open TCP port of PLC 45 | | 3. Set the port for mcprotocol. 46 | | 4. Restart PLC 47 | 48 | | This page will help you. 49 | | English: https://www.faweb.net/en/product/opc/plc/melsec/plc 50 | | Japanese: https://qiita.com/satosisotas/items/38f64c872d161b612071 51 | 52 | | Note: 53 | | - If you select ascii type communiation, 54 | | you also need to set "ascii" mode in setaccessopt method. (default is "bainary" mode) 55 | | - If you would like to write data in PLC, you have to enable online change 56 | 57 | 58 | 2. Connect by Python 59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 60 | .. code-block:: python 61 | 62 | import pymcprotocol 63 | 64 | #If you use Q series PLC 65 | pymc3e = pymcprotocol.Type3E() 66 | #if you use L series PLC, 67 | pymc3e = pymcprotocol.Type3E(plctype="L") 68 | #if you use QnA series PLC, 69 | pymc3e = pymcprotocol.Type3E(plctype="L") 70 | #if you use iQ-L series PLC, 71 | pymc3e = pymcprotocol.Type3E(plctype="iQ-L") 72 | #if you use iQ-R series PLC, 73 | pymc3e = pymcprotocol.Type3E(plctype="iQ-R") 74 | 75 | #If you use 4E type 76 | pymc4e = pymcprotocol.Type4E() 77 | 78 | #If you use ascii byte communication. (default is "binary") 79 | pymc3e.setaccessopt(commtype="ascii") 80 | pymc3e.connect("192.168.1.2", 1025) 81 | 82 | 3. Send command 83 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 84 | .. code-block:: python 85 | 86 | #read from D100 to D110 87 | wordunits_values = pymc3e.batchread_wordunits(headdevice="D100", readsize=10) 88 | 89 | #read from X10 to X20 90 | bitunits_values = pymc3e.batchread_bitunits(headdevice="X10", readsize=10) 91 | 92 | #write from D10 to D15 93 | pymc3e.batchwrite_wordunits(headdevice="D10", values=[0, 10, 20, 30, 40]) 94 | 95 | #write from Y10 to Y15 96 | pymc3e.batchwrite_bitunits(headdevice="Y10", values=[0, 1, 0, 1, 0]) 97 | 98 | #read "D1000", "D2000" and dword "D3000". 99 | word_values, dword_values = pymc3e.randomread(word_devices=["D1000", "D2000"], dword_devices=["D3000"]) 100 | 101 | #write 1000 to "D1000", 2000 to "D2000" and 655362 todword "D3000" 102 | pymc3e.randomwrite(word_devices=["D1000", "D1002"], word_values=[1000, 2000], 103 | dword_devices=["D1004"], dword_values=[655362]) 104 | 105 | #write 1(ON) to "X0", 0(OFF) to "X10" 106 | pymc3e.randomwrite_bitunits(bit_devices=["X0", "X10"], values=[1, 0]) 107 | 108 | 109 | 110 | 4. Remote Operation 111 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 112 | .. code-block:: python 113 | 114 | #Unlock PLC, 115 | #If you set PLC to locked, you need to unlkock to remote operation 116 | #Except iQ-R, password is 4 character. 117 | pymc3e.remote_unlock(password="1234") 118 | #If you want to hide password from program 119 | #You can enter passwrod directly 120 | pymc3e.remote_unlock(request_input=True) 121 | 122 | #Lock PLC 123 | pymc3e.remote_lock(password="1234") 124 | pymc3e.remote_lock(request_input=True) 125 | 126 | 127 | 5. Remote Operation 128 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 129 | | If you connect to your system by E71 module, Ethernet communication module, 130 | | These commands are available. 131 | | If you connect to PLC directly, C059 error returns. 132 | 133 | 134 | .. code-block:: python 135 | 136 | #remote run, clear all device 137 | pymc3e.remote_run(clear_mode=2, force_exec=True) 138 | 139 | #remote stop 140 | pymc3e.remote_stop() 141 | 142 | #remote latch clear. (have to PLC be stopped) 143 | pymc3e.remote_latchclear() 144 | 145 | #remote pause 146 | pymc3e.remote_pause(force_exec=False) 147 | 148 | #remote reset 149 | pymc3e.remote_reset() 150 | 151 | #read PLC type 152 | cpu_type, cpu_code = pymc3e.read_cputype() 153 | 154 | .. toctree:: 155 | :maxdepth: 2 156 | 157 | pymcprotocol 158 | 159 | .. toctree:: 160 | :maxdepth: 2 161 | 162 | Caution 163 | ================== 164 | 165 | | pymcprotocol does not support entire MC protocol since it is very complicated and troublesome. 166 | | If you would like to use unsupported function, please make Github issue. 167 | 168 | License 169 | ================== 170 | 171 | pymcprotocol is Released under the MIT license. 172 | 173 | Indices and tables 174 | ================== 175 | 176 | * :ref:`genindex` 177 | * :ref:`modindex` 178 | * :ref:`search` 179 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/pymcprotocol.rst: -------------------------------------------------------------------------------- 1 | pymcprotocol API referencess 2 | ============================= 3 | 4 | pymcprotocol.type3e module 5 | -------------------------- 6 | 7 | .. automodule:: pymcprotocol.type3e 8 | :members: 9 | :noindex: 10 | :undoc-members: 11 | :show-inheritance: 12 | 13 | pymcprotocol.type4e module 14 | -------------------------- 15 | 16 | .. automodule:: pymcprotocol.type4e 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | :noindex: 21 | 22 | pymcprotocol.mcprotocolerror module 23 | ----------------------------------- 24 | 25 | .. automodule:: pymcprotocol.mcprotocolerror 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | Module contents 31 | --------------- 32 | 33 | .. automodule:: pymcprotocol 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme -------------------------------------------------------------------------------- /docs/runtime.txt: -------------------------------------------------------------------------------- 1 | 3.7 -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "docs" 3 | publish = "_build/html" 4 | command = "sphinx-build ./ _build/html/" -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pymcprotocol" 3 | version = "0.3.0" 4 | description = "MC Protocol(MELSEC Communication Protocol) implementation by Python" 5 | license = "MIT" 6 | authors = ["Yohei Osawa "] 7 | readme = "./README.md" 8 | homepage = "https://pymcprotocol.netlify.app/" 9 | repository = "https://github.com/senrust/pymcprotocol" 10 | keywords = ["Mistubishi", "PLC", "factory"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.0" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^3.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /src/pymcprotocol/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.0' 2 | __license__ = 'MIT' 3 | __author__ = 'Yohei Osawa' 4 | __author_email__ = 'yohei.osawa.318.niko8@gmail.com' 5 | __url__ = 'https://github.com/senrust/pymcprotocol' 6 | 7 | from .type3e import Type3E 8 | from .type4e import Type4E -------------------------------------------------------------------------------- /src/pymcprotocol/mcprotocolconst.py: -------------------------------------------------------------------------------- 1 | """This file defines mcprotocol constant. 2 | """ 3 | #PLC definetion 4 | Q_SERIES = "Q" 5 | L_SERIES = "L" 6 | QnA_SERIES = "QnA" 7 | iQL_SERIES = "iQ-L" 8 | iQR_SERIES = "iQ-R" 9 | 10 | #communication type 11 | COMMTYPE_BINARY = "binary" 12 | COMMTYPE_ASCII = "ascii" 13 | 14 | class DeviceCodeError(Exception): 15 | """devicecode error. Device is not exsist. 16 | 17 | Attributes: 18 | plctype(str): PLC type. "Q", "L", "QnA", "iQ-L", "iQ-R", 19 | devicename(str): devicename. (ex: "Q", "P", both of them does not support mcprotocol.) 20 | 21 | """ 22 | def __init__(self, plctype, devicename): 23 | self.plctype = plctype 24 | self.devicename = devicename 25 | 26 | def __str__(self): 27 | error_txt = "devicename: {} is not support {} series PLC.\n"\ 28 | "If you enter hexadecimal device(X, Y, B, W, SB, SW, DX, DY, ZR) with only alphabet number\n"\ 29 | "(such as XFFF, device name is \"X\", device number is \"FFF\"),\n"\ 30 | "please insert 0 between device name and device number.\neg: XFFF → X0FFF".format(self.devicename, self.plctype) 31 | return error_txt 32 | 33 | class DeviceConstants: 34 | """This class defines mc protocol deveice constatnt. 35 | 36 | Attributes: 37 | D_DEVICE(int): D devide code (0xA8) 38 | 39 | """ 40 | #These device supports all series 41 | SM_DEVICE = 0x91 42 | SD_DEVICE = 0xA9 43 | X_DEVICE = 0x9C 44 | Y_DEVICE = 0x9D 45 | M_DEVICE = 0x90 46 | L_DEVICE = 0x92 47 | F_DEVICE = 0x93 48 | V_DEVICE = 0x94 49 | B_DEVICE = 0xA0 50 | D_DEVICE = 0xA8 51 | W_DEVICE = 0xB4 52 | TS_DEVICE = 0xC1 53 | TC_DEVICE = 0xC0 54 | TN_DEVICE = 0xC2 55 | SS_DEVICE = 0xC7 56 | SC_DEVICE = 0xC6 57 | SN_DEVICE = 0xC8 58 | CS_DEVICE = 0xC4 59 | CC_DEVICE = 0xC3 60 | CN_DEVICE = 0xC5 61 | SB_DEVICE = 0xA1 62 | SW_DEVICE = 0xB5 63 | DX_DEVICE = 0xA2 64 | DY_DEVICE = 0xA3 65 | R_DEVICE = 0xAF 66 | ZR_DEVICE = 0xB0 67 | 68 | #These device supports only "iQ-R" series 69 | LTS_DEVICE = 0x51 70 | LTC_DEVICE = 0x50 71 | LTN_DEVICE = 0x52 72 | LSTS_DEVICE = 0x59 73 | LSTC_DEVICE = 0x58 74 | LSTN_DEVICE = 0x5A 75 | LCS_DEVICE = 0x55 76 | LCC_DEVICE = 0x54 77 | LCN_DEVICE = 0x56 78 | LZ_DEVICE = 0x62 79 | RD_DEVICE = 0x2C 80 | 81 | BIT_DEVICE = "bit" 82 | WORD_DEVICE = "word" 83 | DWORD_DEVICE= "dword" 84 | 85 | 86 | def __init__(self): 87 | """Constructor 88 | """ 89 | pass 90 | 91 | @staticmethod 92 | def get_binary_devicecode(plctype, devicename): 93 | """Static method that returns devicecode from device name. 94 | 95 | Args: 96 | plctype(str): PLC type. "Q", "L", "QnA", "iQ-L", "iQ-R" 97 | devicename(str): Device name. (ex: "D", "X", "Y") 98 | 99 | Returns: 100 | devicecode(int): Device code defined mc protocol (ex: "D" → 0xA8) 101 | Base number: Base number for each device name 102 | 103 | """ 104 | if devicename == "SM": 105 | return DeviceConstants.SM_DEVICE, 10 106 | elif devicename == "SD": 107 | return DeviceConstants.SD_DEVICE, 10 108 | elif devicename == "X": 109 | return DeviceConstants.X_DEVICE, 16 110 | elif devicename == "Y": 111 | return DeviceConstants.Y_DEVICE, 16 112 | elif devicename == "M": 113 | return DeviceConstants.M_DEVICE, 10 114 | elif devicename == "L": 115 | return DeviceConstants.L_DEVICE, 10 116 | elif devicename == "F": 117 | return DeviceConstants.F_DEVICE, 10 118 | elif devicename == "V": 119 | return DeviceConstants.V_DEVICE, 10 120 | elif devicename == "B": 121 | return DeviceConstants.B_DEVICE, 16 122 | elif devicename == "D": 123 | return DeviceConstants.D_DEVICE, 10 124 | elif devicename == "W": 125 | return DeviceConstants.W_DEVICE, 16 126 | elif devicename == "TS": 127 | return DeviceConstants.TS_DEVICE, 10 128 | elif devicename == "TC": 129 | return DeviceConstants.TC_DEVICE, 10 130 | elif devicename == "TN": 131 | return DeviceConstants.TN_DEVICE, 10 132 | elif devicename == "STS": 133 | return DeviceConstants.SS_DEVICE, 10 134 | elif devicename == "STC": 135 | return DeviceConstants.SC_DEVICE, 10 136 | elif devicename == "STN": 137 | return DeviceConstants.SN_DEVICE, 10 138 | elif devicename == "CS": 139 | return DeviceConstants.CS_DEVICE, 10 140 | elif devicename == "CC": 141 | return DeviceConstants.CC_DEVICE, 10 142 | elif devicename == "CN": 143 | return DeviceConstants.CN_DEVICE, 10 144 | elif devicename == "SB": 145 | return DeviceConstants.SB_DEVICE, 16 146 | elif devicename == "SW": 147 | return DeviceConstants.SW_DEVICE, 16 148 | elif devicename == "DX": 149 | return DeviceConstants.DX_DEVICE, 16 150 | elif devicename == "DY": 151 | return DeviceConstants.DY_DEVICE, 16 152 | elif devicename == "R": 153 | return DeviceConstants.R_DEVICE, 10 154 | elif devicename == "ZR": 155 | return DeviceConstants.ZR_DEVICE, 16 156 | elif (devicename == "LTS") and (plctype == iQR_SERIES): 157 | return DeviceConstants.LTS_DEVICE, 10 158 | elif (devicename == "LTC") and (plctype == iQR_SERIES): 159 | return DeviceConstants.LTC_DEVICE, 10 160 | elif (devicename == "LTN") and (plctype == iQR_SERIES): 161 | return DeviceConstants.LTN_DEVICE, 10 162 | elif (devicename == "LSTS") and (plctype == iQR_SERIES): 163 | return DeviceConstants.LSTS_DEVICE, 10 164 | elif (devicename == "LSTN") and (plctype == iQR_SERIES): 165 | return DeviceConstants.LSTN_DEVICE, 10 166 | elif (devicename == "LCS") and (plctype == iQR_SERIES): 167 | return DeviceConstants.LCS_DEVICE, 10 168 | elif (devicename == "LCC") and (plctype == iQR_SERIES): 169 | return DeviceConstants.LCC_DEVICE, 10 170 | elif (devicename == "LCN") and (plctype == iQR_SERIES): 171 | return DeviceConstants.LCN_DEVICE, 10 172 | elif (devicename == "LZ") and (plctype == iQR_SERIES): 173 | return DeviceConstants.LZ_DEVICE, 10 174 | elif (devicename == "RD") and (plctype == iQR_SERIES): 175 | return DeviceConstants.RD_DEVICE, 10 176 | else: 177 | raise DeviceCodeError(plctype, devicename) 178 | 179 | @staticmethod 180 | def get_ascii_devicecode(plctype, devicename): 181 | """Static method that returns devicecode from device name. 182 | 183 | Args: 184 | plctype(str): PLC type. "Q", "L", "QnA", "iQ-L", "iQ-R" 185 | devicename(str): Device name. (ex: "D", "X", "Y") 186 | 187 | Returns: 188 | devicecode(int): Device code defined mc protocol (ex: "D" → "D*") 189 | Base number: Base number for each device name 190 | 191 | """ 192 | if plctype == iQR_SERIES: 193 | padding = 4 194 | else: 195 | padding = 2 196 | if devicename == "SM": 197 | return devicename.ljust(padding, "*"), 10 198 | elif devicename == "SD": 199 | return devicename.ljust(padding, "*"), 10 200 | elif devicename == "X": 201 | return devicename.ljust(padding, "*"), 16 202 | elif devicename == "Y": 203 | return devicename.ljust(padding, "*"), 16 204 | elif devicename == "M": 205 | return devicename.ljust(padding, "*"), 10 206 | elif devicename == "L": 207 | return devicename.ljust(padding, "*"), 10 208 | elif devicename == "F": 209 | return devicename.ljust(padding, "*"), 10 210 | elif devicename == "V": 211 | return devicename.ljust(padding, "*"), 10 212 | elif devicename == "B": 213 | return devicename.ljust(padding, "*"), 16 214 | elif devicename == "D": 215 | return devicename.ljust(padding, "*"), 10 216 | elif devicename == "W": 217 | return devicename.ljust(padding, "*"), 16 218 | elif devicename == "TS": 219 | return devicename.ljust(padding, "*"), 10 220 | elif devicename == "TC": 221 | return devicename.ljust(padding, "*"), 10 222 | elif devicename == "TN": 223 | return devicename.ljust(padding, "*"), 10 224 | elif devicename == "STS": 225 | if plctype == iQR_SERIES: 226 | return "STS".ljust(padding, "*"), 10 227 | else: 228 | return "SS".ljust(padding, "*"), 10 229 | elif devicename == "STC": 230 | if plctype == iQR_SERIES: 231 | return "STC".ljust(padding, "*"), 10 232 | else: 233 | return "SC".ljust(padding, "*"), 10 234 | elif devicename == "STN": 235 | if plctype == iQR_SERIES: 236 | return "STN".ljust(padding, "*"), 10 237 | else: 238 | return "SN".ljust(padding, "*"), 10 239 | elif devicename == "CS": 240 | return devicename.ljust(padding, "*"), 10 241 | elif devicename == "CC": 242 | return devicename.ljust(padding, "*"), 10 243 | elif devicename == "CN": 244 | return devicename.ljust(padding, "*"), 10 245 | elif devicename == "SB": 246 | return devicename.ljust(padding, "*"), 16 247 | elif devicename == "SW": 248 | return devicename.ljust(padding, "*"), 16 249 | elif devicename == "DX": 250 | return devicename.ljust(padding, "*"), 16 251 | elif devicename == "DY": 252 | return devicename.ljust(padding, "*"), 16 253 | elif devicename == "R": 254 | return devicename.ljust(padding, "*"), 10 255 | elif devicename == "ZR": 256 | return devicename.ljust(padding, "*"), 16 257 | elif (devicename == "LTS") and (plctype == iQR_SERIES): 258 | return devicename.ljust(padding, "*"), 10 259 | elif (devicename == "LTC") and (plctype == iQR_SERIES): 260 | return devicename.ljust(padding, "*"), 10 261 | elif (devicename == "LTN") and (plctype == iQR_SERIES): 262 | return devicename.ljust(padding, "*"), 10 263 | elif (devicename == "LSTS") and (plctype == iQR_SERIES): 264 | return devicename.ljust(padding, "*"), 10 265 | elif (devicename == "LSTN") and (plctype == iQR_SERIES): 266 | return devicename.ljust(padding, "*"), 10 267 | elif (devicename == "LCS") and (plctype == iQR_SERIES): 268 | return devicename.ljust(padding, "*"), 10 269 | elif (devicename == "LCC") and (plctype == iQR_SERIES): 270 | return devicename.ljust(padding, "*"), 10 271 | elif (devicename == "LCN") and (plctype == iQR_SERIES): 272 | return devicename.ljust(padding, "*"), 10 273 | elif (devicename == "LZ") and (plctype == iQR_SERIES): 274 | return devicename.ljust(padding, "*"), 10 275 | elif (devicename == "RD") and (plctype == iQR_SERIES): 276 | return devicename.ljust(padding, "*"), 10 277 | else: 278 | raise DeviceCodeError(plctype, devicename) 279 | 280 | @staticmethod 281 | def get_devicetype(plctype, devicename): 282 | """Static method that returns device type "bit" or "wrod" type. 283 | 284 | Args: 285 | plctype(str): PLC type. "Q", "L", "QnA", "iQ-L", "iQ-R" 286 | devicename(str): Device name. (ex: "D", "X", "Y") 287 | 288 | Returns: 289 | devicetyoe(str): Device type. "bit" or "word" 290 | 291 | """ 292 | if devicename == "SM": 293 | return DeviceConstants.BIT_DEVICE 294 | elif devicename == "SD": 295 | return DeviceConstants.WORD_DEVICE 296 | elif devicename == "X": 297 | return DeviceConstants.BIT_DEVICE 298 | elif devicename == "Y": 299 | return DeviceConstants.BIT_DEVICE 300 | elif devicename == "M": 301 | return DeviceConstants.BIT_DEVICE 302 | elif devicename == "L": 303 | return DeviceConstants.BIT_DEVICE 304 | elif devicename == "F": 305 | return DeviceConstants.BIT_DEVICE 306 | elif devicename == "V": 307 | return DeviceConstants.BIT_DEVICE 308 | elif devicename == "B": 309 | return DeviceConstants.BIT_DEVICE 310 | elif devicename == "D": 311 | return DeviceConstants.WORD_DEVICE 312 | elif devicename == "W": 313 | return DeviceConstants.WORD_DEVICE 314 | elif devicename == "TS": 315 | return DeviceConstants.BIT_DEVICE 316 | elif devicename == "TC": 317 | return DeviceConstants.BIT_DEVICE 318 | elif devicename == "TN": 319 | return DeviceConstants.WORD_DEVICE 320 | elif devicename == "STS": 321 | return DeviceConstants.BIT_DEVICE 322 | elif devicename == "STC": 323 | return DeviceConstants.BIT_DEVICE 324 | elif devicename == "STN": 325 | return DeviceConstants.WORD_DEVICE 326 | elif devicename == "CS": 327 | return DeviceConstants.BIT_DEVICE 328 | elif devicename == "CC": 329 | return DeviceConstants.BIT_DEVICE 330 | elif devicename == "CN": 331 | return DeviceConstants.WORD_DEVICE 332 | elif devicename == "SB": 333 | return DeviceConstants.BIT_DEVICE 334 | elif devicename == "SW": 335 | return DeviceConstants.WORD_DEVICE 336 | elif devicename == "DX": 337 | return DeviceConstants.BIT_DEVICE 338 | elif devicename == "DY": 339 | return DeviceConstants.BIT_DEVICE 340 | elif devicename == "R": 341 | return DeviceConstants.WORD_DEVICE 342 | elif devicename == "ZR": 343 | return DeviceConstants.WORD_DEVICE 344 | elif (devicename == "LTS") and (plctype == iQR_SERIES): 345 | return DeviceConstants.BIT_DEVICE 346 | elif (devicename == "LTC") and (plctype == iQR_SERIES): 347 | return DeviceConstants.BIT_DEVICE 348 | elif (devicename == "LTN") and (plctype == iQR_SERIES): 349 | return DeviceConstants.BIT_DEVICE 350 | elif (devicename == "LSTS") and (plctype == iQR_SERIES): 351 | return DeviceConstants.BIT_DEVICE 352 | elif (devicename == "LSTN") and (plctype == iQR_SERIES): 353 | return DeviceConstants.DWORD_DEVICE 354 | elif (devicename == "LCS") and (plctype == iQR_SERIES): 355 | return DeviceConstants.BIT_DEVICE 356 | elif (devicename == "LCC") and (plctype == iQR_SERIES): 357 | return DeviceConstants.BIT_DEVICE 358 | elif (devicename == "LCN") and (plctype == iQR_SERIES): 359 | return DeviceConstants.DWORD_DEVICE 360 | elif (devicename == "LZ") and (plctype == iQR_SERIES): 361 | return DeviceConstants.DWORD_DEVICE 362 | elif (devicename == "RD") and (plctype == iQR_SERIES): 363 | return DeviceConstants.WORD_DEVICE 364 | else: 365 | raise DeviceCodeError(plctype, devicename) -------------------------------------------------------------------------------- /src/pymcprotocol/mcprotocolerror.py: -------------------------------------------------------------------------------- 1 | """This file is collection of mcprotocol error. 2 | 3 | """ 4 | 5 | class MCProtocolError(Exception): 6 | """devicecode error. Device is not exsist. 7 | 8 | Attributes: 9 | plctype(str): PLC type. "Q", "L" or "iQ" 10 | devicename(str): devicename. (ex: "Q", "P", both of them does not support mcprotocol.) 11 | 12 | """ 13 | def __init__(self, errorcode): 14 | self.errorcode = "0x" + format(errorcode, "x").rjust(4, "0").upper() 15 | 16 | def __str__(self): 17 | return "mc protocol error: error code {}".format(self.errorcode) 18 | 19 | class UnsupportedComandError(Exception): 20 | """This command is not supported by the module you connected. 21 | 22 | """ 23 | def __init__(self): 24 | pass 25 | 26 | def __str__(self): 27 | return "This command is not supported by the module you connected." \ 28 | "If you connect with CPU module, please use E71 module." 29 | 30 | 31 | def check_mcprotocol_error(status): 32 | """Check mc protocol command error. 33 | If errot exist(status != 0), raise Error. 34 | 35 | """ 36 | if status == 0: 37 | return None 38 | elif status == 0xC059: 39 | raise UnsupportedComandError 40 | else: 41 | raise MCProtocolError(status) 42 | 43 | -------------------------------------------------------------------------------- /src/pymcprotocol/type3e.py: -------------------------------------------------------------------------------- 1 | """This file implements mcprotocol 3E type communication. 2 | """ 3 | 4 | import re 5 | import time 6 | import socket 7 | import binascii 8 | from . import mcprotocolerror 9 | from . import mcprotocolconst as const 10 | 11 | def isascii(text): 12 | """check text is all ascii character. 13 | Python 3.6 does not support str.isascii() 14 | """ 15 | return all(ord(c) < 128 for c in text) 16 | 17 | def twos_comp(val, mode="short"): 18 | """compute the 2's complement of int value val 19 | """ 20 | if mode =="byte": 21 | bit = 8 22 | elif mode =="short": 23 | bit = 16 24 | elif mode== "long": 25 | bit = 32 26 | else: 27 | raise ValueError("cannnot calculate 2's complement") 28 | if (val & (1 << (bit - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 29 | val = val - (1 << bit) # compute negative value 30 | return val 31 | 32 | def get_device_number(device): 33 | """Extract device number. 34 | 35 | Ex: "D1000" → "1000" 36 | "X0x1A" → "0x1A 37 | """ 38 | device_num = re.search(r"\d.*", device) 39 | if device_num is None: 40 | raise ValueError("Invalid device number, {}".format(device)) 41 | else: 42 | device_num_str = device_num.group(0) 43 | return device_num_str 44 | 45 | 46 | class CommTypeError(Exception): 47 | """Communication type error. Communication type must be "binary" or "ascii" 48 | 49 | """ 50 | def __init__(self): 51 | pass 52 | 53 | def __str__(self): 54 | return "communication type must be \"binary\" or \"ascii\"" 55 | 56 | class PLCTypeError(Exception): 57 | """PLC type error. PLC type must be"Q", "L", "QnA", "iQ-L", "iQ-R" 58 | 59 | """ 60 | def __init__(self): 61 | pass 62 | 63 | def __str__(self): 64 | return "plctype must be \"Q\", \"L\", \"QnA\" \"iQ-L\" or \"iQ-R\"" 65 | 66 | class Type3E: 67 | """mcprotocol 3E communication class. 68 | 69 | Attributes: 70 | plctype(str): connect PLC type. "Q", "L", "QnA", "iQ-L", "iQ-R" 71 | commtype(str): communication type. "binary" or "ascii". (Default: "binary") 72 | subheader(int): Subheader for mc protocol 73 | network(int): network No. of an access target. (0<= network <= 255) 74 | pc(int): network module station No. of an access target. (0<= pc <= 255) 75 | dest_moduleio(int): When accessing a multidrop connection station via network, 76 | specify the start input/output number of a multidrop connection source module. 77 | the CPU module of the multiple CPU system and redundant system. 78 | dest_modulesta(int): accessing a multidrop connection station via network, 79 | specify the station No. of aaccess target module 80 | timer(int): time to raise Timeout error(/250msec). default=4(1sec) 81 | If PLC elapsed this time, PLC returns Timeout answer. 82 | Note: python socket timeout is always set timer+1sec. To recieve Timeout answer. 83 | """ 84 | plctype = const.Q_SERIES 85 | commtype = const.COMMTYPE_BINARY 86 | subheader = 0x5000 87 | network = 0 88 | pc = 0xFF 89 | dest_moduleio = 0X3FF 90 | dest_modulesta = 0X0 91 | timer = 4 # MC protocol timeout. 250msec * 4 = 1 sec 92 | soc_timeout = 2 # 2 sec 93 | _is_connected = False 94 | _SOCKBUFSIZE = 4096 95 | _wordsize = 2 #how many byte is required to describe word value 96 | #binary: 2, ascii:4. 97 | _debug = False 98 | 99 | 100 | def __init__(self, plctype ="Q"): 101 | """Constructor 102 | 103 | """ 104 | self._set_plctype(plctype) 105 | 106 | def _set_debug(self, debug=False): 107 | """Turn on debug mode 108 | """ 109 | self._debug = debug 110 | 111 | def connect(self, ip, port): 112 | """Connect to PLC 113 | 114 | Args: 115 | ip (str): ip address(IPV4) to connect PLC 116 | port (int): port number of connect PLC 117 | timeout (float): timeout second in communication 118 | 119 | """ 120 | self._ip = ip 121 | self._port = port 122 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 123 | self._sock.settimeout(self.soc_timeout) 124 | self._sock.connect((ip, port)) 125 | self._is_connected = True 126 | 127 | def close(self): 128 | """Close connection 129 | 130 | """ 131 | self._sock.close() 132 | self._is_connected = False 133 | 134 | def _send(self, send_data): 135 | """send mc protorocl data 136 | 137 | Args: 138 | send_data(bytes): mc protocol data 139 | 140 | """ 141 | if self._is_connected: 142 | if self._debug: 143 | print(binascii.hexlify(send_data)) 144 | self._sock.send(send_data) 145 | else: 146 | raise Exception("socket is not connected. Please use connect method") 147 | 148 | def _recv(self): 149 | """recieve mc protocol data 150 | 151 | Returns: 152 | recv_data 153 | """ 154 | recv_data = self._sock.recv(self._SOCKBUFSIZE) 155 | return recv_data 156 | 157 | def _set_plctype(self, plctype): 158 | """Check PLC type. If plctype is vaild, set self.commtype. 159 | 160 | Args: 161 | plctype(str): PLC type. "Q", "L", "QnA", "iQ-L", "iQ-R", 162 | 163 | """ 164 | if plctype == "Q": 165 | self.plctype = const.Q_SERIES 166 | elif plctype == "L": 167 | self.plctype = const.L_SERIES 168 | elif plctype == "QnA": 169 | self.plctype = const.QnA_SERIES 170 | elif plctype == "iQ-L": 171 | self.plctype = const.iQL_SERIES 172 | elif plctype == "iQ-R": 173 | self.plctype = const.iQR_SERIES 174 | else: 175 | raise PLCTypeError() 176 | 177 | def _set_commtype(self, commtype): 178 | """Check communication type. If commtype is vaild, set self.commtype. 179 | 180 | Args: 181 | commtype(str): communication type. "binary" or "ascii". (Default: "binary") 182 | 183 | """ 184 | if commtype == "binary": 185 | self.commtype = const.COMMTYPE_BINARY 186 | self._wordsize = 2 187 | elif commtype == "ascii": 188 | self.commtype = const.COMMTYPE_ASCII 189 | self._wordsize = 4 190 | else: 191 | raise CommTypeError() 192 | 193 | def _get_answerdata_index(self): 194 | """Get answer data index from return data byte. 195 | """ 196 | if self.commtype == const.COMMTYPE_BINARY: 197 | return 11 198 | else: 199 | return 22 200 | 201 | def _get_answerstatus_index(self): 202 | """Get command status index from return data byte. 203 | """ 204 | if self.commtype == const.COMMTYPE_BINARY: 205 | return 9 206 | else: 207 | return 18 208 | 209 | def setaccessopt(self, commtype=None, network=None, 210 | pc=None, dest_moduleio=None, 211 | dest_modulesta=None, timer_sec=None): 212 | """Set mc protocol access option. 213 | 214 | Args: 215 | commtype(str): communication type. "binary" or "ascii". (Default: "binary") 216 | network(int): network No. of an access target. (0<= network <= 255) 217 | pc(int): network module station No. of an access target. (0<= pc <= 255) 218 | dest_moduleio(int): When accessing a multidrop connection station via network, 219 | specify the start input/output number of a multidrop connection source module. 220 | the CPU module of the multiple CPU system and redundant system. 221 | dest_modulesta(int): accessing a multidrop connection station via network, 222 | specify the station No. of aaccess target module 223 | timer_sec(int): Time out to return Timeout Error from PLC. 224 | MC protocol time is per 250msec, but for ease, setaccessopt requires per sec. 225 | Socket time out is set timer_sec + 1 sec. 226 | 227 | """ 228 | if commtype: 229 | self._set_commtype(commtype) 230 | if network: 231 | try: 232 | network.to_bytes(1, "little") 233 | self.network = network 234 | except: 235 | raise ValueError("network must be 0 <= network <= 255") 236 | if pc: 237 | try: 238 | pc.to_bytes(1, "little") 239 | self.pc = pc 240 | except: 241 | raise ValueError("pc must be 0 <= pc <= 255") 242 | if dest_moduleio: 243 | try: 244 | dest_moduleio.to_bytes(2, "little") 245 | self.dest_moduleio = dest_moduleio 246 | except: 247 | raise ValueError("dest_moduleio must be 0 <= dest_moduleio <= 65535") 248 | if dest_modulesta: 249 | try: 250 | dest_modulesta.to_bytes(1, "little") 251 | self.dest_modulesta = dest_modulesta 252 | except: 253 | raise ValueError("dest_modulesta must be 0 <= dest_modulesta <= 255") 254 | if timer_sec: 255 | try: 256 | timer_250msec = 4 * timer_sec 257 | timer_250msec.to_bytes(2, "little") 258 | self.timer = timer_250msec 259 | self.soc_timeout = timer_sec + 1 260 | if self._is_connected: 261 | self._sock.settimeout(self.soc_timeout) 262 | except: 263 | raise ValueError("timer_sec must be 0 <= timer_sec <= 16383, / sec") 264 | return None 265 | 266 | def _make_senddata(self, requestdata): 267 | """Makes send mc protorocl data. 268 | 269 | Args: 270 | requestdata(bytes): mc protocol request data. 271 | data must be converted according to self.commtype 272 | 273 | Returns: 274 | mc_data(bytes): send mc protorocl data 275 | 276 | """ 277 | mc_data = bytes() 278 | # subheader is big endian 279 | if self.commtype == const.COMMTYPE_BINARY: 280 | mc_data += self.subheader.to_bytes(2, "big") 281 | else: 282 | mc_data += format(self.subheader, "x").ljust(4, "0").upper().encode() 283 | mc_data += self._encode_value(self.network, "byte") 284 | mc_data += self._encode_value(self.pc, "byte") 285 | mc_data += self._encode_value(self.dest_moduleio, "short") 286 | mc_data += self._encode_value(self.dest_modulesta, "byte") 287 | #add self.timer size 288 | mc_data += self._encode_value(self._wordsize + len(requestdata), "short") 289 | mc_data += self._encode_value(self.timer, "short") 290 | mc_data += requestdata 291 | return mc_data 292 | 293 | def _make_commanddata(self, command, subcommand): 294 | """make mc protocol command and subcommand data 295 | 296 | Args: 297 | command(int): command code 298 | subcommand(int): subcommand code 299 | 300 | Returns: 301 | command_data(bytes):command data 302 | 303 | """ 304 | command_data = bytes() 305 | command_data += self._encode_value(command, "short") 306 | command_data += self._encode_value(subcommand, "short") 307 | return command_data 308 | 309 | def _make_devicedata(self, device): 310 | """make mc protocol device data. (device code and device number) 311 | 312 | Args: 313 | device(str): device. (ex: "D1000", "Y1") 314 | 315 | Returns: 316 | device_data(bytes): device data 317 | 318 | """ 319 | 320 | device_data = bytes() 321 | 322 | devicetype = re.search(r"\D+", device) 323 | if devicetype is None: 324 | raise ValueError("Invalid device ") 325 | else: 326 | devicetype = devicetype.group(0) 327 | 328 | if self.commtype == const.COMMTYPE_BINARY: 329 | devicecode, devicebase = const.DeviceConstants.get_binary_devicecode(self.plctype, devicetype) 330 | devicenum = int(get_device_number(device), devicebase) 331 | if self.plctype is const.iQR_SERIES: 332 | device_data += devicenum.to_bytes(4, "little") 333 | device_data += devicecode.to_bytes(2, "little") 334 | else: 335 | device_data += devicenum.to_bytes(3, "little") 336 | device_data += devicecode.to_bytes(1, "little") 337 | else: 338 | devicecode, devicebase = const.DeviceConstants.get_ascii_devicecode(self.plctype, devicetype) 339 | devicenum = str(int(get_device_number(device), devicebase)) 340 | if self.plctype is const.iQR_SERIES: 341 | device_data += devicecode.encode() 342 | device_data += devicenum.rjust(8, "0").upper().encode() 343 | else: 344 | device_data += devicecode.encode() 345 | device_data += devicenum.rjust(6, "0").upper().encode() 346 | return device_data 347 | 348 | def _encode_value(self, value, mode="short", isSigned=False): 349 | """encode mc protocol value data to byte. 350 | 351 | Args: 352 | value(int): readsize, write value, and so on. 353 | mode(str): value type. 354 | isSigned(bool): convert as sigend value 355 | 356 | Returns: 357 | value_byte(bytes): value data 358 | 359 | """ 360 | try: 361 | if self.commtype == const.COMMTYPE_BINARY: 362 | if mode == "byte": 363 | value_byte = value.to_bytes(1, "little", signed=isSigned) 364 | elif mode == "short": 365 | value_byte = value.to_bytes(2, "little", signed=isSigned) 366 | elif mode == "long": 367 | value_byte = value.to_bytes(4, "little", signed=isSigned) 368 | else: 369 | raise ValueError("Please input value type") 370 | else: 371 | #check value range by to_bytes 372 | #convert to unsigned value 373 | if mode == "byte": 374 | value.to_bytes(1, "little", signed=isSigned) 375 | value = value & 0xff 376 | value_byte = format(value, "x").rjust(2, "0").upper().encode() 377 | elif mode == "short": 378 | value.to_bytes(2, "little", signed=isSigned) 379 | value = value & 0xffff 380 | value_byte = format(value, "x").rjust(4, "0").upper().encode() 381 | elif mode == "long": 382 | value.to_bytes(4, "little", signed=isSigned) 383 | value = value & 0xffffffff 384 | value_byte = format(value, "x").rjust(8, "0").upper().encode() 385 | else: 386 | raise ValueError("Please input value type") 387 | except: 388 | raise ValueError("Exceeeded Device value range") 389 | return value_byte 390 | 391 | def _decode_value(self, byte, mode="short", isSigned=False): 392 | """decode byte to value 393 | 394 | Args: 395 | byte(bytes): readsize, write value, and so on. 396 | mode(str): value type. 397 | isSigned(bool): convert as sigend value 398 | 399 | Returns: 400 | value_data(int): value data 401 | 402 | """ 403 | try: 404 | if self.commtype == const.COMMTYPE_BINARY: 405 | value =int.from_bytes(byte, "little", signed = isSigned) 406 | else: 407 | value = int(byte.decode(), 16) 408 | if isSigned: 409 | value = twos_comp(value, mode) 410 | except: 411 | raise ValueError("Could not decode byte to value") 412 | return value 413 | 414 | def _check_cmdanswer(self, recv_data): 415 | """check command answer. If answer status is not 0, raise error according to answer 416 | 417 | """ 418 | answerstatus_index = self._get_answerstatus_index() 419 | answerstatus = self._decode_value(recv_data[answerstatus_index:answerstatus_index+self._wordsize], "short") 420 | mcprotocolerror.check_mcprotocol_error(answerstatus) 421 | return None 422 | 423 | def batchread_wordunits(self, headdevice, readsize): 424 | """batch read in word units. 425 | 426 | Args: 427 | headdevice(str): Read head device. (ex: "D1000") 428 | readsize(int): Number of read device points 429 | 430 | Returns: 431 | wordunits_values(list[int]): word units value list 432 | 433 | """ 434 | command = 0x0401 435 | if self.plctype == const.iQR_SERIES: 436 | subcommand = 0x0002 437 | else: 438 | subcommand = 0x0000 439 | 440 | request_data = bytes() 441 | request_data += self._make_commanddata(command, subcommand) 442 | request_data += self._make_devicedata(headdevice) 443 | request_data += self._encode_value(readsize) 444 | send_data = self._make_senddata(request_data) 445 | 446 | #send mc data 447 | self._send(send_data) 448 | #reciev mc data 449 | recv_data = self._recv() 450 | self._check_cmdanswer(recv_data) 451 | 452 | word_values = [] 453 | data_index = self._get_answerdata_index() 454 | for _ in range(readsize): 455 | wordvalue = self._decode_value(recv_data[data_index:data_index+self._wordsize], mode="short", isSigned=True) 456 | word_values.append(wordvalue) 457 | data_index += self._wordsize 458 | return word_values 459 | 460 | def batchread_bitunits(self, headdevice, readsize): 461 | """batch read in bit units. 462 | 463 | Args: 464 | headdevice(str): Read head device. (ex: "X1") 465 | size(int): Number of read device points 466 | 467 | Returns: 468 | bitunits_values(list[int]): bit units value(0 or 1) list 469 | 470 | """ 471 | command = 0x0401 472 | if self.plctype == const.iQR_SERIES: 473 | subcommand = 0x0003 474 | else: 475 | subcommand = 0x0001 476 | 477 | request_data = bytes() 478 | request_data += self._make_commanddata(command, subcommand) 479 | request_data += self._make_devicedata(headdevice) 480 | request_data += self._encode_value(readsize) 481 | send_data = self._make_senddata(request_data) 482 | 483 | #send mc data 484 | self._send(send_data) 485 | #reciev mc data 486 | recv_data = self._recv() 487 | self._check_cmdanswer(recv_data) 488 | 489 | bit_values = [] 490 | if self.commtype == const.COMMTYPE_BINARY: 491 | for i in range(readsize): 492 | data_index = i//2 + self._get_answerdata_index() 493 | value = int.from_bytes(recv_data[data_index:data_index+1], "little") 494 | #if i//2==0, bit value is 4th bit 495 | if(i%2==0): 496 | bitvalue = 1 if value & (1<<4) else 0 497 | else: 498 | bitvalue = 1 if value & (1<<0) else 0 499 | bit_values.append(bitvalue) 500 | else: 501 | data_index = self._get_answerdata_index() 502 | byte_range = 1 503 | for i in range(readsize): 504 | bitvalue = int(recv_data[data_index:data_index+byte_range].decode()) 505 | bit_values.append(bitvalue) 506 | data_index += byte_range 507 | return bit_values 508 | 509 | def batchwrite_wordunits(self, headdevice, values): 510 | """batch write in word units. 511 | 512 | Args: 513 | headdevice(str): Write head device. (ex: "D1000") 514 | values(list[int]): Write values. 515 | 516 | """ 517 | write_size = len(values) 518 | 519 | command = 0x1401 520 | if self.plctype == const.iQR_SERIES: 521 | subcommand = 0x0002 522 | else: 523 | subcommand = 0x0000 524 | 525 | request_data = bytes() 526 | request_data += self._make_commanddata(command, subcommand) 527 | request_data += self._make_devicedata(headdevice) 528 | request_data += self._encode_value(write_size) 529 | for value in values: 530 | request_data += self._encode_value(value, isSigned=True) 531 | send_data = self._make_senddata(request_data) 532 | 533 | #send mc data 534 | self._send(send_data) 535 | #reciev mc data 536 | recv_data = self._recv() 537 | self._check_cmdanswer(recv_data) 538 | 539 | return None 540 | 541 | def batchwrite_bitunits(self, headdevice, values): 542 | """batch read in bit units. 543 | 544 | Args: 545 | headdevice(str): Write head device. (ex: "X10") 546 | values(list[int]): Write values. each value must be 0 or 1. 0 is OFF, 1 is ON. 547 | 548 | """ 549 | write_size = len(values) 550 | #check values 551 | for value in values: 552 | if not (value == 0 or value == 1): 553 | raise ValueError("Each value must be 0 or 1. 0 is OFF, 1 is ON.") 554 | 555 | command = 0x1401 556 | if self.plctype == const.iQR_SERIES: 557 | subcommand = 0x0003 558 | else: 559 | subcommand = 0x0001 560 | 561 | request_data = bytes() 562 | request_data += self._make_commanddata(command, subcommand) 563 | request_data += self._make_devicedata(headdevice) 564 | request_data += self._encode_value(write_size) 565 | if self.commtype == const.COMMTYPE_BINARY: 566 | #evary value is 0 or 1. 567 | #Even index's value turns on or off 4th bit, odd index's value turns on or off 0th bit. 568 | #First, create send data list. Length must be ceil of len(values). 569 | bit_data = [0 for _ in range((len(values) + 1)//2)] 570 | for index, value in enumerate(values): 571 | #calc which index data should be turns on. 572 | value_index = index//2 573 | #calc which bit should be turns on. 574 | bit_index = 4 if index%2 == 0 else 0 575 | #turns on or off value of 4th or 0th bit, depends on value 576 | bit_value = value << bit_index 577 | #Take or of send data 578 | bit_data[value_index] |= bit_value 579 | request_data += bytes(bit_data) 580 | else: 581 | for value in values: 582 | request_data += str(value).encode() 583 | send_data = self._make_senddata(request_data) 584 | 585 | #send mc data 586 | self._send(send_data) 587 | #reciev mc data 588 | recv_data = self._recv() 589 | self._check_cmdanswer(recv_data) 590 | 591 | return None 592 | 593 | def randomread(self, word_devices, dword_devices): 594 | """read word units and dword units randomly. 595 | Moniter condition does not support. 596 | 597 | Args: 598 | word_devices(list[str]): Read device word units. (ex: ["D1000", "D1010"]) 599 | dword_devices(list[str]): Read device dword units. (ex: ["D1000", "D1012"]) 600 | 601 | Returns: 602 | word_values(list[int]): word units value list 603 | dword_values(list[int]): dword units value list 604 | 605 | """ 606 | command = 0x0403 607 | if self.plctype == const.iQR_SERIES: 608 | subcommand = 0x0002 609 | else: 610 | subcommand = 0x0000 611 | 612 | word_size = len(word_devices) 613 | dword_size = len(dword_devices) 614 | 615 | request_data = bytes() 616 | request_data += self._make_commanddata(command, subcommand) 617 | request_data += self._encode_value(word_size, mode="byte") 618 | request_data += self._encode_value(dword_size, mode="byte") 619 | for word_device in word_devices: 620 | request_data += self._make_devicedata(word_device) 621 | for dword_device in dword_devices: 622 | request_data += self._make_devicedata(dword_device) 623 | send_data = self._make_senddata(request_data) 624 | 625 | #send mc data 626 | self._send(send_data) 627 | #reciev mc data 628 | recv_data = self._recv() 629 | self._check_cmdanswer(recv_data) 630 | data_index = self._get_answerdata_index() 631 | word_values= [] 632 | dword_values= [] 633 | for word_device in word_devices: 634 | wordvalue = self._decode_value(recv_data[data_index:data_index+self._wordsize], mode="short", isSigned=True) 635 | word_values.append(wordvalue) 636 | data_index += self._wordsize 637 | for dword_device in dword_devices: 638 | dwordvalue = self._decode_value(recv_data[data_index:data_index+self._wordsize*2], mode="long", isSigned=True) 639 | dword_values.append(dwordvalue) 640 | data_index += self._wordsize*2 641 | return word_values, dword_values 642 | 643 | def randomwrite(self, word_devices, word_values, 644 | dword_devices, dword_values): 645 | """write word units and dword units randomly. 646 | 647 | Args: 648 | word_devices(list[str]): Write word devices. (ex: ["D1000", "D1020"]) 649 | word_values(list[int]): Values for each word devices. (ex: [100, 200]) 650 | dword_devices(list[str]): Write dword devices. (ex: ["D1000", "D1020"]) 651 | dword_values(list[int]): Values for each dword devices. (ex: [100, 200]) 652 | 653 | """ 654 | if len(word_devices) != len(word_values): 655 | raise ValueError("word_devices and word_values must be same length") 656 | if len(dword_devices) != len(dword_values): 657 | raise ValueError("dword_devices and dword_values must be same length") 658 | 659 | word_size = len(word_devices) 660 | dword_size = len(dword_devices) 661 | 662 | command = 0x1402 663 | if self.plctype == const.iQR_SERIES: 664 | subcommand = 0x0002 665 | else: 666 | subcommand = 0x0000 667 | 668 | request_data = bytes() 669 | request_data += self._make_commanddata(command, subcommand) 670 | request_data += self._encode_value(word_size, mode="byte") 671 | request_data += self._encode_value(dword_size, mode="byte") 672 | for word_device, word_value in zip(word_devices, word_values): 673 | request_data += self._make_devicedata(word_device) 674 | request_data += self._encode_value(word_value, mode="short", isSigned=True) 675 | for dword_device, dword_value in zip(dword_devices, dword_values): 676 | request_data += self._make_devicedata(dword_device) 677 | request_data += self._encode_value(dword_value, mode="long", isSigned=True) 678 | send_data = self._make_senddata(request_data) 679 | 680 | #send mc data 681 | self._send(send_data) 682 | #reciev mc data 683 | recv_data = self._recv() 684 | self._check_cmdanswer(recv_data) 685 | return None 686 | 687 | def randomwrite_bitunits(self, bit_devices, values): 688 | """write bit units randomly. 689 | 690 | Args: 691 | bit_devices(list[str]): Write bit devices. (ex: ["X10", "X20"]) 692 | values(list[int]): Write values. each value must be 0 or 1. 0 is OFF, 1 is ON. 693 | 694 | """ 695 | if len(bit_devices) != len(values): 696 | raise ValueError("bit_devices and values must be same length") 697 | write_size = len(values) 698 | #check values 699 | for value in values: 700 | if not (value == 0 or value == 1): 701 | raise ValueError("Each value must be 0 or 1. 0 is OFF, 1 is ON.") 702 | 703 | command = 0x1402 704 | if self.plctype == const.iQR_SERIES: 705 | subcommand = 0x0003 706 | else: 707 | subcommand = 0x0001 708 | 709 | request_data = bytes() 710 | request_data += self._make_commanddata(command, subcommand) 711 | request_data += self._encode_value(write_size, mode="byte") 712 | for bit_device, value in zip(bit_devices, values): 713 | request_data += self._make_devicedata(bit_device) 714 | #byte value for iQ-R requires 2 byte data 715 | if self.plctype == const.iQR_SERIES: 716 | request_data += self._encode_value(value, mode="short", isSigned=True) 717 | else: 718 | request_data += self._encode_value(value, mode="byte", isSigned=True) 719 | send_data = self._make_senddata(request_data) 720 | 721 | #send mc data 722 | self._send(send_data) 723 | #reciev mc data 724 | recv_data = self._recv() 725 | self._check_cmdanswer(recv_data) 726 | 727 | return None 728 | 729 | def remote_run(self, clear_mode, force_exec=False): 730 | """Run PLC 731 | 732 | Args: 733 | clear_mode(int): Clear mode. 0: does not clear. 1: clear except latch device. 2: clear all. 734 | force_exec(bool): Force to execute if PLC is operated remotely by other device. 735 | 736 | """ 737 | if not (clear_mode == 0 or clear_mode == 1 or clear_mode == 2): 738 | raise ValueError("clear_device must be 0, 1 or 2. 0: does not clear. 1: clear except latch device. 2: clear all.") 739 | if not (force_exec is True or force_exec is False): 740 | raise ValueError("force_exec must be True or False") 741 | 742 | command = 0x1001 743 | subcommand = 0x0000 744 | 745 | if force_exec: 746 | mode = 0x0003 747 | else: 748 | mode = 0x0001 749 | 750 | request_data = bytes() 751 | request_data += self._make_commanddata(command, subcommand) 752 | request_data += self._encode_value(mode, mode="short") 753 | request_data += self._encode_value(clear_mode, mode="byte") 754 | request_data += self._encode_value(0, mode="byte") 755 | send_data = self._make_senddata(request_data) 756 | 757 | #send mc data 758 | self._send(send_data) 759 | 760 | #reciev mc data 761 | recv_data = self._recv() 762 | self._check_cmdanswer(recv_data) 763 | return None 764 | 765 | def remote_stop(self): 766 | """ Stop remotely. 767 | 768 | """ 769 | command = 0x1002 770 | subcommand = 0x0000 771 | 772 | request_data = bytes() 773 | request_data += self._make_commanddata(command, subcommand) 774 | request_data += self._encode_value(0x0001, mode="short") #fixed value 775 | send_data = self._make_senddata(request_data) 776 | 777 | #send mc data 778 | self._send(send_data) 779 | #reciev mc data 780 | recv_data = self._recv() 781 | self._check_cmdanswer(recv_data) 782 | return None 783 | 784 | def remote_pause(self, force_exec=False): 785 | """pause PLC remotely. 786 | 787 | Args: 788 | force_exec(bool): Force to execute if PLC is operated remotely by other device. 789 | 790 | """ 791 | if not (force_exec is True or force_exec is False): 792 | raise ValueError("force_exec must be True or False") 793 | 794 | command = 0x1003 795 | subcommand = 0x0000 796 | 797 | if force_exec: 798 | mode = 0x0003 799 | else: 800 | mode = 0x0001 801 | 802 | request_data = bytes() 803 | request_data += self._make_commanddata(command, subcommand) 804 | request_data += self._encode_value(mode, mode="short") 805 | send_data = self._make_senddata(request_data) 806 | 807 | #send mc data 808 | self._send(send_data) 809 | #reciev mc data 810 | recv_data = self._recv() 811 | self._check_cmdanswer(recv_data) 812 | return None 813 | 814 | def remote_latchclear(self): 815 | """Clear latch remotely. 816 | PLC must be stop when use this command. 817 | """ 818 | 819 | command = 0x1005 820 | subcommand = 0x0000 821 | 822 | request_data = bytes() 823 | request_data += self._make_commanddata(command, subcommand) 824 | request_data += self._encode_value(0x0001, mode="short") #fixed value 825 | send_data = self._make_senddata(request_data) 826 | 827 | #send mc data 828 | self._send(send_data) 829 | #reciev mc data 830 | recv_data = self._recv() 831 | self._check_cmdanswer(recv_data) 832 | 833 | return None 834 | 835 | def remote_reset(self): 836 | """Reset remotely. 837 | PLC must be stop when use this command. 838 | 839 | """ 840 | 841 | command = 0x1006 842 | subcommand = 0x0000 843 | 844 | request_data = bytes() 845 | request_data += self._make_commanddata(command, subcommand) 846 | request_data += self._encode_value(0x0001, mode="short") #fixed value 847 | send_data = self._make_senddata(request_data) 848 | 849 | #send mc data 850 | self._send(send_data) 851 | #reciev mc data 852 | #set time out 1 seconds. Because remote reset may not return data since clone socket 853 | try: 854 | self._sock.settimeout(1) 855 | recv_data = self._recv() 856 | self._check_cmdanswer(recv_data) 857 | except: 858 | self._is_connected = False 859 | # after wait 1 sec 860 | # try reconnect 861 | time.sleep(1) 862 | self.connect(self._ip, self._port) 863 | return None 864 | 865 | def read_cputype(self): 866 | """Read CPU type 867 | 868 | Returns: 869 | CPU type(str): CPU type 870 | CPU code(str): CPU code (4 length number) 871 | 872 | """ 873 | 874 | command = 0x0101 875 | subcommand = 0x0000 876 | 877 | request_data = bytes() 878 | request_data += self._make_commanddata(command, subcommand) 879 | send_data = self._make_senddata(request_data) 880 | 881 | #send mc data 882 | self._send(send_data) 883 | #reciev mc data 884 | recv_data = self._recv() 885 | self._check_cmdanswer(recv_data) 886 | data_index = self._get_answerdata_index() 887 | cpu_name_length = 16 888 | if self.commtype == const.COMMTYPE_BINARY: 889 | cpu_type = recv_data[data_index:data_index+cpu_name_length].decode() 890 | cpu_type = cpu_type.replace("\x20", "") 891 | cpu_code = int.from_bytes(recv_data[data_index+cpu_name_length:], "little") 892 | cpu_code = format(cpu_code, "x").rjust(4, "0") 893 | else: 894 | cpu_type = recv_data[data_index:data_index+cpu_name_length].decode() 895 | cpu_type = cpu_type.replace("\x20", "") 896 | cpu_code = recv_data[data_index+cpu_name_length:].decode() 897 | return cpu_type, cpu_code 898 | 899 | def remote_unlock(self, password="", request_input=False): 900 | """Unlock PLC by inputting password. 901 | 902 | Args: 903 | password(str): Remote password 904 | request_input(bool): If true, require inputting password. 905 | If false, use password. 906 | """ 907 | if request_input: 908 | password = input("Please enter password\n") 909 | if isascii(password) is False: 910 | raise ValueError("password must be only ascii code") 911 | if self.plctype is const.iQR_SERIES: 912 | if not (6 <= len(password) <= 32): 913 | raise ValueError("password length must be from 6 to 32") 914 | else: 915 | if not (4 == len(password)): 916 | raise ValueError("password length must be 4") 917 | 918 | 919 | command = 0x1630 920 | subcommand = 0x0000 921 | request_data = bytes() 922 | request_data += self._make_commanddata(command, subcommand) 923 | request_data += self._encode_value(len(password), mode="short") 924 | request_data += password.encode() 925 | 926 | send_data = self._make_senddata(request_data) 927 | 928 | self._send(send_data) 929 | #reciev mc data 930 | recv_data = self._recv() 931 | self._check_cmdanswer(recv_data) 932 | return None 933 | 934 | def remote_lock(self, password="", request_input=False): 935 | """Lock PLC by inputting password. 936 | 937 | Args: 938 | password(str): Remote password 939 | request_input(bool): If true, require inputting password. 940 | If false, use password. 941 | """ 942 | if request_input: 943 | password = input("Please enter password\n") 944 | if isascii(password) is False: 945 | raise ValueError("password must be only ascii code") 946 | if self.plctype is const.iQR_SERIES: 947 | if not (6 <= len(password) <= 32): 948 | raise ValueError("password length must be from 6 to 32") 949 | else: 950 | if not (4 == len(password)): 951 | raise ValueError("password length must be 4") 952 | 953 | command = 0x1631 954 | subcommand = 0x0000 955 | 956 | request_data = bytes() 957 | request_data += self._make_commanddata(command, subcommand) 958 | request_data += self._encode_value(len(password), mode="short") 959 | request_data += password.encode() 960 | 961 | send_data = self._make_senddata(request_data) 962 | 963 | #send mc data 964 | self._send(send_data) 965 | #reciev mc data 966 | recv_data = self._recv() 967 | self._check_cmdanswer(recv_data) 968 | return None 969 | 970 | def echo_test(self, echo_data): 971 | """Do echo test. 972 | Send data and answer data should be same. 973 | 974 | Args: 975 | echo_data(str): send data to PLC 976 | 977 | Returns: 978 | answer_len(int): answer data length from PLC 979 | answer_data(str): answer data from PLC 980 | 981 | """ 982 | if echo_data.isalnum() is False: 983 | raise ValueError("echo_data must be only alphabet or digit code") 984 | if not ( 1 <= len(echo_data) <= 960): 985 | raise ValueError("echo_data length must be from 1 to 960") 986 | 987 | command = 0x0619 988 | subcommand = 0x0000 989 | 990 | request_data = bytes() 991 | request_data += self._make_commanddata(command, subcommand) 992 | request_data += self._encode_value(len(echo_data), mode="short") 993 | request_data += echo_data.encode() 994 | 995 | send_data = self._make_senddata(request_data) 996 | 997 | #send mc data 998 | self._send(send_data) 999 | #reciev mc data 1000 | recv_data = self._recv() 1001 | self._check_cmdanswer(recv_data) 1002 | 1003 | data_index = self._get_answerdata_index() 1004 | 1005 | answer_len = self._decode_value(recv_data[data_index:data_index+self._wordsize], mode="short") 1006 | answer = recv_data[data_index+self._wordsize:].decode() 1007 | return answer_len, answer -------------------------------------------------------------------------------- /src/pymcprotocol/type4e.py: -------------------------------------------------------------------------------- 1 | """This file implements mcprotocol 4E type communication. 2 | """ 3 | from . import mcprotocolconst as const 4 | from .type3e import Type3E 5 | 6 | class Type4E(Type3E): 7 | """mcprotocol 4E communication class. 8 | Type 4e is almost same to Type 3E. Difference is only subheader. 9 | So, Changed self.subhear and self._make_senddata() 10 | 11 | Arributes: 12 | subheader(int): Subheader for mc protocol 13 | subheaderserial(int): Subheader serial for mc protocol to identify client 14 | """ 15 | subheader = 0x5400 16 | subheaderserial = 0X0000 17 | 18 | def set_subheaderserial(self, subheaderserial): 19 | """Change subheader serial 20 | 21 | Args: 22 | subheaderserial(int): Subheader serial to change 23 | 24 | """ 25 | if(0 <= subheaderserial <= 65535): 26 | self.subheaderserial = subheaderserial 27 | else: 28 | raise ValueError("subheaderserial must be 0 <= subheaderserial <= 65535") 29 | return None 30 | 31 | def _get_answerdata_index(self): 32 | """Get answer data index from return data byte. 33 | 4e type's data index is defferent from 3e type's. 34 | """ 35 | if self.commtype == const.COMMTYPE_BINARY: 36 | return 15 37 | else: 38 | return 30 39 | 40 | def _get_answerstatus_index(self): 41 | """Get command status index from return data byte. 42 | """ 43 | if self.commtype == const.COMMTYPE_BINARY: 44 | return 13 45 | else: 46 | return 26 47 | 48 | def _make_senddata(self, requestdata): 49 | """Makes send mc protorocl data. 50 | 51 | Args: 52 | requestdata(bytes): mc protocol request data. 53 | data must be converted according to self.commtype 54 | 55 | Returns: 56 | mc_data(bytes): send mc protorocl data 57 | 58 | """ 59 | mc_data = bytes() 60 | # subheader is big endian 61 | if self.commtype == const.COMMTYPE_BINARY: 62 | mc_data += self.subheader.to_bytes(2, "big") 63 | else: 64 | mc_data += format(self.subheader, "x").ljust(4, "0").upper().encode() 65 | mc_data += self._encode_value(self.subheaderserial, "short") 66 | mc_data += self._encode_value(0, "short") 67 | mc_data += self._encode_value(self.network, "byte") 68 | mc_data += self._encode_value(self.pc, "byte") 69 | mc_data += self._encode_value(self.dest_moduleio, "short") 70 | mc_data += self._encode_value(self.dest_modulesta, "byte") 71 | #add self.timer size 72 | mc_data += self._encode_value(self._wordsize + len(requestdata), "short") 73 | mc_data += self._encode_value(self.timer, "short") 74 | mc_data += requestdata 75 | return mc_data 76 | 77 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senrust/pymcprotocol/aa06b03c0fdc616c3f1916092e8ecc962a3320b4/tests/__init__.py -------------------------------------------------------------------------------- /tests/config.ini: -------------------------------------------------------------------------------- 1 | [settings] 2 | plc = Q 3 | ip = 192.168.1.200 4 | port = 1025 -------------------------------------------------------------------------------- /tests/test_pymcprotocol.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | try: 3 | # pytest import 4 | from src.pymcprotocol import Type3E 5 | istestsdir = False 6 | except: 7 | # relative import from parent directory 8 | import sys 9 | import os 10 | try: 11 | sys.path.append(os.path.abspath("..")) 12 | from src.pymcprotocol import Type3E 13 | istestsdir = True 14 | except: 15 | sys.path.append(os.path.abspath(".")) 16 | from src.pymcprotocol import Type3E 17 | istestsdir = False 18 | 19 | def get_config(istestsdir=False): 20 | #pytest execute this file in parent directory 21 | ini = configparser.ConfigParser() 22 | if istestsdir: 23 | ini.read('./config.ini') 24 | else: 25 | ini.read('./tests/config.ini') 26 | plctype = ini['settings']["PLC"] 27 | ip = ini['settings']["ip"] 28 | port = ini['settings'].getint("port") 29 | return plctype, ip, port 30 | 31 | def type3e_test(plctype, ip, port): 32 | pyplc = Type3E(plctype) 33 | pyplc.connect(ip, port) 34 | # check batch access to word units 35 | pyplc.batchwrite_wordunits("D1000", [0, 1000, -1000]) 36 | value = pyplc.batchread_wordunits("D1000", 3) 37 | assert [0, 1000, -1000] == value 38 | 39 | # check batch access to bit units 40 | # odd size test 41 | pyplc.batchwrite_bitunits("M10", [0, 1, 1]) 42 | value = pyplc.batchread_bitunits("M10", 3) 43 | assert [0, 1, 1] == value 44 | #even size test 45 | pyplc.batchwrite_bitunits("M20", [1, 0, 1, 0]) 46 | value = pyplc.batchread_bitunits("M20", 4) 47 | assert [1, 0, 1, 0] == value 48 | #test word access 49 | pyplc.batchwrite_bitunits("M30", [1, 1, 1, 1]) 50 | value = pyplc.batchread_wordunits("M30", 1) 51 | assert [15] == value 52 | 53 | # test random access 54 | pyplc.randomwrite(["D2000", "D2010", "D2020"], [-10, 0, 10], ["D2040", "D2050", "D2060", "D2070", "D2080"], [-10000000, -1, 0, 1, 10000000]) 55 | word_values, dword_values = pyplc.randomread(["D2000", "D2010", "D2020"], ["D2040", "D2050", "D2060", "D2070", "D2080"]) 56 | assert word_values == [-10, 0, 10] 57 | assert dword_values == [-10000000, -1, 0, 1, 10000000] 58 | 59 | #test random bit access 60 | pyplc.randomwrite_bitunits(["M40", "M45", "M50", "M60"], [1, 1, 1, 1]) 61 | word_values, dword_values = pyplc.randomread(["M40"], ["M40"]) 62 | assert word_values == [1057] 63 | assert dword_values == [1049633] 64 | 65 | def test_pymcprotocol(): 66 | """test function for pytest 67 | """ 68 | plctype, ip, port = get_config(istestsdir) 69 | type3e_test(plctype, ip, port) 70 | 71 | if __name__ == "__main__": 72 | plctype, ip, port = get_config(istestsdir) 73 | type3e_test(plctype, ip, port) --------------------------------------------------------------------------------