├── .gitignore ├── README.md ├── build-epydoc.cfg ├── build.xml ├── copying.txt ├── examples ├── modbus_system_monitor.py ├── mysimu.py ├── rtumaster_example.py ├── rtuslave_example.py ├── tcpmaster_example.py └── tcpslave_example.py ├── hmi ├── db │ └── master_webhmi.db ├── master_webhmi.py ├── media │ ├── jquery-1.4.2.min.js │ └── style.css └── templates │ ├── master_index.tpl │ ├── master_results.tpl │ ├── master_results_all_hr.tpl │ ├── masters_list.tpl │ └── modbus_error.tpl ├── license.txt ├── modbus_tk ├── __init__.py ├── defines.py ├── hooks.py ├── modbus.py ├── modbus_rtu.py ├── modbus_tcp.py ├── simulator.py ├── simulator_rpc_client.py └── utils.py ├── setup.py ├── tests ├── functest_modbus.py ├── functest_modbus_rtu.py ├── functest_modbus_tcp.py ├── perftest_modbus_tcp.py ├── unittest_modbus.py ├── unittest_modbus_rtu.py └── unittest_modbus_tcp.py └── tools └── zip_module.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Modbus-tk 2 | ========= 3 | 4 | This fork is discontinued in favor of https://github.com/ljean/modbus-tk 5 | -------------------------------------------------------------------------------- /build-epydoc.cfg: -------------------------------------------------------------------------------- 1 | [epydoc] # Epydoc section marker (required by ConfigParser) 2 | 3 | # The list of objects to document. Objects can be named using 4 | # dotted names, module filenames, or package directory names. 5 | # Alases for this option include "objects" and "values". 6 | modules: modbus_tk 7 | 8 | # The type of output that should be generated. Should be one 9 | # of: html, text, latex, dvi, ps, pdf. 10 | output: html 11 | 12 | # The path to the output directory. May be relative or absolute. 13 | target: docs/ 14 | 15 | # An integer indicating how verbose epydoc should be. The default 16 | # value is 0; negative values will supress warnings and errors; 17 | # positive values will give more verbose output. 18 | verbosity: -2 19 | 20 | # A boolean value indicating that Epydoc should show a tracaback 21 | # in case of unexpected error. By default don't show tracebacks 22 | debug: 0 23 | 24 | # If True, don't try to use colors or cursor control when doing 25 | # textual output. The default False assumes a rich text prompt 26 | simple-term: 0 27 | 28 | 29 | ### Generation options 30 | 31 | # The default markup language for docstrings, for modules that do 32 | # not define __docformat__. Defaults to epytext. 33 | docformat: epytext 34 | 35 | # Whether or not parsing should be used to examine objects. 36 | parse: yes 37 | 38 | # Whether or not introspection should be used to examine objects. 39 | introspect: yes 40 | 41 | # Don't examine in any way the modules whose dotted name match this 42 | # regular expression pattern. 43 | #exclude 44 | 45 | # Don't perform introspection on the modules whose dotted name match this 46 | # regular expression pattern. 47 | #exclude-introspect 48 | 49 | # Don't perform parsing on the modules whose dotted name match this 50 | # regular expression pattern. 51 | #exclude-parse 52 | 53 | # The format for showing inheritance objects. 54 | # It should be one of: 'grouped', 'listed', 'included'. 55 | inheritance: listed 56 | 57 | # Whether or not to inclue private variables. (Even if included, 58 | # private variables will be hidden by default.) 59 | private: yes 60 | 61 | # Whether or not to list each module's imports. 62 | imports: no 63 | 64 | # Whether or not to include syntax highlighted source code in 65 | # the output (HTML only). 66 | sourcecode: yes 67 | 68 | # Whether or not to includea a page with Epydoc log, containing 69 | # effective option at the time of generation and the reported logs. 70 | include-log: no 71 | 72 | 73 | ### Output options 74 | 75 | # The documented project's name. 76 | name: Modbus Test Kit 77 | 78 | # The CSS stylesheet for HTML output. Can be the name of a builtin 79 | # stylesheet, or the name of a file. 80 | css: white 81 | 82 | # The documented project's URL. 83 | url: http://code.google.com/p/modbus-tk/ 84 | 85 | # HTML code for the project link in the navigation bar. If left 86 | # unspecified, the project link will be generated based on the 87 | # project's name and URL. 88 | #link: My Cool Project 89 | 90 | # The "top" page for the documentation. Can be a URL, the name 91 | # of a module or class, or one of the special names "trees.html", 92 | # "indices.html", or "help.html" 93 | top: modbus-tk.modbus 94 | 95 | # An alternative help file. The named file should contain the 96 | # body of an HTML file; navigation bars will be added to it. 97 | #help: my_helpfile.html 98 | 99 | # Whether or not to include a frames-based table of contents. 100 | frames: yes 101 | 102 | # Whether each class should be listed in its own section when 103 | # generating LaTeX or PDF output. 104 | separate-classes: no 105 | 106 | 107 | ### API linking options 108 | 109 | # Define a new API document. A new interpreted text role 110 | # will be created 111 | #external-api: epydoc 112 | 113 | # Use the records in this file to resolve objects in the API named NAME. 114 | #external-api-file: epydoc:api-objects.txt 115 | 116 | # Use this URL prefix to configure the string returned for external API. 117 | #external-api-root: epydoc:http://epydoc.sourceforge.net/api 118 | 119 | 120 | ### Graph options 121 | 122 | # The list of graph types that should be automatically included 123 | # in the output. Graphs are generated using the Graphviz "dot" 124 | # executable. Graph types include: "classtree", "callgraph", 125 | # "umlclass". Use "all" to include all graph types 126 | graph: all 127 | 128 | # The path to the Graphviz "dot" executable, used to generate 129 | # graphs. 130 | dotpath: C:\Program Files\Graphviz2.24\bin\dot.exe 131 | 132 | # The name of one or more pstat files (generated by the profile 133 | # or hotshot module). These are used to generate call graphs. 134 | #pstat: profile.out 135 | 136 | # Specify the font used to generate Graphviz graphs. 137 | # (e.g., helvetica or times). 138 | graph-font: Times 139 | 140 | # Specify the font size used to generate Graphviz graphs. 141 | graph-font-size: 10 142 | 143 | 144 | ### Return value options 145 | 146 | # The condition upon which Epydoc should exit with a non-zero 147 | # exit status. Possible values are error, warning, docstring_warning 148 | #fail-on: error 149 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /copying.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | 461 | -------------------------------------------------------------------------------- /examples/modbus_system_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | 11 | This example shows how to create a modbus server in charge of monitoring 12 | cpu consumption the machine 13 | 14 | """ 15 | 16 | from modbus_tk.simulator import * 17 | from modbus_tk.simulator_rpc_client import SimulatorRpcClient 18 | from modbus_tk.utils import WorkerThread 19 | from modbus_tk.defines import * 20 | from modbus_tk.modbus_tcp import TcpServer 21 | import time 22 | 23 | 24 | class SystemDataCollector: 25 | """The class in charge of getting the CPU load""" 26 | def __init__(self, refresh_rate_in_sec): 27 | """Constructor""" 28 | self._simu = SimulatorRpcClient() 29 | self._max_count = refresh_rate_in_sec * 10 30 | self._count = self._max_count-1 31 | 32 | def collect(self): 33 | """get the CPU load thanks to WMI""" 34 | try: 35 | self._count += 1 36 | if self._count >= self._max_count: 37 | self._count = 0 38 | #WMI get the load percentage of the machine 39 | from win32com.client import GetObject 40 | wmi = GetObject('winmgmts:') 41 | cpu = wmi.InstancesOf('Win32_Processor') 42 | for (_cpu, i) in zip(cpu, xrange(10)): 43 | value = _cpu.Properties_('LoadPercentage').Value 44 | cpu_usage = int(str(value)) if value else 0 45 | 46 | #execute a RPC command for changing the value 47 | self._simu.set_values(1, "Cpu", i, (cpu_usage, )) 48 | except Exception, excpt: 49 | LOGGER.debug("SystemDataCollector error: %s", str(excpt)) 50 | time.sleep(0.1) 51 | 52 | if __name__ == "__main__": 53 | #create the object for getting CPU data 54 | data_collector = SystemDataCollector(5) 55 | #create the thread in charge of calling the data collector 56 | system_monitor = WorkerThread(data_collector.collect) 57 | 58 | #create the modbus TCP simulator and one slave 59 | #and one block of analog inputs 60 | simu = Simulator(TcpServer()) 61 | slave = simu.server.add_slave(1) 62 | slave.add_block("Cpu", ANALOG_INPUTS, 0, 10) 63 | 64 | try: 65 | LOGGER.info("'quit' for closing the server") 66 | 67 | #start the data collect 68 | system_monitor.start() 69 | 70 | #start the simulator! will block until quit command is received 71 | simu.start() 72 | 73 | except Exception, excpt: 74 | print excpt 75 | 76 | finally: 77 | #close the simulator 78 | simu.close() 79 | #stop the data collect 80 | system_monitor.stop() 81 | 82 | -------------------------------------------------------------------------------- /examples/mysimu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: example of a custom simulator 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | 11 | """ 12 | from modbus_tk.utils import create_logger 13 | from modbus_tk.simulator import Simulator, LOGGER 14 | from modbus_tk.defines import * 15 | 16 | class MySimulator(Simulator): 17 | """A custom simulator""" 18 | def __init__(self): 19 | """Constructor""" 20 | Simulator.__init__(self) 21 | # add a new command: cv will make possible to change a value 22 | self.add_command("cv", self.change_value) 23 | 24 | # create a slave and block 25 | slave = self.server.add_slave(1) 26 | slave.add_block("foo", HOLDING_REGISTERS, 0, 100) 27 | 28 | def change_value(self, args): 29 | """change the value of some registers""" 30 | address = int(args[1]) 31 | 32 | #get the list of values and cast it to integers 33 | values = [] 34 | for val in args[2:]: 35 | values.append(int(val)) 36 | 37 | #custom rules: if the value of reg0 is greater than 30 then reg1 is set to 1 38 | if address==0 and values[0]>30: 39 | try: 40 | values[1] = 1 41 | except: 42 | values.append(1) 43 | 44 | #operates on slave 1 and block foo 45 | slave = self.server.get_slave(1) 46 | slave.set_values("foo", address, values) 47 | 48 | #get the register values for info 49 | values = slave.get_values("foo", address, len(values)) 50 | return self._tuple_to_str(values) 51 | 52 | if __name__ == "__main__": 53 | simu = MySimulator() 54 | 55 | try: 56 | LOGGER.info("'quit' for closing the server") 57 | simu.start() 58 | 59 | except Exception, excpt: 60 | print excpt 61 | 62 | finally: 63 | simu.close() 64 | 65 | -------------------------------------------------------------------------------- /examples/rtumaster_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import sys 13 | import serial 14 | 15 | #add logging capability 16 | import logging 17 | 18 | import modbus_tk 19 | import modbus_tk.defines as cst 20 | import modbus_tk.modbus_rtu as modbus_rtu 21 | 22 | logger = modbus_tk.utils.create_logger("console") 23 | 24 | if __name__ == "__main__": 25 | try: 26 | #Connect to the slave 27 | master = modbus_rtu.RtuMaster(serial.Serial(port=1, baudrate=9600, bytesize=8, parity='N', stopbits=1, xonxoff=0)) 28 | master.set_timeout(5.0) 29 | master.set_verbose(True) 30 | logger.info("connected") 31 | 32 | logger.info(master.execute(1, cst.READ_HOLDING_REGISTERS, 100, 3)) 33 | 34 | #send some queries 35 | #logger.info(master.execute(1, cst.READ_COILS, 0, 10)) 36 | #logger.info(master.execute(1, cst.READ_DISCRETE_INPUTS, 0, 8)) 37 | #logger.info(master.execute(1, cst.READ_INPUT_REGISTERS, 100, 3)) 38 | #logger.info(master.execute(1, cst.READ_HOLDING_REGISTERS, 100, 12)) 39 | #logger.info(master.execute(1, cst.WRITE_SINGLE_COIL, 7, output_value=1)) 40 | #logger.info(master.execute(1, cst.WRITE_SINGLE_REGISTER, 100, output_value=54)) 41 | #logger.info(master.execute(1, cst.WRITE_MULTIPLE_COILS, 0, output_value=[1, 1, 0, 1, 1, 0, 1, 1])) 42 | #logger.info(master.execute(1, cst.WRITE_MULTIPLE_REGISTERS, 100, output_value=xrange(12))) 43 | 44 | except modbus_tk.modbus.ModbusError, e: 45 | logger.error("%s- Code=%d" % (e, e.get_exception_code())) 46 | -------------------------------------------------------------------------------- /examples/rtuslave_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import sys 13 | 14 | #add logging capability 15 | import logging 16 | import threading 17 | 18 | import modbus_tk 19 | import modbus_tk.defines as cst 20 | import modbus_tk.modbus as modbus 21 | import modbus_tk.modbus_rtu as modbus_rtu 22 | import serial 23 | 24 | logger = modbus_tk.utils.create_logger(name="console", record_format="%(message)s") 25 | 26 | if __name__ == "__main__": 27 | #Create the server 28 | server = modbus_rtu.RtuServer(serial.Serial(0)) 29 | 30 | try: 31 | logger.info("running...") 32 | logger.info("enter 'quit' for closing the server") 33 | 34 | server.start() 35 | 36 | slave_1 = server.add_slave(1) 37 | slave_1.add_block('0', cst.HOLDING_REGISTERS, 100, 100) 38 | while True: 39 | cmd = sys.stdin.readline() 40 | args = cmd.split(' ') 41 | if cmd.find('quit')==0: 42 | sys.stdout.write('bye-bye\r\n') 43 | break 44 | elif args[0]=='add_slave': 45 | slave_id = int(args[1]) 46 | server.add_slave(slave_id) 47 | sys.stdout.write('done: slave %d added\r\n' % (slave_id)) 48 | elif args[0]=='add_block': 49 | slave_id = int(args[1]) 50 | name = args[2] 51 | block_type = int(args[3]) 52 | starting_address = int(args[4]) 53 | length = int(args[5]) 54 | slave = server.get_slave(slave_id) 55 | slave.add_block(name, block_type, starting_address, length) 56 | sys.stdout.write('done: block %s added\r\n' % (name)) 57 | elif args[0]=='set_values': 58 | slave_id = int(args[1]) 59 | name = args[2] 60 | address = int(args[3]) 61 | values = [] 62 | for v in args[4:]: 63 | values.append(int(v)) 64 | slave = server.get_slave(slave_id) 65 | slave.set_values(name, address, values) 66 | values = slave.get_values(name, address, len(values)) 67 | sys.stdout.write('done: values written: %s\r\n' % (str(values))) 68 | elif args[0]=='get_values': 69 | slave_id = int(args[1]) 70 | name = args[2] 71 | address = int(args[3]) 72 | length = int(args[4]) 73 | slave = server.get_slave(slave_id) 74 | values = slave.get_values(name, address, length) 75 | sys.stdout.write('done: values read: %s\r\n' % (str(values))) 76 | else: 77 | sys.stdout.write("unknown command %s\r\n" % (args[0])) 78 | finally: 79 | server.stop() -------------------------------------------------------------------------------- /examples/tcpmaster_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import sys 13 | 14 | #add logging capability 15 | import logging 16 | 17 | import modbus_tk 18 | import modbus_tk.defines as cst 19 | import modbus_tk.modbus_tcp as modbus_tcp 20 | 21 | logger = modbus_tk.utils.create_logger("console") 22 | 23 | if __name__ == "__main__": 24 | try: 25 | #Connect to the slave 26 | master = modbus_tcp.TcpMaster() 27 | master.set_timeout(5.0) 28 | logger.info("connected") 29 | 30 | logger.info(master.execute(1, cst.READ_HOLDING_REGISTERS, 100, 3)) 31 | 32 | #send some queries 33 | #logger.info(master.execute(1, cst.READ_COILS, 0, 10)) 34 | #logger.info(master.execute(1, cst.READ_DISCRETE_INPUTS, 0, 8)) 35 | #logger.info(master.execute(1, cst.READ_INPUT_REGISTERS, 100, 3)) 36 | #logger.info(master.execute(1, cst.READ_HOLDING_REGISTERS, 100, 12)) 37 | #logger.info(master.execute(1, cst.WRITE_SINGLE_COIL, 7, output_value=1)) 38 | #logger.info(master.execute(1, cst.WRITE_SINGLE_REGISTER, 100, output_value=54)) 39 | #logger.info(master.execute(1, cst.WRITE_MULTIPLE_COILS, 0, output_value=[1, 1, 0, 1, 1, 0, 1, 1])) 40 | #logger.info(master.execute(1, cst.WRITE_MULTIPLE_REGISTERS, 100, output_value=xrange(12))) 41 | 42 | except modbus_tk.modbus.ModbusError, e: 43 | logger.error("%s- Code=%d" % (e, e.get_exception_code())) 44 | -------------------------------------------------------------------------------- /examples/tcpslave_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import sys 13 | 14 | #add logging capability 15 | import logging 16 | import threading 17 | 18 | import modbus_tk 19 | import modbus_tk.defines as cst 20 | import modbus_tk.modbus as modbus 21 | import modbus_tk.modbus_tcp as modbus_tcp 22 | 23 | logger = modbus_tk.utils.create_logger(name="console", record_format="%(message)s") 24 | 25 | if __name__ == "__main__": 26 | 27 | try: 28 | #Create the server 29 | server = modbus_tcp.TcpServer() 30 | logger.info("running...") 31 | logger.info("enter 'quit' for closing the server") 32 | 33 | server.start() 34 | 35 | slave_1 = server.add_slave(1) 36 | slave_1.add_block('0', cst.HOLDING_REGISTERS, 100, 100) 37 | while True: 38 | cmd = sys.stdin.readline() 39 | args = cmd.split(' ') 40 | if cmd.find('quit')==0: 41 | sys.stdout.write('bye-bye\r\n') 42 | break 43 | elif args[0]=='add_slave': 44 | slave_id = int(args[1]) 45 | server.add_slave(slave_id) 46 | sys.stdout.write('done: slave %d added\r\n' % (slave_id)) 47 | elif args[0]=='add_block': 48 | slave_id = int(args[1]) 49 | name = args[2] 50 | block_type = int(args[3]) 51 | starting_address = int(args[4]) 52 | length = int(args[5]) 53 | slave = server.get_slave(slave_id) 54 | slave.add_block(name, block_type, starting_address, length) 55 | sys.stdout.write('done: block %s added\r\n' % (name)) 56 | elif args[0]=='set_values': 57 | slave_id = int(args[1]) 58 | name = args[2] 59 | address = int(args[3]) 60 | values = [] 61 | for v in args[4:]: 62 | values.append(int(v)) 63 | slave = server.get_slave(slave_id) 64 | slave.set_values(name, address, values) 65 | values = slave.get_values(name, address, len(values)) 66 | sys.stdout.write('done: values written: %s\r\n' % (str(values))) 67 | elif args[0]=='get_values': 68 | slave_id = int(args[1]) 69 | name = args[2] 70 | address = int(args[3]) 71 | length = int(args[4]) 72 | slave = server.get_slave(slave_id) 73 | values = slave.get_values(name, address, length) 74 | sys.stdout.write('done: values read: %s\r\n' % (str(values))) 75 | else: 76 | sys.stdout.write("unknown command %s\r\n" % (args[0])) 77 | finally: 78 | server.stop() 79 | -------------------------------------------------------------------------------- /hmi/db/master_webhmi.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mushorg/modbus-tk/3ad1f0720b248c0014664e0988ecf33b08abf1d1/hmi/db/master_webhmi.db -------------------------------------------------------------------------------- /hmi/master_webhmi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | from __future__ import with_statement 12 | from bottle import route, run, template, TEMPLATES, send_file, redirect, request 13 | import modbus_tk.modbus_tcp as modbus_tcp 14 | import modbus_tk.modbus_rtu as modbus_rtu 15 | import modbus_tk.modbus as modbus 16 | import sqlite3 17 | import webbrowser 18 | 19 | SERIAL = True 20 | 21 | if SERIAL: 22 | SERIAL_PORTS = {} 23 | try: 24 | import serial 25 | except Exception, msg: 26 | SERIAL = False 27 | print "Warning: serial communication is disabled" 28 | DEBUG = False if SERIAL else True #reload mode must not be set with serial com. It would cause an Access Denied 29 | 30 | class Master: 31 | def __init__(self, protocol, address, id, db): 32 | if protocol == "tcp": 33 | try: 34 | (host, port) = address.split(":") 35 | self.modbus = modbus_tcp.TcpMaster(str(host), int(port)) 36 | except: 37 | self.modbus = modbus_tcp.TcpMaster(address) 38 | self.modbus.set_timeout(5.0) 39 | elif protocol == "rtu": 40 | if SERIAL: 41 | args = unicode(address).split(',') 42 | kwargs = {} 43 | for a in args: 44 | key, val = a.split(':') 45 | if key=='port': 46 | try: 47 | serial_port = int(val) 48 | except: 49 | serial_port = val 50 | else: 51 | kwargs[key] = val 52 | try: 53 | try: 54 | s = SERIAL_PORTS[serial_port] 55 | except IndexError: 56 | SERIAL_PORTS[serial_port] = s = serial.Serial(port=serial_port, **kwargs) 57 | self.modbus = modbus_rtu.RtuMaster(s) 58 | except Exception, msg: 59 | raise Exception("Protocol {0} error! {1}".format(protocol, msg)) 60 | else: 61 | raise Exception("Protocol {0} is disabled!".format(protocol)) 62 | else: 63 | raise Exception("Protocol {0} is not supported!".format(protocol)) 64 | 65 | self.id = id 66 | self._db = db 67 | self.address = address 68 | self.protocol = protocol 69 | 70 | self.requests = self._db.get_requests(self.id) 71 | 72 | def get_slaves(self): 73 | return self._db.get_slaves(self.id) 74 | 75 | def add_request(self, id, fct, start, length): 76 | self._db.add_request(self.id, id, fct, start, length) 77 | self.requests = self._db.get_requests(self.id) 78 | 79 | def delete_request(self, id): 80 | self._db.delete_request(id) 81 | self.requests = self._db.get_requests(self.id) 82 | 83 | class Persistence: 84 | def __init__(self): 85 | with self._get_db() as conn: 86 | cursor = conn.cursor() 87 | try: 88 | cursor.execute("SELECT * FROM request;") 89 | except: 90 | cursor.execute("""CREATE TABLE master (id INTEGER PRIMARY KEY AUTOINCREMENT, 91 | protocol TEXT, 92 | server_address TEXT);""") 93 | 94 | cursor.execute("""CREATE TABLE request (id INTEGER PRIMARY KEY AUTOINCREMENT, 95 | master_id INTEGER, 96 | slave INTEGER, 97 | function INTEGER, 98 | address INTEGER, 99 | length INTEGER, 100 | FOREIGN KEY(master_id) REFERENCES master(id));""") 101 | 102 | cursor.execute("""INSERT INTO master (protocol, server_address) VALUES ('tcp', 'localhost');""") 103 | 104 | 105 | def _get_db(self): 106 | return sqlite3.connect("./db/master_webhmi.db") 107 | 108 | def get_data(self, sql_query, args=None): 109 | with self._get_db() as conn: 110 | conn.row_factory = sqlite3.Row 111 | cursor = conn.cursor() 112 | if args: 113 | cursor.execute(sql_query, args) 114 | else: 115 | cursor.execute(sql_query) 116 | l = [] 117 | for row in cursor: 118 | d = {} 119 | for x in row.keys(): 120 | d[x] = row[x] 121 | l.append(d) 122 | return l 123 | 124 | def get_masters(self): 125 | return self.get_data("SELECT * FROM master;") 126 | 127 | def get_requests(self, master_id): 128 | reqs = self.get_data("SELECT * FROM request where master_id=?;", (master_id,)) 129 | for r in reqs: 130 | name = ' - '.join(['{0}: {1}'.format(x.capitalize(), r[x]) for x in r.keys() if x.find('id')<0]) 131 | r['name'] = name 132 | return reqs 133 | 134 | def get_slaves(self, master_id): 135 | reqs = self.get_data("SELECT DISTINCT slave FROM request WHERE master_id=?;", (master_id,)) 136 | return reqs 137 | 138 | def get_hr_requests_for_slave(self, master_id, slave_id): 139 | reqs = self.get_data("SELECT * FROM request WHERE master_id=? AND slave=? AND function=3;", (master_id,slave_id)) 140 | return reqs 141 | 142 | def add_request(self, master, slave, fct, start, length): 143 | with self._get_db() as conn: 144 | cursor = conn.cursor() 145 | cursor.execute("INSERT INTO request (master_id, slave, function, address, length) VALUES (?, ?, ?, ?, ?)", \ 146 | (master, slave, fct, start, length)) 147 | 148 | def delete_request(self, id): 149 | with self._get_db() as conn: 150 | cursor = conn.cursor() 151 | cursor.execute("DELETE FROM request WHERE id = ?", (id, )) 152 | 153 | def add_master(self, protocol, server_address): 154 | with self._get_db() as conn: 155 | cursor = conn.cursor() 156 | cursor.execute("INSERT INTO master (protocol, server_address) VALUES (?, ?)", (protocol, server_address)) 157 | return self.get_data("SELECT MAX(id) FROM master;")[0]['MAX(id)'] 158 | 159 | def delete_master(self, id): 160 | with self._get_db() as conn: 161 | cursor = conn.cursor() 162 | cursor.execute("DELETE FROM master WHERE id=?", (id,)) 163 | 164 | class App: 165 | def __init__(self): 166 | self._db = Persistence() 167 | self._load_masters() 168 | 169 | def _load_masters(self): 170 | self._masters = {} 171 | for m in self._db.get_masters(): 172 | id = m['id'] 173 | try: 174 | self._masters[id] = Master(m['protocol'], m['server_address'], id, self._db) 175 | except Exception, msg: 176 | print "WARNING! can't load master {0} - {1}: {2}".format(m['protocol'], m['server_address'], msg) 177 | 178 | def get_masters(self): 179 | the_keys = self._masters.keys() 180 | the_keys.sort() 181 | return [self._masters[k] for k in the_keys] 182 | 183 | def get_master(self, id): 184 | id = int(id) 185 | return self._masters[id] 186 | 187 | def add_master(self, protocol, server_address): 188 | id = self._db.add_master(protocol, server_address) 189 | self._load_masters() 190 | return id 191 | 192 | def delete_master(self, id): 193 | self._db.delete_master(id) 194 | self._load_masters() 195 | 196 | APP = App() 197 | 198 | @route('/media/:filename') 199 | def static_file(filename): 200 | send_file(filename, root='./media/') 201 | 202 | @route("/") 203 | def index(): 204 | redirect("/masters") 205 | #TEMPLATES.clear() 206 | #return template('templates/master_index', masters=APP.get_masters()) 207 | 208 | @route("/masters") 209 | def master_list(): 210 | TEMPLATES.clear() 211 | return template('templates/masters_list', masters=APP.get_masters()) 212 | 213 | @route("/master/:id") 214 | def master_detail(id): 215 | TEMPLATES.clear() 216 | return template('templates/master_index', master=APP.get_master(id)) 217 | 218 | @route("/delete-master/:id") 219 | def delete_master(id): 220 | TEMPLATES.clear() 221 | APP.delete_master(id) 222 | return redirect('/masters') 223 | 224 | 225 | @route("modbus-read/:master_id/:slave_id/:function_code/:start_address/:length") 226 | def show_results(master_id, slave_id, function_code, start_address, length): 227 | TEMPLATES.clear() 228 | master = APP.get_master(master_id) 229 | name = 'Slave {0} - Function {1}'.format(slave_id, function_code) 230 | friendly_name = 'Slave {0} - Function {1} - Address {2} - Count {3}'.format(slave_id, function_code, start_address, length) 231 | url = "/modbus-read/{0}/{1}/{2}/{3}/{4}".format(master_id, slave_id, function_code, start_address, length) 232 | try: 233 | results = master.modbus.execute(int(slave_id), int(function_code), int(start_address), int(length)) 234 | lines = [i*16 for i in range(int(len(results)%16>0)+len(results)//16)] 235 | return template('templates/master_results', results=results, start=int(start_address), 236 | lines=lines, name=name, url=url, master=master, friendly_name=friendly_name) 237 | except modbus.ModbusError, msg: 238 | return template('templates/modbus_error', msg=msg, name=name, url=url, master=master, friendly_name=friendly_name) 239 | 240 | @route("/modbus-read-all-hr/:master_id/:slave_id") 241 | def show_results_all_hr(master_id, slave_id): 242 | TEMPLATES.clear() 243 | master = APP.get_master(master_id) 244 | requests = APP._db.get_hr_requests_for_slave(master_id, slave_id) 245 | name = 'Slave {0} - Function {1}'.format(slave_id, 3) 246 | friendly_name = 'Slave {0} - Function {1} - All Registers'.format(slave_id, 3) 247 | url = "/modbus-read-all-hr/{0}/{1}".format(master_id, slave_id) 248 | all_results = {} 249 | try: 250 | for req in requests: 251 | i = int(req['address']) 252 | #print "%s, %s, %s"%(int(slave_id), int(req['address']), int(req['length'])) 253 | results = master.modbus.execute(int(slave_id), int(3), int(req['address']), int(req['length'])) 254 | for result in results: 255 | all_results[i] = {'result': result, 'register_hex': hex(i)} 256 | i += 1 257 | return template('templates/master_results_all_hr', results=all_results, url=url, 258 | name=name, master=master, friendly_name=friendly_name) 259 | except modbus.ModbusError, msg: 260 | return template('templates/modbus_error', msg=msg, name=name, url=url, master=master, friendly_name=friendly_name) 261 | 262 | 263 | @route('add-request/:master', method='POST') 264 | def add_request(master): 265 | slave = request.POST['slave'] 266 | function = request.POST['function'] 267 | address = request.POST['address'] 268 | length = request.POST['length'] 269 | APP.get_master(master).add_request(slave, function, address, length) 270 | return show_results(master, slave, function, address, length) 271 | 272 | @route('/delete-request/:master/:id') 273 | def delete_request(master, id): 274 | APP.get_master(master).delete_request(id) 275 | return redirect('/master/%s'%(master)) 276 | 277 | @route('/add-master', method='POST') 278 | def add_master(): 279 | protocol = request.POST['protocol'] 280 | server_address = request.POST['server_address'] 281 | id = APP.add_master(protocol, server_address) 282 | return master_detail(id) 283 | 284 | @route('/modbus-read-json/:master_id/:slave_id/:function_code/:start_address/:length') 285 | def get_json_data(master_id, slave_id, function_code, start_address, length): 286 | master = APP.get_master(master_id) 287 | try: 288 | results = master.modbus.execute(int(slave_id), int(function_code), int(start_address), int(length)) 289 | json_data = {} 290 | i = 0 291 | for r in results: 292 | json_data[str(i)] = r 293 | i += 1 294 | except modbus.ModbusError, msg: 295 | return template('templates/modbus_error', msg=msg, name=name, url=url, master=master, friendly_name=friendly_name) 296 | return json_data 297 | 298 | @route('/modbus-read-json-all-hr/:master_id/:slave_id') 299 | def get_all_json_data(master_id, slave_id): 300 | master = APP.get_master(master_id) 301 | requests = APP._db.get_hr_requests_for_slave(master_id, slave_id); 302 | try: 303 | json_data = {} 304 | for req in requests: 305 | i = int(req['address']) 306 | results = master.modbus.execute(int(slave_id), int(3), int(req['address']), int(req['length'])) 307 | for result in results: 308 | json_data[str(i)] = result 309 | i += 1 310 | except modbus.ModbusError, msg: 311 | return template('templates/modbus_error', msg=msg, name=name, url=url, master=master, friendly_name=friendly_name) 312 | return json_data 313 | 314 | 315 | 316 | webbrowser.open_new_tab('http://localhost:8075/') 317 | run(reloader=DEBUG, port=8075) 318 | -------------------------------------------------------------------------------- /hmi/media/style.css: -------------------------------------------------------------------------------- 1 | #main { 2 | width: 800px; 3 | margin: 0 auto; 4 | background: #ddd; 5 | padding-bottom: 20px; 6 | } 7 | 8 | h1 { 9 | font-size: 1.2em; 10 | } 11 | 12 | .master td, .results td { 13 | border: solid thin #eee; 14 | padding: 2px 2px 2px 2px; 15 | text-align: center; 16 | } 17 | 18 | .breadcrumbs { 19 | padding-bottom: 10px; 20 | margin-bottom: 20px; 21 | } 22 | 23 | .breadcrumbs td { 24 | font-size: small; 25 | padding-right: 10px; 26 | } 27 | 28 | .result { 29 | text-align:center; 30 | } 31 | 32 | .hover { 33 | cursor: pointer; 34 | } 35 | 36 | #polling_commands { 37 | position: fixed; 38 | top: 10px; 39 | left: 10px; 40 | } 41 | 42 | th { 43 | vertical-align: bottom; 44 | } 45 | 46 | #poll_rate_input { 47 | width: 40px; 48 | } 49 | 50 | .binary_on { 51 | background-color: #ff0000; 52 | } 53 | 54 | .binary_off { 55 | background-color: #00ff00; 56 | } -------------------------------------------------------------------------------- /hmi/templates/master_index.tpl: -------------------------------------------------------------------------------- 1 | 2 | modbus-tk - Master HMI 3 | 4 | 5 | 26 | 27 | 28 |
29 |

Modbus Master {{master.id}} - {{master.address}}

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 79 | 80 |
40 | 41 | %for req in master.requests: 42 | 43 | 44 | 45 | 46 | 47 | 48 | %end 49 | %for req in master.get_slaves(): 50 | 51 | 52 | 53 | %end 54 | 55 | 56 | 76 | 77 |
{{req['name']}}viewdelete
All Holding Registers for Slave: {{req['slave']}}
57 | add 58 |
59 | 60 | 61 | 62 | 69 | 70 | 71 | 72 | 73 |
Slave
Function Code 63 | 68 |
Starting Address
length
Cancel
74 |
75 |
78 |
81 |
82 | 83 | -------------------------------------------------------------------------------- /hmi/templates/master_results.tpl: -------------------------------------------------------------------------------- 1 | 2 | modbus-tk - Master HMI - Result of query {{name}} 3 | 4 | 5 | 6 | 19 | 20 | 21 |
22 |

Results of request {{friendly_name}}

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | %for i in range(16): 33 | 34 | %end 35 | 36 | %i=0 37 | %for l in lines: 38 | 39 | %for r in results[l:l+16]: 40 | 41 | %i=i+1 42 | %end 43 | 44 | %end 45 |
{{i}}
{{l+start}}{{r}}
46 |
47 | -------------------------------------------------------------------------------- /hmi/templates/master_results_all_hr.tpl: -------------------------------------------------------------------------------- 1 | 2 | modbus-tk - Master HMI - Result of query {{name}} 3 | 4 | 5 | 112 | 113 | 114 |
115 |

Results of request {{friendly_name}}

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 |

Enable Polling

125 |

Disable Polling

126 |
127 | 128 | 129 |
130 |

Start Time: 0

131 |

Finish Time: 0

132 |

Execution Time: 0

133 |
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | %i=0 163 | %for register in results: 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | %i=i+1 188 | 189 | %end 190 | %end 191 |
Register (dec)Register (hex)Modbus Register (0 Based)Modbus Register (1 Based)ValueHex ValueBinary Value
16151413121110987654321
{{register}}{{results[register]['register_hex']}}{{register}}{{register}}{{results[register]['result']}}
192 |
193 | -------------------------------------------------------------------------------- /hmi/templates/masters_list.tpl: -------------------------------------------------------------------------------- 1 | 2 | modbus-tk - Master HMI 3 | 4 | 5 | 34 | 35 | 36 |
37 |

Modbus Masters

38 | 39 | 40 | 41 | 42 | 43 | %for master in masters: 44 | 45 | 46 | 47 | 48 | 49 | 50 | %end 51 | 52 | 66 | 67 |
{{master.id}}{{master.protocol}} - {{master.address}}delete
53 | add 54 |
55 | 56 | 57 | 61 | 62 | 63 |
Protocol
Server Address
Cancel
64 |
65 |
68 |
69 | 70 | -------------------------------------------------------------------------------- /hmi/templates/modbus_error.tpl: -------------------------------------------------------------------------------- 1 | 2 | modbus-tk - Master HMI - Error 3 | 4 | 5 | 6 |
7 |

Results of {{name}}

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | {{msg}} 17 |
18 |
19 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Modbus Test Kit: a python implementation of the modbus protocol for testing purpose 2 | ~~~~~ 3 | License for use and distribution 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Modbus TestKit Copyright 7 | (C)2009 - Luc Jean - luc.jean@gmail.com 8 | (C)2009 - Apidev - http://www.apidev.fr 9 | 10 | This is distributed under GNU LGPL license (see copying.txt for details) 11 | 12 | GNU LGPL information 13 | -------------------- 14 | 15 | This library is free software; you can redistribute it and/or 16 | modify it under the terms of the GNU Lesser General Public 17 | License as published by the Free Software Foundation; either 18 | version 2.1 of the License, or (at your option) any later version. 19 | 20 | This library is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 23 | Lesser General Public License for more details. 24 | 25 | You should have received a copy of the GNU Lesser General Public 26 | License along with this library; if not, write to the Free Software 27 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 28 | 29 | Apidev www.apidev.fr., hereby disclaims all copyright interest in the 30 | library 'Modbus Test Kit' written by Luc JEAN. 31 | 32 | Apidev 33 | Luc JEAN 34 | 296 rue Saint Pierre 35 | 42810 Rozier en Donzy 36 | France 37 | 38 | -------------------------------------------------------------------------------- /modbus_tk/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | 11 | Make possible to write modbus TCP and RTU master and slave for testing purpose 12 | Modbus TestKit is different from pymodbus which is another implementation of 13 | the modbus stack in python 14 | """ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /modbus_tk/defines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | #modbus exception codes 13 | ILLEGAL_FUNCTION = 1 14 | ILLEGAL_DATA_ADDRESS = 2 15 | ILLEGAL_DATA_VALUE = 3 16 | SLAVE_DEVICE_FAILURE = 4 17 | COMMAND_ACKNOWLEDGE = 5 18 | SLAVE_DEVICE_BUSY = 6 19 | MEMORY_PARITY_ERROR = 8 20 | 21 | #supported modbus functions 22 | READ_COILS = 1 23 | READ_DISCRETE_INPUTS = 2 24 | READ_HOLDING_REGISTERS = 3 25 | READ_INPUT_REGISTERS = 4 26 | WRITE_SINGLE_COIL = 5 27 | WRITE_SINGLE_REGISTER = 6 28 | READ_EXCEPTION_STATUS = 7 29 | DIAGNOSTIC = 8 30 | WRITE_MULTIPLE_COILS = 15 31 | WRITE_MULTIPLE_REGISTERS = 16 32 | READ_WRITE_MULTIPLE_REGISTERS = 23 33 | DEVICE_INFO = 43 34 | 35 | #supported block types 36 | COILS = 1 37 | DISCRETE_INPUTS = 2 38 | HOLDING_REGISTERS = 3 39 | ANALOG_INPUTS = 4 40 | -------------------------------------------------------------------------------- /modbus_tk/hooks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | from __future__ import with_statement 13 | import threading 14 | 15 | _lock = threading.RLock() 16 | _hooks = {} 17 | 18 | def install_hook(name, fct): 19 | """ 20 | Install one of the following hook 21 | 22 | modbus_rtu.RtuMaster.before_open((master,)) 23 | modbus_rtu.RtuMaster.after_close((master,) 24 | modbus_rtu.RtuMaster.before_send((master, request)) returns modified request or None 25 | modbus_rtu.RtuMaster.after_recv((master, response)) returns modified response or None 26 | 27 | modbus_rtu.RtuServer.before_close((server, )) 28 | modbus_rtu.RtuServer.after_close((server, )) 29 | modbus_rtu.RtuServer.before_open((server, )) 30 | modbus_rtu.RtuServer.after_open(((server, )) 31 | modbus_rtu.RtuServer.after_read((server, request)) returns modified request or None 32 | modbus_rtu.RtuServer.before_write((server, response)) returns modified response or None 33 | modbus_rtu.RtuServer.on_error((server, excpt)) 34 | 35 | modbus_tcp.TcpMaster.before_connect((master, )) 36 | modbus_tcp.TcpMaster.after_connect((master, )) 37 | modbus_tcp.TcpMaster.before_close((master, )) 38 | modbus_tcp.TcpMaster.after_close((master, )) 39 | modbus_tcp.TcpMaster.before_send((master, request)) 40 | modbus_tcp.TcpMaster.after_recv((master, response)) 41 | 42 | modbus_tcp.TcpServer.on_connect((server, client, address)) 43 | modbus_tcp.TcpServer.on_disconnect((server, sock)) 44 | modbus_tcp.TcpServer.after_recv((server, sock, request)) returns modified request or None 45 | modbus_tcp.TcpServer.before_send((server, sock, response)) returns modified response or None 46 | modbus_tcp.TcpServer.on_error((server, sock, excpt)) 47 | 48 | modbus.Master.before_send((master, request)) returns modified request or None 49 | modbus.Master.after_send((master)) 50 | modbus.Master.after_recv((master, response)) returns modified response or None 51 | 52 | modbus.Slave.handle_request((slave, request_pdu)) returns modified response or None 53 | modbus.Slave.on_handle_broadcast((slave, response_pdu)) returns modified response or None 54 | modbus.Slave.on_exception((slave, function_code, excpt)) 55 | 56 | modbus.Databank.on_error((db, excpt, request_pdu)) 57 | 58 | modbus.ModbusBlock.setitem((self, slice, value)) 59 | 60 | modbus.Server.before_handle_request((server, request)) returns modified request or None 61 | modbus.Server.after_handle_request((server, response)) returns modified response or None 62 | """ 63 | with _lock: 64 | try: 65 | _hooks[name].append(fct) 66 | except KeyError: 67 | _hooks[name] = [fct] 68 | 69 | def uninstall_hook(name, fct=None): 70 | """remove the function from the hooks""" 71 | with _lock: 72 | if fct: 73 | _hooks[name].remove(fct) 74 | else: 75 | del _hooks[name][:] 76 | 77 | def call_hooks(name, args): 78 | """call the function associated with the hook and pass the given args""" 79 | with _lock: 80 | try: 81 | for fct in _hooks[name]: 82 | retval = fct(args) 83 | if retval <> None: 84 | return retval 85 | except KeyError: 86 | pass 87 | return None 88 | 89 | -------------------------------------------------------------------------------- /modbus_tk/modbus_rtu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | 11 | """ 12 | 13 | from modbus import * 14 | import logging 15 | import sys 16 | import time 17 | from hooks import call_hooks 18 | 19 | #------------------------------------------------------------------------------- 20 | class RtuQuery(Query): 21 | """Subclass of a Query. Adds the Modbus RTU specific part of the protocol""" 22 | 23 | def __init__(self): 24 | """Constructor""" 25 | Query.__init__(self) 26 | self._request_address = 0 27 | self._response_address = 0 28 | 29 | def build_request(self, pdu, slave): 30 | """Add the Modbus RTU part to the request""" 31 | self._request_address = slave 32 | if (self._request_address < 0) or (self._request_address > 255): 33 | raise InvalidArgumentError, "Invalid address %d" % (self._request_address) 34 | data = struct.pack(">B", self._request_address) + pdu 35 | crc = struct.pack(">H", utils.calculate_crc(data)) 36 | return (data + crc) 37 | 38 | def parse_response(self, response): 39 | """Extract the pdu from the Modbus RTU response""" 40 | if len(response) < 3: 41 | raise ModbusInvalidResponseError, "Response length is invalid %d" % (len(response)) 42 | 43 | (self._response_address, ) = struct.unpack(">B", response[0]) 44 | if self._request_address != self._response_address: 45 | raise ModbusInvalidResponseError, "Response address %d is different from request address %d" % \ 46 | (self._response_address, self._request_address) 47 | 48 | (crc, ) = struct.unpack(">H", response[-2:]) 49 | 50 | if crc != utils.calculate_crc(response[:-2]): 51 | raise ModbusInvalidResponseError, "Invalid CRC in response" 52 | 53 | return response[1:-2] 54 | 55 | def parse_request(self, request): 56 | """Extract the pdu from the Modbus RTU request""" 57 | if len(request) < 3: 58 | raise ModbusInvalidRequestError, "Request length is invalid %d" % (len(request)) 59 | 60 | (self._request_address, ) = struct.unpack(">B", request[0]) 61 | 62 | (crc, ) = struct.unpack(">H", request[-2:]) 63 | if crc != utils.calculate_crc(request[:-2]): 64 | raise ModbusInvalidRequestError, "Invalid CRC in request" 65 | 66 | return (self._request_address, request[1:-2]) 67 | 68 | def build_response(self, response_pdu): 69 | """Build the response""" 70 | self._response_address = self._request_address 71 | data = struct.pack(">B", self._response_address) + response_pdu 72 | crc = struct.pack(">H", utils.calculate_crc(data)) 73 | return (data + crc) 74 | 75 | #------------------------------------------------------------------------------- 76 | class RtuMaster(Master): 77 | """Subclass of Master. Implements the Modbus RTU MAC layer""" 78 | 79 | def __init__(self, serial, interchar_multiplier=1.5): 80 | """Constructor. Pass the pyserial.Serial object""" 81 | self._serial = serial 82 | LOGGER.info("RtuMaster %s is %s" % (self._serial.portstr, "opened" if self._serial.isOpen() else "closed")) 83 | Master.__init__(self, self._serial.timeout) 84 | self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate) 85 | self._serial.interCharTimeout = interchar_multiplier * self._t0 86 | #self._serial.timeout = interchar_multiplier * self._t0 87 | 88 | def _do_open(self): 89 | """Open the given serial port if not already opened""" 90 | if not self._serial.isOpen(): 91 | call_hooks("modbus_rtu.RtuMaster.before_open", (self, )) 92 | self._serial.open() 93 | 94 | def _do_close(self): 95 | """Close the serial port if still opened""" 96 | if self._serial.isOpen(): 97 | self._serial.close() 98 | call_hooks("modbus_rtu.RtuMaster.after_close", (self, )) 99 | 100 | def set_timeout(self, timeout_in_sec): 101 | """Change the timeout value""" 102 | Master.set_timeout(self, timeout_in_sec) 103 | self._serial.timeout = timeout_in_sec 104 | 105 | def _send(self, request): 106 | """Send request to the slave""" 107 | retval = call_hooks("modbus_rtu.RtuMaster.before_send", (self, request)) 108 | if retval <> None: 109 | request = retval 110 | 111 | self._serial.flushInput() 112 | self._serial.flushOutput() 113 | 114 | self._serial.write(request) 115 | time.sleep(3.5 * self._t0) 116 | 117 | def _recv(self, expected_length=-1): 118 | """Receive the response from the slave""" 119 | response = "" 120 | read_bytes = "dummy" 121 | while read_bytes: 122 | read_bytes = self._serial.read(1) 123 | response += read_bytes 124 | if expected_length>=0 and len(response)>=expected_length: 125 | #if the expected number of byte is received consider that the response is done 126 | #improve performance by avoiding end-of-response detection by timeout 127 | break 128 | 129 | retval = call_hooks("modbus_rtu.RtuMaster.after_recv", (self, response)) 130 | if retval <> None: 131 | return retval 132 | return response 133 | 134 | def _make_query(self): 135 | """Returns an instance of a Query subclass implementing the modbus RTU protocol""" 136 | return RtuQuery() 137 | 138 | #------------------------------------------------------------------------------- 139 | class RtuServer(Server): 140 | """This class implements a simple and mono-threaded modbus rtu server""" 141 | 142 | def __init__(self, serial, databank=None): 143 | """Constructor: initializes the server settings""" 144 | Server.__init__(self, databank if databank else Databank()) 145 | self._serial = serial 146 | LOGGER.info("RtuServer %s is %s" % (self._serial.portstr, "opened" if self._serial.isOpen() else "closed")) 147 | self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate) 148 | self._serial.interCharTimeout = 1.5 * self._t0 149 | self._serial.timeout = 10 * self._t0 150 | 151 | def close(self): 152 | """close the serial communication""" 153 | if self._serial.isOpen(): 154 | call_hooks("modbus_rtu.RtuServer.before_close", (self, )) 155 | self._serial.close() 156 | call_hooks("modbus_rtu.RtuServer.after_close", (self, )) 157 | 158 | def __del__(self): 159 | """Destructor""" 160 | self.close() 161 | 162 | def _make_query(self): 163 | """Returns an instance of a Query subclass implementing the modbus RTU protocol""" 164 | return RtuQuery() 165 | 166 | def stop(self): 167 | """Force the server thread to exit""" 168 | Server.stop(self) 169 | 170 | def _do_init(self): 171 | """initialize the serial connection""" 172 | if not self._serial.isOpen(): 173 | call_hooks("modbus_rtu.RtuServer.before_open", (self, )) 174 | self._serial.open() 175 | call_hooks("modbus_rtu.RtuServer.after_open", (self, )) 176 | 177 | def _do_exit(self): 178 | """close the serial connection""" 179 | self.close() 180 | 181 | def _do_run(self): 182 | """main function of the server""" 183 | try: 184 | #check the status of every socket 185 | response = "" 186 | request = "" 187 | read_bytes = "dummy" 188 | while read_bytes: 189 | read_bytes = self._serial.read(128) 190 | request += read_bytes 191 | 192 | #parse the request 193 | if request: 194 | retval = call_hooks("modbus_rtu.RtuServer.after_read", (self, request)) 195 | if retval <> None: 196 | request = retval 197 | response = self._handle(request) 198 | 199 | #send back the response 200 | retval = call_hooks("modbus_rtu.RtuServer.before_write", (self, response)) 201 | if retval <> None: 202 | response = retval 203 | 204 | if response: 205 | self._serial.write(response) 206 | time.sleep(3.5 * self._t0) 207 | 208 | except Exception, excpt: 209 | LOGGER.error("Error while handling request, Exception occurred: %s", excpt) 210 | call_hooks("modbus_rtu.RtuServer.on_error", (self, excpt)) 211 | -------------------------------------------------------------------------------- /modbus_tk/modbus_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | from modbus import * 13 | import socket 14 | import select 15 | import logging 16 | from hooks import call_hooks 17 | from utils import threadsafe_function 18 | import sys 19 | 20 | #------------------------------------------------------------------------------- 21 | class ModbusInvalidMbapError(Exception): 22 | """Exception raised when the modbus TCP header doesn't correspond to what is expected""" 23 | def __init__(self, value): 24 | Exception.__init__(self, value) 25 | 26 | #------------------------------------------------------------------------------- 27 | class TcpMbap: 28 | """Defines the information added by the Modbus TCP layer""" 29 | def __init__(self): 30 | """Constructor: initializes with 0""" 31 | self.transaction_id = 0 32 | self.protocol_id = 0 33 | self.length = 0 34 | self.unit_id = 0 35 | 36 | def clone(self, mbap): 37 | """Set the value of each fields from another TcpMbap instance""" 38 | self.transaction_id = mbap.transaction_id 39 | self.protocol_id = mbap.protocol_id 40 | self.length = mbap.length 41 | self.unit_id = mbap.unit_id 42 | 43 | def _check_ids(self, request_mbap): 44 | """ 45 | Check that the ids in the request and the response are similar. 46 | if not returns a string describing the error 47 | """ 48 | error_str = "" 49 | 50 | if request_mbap.transaction_id != self.transaction_id: 51 | error_str += "Invalid transaction id: request=%d - response=%d. " % \ 52 | (request_mbap.transaction_id, self.transaction_id) 53 | 54 | if request_mbap.protocol_id != self.protocol_id: 55 | error_str += "Invalid protocol id: request=%d - response=%d. " % \ 56 | (request_mbap.protocol_id, self.protocol_id) 57 | 58 | if request_mbap.unit_id != self.unit_id: 59 | error_str += "Invalid unit id: request=%d - response=%d. " % (request_mbap.unit_id, self.unit_id) 60 | 61 | return error_str 62 | 63 | def check_length(self, pdu_length): 64 | """Check the length field is valid. If not raise an exception""" 65 | following_bytes_length = pdu_length+1 66 | if self.length != following_bytes_length: 67 | return "Response length is %d while receiving %d bytes. " % (self.length, following_bytes_length) 68 | return "" 69 | 70 | def check_response(self, request_mbap, response_pdu_length): 71 | """Check that the MBAP of the response is valid. If not raise an exception""" 72 | error_str = self._check_ids(request_mbap) 73 | error_str += self.check_length(response_pdu_length) 74 | if len(error_str)>0: 75 | raise ModbusInvalidMbapError, error_str 76 | 77 | def pack(self): 78 | """convert the TCP mbap into a string""" 79 | return struct.pack(">HHHB", self.transaction_id, self.protocol_id, self.length, self.unit_id) 80 | 81 | def unpack(self, value): 82 | """extract the TCP mbap from a string""" 83 | (self.transaction_id, self.protocol_id, self.length, self.unit_id) = struct.unpack(">HHHB", value) 84 | 85 | #------------------------------------------------------------------------------- 86 | class TcpQuery(Query): 87 | """Subclass of a Query. Adds the Modbus TCP specific part of the protocol""" 88 | 89 | #static variable for giving a unique id to each query 90 | _last_transaction_id = 0 91 | 92 | def __init__(self): 93 | """Constructor""" 94 | Query.__init__(self) 95 | self._request_mbap = TcpMbap() 96 | self._response_mbap = TcpMbap() 97 | 98 | @threadsafe_function 99 | def _get_transaction_id(self): 100 | """returns an identifier for the query""" 101 | if TcpQuery._last_transaction_id < 0xffff: 102 | TcpQuery._last_transaction_id += 1 103 | else: 104 | TcpQuery._last_transaction_id = 0 105 | return TcpQuery._last_transaction_id 106 | 107 | def build_request(self, pdu, slave): 108 | """Add the Modbus TCP part to the request""" 109 | if (slave < 0) or (slave > 255): 110 | raise InvalidArgumentError, "%d Invalid value for slave id" % (slave) 111 | self._request_mbap.length = len(pdu)+1 112 | self._request_mbap.transaction_id = self._get_transaction_id() 113 | self._request_mbap.unit_id = slave 114 | mbap = self._request_mbap.pack() 115 | return mbap+pdu 116 | 117 | def parse_response(self, response): 118 | """Extract the pdu from the Modbus TCP response""" 119 | if len(response) > 6: 120 | mbap, pdu = response[:7], response[7:] 121 | self._response_mbap.unpack(mbap) 122 | self._response_mbap.check_response(self._request_mbap, len(pdu)) 123 | return pdu 124 | else: 125 | raise ModbusInvalidResponseError, "Response length is only %d bytes. " % (len(response)) 126 | 127 | def parse_request(self, request): 128 | """Extract the pdu from a modbus request""" 129 | if len(request) > 6: 130 | mbap, pdu = request[:7], request[7:] 131 | self._request_mbap.unpack(mbap) 132 | error_str = self._request_mbap.check_length(len(pdu)) 133 | if len(error_str) > 0: 134 | raise ModbusInvalidMbapError, error_str 135 | return (self._request_mbap.unit_id, pdu) 136 | else: 137 | raise ModbusInvalidRequestError, "Request length is only %d bytes. " % (len(request)) 138 | 139 | def build_response(self, response_pdu): 140 | """Build the response""" 141 | self._response_mbap.clone(self._request_mbap) 142 | self._response_mbap.length = len(response_pdu) + 1 143 | return self._response_mbap.pack() + response_pdu 144 | 145 | #------------------------------------------------------------------------------- 146 | class TcpMaster(Master): 147 | """Subclass of Master. Implements the Modbus TCP MAC layer""" 148 | 149 | def __init__(self, host="127.0.0.1", port=502, timeout_in_sec=5.0): 150 | """Constructor. Set the communication settings""" 151 | Master.__init__(self, timeout_in_sec) 152 | self._host = host 153 | self._port = port 154 | self._sock = None 155 | 156 | def _do_open(self): 157 | """Connect to the Modbus slave""" 158 | if self._sock: 159 | self._sock.close() 160 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 161 | self.set_timeout(self.get_timeout()) 162 | call_hooks("modbus_tcp.TcpMaster.before_connect", (self, )) 163 | self._sock.connect((self._host, self._port)) 164 | call_hooks("modbus_tcp.TcpMaster.after_connect", (self, )) 165 | 166 | def _do_close(self): 167 | """Close the connection with the Modbus Slave""" 168 | if self._sock: 169 | call_hooks("modbus_tcp.TcpMaster.before_close", (self, )) 170 | self._sock.close() 171 | call_hooks("modbus_tcp.TcpMaster.after_close", (self, )) 172 | self._sock = None 173 | 174 | def set_timeout(self, timeout_in_sec): 175 | """Change the timeout value""" 176 | Master.set_timeout(self, timeout_in_sec) 177 | if self._sock: 178 | self._sock.setblocking(timeout_in_sec>0) 179 | if timeout_in_sec: 180 | self._sock.settimeout(timeout_in_sec) 181 | 182 | def _send(self, request): 183 | """Send request to the slave""" 184 | retval = call_hooks("modbus_tcp.TcpMaster.before_send", (self, request)) 185 | if retval <> None: 186 | request = retval 187 | try: 188 | utils.flush_socket(self._sock, 3) 189 | except Exception, msg: 190 | #if we can't flush the socket successfully: a disconnection may happened 191 | #try to reconnect 192 | LOGGER.error('Error while flushing the socket: {0}'.format(msg)) 193 | #raise ModbusNotConnectedError(msg) 194 | self._do_open(); 195 | self._sock.send(request) 196 | 197 | def _recv(self, expected_length=-1): 198 | """ 199 | Receive the response from the slave 200 | Do not take expected_length into account because the length of the response is 201 | written in the mbap. Used for RTU only 202 | """ 203 | 204 | response = "" 205 | length = 255 206 | while len(response)HHH", response) 212 | length = to_be_recv_length + 6 213 | else: 214 | break 215 | retval = call_hooks("modbus_tcp.TcpMaster.after_recv", (self, response)) 216 | if retval <> None: 217 | return response 218 | return response 219 | 220 | def _make_query(self): 221 | """Returns an instance of a Query subclass implementing the modbus TCP protocol""" 222 | return TcpQuery() 223 | 224 | #------------------------------------------------------------------------------- 225 | class TcpServer(Server): 226 | """This class implements a simple and mono-threaded modbus tcp server""" 227 | 228 | def __init__(self, port=502, address='localhost', timeout_in_sec=1, databank=None): 229 | """Constructor: initializes the server settings""" 230 | Server.__init__(self, databank if databank else Databank()) 231 | self._sock = None 232 | self._sa = (address, port) 233 | self._timeout_in_sec = timeout_in_sec 234 | self._sockets = [] 235 | 236 | def _make_query(self): 237 | """Returns an instance of a Query subclass implementing the modbus TCP protocol""" 238 | return TcpQuery() 239 | 240 | def _get_request_length(self, mbap): 241 | """Parse the mbap and returns the number of bytes to be read""" 242 | if len(mbap) < 6: 243 | raise ModbusInvalidRequestError("The mbap is only %d bytes long", len(mbap)) 244 | (tr_id, pr_id, length) = struct.unpack(">HHH", mbap[:6]) 245 | return length 246 | 247 | def _do_init(self): 248 | """initialize server""" 249 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 250 | if self._timeout_in_sec: 251 | self._sock.settimeout(self._timeout_in_sec) 252 | self._sock.setblocking(0) 253 | self._sock.bind(self._sa) 254 | self._sock.listen(10) 255 | self._sockets.append(self._sock) 256 | 257 | def _do_exit(self): 258 | """clean the server tasks""" 259 | #close the sockets 260 | for sock in self._sockets: 261 | try: 262 | sock.close() 263 | self._sockets.remove(sock) 264 | except Exception, msg: 265 | LOGGER.warning("Error while closing socket, Exception occurred: %s", msg) 266 | self._sock.close() 267 | self._sock = None 268 | 269 | def _do_run(self): 270 | """called in a almost-for-ever loop by the server""" 271 | #check the status of every socket 272 | inputready, outputready, exceptready = select.select(self._sockets, [], [], 1.0) 273 | 274 | for sock in inputready: #handle data on each a socket 275 | try: 276 | if sock == self._sock: 277 | # handle the server socket 278 | client, address = self._sock.accept() 279 | client.setblocking(0) 280 | LOGGER.info("%s is connected with socket %d..." % (str(address), client.fileno())) 281 | self._sockets.append(client) 282 | call_hooks("modbus_tcp.TcpServer.on_connect", (self, client, address)) 283 | else: 284 | if len(sock.recv(1, socket.MSG_PEEK)) == 0: 285 | #socket is disconnected 286 | LOGGER.info("%d is disconnected" % (sock.fileno())) 287 | call_hooks("modbus_tcp.TcpServer.on_disconnect", (self, sock)) 288 | sock.close() 289 | self._sockets.remove(sock) 290 | break 291 | 292 | # handle all other sockets 293 | sock.settimeout(1.0) 294 | request = "" 295 | is_ok = True 296 | 297 | #read the 7 bytes of the mbap 298 | while (len(request) < 7) and is_ok: 299 | new_byte = sock.recv(1) 300 | if len(new_byte) == 0: 301 | is_ok = False 302 | else: 303 | request += new_byte 304 | 305 | retval = call_hooks("modbus_tcp.TcpServer.after_recv", (self, sock, request)) 306 | if retval <> None: 307 | request = retval 308 | 309 | if is_ok: 310 | #read the rest of the request 311 | length = self._get_request_length(request) 312 | while (len(request) < (length + 6)) and is_ok: 313 | new_byte = sock.recv(1) 314 | if len(new_byte) == 0: 315 | is_ok = False 316 | else: 317 | request += new_byte 318 | 319 | if is_ok: 320 | response = "" 321 | #parse the request 322 | try: 323 | response = self._handle(request) 324 | except Exception, msg: 325 | LOGGER.error("Error while handling a request, Exception occurred: %s", msg) 326 | 327 | #send back the response 328 | if response: 329 | try: 330 | retval = call_hooks("modbus_tcp.TcpServer.before_send", (self, sock, response)) 331 | if retval <> None: 332 | response = retval 333 | sock.send(response) 334 | except Exception, msg: 335 | is_ok = False 336 | LOGGER.error("Error while sending on socket %d, Exception occurred: %s", \ 337 | sock.fileno(), msg) 338 | except Exception, excpt: 339 | LOGGER.warning("Error while processing data on socket %d: %s", sock.fileno(), excpt) 340 | call_hooks("modbus_tcp.TcpServer.on_error", (self, sock, excpt)) 341 | sock.close() 342 | self._sockets.remove(sock) 343 | 344 | -------------------------------------------------------------------------------- /modbus_tk/simulator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | 11 | The modbus_tk simulator is a console application which is running a server with TCP and RTU communication 12 | It is possible to interact with the server from the command line or from a RPC (Remote Process Call) 13 | """ 14 | 15 | import sys, os, time 16 | 17 | import threading 18 | 19 | import modbus_tk 20 | import modbus_tk.defines as defines 21 | import modbus_tk.modbus as modbus 22 | import modbus_tk.modbus_tcp as modbus_tcp 23 | import modbus_tk.modbus_rtu as modbus_rtu 24 | import serial 25 | import modbus_tk.hooks as hooks 26 | 27 | import Queue 28 | import SocketServer 29 | import ctypes 30 | 31 | #add logging capability 32 | LOGGER = modbus_tk.utils.create_logger(name="console", record_format="%(message)s") 33 | 34 | #The communication between the server and the user interfaces (console or rpc) 35 | #are done through queues 36 | INPUT_QUEUE = Queue.Queue() #command received from the interfaces 37 | OUTPUT_QUEUE = Queue.Queue() #response to be sent back by the interfaces 38 | 39 | #------------------------------------------------------------------------------- 40 | class CompositeServer(modbus.Server): 41 | """make possible to have several servers sharing the same databank""" 42 | 43 | def __init__(self, list_of_server_classes, list_of_server_args, databank=None): 44 | """Constructor""" 45 | modbus.Server.__init__(self, databank) 46 | self._servers = [the_class(*the_args, **{"databank":self.get_db()}) 47 | for the_class, the_args 48 | in zip(list_of_server_classes, list_of_server_args) 49 | if issubclass(the_class, modbus.Server)] 50 | 51 | def set_verbose(self, verbose): 52 | """if verbose is true the sent and received packets will be logged""" 53 | for srv in self._servers: 54 | srv.set_verbose(verbose) 55 | 56 | def _make_thread(self): 57 | """should initialize the main thread of the server. You don't need it here""" 58 | pass 59 | 60 | def _make_query(self): 61 | """Returns an instance of a Query subclass implementing the MAC layer protocol""" 62 | raise NotImplementedError() 63 | 64 | 65 | def start(self): 66 | """Start the server. It will handle request""" 67 | for srv in self._servers: 68 | srv.start() 69 | 70 | def stop(self): 71 | """stop the server. It doesn't handle request anymore""" 72 | for srv in self._servers: 73 | srv.stop() 74 | 75 | class RpcHandler(SocketServer.BaseRequestHandler): 76 | """An instance of this class is created every time an RPC call is received by the server""" 77 | 78 | def handle(self): 79 | """This function is called automatically by the SocketServer""" 80 | # self.request is the TCP socket connected to the client 81 | # read the incoming command 82 | request = self.request.recv(1024).strip() 83 | # write to the queue waiting to be processed by the server 84 | INPUT_QUEUE.put(request) 85 | # wait for the server answer in the output queue 86 | response = OUTPUT_QUEUE.get(timeout=5.0) 87 | # send back the answer 88 | self.request.send(response) 89 | 90 | class RpcInterface(threading.Thread): 91 | """Manage RPC call over TCP/IP thanks to the SocketServer module""" 92 | 93 | def __init__(self): 94 | """Constructor""" 95 | threading.Thread.__init__(self) 96 | self.rpc_server = SocketServer.TCPServer(("", 2711), RpcHandler) 97 | 98 | def run(self): 99 | """run the server and wait that it returns""" 100 | self.rpc_server.serve_forever(0.5) 101 | 102 | def close(self): 103 | """force the socket server to exit""" 104 | try: 105 | self.rpc_server.shutdown() 106 | self.join(1.0) 107 | except: 108 | LOGGER.warning("An error occurred while closing RPC interface") 109 | 110 | class ConsoleInterface(threading.Thread): 111 | """Manage user actions from the console""" 112 | 113 | def __init__(self): 114 | """constructor: initialize communication with the console""" 115 | threading.Thread.__init__(self) 116 | self.inq = INPUT_QUEUE 117 | self.outq = OUTPUT_QUEUE 118 | if os.name == "nt": 119 | ctypes.windll.Kernel32.GetStdHandle.restype = ctypes.c_ulong 120 | self.console_handle = ctypes.windll.Kernel32.GetStdHandle(ctypes.c_ulong(0xfffffff5)) 121 | ctypes.windll.Kernel32.WaitForSingleObject.restype = ctypes.c_ulong 122 | elif os.name == "posix": 123 | import select 124 | else: 125 | raise Exception("%s platform is not supported yet" % os.name) 126 | self._go = threading.Event() 127 | self._go.set() 128 | 129 | def _check_console_input(self): 130 | """test if there is something to read on the console""" 131 | if os.name == "nt": 132 | if 0 == ctypes.windll.Kernel32.WaitForSingleObject(self.console_handle, 500): 133 | return True 134 | elif os.name == "posix": 135 | (inputready, abcd, efgh) = select.select([sys.stdin], [], [], 0.5) 136 | if len(inputready)>0: 137 | return True 138 | else: 139 | raise Exception("%s platform is not supported yet" % os.name) 140 | return False 141 | 142 | def run(self): 143 | """read from the console, transfer to the server and write the answer""" 144 | while self._go.isSet(): #while app is running 145 | if self._check_console_input(): #if something to read on the console 146 | cmd = sys.stdin.readline() #read it 147 | self.inq.put(cmd) #dispatch it tpo the server 148 | response = self.outq.get(timeout=2.0) #wait for an answer 149 | sys.stdout.write(response) #write the answer on the console 150 | 151 | def close(self): 152 | """terminates the thread""" 153 | self._go.clear() 154 | self.join(1.0) 155 | 156 | class Simulator: 157 | """The main class of the app in charge of running everything""" 158 | 159 | def __init__(self, server=None): 160 | """Constructor""" 161 | if server == None: 162 | self.server = CompositeServer([modbus_rtu.RtuServer, modbus_tcp.TcpServer], [(serial.Serial(0),), ()]) 163 | else: 164 | self.server = server 165 | self.rpc = RpcInterface() 166 | self.console = ConsoleInterface() 167 | self.inq, self.outq = INPUT_QUEUE, OUTPUT_QUEUE 168 | self._hooks_fct = {} 169 | 170 | self.cmds = {"add_slave":self._do_add_slave, 171 | "has_slave":self._do_has_slave, 172 | "remove_slave":self._do_remove_slave, 173 | "remove_all_slaves":self._do_remove_all_slaves, 174 | "add_block":self._do_add_block, 175 | "remove_block":self._do_remove_block, 176 | "remove_all_blocks":self._do_remove_all_blocks, 177 | "set_values":self._do_set_values, 178 | "get_values":self._do_get_values, 179 | "install_hook":self._do_install_hook, 180 | "uninstall_hook":self._do_uninstall_hook, 181 | "set_verbose":self._do_set_verbose, 182 | } 183 | 184 | def add_command(self, name, fct): 185 | """add a custom command""" 186 | self.cmds[name] = fct 187 | 188 | def start(self): 189 | """run the servers""" 190 | self.server.start() 191 | self.console.start() 192 | self.rpc.start() 193 | 194 | LOGGER.info("modbus_tk.simulator is running...") 195 | 196 | self._handle() 197 | 198 | def declare_hook(self, fct_name, fct): 199 | """declare a hook function by its name. It must be installed by an install hook command""" 200 | self._hooks_fct[fct_name] = fct 201 | 202 | def _tuple_to_str(self, the_tuple): 203 | """convert a tuple to a string""" 204 | ret = "" 205 | for item in the_tuple: 206 | ret += (" " + str(item)) 207 | return ret[1:] 208 | 209 | def _do_add_slave(self, args): 210 | """execute the add_slave command""" 211 | slave_id = int(args[1]) 212 | self.server.add_slave(slave_id) 213 | return "%d" % (slave_id) 214 | 215 | def _do_has_slave(self, args): 216 | """execute the has_slave command""" 217 | slave_id = int(args[1]) 218 | try: 219 | self.server.get_slave(slave_id) 220 | except: 221 | return "0" 222 | return "1" 223 | 224 | def _do_remove_slave(self, args): 225 | """execute the remove_slave command""" 226 | slave_id = int(args[1]) 227 | self.server.remove_slave(slave_id) 228 | return "" 229 | 230 | def _do_remove_all_slaves(self, args): 231 | """execute the remove_slave command""" 232 | self.server.remove_all_slaves() 233 | return "" 234 | 235 | def _do_add_slave(self, args): 236 | """execute the add_slave command""" 237 | slave_id = int(args[1]) 238 | self.server.add_slave(slave_id) 239 | return "%d" % (slave_id) 240 | 241 | def _do_add_block(self, args): 242 | """execute the add_block command""" 243 | slave_id = int(args[1]) 244 | name = args[2] 245 | block_type = int(args[3]) 246 | starting_address = int(args[4]) 247 | length = int(args[5]) 248 | slave = self.server.get_slave(slave_id) 249 | slave.add_block(name, block_type, starting_address, length) 250 | return name 251 | 252 | def _do_remove_block(self, args): 253 | """execute the remove_block command""" 254 | slave_id = int(args[1]) 255 | name = args[2] 256 | slave = self.server.get_slave(slave_id) 257 | slave.remove_block(name) 258 | 259 | def _do_remove_all_blocks(self, args): 260 | """execute the remove_all_blocks command""" 261 | slave_id = int(args[1]) 262 | slave = self.server.get_slave(slave_id) 263 | slave.remove_all_blocks() 264 | 265 | def _do_set_values(self, args): 266 | """execute the set_values command""" 267 | slave_id = int(args[1]) 268 | name = args[2] 269 | address = int(args[3]) 270 | values = [] 271 | for val in args[4:]: 272 | values.append(int(val)) 273 | slave = self.server.get_slave(slave_id) 274 | slave.set_values(name, address, values) 275 | values = slave.get_values(name, address, len(values)) 276 | return self._tuple_to_str(values) 277 | 278 | def _do_get_values(self, args): 279 | """execute the get_values command""" 280 | slave_id = int(args[1]) 281 | name = args[2] 282 | address = int(args[3]) 283 | length = int(args[4]) 284 | slave = self.server.get_slave(slave_id) 285 | values = slave.get_values(name, address, length) 286 | return self._tuple_to_str(values) 287 | 288 | def _do_install_hook(self, args): 289 | """install a function as a hook""" 290 | hook_name = args[1] 291 | fct_name = args[2] 292 | hooks.install_hook(hook_name, self._hooks_fct[fct_name]) 293 | 294 | def _do_uninstall_hook(self, args): 295 | """ 296 | uninstall a function as a hook. 297 | If no function is given, uninstall all functions 298 | """ 299 | hook_name = args[1] 300 | try: 301 | hooks.uninstall_hook(hook_name) 302 | except KeyError, e: 303 | LOGGER.error(str(e)) 304 | 305 | def _do_set_verbose(self, args): 306 | """change the verbosity of the server""" 307 | verbose = int(args[1]) 308 | self.server.set_verbose(verbose) 309 | return "%d" % verbose 310 | 311 | def _handle(self): 312 | """almost-for-ever loop in charge of listening for command and executing it""" 313 | while True: 314 | cmd = self.inq.get() 315 | args = cmd.strip('\r\n').split(' ') 316 | if cmd.find('quit') == 0: 317 | self.outq.put('bye-bye\r\n') 318 | break 319 | elif self.cmds.has_key(args[0]): 320 | try: 321 | answer = self.cmds[args[0]](args) 322 | self.outq.put("%s done: %s\r\n" % (args[0], answer)) 323 | except Exception, msg: 324 | self.outq.put("%s error: %s\r\n" % (args[0], msg)) 325 | else: 326 | self.outq.put("error: unknown command %s\r\n" % (args[0])) 327 | 328 | def close(self): 329 | """close every server""" 330 | self.console.close() 331 | self.rpc.close() 332 | self.server.stop() 333 | 334 | 335 | def print_me(args): 336 | """hook function example""" 337 | (server, request) = args 338 | print "print_me: len = ", len(request) 339 | 340 | if __name__ == "__main__": 341 | simu = Simulator() 342 | 343 | try: 344 | LOGGER.info("'quit' for closing the server") 345 | 346 | simu.declare_hook("print_me", print_me) 347 | simu.start() 348 | 349 | except Exception, excpt: 350 | print excpt 351 | 352 | finally: 353 | simu.close() 354 | LOGGER.info("modbus_tk.simulator has stopped!") 355 | #In python 2.5, the SocketServer shutdown is not working Ok 356 | #The 2 lines below are an ugly temporary workaround 357 | time.sleep(1.0) 358 | sys.exit() -------------------------------------------------------------------------------- /modbus_tk/simulator_rpc_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import socket 13 | import modbus_tk.defines 14 | 15 | class SimulatorRpcClient: 16 | """Make possible to send command to the modbus_tk.Simulator thanks to Remote Process Call""" 17 | 18 | def __init__(self, host="127.0.0.1", port=2711, timeout=0.5): 19 | """Constructor""" 20 | self.host = host 21 | self.port = port 22 | self.timeout = timeout 23 | 24 | def _rpc_call(self, query): 25 | """send a rpc call and return the result""" 26 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 27 | sock.settimeout(self.timeout) 28 | sock.connect((self.host, self.port)) 29 | sock.send(query) 30 | response = sock.recv(1024) 31 | sock.close() 32 | return self._response_to_values(response.strip("\r\n"), query.split(" ")[0]) 33 | 34 | def _response_to_values(self, response, command): 35 | """extract the return value from the response""" 36 | prefix = command + " done: " 37 | if response.find(prefix) == 0: 38 | return response[len(prefix):] 39 | else: 40 | raise Exception(response) 41 | 42 | def add_slave(self, slave_id): 43 | """add a new slave with the given id""" 44 | query = "add_slave %d" % (slave_id) 45 | return self._rpc_call(query) 46 | 47 | def remove_slave(self, slave_id): 48 | """add a new slave with the given id""" 49 | query = "remove_slave %d" % (slave_id) 50 | return self._rpc_call(query) 51 | 52 | def remove_all_slaves(self): 53 | """add a new slave with the given id""" 54 | query = "remove_all_slaves" 55 | self._rpc_call(query) 56 | 57 | def has_slave(self, slave_id): 58 | """add a new slave with the given id""" 59 | query = "has_slave %d" % (slave_id) 60 | if "1"==self._rpc_call(query): 61 | return True 62 | return False 63 | 64 | def add_slave(self, slave_id): 65 | """add a new slave with the given id""" 66 | query = "add_slave %d" % (slave_id) 67 | return self._rpc_call(query) 68 | 69 | def add_block(self, slave_id, block_name, block_type, starting_address, length): 70 | """add a new modbus block into the slave""" 71 | query = "add_block %d %s %d %d %d" % (slave_id, block_name, block_type, starting_address, length) 72 | return self._rpc_call(query) 73 | 74 | def remove_block(self, slave_id, block_name): 75 | """remove the modbus block with the given name and slave""" 76 | query = "remove_block %d %s" % (slave_id, block_name) 77 | self._rpc_call(query) 78 | 79 | def remove_all_blocks(self, slave_id): 80 | """remove the modbus block with the given name and slave""" 81 | query = "remove_all_blocks %d" % (slave_id) 82 | self._rpc_call(query) 83 | 84 | def set_values(self, slave_id, block_name, address, values): 85 | """set the values of registers""" 86 | query = "set_values %d %s %d" % (slave_id, block_name, address) 87 | for val in values: 88 | query += (" " + str(val)) 89 | return self._rpc_call(query) 90 | 91 | def get_values(self, slave_id, block_name, address, length): 92 | """get the values of some registers""" 93 | query = "get_values %d %s %d %d" % (slave_id, block_name, address, length) 94 | ret_values = self._rpc_call(query) 95 | return tuple([int(val) for val in ret_values.split(' ')]) 96 | 97 | def install_hook(self, hook_name, fct_name): 98 | query = "install_hook %s %s" % (hook_name, fct_name) 99 | ret_values = self._rpc_call(query) 100 | 101 | def uninstall_hook(self, hook_name, fct_name=""): 102 | query = "uninstall_hook %s %s" % (hook_name, fct_name) 103 | ret_values = self._rpc_call(query) 104 | 105 | if __name__ == "__main__": 106 | modbus_simu = SimulatorRpcClient() 107 | modbus_simu.remove_all_slaves() 108 | print modbus_simu.add_slave(12) 109 | print modbus_simu.add_block(12, "toto", modbus_tk.defines.COILS, 0, 100) 110 | print modbus_simu.set_values(12, "toto", 0, [5, 8, 7, 6, 41]) 111 | print modbus_simu.get_values(12, "toto", 0, 5) 112 | print modbus_simu.set_values(12, "toto", 2, [9]) 113 | print modbus_simu.get_values(12, "toto", 0, 5) 114 | print modbus_simu.has_slave(12) 115 | print modbus_simu.add_block(12, "titi", modbus_tk.defines.COILS, 100, 100) 116 | print modbus_simu.remove_block(12, "titi") 117 | print modbus_simu.add_slave(25) 118 | print modbus_simu.has_slave(25) 119 | print modbus_simu.add_slave(28) 120 | modbus_simu.remove_slave(25) 121 | print modbus_simu.has_slave(25) 122 | print modbus_simu.has_slave(28) 123 | modbus_simu.remove_all_blocks(12) 124 | modbus_simu.remove_all_slaves() 125 | print modbus_simu.has_slave(28) 126 | print modbus_simu.has_slave(12) 127 | modbus_simu.install_hook("modbus.Server.before_handle_request", "print_me") 128 | 129 | -------------------------------------------------------------------------------- /modbus_tk/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import threading 13 | import logging 14 | import socket 15 | import select 16 | 17 | def threadsafe_function(fcn): 18 | """decorator making sure that the decorated function is thread safe""" 19 | lock = threading.Lock() 20 | def new(*args, **kwargs): 21 | """lock and call the decorated function""" 22 | lock.acquire() 23 | try: 24 | ret = fcn(*args, **kwargs) 25 | except Exception, excpt: 26 | raise excpt 27 | finally: 28 | lock.release() 29 | return ret 30 | return new 31 | 32 | def flush_socket(socks, lim=0): 33 | """remove the data present on the socket""" 34 | input_socks = [socks] 35 | cnt = 0 36 | while 1: 37 | i_socks, o_socks, e_socks = select.select(input_socks, input_socks, input_socks, 0.0) 38 | if len(i_socks)==0: 39 | break 40 | for sock in i_socks: 41 | sock.recv(1024) 42 | if lim>0: 43 | cnt += 1 44 | if cnt>=lim: 45 | #avoid infinite loop due to loss of connection 46 | raise Exception("flush_socket: maximum number of iterations reached") 47 | 48 | def get_log_buffer(prefix, buff): 49 | """Format binary data into a string for debug purpose""" 50 | log = prefix 51 | for i in buff: 52 | log += str(ord(i)) + "-" 53 | return log[:-1] 54 | 55 | class ConsoleHandler(logging.Handler): 56 | """This class is a logger handler. It prints on the console""" 57 | 58 | def __init__(self): 59 | """Constructor""" 60 | logging.Handler.__init__(self) 61 | 62 | def emit(self, record): 63 | """format and print the record on the console""" 64 | print self.format(record) 65 | 66 | class LogitHandler(logging.Handler): 67 | """This class is a logger handler. It send to a udp socket""" 68 | 69 | def __init__(self, dest): 70 | """Constructor""" 71 | logging.Handler.__init__(self) 72 | self._dest = dest 73 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 74 | 75 | def emit(self, record): 76 | """format and send the record over udp""" 77 | self._sock.sendto(self.format(record)+"\r\n", self._dest) 78 | 79 | class DummyHandler(logging.Handler): 80 | """This class is a logger handler. It doesn't do anything""" 81 | 82 | def __init__(self): 83 | """Constructor""" 84 | logging.Handler.__init__(self) 85 | 86 | def emit(self, record): 87 | """do nothinbg with the given record""" 88 | pass 89 | 90 | def create_logger(name="dummy", level=logging.DEBUG, \ 91 | record_format="%(asctime)s\t%(levelname)s\t%(module)s.%(funcName)s\t%(threadName)s\t%(message)s"): 92 | """Create a logger according to the given settings""" 93 | logger = logging.getLogger("modbus_tk") 94 | logger.setLevel(level) 95 | formatter = logging.Formatter(record_format) 96 | if name == "udp": 97 | log_handler = LogitHandler(("127.0.0.1", 1975)) 98 | elif name == "console": 99 | log_handler = ConsoleHandler() 100 | elif name == "dummy": 101 | log_handler = DummyHandler() 102 | else: 103 | raise Exception("Unknown handler %s" % name) 104 | log_handler.setFormatter(formatter) 105 | logger.addHandler(log_handler) 106 | return logger 107 | 108 | def swap_bytes(word_val): 109 | """swap lsb and msb of a word""" 110 | msb = word_val >> 8 111 | lsb = word_val % 256 112 | return (lsb << 8) + msb 113 | 114 | def calculate_crc(data): 115 | """Calculate the CRC16 of a datagram""" 116 | crc = 0xFFFF 117 | for i in data: 118 | crc = crc ^ ord(i) 119 | for j in xrange(8): 120 | tmp = crc & 1 121 | crc = crc >> 1 122 | if tmp: 123 | crc = crc ^ 0xA001 124 | return swap_bytes(crc) 125 | 126 | def calculate_rtu_inter_char(baudrate): 127 | """calculates the interchar delay from the baudrate""" 128 | if baudrate <= 19200: 129 | return 11.0 / baudrate 130 | else: 131 | return 0.0005 132 | 133 | class WorkerThread: 134 | """ 135 | A thread which is running an almost-ever loop 136 | It can be stopped by calling the stop function 137 | """ 138 | def __init__(self, main_fct, args=(), init_fct=None, exit_fct=None): 139 | """Constructor""" 140 | self._fcts = [init_fct, main_fct, exit_fct] 141 | self._args = args 142 | self._thread = threading.Thread(target=WorkerThread._run, args=(self,)) 143 | self._go = threading.Event() 144 | 145 | def start(self): 146 | """Start the thread""" 147 | self._go.set() 148 | self._thread.start() 149 | 150 | def stop(self): 151 | """stop the thread""" 152 | if self._thread.isAlive(): 153 | self._go.clear() 154 | self._thread.join() 155 | 156 | def _run(self): 157 | """main function of the thread execute _main_fct until stop is called""" 158 | try: 159 | if self._fcts[0]: 160 | self._fcts[0](*self._args) 161 | while self._go.isSet(): 162 | self._fcts[1](*self._args) 163 | except Exception, excpt: 164 | LOGGER.error("error: %s" % str(excpt)) 165 | finally: 166 | if self._fcts[2]: 167 | self._fcts[2](*self._args) 168 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | from setuptools import setup 13 | 14 | 15 | version = '0.4' 16 | 17 | setup(name='modbus_tk', 18 | version=version, 19 | description="Implementation of modbus protocol in python", 20 | long_description=""" 21 | Modbus Test Kit provides implementation of slave and master for Modbus TCP and RTU 22 | The main goal is to be used as testing tools. 23 | """, 24 | classifiers=[ 25 | 'Development Status :: 2 - Pre-Alpha', 26 | 'Environment :: Console', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Topic :: Communications', 32 | 'Topic :: Software Development' 33 | ], 34 | keywords='modbus, serial, tcp', 35 | author='Luc Jean', 36 | author_email='luc.jean@gmail.com', 37 | maintainer='Luc Jean', 38 | maintainer_email='luc.jean@gmail.com', 39 | url='http://code.google.com/p/modbus-tk/', 40 | license='LGPL', 41 | packages=['modbus_tk'], 42 | platforms=["Linux", "Mac OS X", "Win"], 43 | ) 44 | -------------------------------------------------------------------------------- /tests/functest_modbus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import unittest 13 | import modbus_tk 14 | import modbus_tk.modbus_rtu as modbus_rtu 15 | import threading 16 | import struct 17 | import logging 18 | import modbus_tk.utils as utils 19 | import time 20 | import sys 21 | 22 | LOGGER = modbus_tk.utils.create_logger("udp") 23 | 24 | class TestQueriesSetupAndTeardown: 25 | """This is not a test case, but can be used by Rtu and Tcp test cases""" 26 | 27 | def setUp(self): 28 | self.master = self._get_master() 29 | self.server = self._get_server() 30 | 31 | self.slave1 = self.server.add_slave(1) 32 | self.slave1.add_block("hr0-100", modbus_tk.defines.HOLDING_REGISTERS, 0, 100) 33 | self.slave1.add_block("hr100-100", modbus_tk.defines.HOLDING_REGISTERS, 100, 200) 34 | self.slave1.add_block("ai100-50", modbus_tk.defines.ANALOG_INPUTS, 100, 150) 35 | self.slave1.add_block("c0-100", modbus_tk.defines.COILS, 0, 100) 36 | self.slave1.add_block("di100-1", modbus_tk.defines.DISCRETE_INPUTS, 100, 1) 37 | self.slave1.add_block("di200-10", modbus_tk.defines.DISCRETE_INPUTS, 200, 10) 38 | self.slave1.add_block("hr1000-1500", modbus_tk.defines.HOLDING_REGISTERS, 1000, 500) 39 | self.slave1.add_block("c1000-4000", modbus_tk.defines.COILS, 1000, 3000) 40 | 41 | self.slave5 = self.server.add_slave(5) 42 | self.slave5.add_block("hr0-100", modbus_tk.defines.HOLDING_REGISTERS, 300, 20) 43 | 44 | self.server.start() 45 | time.sleep(0.1) 46 | self.master.set_timeout(1.0) 47 | self.master.open() 48 | 49 | def tearDown(self): 50 | self.master.close() 51 | self.server.stop() 52 | 53 | class TestQueries(TestQueriesSetupAndTeardown): 54 | """This is not a test case, but can be used by Rtu and Tcp test cases""" 55 | 56 | """Test the modbus protocol""" 57 | def testReadHoldingRegisters(self): 58 | """set the values of holding registers check that the values are read correctly""" 59 | self.slave1.set_values("hr0-100", 0, range(100)) 60 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100) 61 | self.assertEqual(tuple(range(100)), result) 62 | 63 | self.slave1.set_values("hr1000-1500", 1000, range(125)) 64 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 1000, 125) 65 | self.assertEqual(tuple(range(125)), result) 66 | 67 | def testRead100HoldingRegistersOn2Blocks(self): 68 | """check that an error is raised where reading on 2 consecutive blocks""" 69 | try: 70 | self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 50, 100) 71 | except modbus_tk.modbus.ModbusError, ex: 72 | self.assertEqual(ex.get_exception_code(), 2) 73 | return 74 | self.assert_(False) 75 | 76 | def testRead5HoldingRegisters(self): 77 | """set the values of 3 registers and let's the master reading 5 values""" 78 | self.slave1.set_values("hr0-100", 1, [2, 2, 2]) 79 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 5) 80 | self.assertEqual((0, 2, 2, 2, 0), result) 81 | 82 | def testReadOneHoldingRegisters(self): 83 | """set the values of 3 registers and let's the master reading 5 values""" 84 | self.slave1.set_values("hr0-100", 6, 4) 85 | 86 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 6, 1) 87 | self.assertEqual((4, ), result) 88 | 89 | def testReadHoldingRegistersOnSlave5(self): 90 | """set and read the values of some registers on slave 5""" 91 | self.slave5.set_values("hr0-100", 310, (1, 2, 3)) 92 | 93 | result = self.master.execute(5, modbus_tk.defines.READ_HOLDING_REGISTERS, 308, 10) 94 | self.assertEqual((0, 0, 1, 2, 3, 0, 0, 0, 0, 0), result) 95 | 96 | def testReadHoldingRegistersOutOfBlock(self): 97 | """read out of block and make sure that the correct exception is raised""" 98 | self.assertRaises(modbus_tk.modbus.ModbusError, self.master.execute, 1, modbus_tk.defines.READ_HOLDING_REGISTERS, 300, 10) 99 | 100 | try: 101 | self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 300, 10) 102 | except modbus_tk.modbus.ModbusError, ex: 103 | self.assertEqual(ex.get_exception_code(), 2) 104 | return 105 | self.assert_(False) 106 | 107 | def testReadTooManyHoldingRegisters(self): 108 | """check than an error is raised when reading too many holding registers""" 109 | try: 110 | self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 1000, 126) 111 | except modbus_tk.modbus.ModbusError, ex: 112 | self.assertEqual(ex.get_exception_code(), 3) 113 | return 114 | self.assert_(False) 115 | 116 | def testReadAnalogInputs(self): 117 | """Test that response for read analog inputs function is ok""" 118 | self.slave1.set_values("ai100-50", 120, range(10)) 119 | result = self.master.execute(1, modbus_tk.defines.READ_INPUT_REGISTERS, 120, 10) 120 | self.assertEqual(tuple(range(10)), result) 121 | result = self.master.execute(1, modbus_tk.defines.READ_INPUT_REGISTERS, 120, 1) 122 | self.assertEqual((0,), result) 123 | 124 | def testReadCoils(self): 125 | """Test that response for read coils function is ok""" 126 | self.slave1.set_values("c0-100", 0, [0, 1, 1]+[0, 1]*20) 127 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 0, 1) 128 | self.assertEqual((0,), result) 129 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 0, 3) 130 | self.assertEqual((0,1,1), result) 131 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 0, 8) 132 | self.assertEqual(tuple([0, 1, 1]+[0,1]*2+[0]), result) 133 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 0, 9) 134 | self.assertEqual(tuple([0, 1, 1]+[0,1]*3), result) 135 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 0, 20) 136 | self.assertEqual(tuple([0, 1, 1]+[0,1]*8+[0]), result) 137 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 0, 21) 138 | self.assertEqual(tuple([0, 1, 1]+[0,1]*9), result) 139 | 140 | def testReadManyCoils(self): 141 | """Test that response for read many coils function is ok""" 142 | self.slave1.set_values("c1000-4000", 1000, tuple([0,1]*1000)) 143 | 144 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 1000, 1) 145 | self.assertEqual(1, len(result)) 146 | self.assertEqual((0,), result) 147 | 148 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 1000, 8) 149 | self.assertEqual(8, len(result)) 150 | self.assertEqual(tuple([0,1]*4), result) 151 | 152 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 1000, 1999) 153 | self.assertEqual(1999, len(result)) 154 | self.assertEqual(tuple([0,1]*999+[0]), result) 155 | 156 | result = self.master.execute(1, modbus_tk.defines.READ_COILS, 1000, 2000) 157 | self.assertEqual(2000, len(result)) 158 | self.assertEqual(tuple([0,1]*1000), result) 159 | 160 | def testReadCoilsOutOfBlocks(self): 161 | """Test that an error is raised when reading an invalid address""" 162 | try: 163 | self.master.execute(1, modbus_tk.defines.READ_COILS, 500, 12) 164 | except modbus_tk.modbus.ModbusError, ex: 165 | self.assertEqual(ex.get_exception_code(), 2) 166 | return 167 | self.assert_(False) 168 | 169 | def testReadTooManyCoils(self): 170 | """Test that an error is raised when too many coils are read""" 171 | try: 172 | self.master.execute(1, modbus_tk.defines.READ_COILS, 1000, 2001) 173 | except modbus_tk.modbus.ModbusError, ex: 174 | self.assertEqual(ex.get_exception_code(), 3) 175 | return 176 | self.assert_(False) 177 | 178 | def testReadDiscreteInputs(self): 179 | """Test that response for read digital inputs function is ok""" 180 | result = self.master.execute(1, modbus_tk.defines.READ_DISCRETE_INPUTS, 100, 1) 181 | self.assertEqual((0,), result) 182 | self.slave1.set_values("di100-1", 100, 1) 183 | result = self.master.execute(1, modbus_tk.defines.READ_DISCRETE_INPUTS, 100, 1) 184 | self.assertEqual((1,), result) 185 | self.slave1.set_values("di200-10", 200, range(8)) 186 | result = self.master.execute(1, modbus_tk.defines.READ_DISCRETE_INPUTS, 200, 1) 187 | self.assertEqual((0,), result) 188 | result = self.master.execute(1, modbus_tk.defines.READ_DISCRETE_INPUTS, 200, 3) 189 | self.assertEqual((0,1,1), result) 190 | result = self.master.execute(1, modbus_tk.defines.READ_DISCRETE_INPUTS, 200, 8) 191 | self.assertEqual(tuple([0]+[1]*7), result) 192 | result = self.master.execute(1, modbus_tk.defines.READ_DISCRETE_INPUTS, 200, 10) 193 | self.assertEqual(tuple([0]+[1]*7+[0]*2), result) 194 | 195 | def testWriteSingleRegisters(self): 196 | """Write the values of a single register and check that it is correctly written""" 197 | result = self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_REGISTER, 0, output_value=54) 198 | self.assertEqual((0, 54), result) 199 | self.assertEqual((54,), self.slave1.get_values("hr0-100", 0, 1)) 200 | 201 | result = self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_REGISTER, 10, output_value=33) 202 | self.assertEqual((10, 33), result) 203 | self.assertEqual((33,), self.slave1.get_values("hr0-100", 10, 1)) 204 | 205 | def testWriteSingleRegisterNegativeValue(self): 206 | """Write a negative value of a single register and check that it is correctly written""" 207 | result = self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_REGISTER, 0, output_value=-123) 208 | self.assertEqual((0, 0x10000-123), result) 209 | self.assertEqual((0x10000-123,), self.slave1.get_values("hr0-100", 0, 1)) 210 | 211 | result = self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_REGISTER, 10, output_value=-23) 212 | self.assertEqual((10, 0x10000-23), result) 213 | self.assertEqual((0x10000-23,), self.slave1.get_values("hr0-100", 10, 1)) 214 | 215 | def testWriteSingleRegistersOutOfBlocks(self): 216 | """Check taht an error is raised when writing a register out of block""" 217 | try: 218 | self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_REGISTER, 500, output_value=11) 219 | except modbus_tk.modbus.ModbusError, ex: 220 | self.assertEqual(ex.get_exception_code(), 2) 221 | return 222 | self.assert_(False) 223 | 224 | def testWriteSingleCoil(self): 225 | """Write the values of coils and check that it is correctly written""" 226 | result = self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_COIL, 0, output_value=1) 227 | self.assertEqual((0, int("ff00", 16)), result) 228 | self.assertEqual((1,), self.slave1.get_values("c0-100", 0, 1)) 229 | 230 | result = self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_COIL, 22, output_value=1) 231 | self.assertEqual((22, int("ff00", 16)), result) 232 | self.assertEqual((1,), self.slave1.get_values("c0-100", 22, 1)) 233 | 234 | result = self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_COIL, 22, output_value=0) 235 | self.assertEqual((22, 0), result) 236 | self.assertEqual((0,), self.slave1.get_values("c0-100", 22, 1)) 237 | 238 | def testWriteSingleCoilOutOfBlocks(self): 239 | """Check taht an error is raised when writing a coil out of block""" 240 | try: 241 | self.master.execute(1, modbus_tk.defines.WRITE_SINGLE_COIL, 500, output_value=1) 242 | except modbus_tk.modbus.ModbusError, ex: 243 | self.assertEqual(ex.get_exception_code(), modbus_tk.defines.ILLEGAL_DATA_ADDRESS) 244 | return 245 | self.assert_(False) 246 | 247 | def testWriteMultipleRegisters(self): 248 | """Write the values of a multiple registers and check that it is correctly written""" 249 | result = self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_REGISTERS, 0, output_value=(54, )) 250 | self.assertEqual((0, 1), result) 251 | self.assertEqual((54,), self.slave1.get_values("hr0-100", 0, 1)) 252 | 253 | result = self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_REGISTERS, 10, output_value=range(20)) 254 | self.assertEqual((10, 20), result) 255 | self.assertEqual(tuple(range(20)), self.slave1.get_values("hr0-100", 10, 20)) 256 | 257 | result = self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_REGISTERS, 1000, output_value=range(123)) 258 | self.assertEqual((1000, 123), result) 259 | self.assertEqual(tuple(range(123)), self.slave1.get_values("hr1000-1500", 1000, 123)) 260 | 261 | def testWriteMultipleRegistersNegativeValue(self): 262 | """Write the values of a multiple registers with negative values and check that it is correctly written""" 263 | result = self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_REGISTERS, 0, output_value=(0, -5, 10)) 264 | self.assertEqual((0, 3), result) 265 | self.assertEqual((0, 0x10000-5, 10), self.slave1.get_values("hr0-100", 0, 3)) 266 | 267 | def testWriteMultipleRegistersOutOfBlocks(self): 268 | """Check that an error is raised when writing a register out of block""" 269 | try: 270 | self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_REGISTERS, 500, output_value=(11, 12)) 271 | except modbus_tk.modbus.ModbusError, ex: 272 | self.assertEqual(ex.get_exception_code(), modbus_tk.defines.ILLEGAL_DATA_ADDRESS) 273 | return 274 | self.assert_(False) 275 | 276 | def testWriteTooManyMultipleRegisters(self): 277 | """Check that an error is raised when writing too many registers""" 278 | try: 279 | self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_REGISTERS, 1000, output_value=range(124)) 280 | except modbus_tk.modbus.ModbusError, ex: 281 | self.assertEqual(ex.get_exception_code(), modbus_tk.defines.ILLEGAL_DATA_VALUE) 282 | return 283 | self.assert_(False) 284 | 285 | def testWriteMultipleCoils(self): 286 | """Write the values of a multiple coils and check that it is correctly written""" 287 | result = self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_COILS, 0, output_value=(1, )) 288 | self.assertEqual((0, 1), result) 289 | self.assertEqual((1,), self.slave1.get_values("c0-100", 0, 1)) 290 | 291 | result = self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_COILS, 10, output_value=[1]*20) 292 | self.assertEqual((10, 20), result) 293 | self.assertEqual(tuple([1]*20), self.slave1.get_values("c0-100", 10, 20)) 294 | 295 | result = self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_COILS, 1000, output_value=[1]*1968) 296 | self.assertEqual((1000, 1968), result) 297 | self.assertEqual(tuple([1]*1968), self.slave1.get_values("c1000-4000", 1000, 1968)) 298 | 299 | def testWriteMultipleCoilsIssue23(self): 300 | """Write the values of a multiple coils and check that it is correctly written""" 301 | slave10 = self.server.add_slave(10) 302 | slave10.add_block("block", modbus_tk.defines.COILS, 0, 256) 303 | result = self.master.execute(10, modbus_tk.defines.WRITE_MULTIPLE_COILS, 0, output_value=[0, 0, 1, 1]*8) 304 | self.assertEqual((0, 32), result) 305 | self.assertEqual(tuple([0, 0, 1, 1]*8), slave10.get_values("block", 0, 32)) 306 | 307 | def testWriteMultipleCoilsOutOfBlocks(self): 308 | """Check that an error is raised when writing a register out of block""" 309 | try: 310 | self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_COILS, 500, output_value=(11, 12)) 311 | except modbus_tk.modbus.ModbusError, ex: 312 | self.assertEqual(ex.get_exception_code(), modbus_tk.defines.ILLEGAL_DATA_ADDRESS) 313 | return 314 | self.assert_(False) 315 | 316 | def testWriteTooManyMultipleCoils(self): 317 | """Check that an error is raised when writing too many registers""" 318 | try: 319 | self.master.execute(1, modbus_tk.defines.WRITE_MULTIPLE_COILS, 1000, output_value=[1]*(int("7B0", 16)+1)) 320 | except modbus_tk.modbus.ModbusError, ex: 321 | self.assertEqual(ex.get_exception_code(), modbus_tk.defines.ILLEGAL_DATA_VALUE) 322 | return 323 | self.assert_(False) 324 | 325 | def testBroadcast(self): 326 | """Check that broadcast queries are handled correctly""" 327 | self.slave1.add_block("D", modbus_tk.defines.COILS, 5000, 50) 328 | self.slave1.add_block("A", modbus_tk.defines.HOLDING_REGISTERS, 5000, 50) 329 | 330 | self.slave5.add_block("D", modbus_tk.defines.COILS, 5000, 50) 331 | self.slave5.add_block("A", modbus_tk.defines.HOLDING_REGISTERS, 5000, 50) 332 | 333 | self.master.execute(0, modbus_tk.defines.WRITE_MULTIPLE_REGISTERS, 5000, output_value=range(20)) 334 | time.sleep(0.5) 335 | self.master.execute(0, modbus_tk.defines.WRITE_SINGLE_REGISTER, 5030, output_value=12) 336 | time.sleep(0.5) 337 | self.master.execute(0, modbus_tk.defines.WRITE_MULTIPLE_COILS, 5000, output_value=tuple([1]*10)) 338 | time.sleep(0.5) 339 | self.master.execute(0, modbus_tk.defines.WRITE_SINGLE_COIL, 5030, output_value=1) 340 | time.sleep(0.5) 341 | 342 | self.assertEqual(self.slave1.get_values("A", 5000, 20), tuple(range(20))) 343 | self.assertEqual(self.slave5.get_values("A", 5000, 20), tuple(range(20))) 344 | 345 | self.assertEqual(self.slave1.get_values("A", 5030, 1), (12,)) 346 | self.assertEqual(self.slave5.get_values("A", 5030, 1), (12,)) 347 | 348 | self.assertEqual(self.slave1.get_values("D", 5000, 10), tuple([1]*10)) 349 | self.assertEqual(self.slave5.get_values("D", 5000, 10), tuple([1]*10)) 350 | 351 | self.assertEqual(self.slave1.get_values("D", 5030, 1), (1,)) 352 | self.assertEqual(self.slave5.get_values("D", 5030, 1), (1,)) 353 | 354 | def testBroadcastReading(self): 355 | """Check that broadcast queries are handled correctly""" 356 | functions = (modbus_tk.defines.READ_HOLDING_REGISTERS, modbus_tk.defines.READ_COILS, 357 | modbus_tk.defines.READ_INPUT_REGISTERS, modbus_tk.defines.READ_DISCRETE_INPUTS) 358 | for fct in functions: 359 | self.assertEqual(None, self.master.execute(0, fct, 0, 5)) 360 | 361 | def testInvalidRequest(self): 362 | """Check that an error is returned when the request is invalid""" 363 | query = self.server._make_query() 364 | requests = ((chr(1), 0x81), (chr(3), 0x83), ("", 0x81)) 365 | for (request_pdu, rc) in requests: 366 | request = query.build_request(request_pdu, 1) 367 | response = self.server._databank.handle_request(query, request) 368 | expected_response = struct.pack(">BB", rc, modbus_tk.defines.SLAVE_DEVICE_FAILURE) 369 | self.assertEquals(expected_response, response) 370 | 371 | 372 | -------------------------------------------------------------------------------- /tests/functest_modbus_rtu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import unittest 13 | import modbus_tk 14 | import modbus_tk.modbus_rtu as modbus_rtu 15 | import modbus_tk.hooks as hooks 16 | import threading 17 | import struct 18 | import logging 19 | import modbus_tk.utils as utils 20 | import time 21 | import sys 22 | import serial 23 | from functest_modbus import TestQueries, TestQueriesSetupAndTeardown 24 | 25 | LOGGER = modbus_tk.utils.create_logger("udp") 26 | 27 | import os 28 | if os.name == "nt": 29 | SERVER_PORT = "COM1" 30 | MASTER_PORT = "COM2" 31 | elif os.name == "posix": 32 | SERVER_PORT = "/dev/ttyS0" 33 | MASTER_PORT = "/dev/ttyS1" 34 | else: 35 | raise Exception("The %d os is not supported yet" % (os.name)) 36 | 37 | class TestConnection(TestQueriesSetupAndTeardown, unittest.TestCase): 38 | def _get_server(self): 39 | return modbus_rtu.RtuServer(serial.Serial(port=SERVER_PORT, baudrate=9600)) 40 | 41 | def _get_master(self): 42 | return modbus_rtu.RtuMaster(serial.Serial(port=MASTER_PORT, baudrate=9600)) 43 | 44 | def testOpenConnection(self): 45 | """Check that master and server can open the serial port""" 46 | #close everything 47 | self.master.close() 48 | self.server.stop() 49 | time.sleep(1.0) 50 | 51 | self.master.open() 52 | self.master.open() 53 | 54 | def testErrorOnOpeningInUsePort(self): 55 | """Check that an error is raised if opening a port twice""" 56 | self.assertRaises(serial.SerialException, serial.Serial, SERVER_PORT) 57 | 58 | def testReadBlock(self): 59 | """Add 1 block on the slave and let's the master running the values""" 60 | slave = self.server.get_slave(1) 61 | slave.add_block("myblock", modbus_tk.defines.HOLDING_REGISTERS, 500, 100) 62 | slave.set_values("myblock", 500, range(100)) 63 | 64 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 500, 100) 65 | self.assertEqual(tuple(range(100)), result) 66 | 67 | self.master.close() 68 | self.server.stop() 69 | 70 | def testReopenMaster(self): 71 | """Check that master can open the serial port several times""" 72 | #close everything 73 | slave = self.server.get_slave(1) 74 | slave.add_block("myblock", modbus_tk.defines.HOLDING_REGISTERS, 500, 100) 75 | slave.set_values("myblock", 500, range(100)) 76 | 77 | for x in xrange(5): 78 | self.master.close() 79 | time.sleep(1.0) 80 | self.master.open() 81 | time.sleep(1.0) 82 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 500, 100) 83 | self.assertEqual(tuple(range(100)), result) 84 | 85 | def testReopenServer(self): 86 | """Check that server can open the serial port several times""" 87 | slave = self.server.get_slave(1) 88 | slave.add_block("myblock", modbus_tk.defines.HOLDING_REGISTERS, 500, 100) 89 | slave.set_values("myblock", 500, range(100)) 90 | 91 | for x in xrange(5): 92 | self.server.stop() 93 | time.sleep(1.0) 94 | self.server.start() 95 | time.sleep(1.0) 96 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 500, 100) 97 | self.assertEqual(tuple(range(100)), result) 98 | 99 | 100 | 101 | class TestRtuSpecific(TestQueriesSetupAndTeardown, unittest.TestCase): 102 | def _get_server(self): 103 | return modbus_rtu.RtuServer(serial.Serial(port=SERVER_PORT, baudrate=9600)) 104 | 105 | def _get_master(self): 106 | return modbus_rtu.RtuMaster(serial.Serial(port=MASTER_PORT, baudrate=9600)) 107 | 108 | def testExpectedLength(self): 109 | """check that expected length doesn't cause an error""" 110 | self.slave1.set_values("hr0-100", 0, range(100)) 111 | self.master.set_verbose(True) 112 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100, expected_length=205) 113 | self.master.set_verbose(False) 114 | self.assertEqual(tuple(range(100)), result) 115 | 116 | def testExpectedLengthTooShort(self): 117 | """check that an error is raised if expected_length is too low""" 118 | self.slave1.set_values("hr0-100", 0, range(100)) 119 | ok = True 120 | def check_length_hook(args): 121 | (master, response) = args 122 | LOGGER.debug("expected: %d - actual: %d", check_length_hook.expected_length, len(response)) 123 | check_length_hook.test.assertEqual(check_length_hook.expected_length, len(response)) 124 | 125 | check_length_hook.test = self 126 | hooks.install_hook("modbus_rtu.RtuMaster.after_recv", check_length_hook) 127 | 128 | for x in (5, 204): 129 | try: 130 | check_length_hook.expected_length = x 131 | self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100, expected_length=x) 132 | except: 133 | pass 134 | else: 135 | ok = False 136 | hooks.uninstall_hook("modbus_rtu.RtuMaster.after_recv", check_length_hook) 137 | 138 | self.assert_(ok) 139 | 140 | def testExpectedLengthTooLong(self): 141 | """check that no impact if expected_length is too high""" 142 | self.slave1.set_values("hr0-100", 0, range(100)) 143 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100, expected_length=3000) 144 | self.assertEqual(tuple(range(100)), result) 145 | 146 | def testReadWithWrongFunction(self): 147 | """check that an error is raised when sending a query with an invalid function code""" 148 | self.assertRaises(modbus_tk.modbus.ModbusFunctionNotSupportedError, self.master.execute, 1, 55, 0, 10) 149 | bad_query = struct.pack(">BB", 1, 55) 150 | crc = struct.pack(">H", utils.calculate_crc(bad_query)) 151 | bad_query += crc 152 | try: 153 | self.master._send(bad_query) 154 | self.master._recv() 155 | except modbus_tk.modbus.ModbusError, ex: 156 | self.assertEqual(ex.get_exception_code(), 1) 157 | return 158 | 159 | def testWriteSingleCoilInvalidValue(self): 160 | """Check that an error is raised when writing a coil with an invalid value""" 161 | bad_query = struct.pack(">BBHH", 1, modbus_tk.defines.WRITE_SINGLE_COIL, 0, 1) 162 | crc = struct.pack(">H", utils.calculate_crc(bad_query)) 163 | bad_query += crc 164 | self.master.set_verbose(True) 165 | self.master._send(bad_query) 166 | response = self.master._recv() 167 | self.assertEqual(response[:-2], struct.pack(">BBB", 1, modbus_tk.defines.WRITE_SINGLE_COIL+128, 3)) 168 | 169 | def testMultiThreadAccess(self): 170 | """check that the modbus call are thread safe""" 171 | 172 | slaves = [] 173 | slaves.append(self.server.add_slave(11)) 174 | slaves.append(self.server.add_slave(12)) 175 | import Queue 176 | 177 | q = Queue.Queue() 178 | 179 | for s in slaves: 180 | s.add_block("a", modbus_tk.defines.HOLDING_REGISTERS, 0, 100) 181 | 182 | def set_val(self_, slaves, q): 183 | try: 184 | id = 11 185 | for i in xrange(5): 186 | for s in slaves: 187 | s.set_values("a", 0, [i]*100) 188 | result = self_.master.execute(id, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100) 189 | id += 1 190 | if id > 12: id = 11 191 | except Exception, msg: 192 | LOGGER.error(msg) 193 | q.put(1) 194 | 195 | threads = [threading.Thread(target=set_val, args=(self, slaves, q)) for i in xrange(3)] 196 | for t in threads: t.start() 197 | LOGGER.debug("all threads have been started") 198 | for t in threads: t.join() 199 | LOGGER.debug("all threads have done") 200 | self.assert_(q.empty()) 201 | 202 | 203 | class RtuTestQueries(TestQueries, unittest.TestCase): 204 | """Test the modbus protocol over RTU communication""" 205 | def _get_server(self): 206 | port = serial.Serial(port=SERVER_PORT, baudrate=9600, bytesize=8, parity='N', stopbits=1, xonxoff=0) 207 | server = modbus_rtu.RtuServer(port) 208 | #server.set_verbose(True) 209 | return server 210 | 211 | def _get_master(self): 212 | port = serial.Serial(port=MASTER_PORT, baudrate=9600, bytesize=8, parity='N', stopbits=1, xonxoff=0) 213 | master = modbus_rtu.RtuMaster(port) 214 | #master.set_verbose(True) 215 | return master 216 | 217 | if __name__ == '__main__': 218 | unittest.main(argv = sys.argv) 219 | -------------------------------------------------------------------------------- /tests/functest_modbus_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import unittest 13 | import modbus_tk 14 | import modbus_tk.modbus_tcp as modbus_tcp 15 | import threading 16 | import struct 17 | import logging 18 | import socket 19 | import modbus_tk.utils as utils 20 | import time 21 | import sys 22 | from functest_modbus import TestQueries, TestQueriesSetupAndTeardown 23 | 24 | LOGGER = modbus_tk.utils.create_logger("udp") 25 | 26 | class TestConnection(unittest.TestCase): 27 | """Test the TcpMbap class""" 28 | def setUp(self): 29 | self.server = modbus_tcp.TcpServer() 30 | self.master = modbus_tcp.TcpMaster() 31 | 32 | def tearDown(self): 33 | self.master.close() 34 | self.server.stop() 35 | 36 | def testConnectOnSlave(self): 37 | """Setup a slave and check that the master can connect""" 38 | self.server.start() 39 | time.sleep(1.0) 40 | 41 | self.master.set_timeout(1.0) 42 | self.master.open() 43 | time.sleep(1.0) 44 | 45 | #close everything 46 | self.master.close() 47 | self.server.stop() 48 | time.sleep(1.0) 49 | 50 | #and try to reconnect --> should fail 51 | try: 52 | self.master.open() 53 | except socket.error, message: 54 | return 55 | self.assert_(False) 56 | 57 | def testConnectionErrorNoTimeoutDefined(self): 58 | """Check that an error is raised on connection error""" 59 | master = modbus_tcp.TcpMaster() 60 | try: 61 | master.open() 62 | except socket.error, message: 63 | return 64 | self.assert_(False) 65 | 66 | def testReadBlock(self): 67 | """Add 1 block on the slave and let's the master running the values""" 68 | slave = self.server.add_slave(1) 69 | slave.add_block("hr0-100", modbus_tk.defines.HOLDING_REGISTERS, 0, 100) 70 | slave.set_values("hr0-100", 0, range(100)) 71 | 72 | self.server.start() 73 | time.sleep(1.0) 74 | self.master.open() 75 | time.sleep(1.0) 76 | 77 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100) 78 | self.assertEqual(tuple(range(100)), result) 79 | 80 | self.master.close() 81 | self.server.stop() 82 | 83 | def testCleanConnections(self): 84 | """Check that the server is cleaning closed connections""" 85 | self.server.start() 86 | time.sleep(1.0) 87 | 88 | masters = [modbus_tcp.TcpMaster() for i in xrange(10)] 89 | for m in masters: 90 | m.open() 91 | 92 | for m in masters: 93 | m.close() 94 | 95 | time.sleep(5.0) 96 | self.assertEqual(1, len(self.server._sockets)) 97 | 98 | def testReopenMaster(self): 99 | """Check that master can open the connection several times""" 100 | slave = self.server.add_slave(1) 101 | slave.add_block("myblock", modbus_tk.defines.HOLDING_REGISTERS, 500, 100) 102 | slave.set_values("myblock", 500, range(100)) 103 | 104 | self.server.start() 105 | time.sleep(1.0) 106 | 107 | self.master.set_timeout(1.0) 108 | self.master.open() 109 | 110 | for x in xrange(5): 111 | self.master.close() 112 | time.sleep(1.0) 113 | self.master.open() 114 | time.sleep(1.0) 115 | result = self.master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 500, 100) 116 | self.assertEqual(tuple(range(100)), result) 117 | 118 | def testReopenServer(self): 119 | """Check that server can open the connection several times""" 120 | slave = self.server.add_slave(1) 121 | slave.add_block("myblock", modbus_tk.defines.HOLDING_REGISTERS, 500, 100) 122 | slave.set_values("myblock", 500, range(100)) 123 | 124 | self.server.start() 125 | time.sleep(1.0) 126 | 127 | for x in xrange(3): 128 | time.sleep(1.0) 129 | self.server.stop() 130 | time.sleep(1.0) 131 | self.server.start() 132 | time.sleep(1.0) 133 | 134 | m = modbus_tcp.TcpMaster() 135 | m.open() 136 | result = m.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 500, 100) 137 | self.assertEqual(tuple(range(100)), result) 138 | 139 | 140 | class TcpTestQueries(TestQueries, unittest.TestCase): 141 | """Test the modbus protocol over TCP communication""" 142 | def _get_server(self): 143 | return modbus_tcp.TcpServer() 144 | 145 | def _get_master(self): 146 | return modbus_tcp.TcpMaster() 147 | 148 | 149 | class TestTcpSpecific(TestQueriesSetupAndTeardown, unittest.TestCase): 150 | 151 | def _get_server(self): 152 | return modbus_tcp.TcpServer() 153 | 154 | def _get_master(self): 155 | return modbus_tcp.TcpMaster() 156 | 157 | def testReadWithWrongFunction(self): 158 | """check that an error is raised where reading on 2 consecutive blocks""" 159 | self.assertRaises(modbus_tk.modbus.ModbusFunctionNotSupportedError, self.master.execute, 1, 55, 0, 10) 160 | bad_query = struct.pack(">HHHBH", 0, 0, 3, 0, 55) 161 | try: 162 | self.master._sock.send(bad_query) 163 | except modbus_tk.modbus.ModbusError, ex: 164 | self.assertEqual(ex.get_exception_code(), 1) 165 | return 166 | 167 | def testMultiThreadAccess(self): 168 | """check that the modbus call are thread safe""" 169 | 170 | slaves = [] 171 | slaves.append(self.server.add_slave(11)) 172 | slaves.append(self.server.add_slave(12)) 173 | import Queue 174 | 175 | q = Queue.Queue() 176 | 177 | for s in slaves: 178 | s.add_block("a", modbus_tk.defines.HOLDING_REGISTERS, 0, 100) 179 | 180 | def set_val(self_, slaves, q): 181 | try: 182 | LOGGER.debug("set_val started") 183 | id = 11 184 | for i in xrange(50): 185 | for s in slaves: 186 | s.set_values("a", 0, [i]*100) 187 | result = self_.master.execute(id, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100) 188 | id += 1 189 | if id > 12: id = 11 190 | #time.sleep(0.2) 191 | except Exception, msg: 192 | LOGGER.error(msg) 193 | q.put(1) 194 | 195 | threads = [threading.Thread(target=set_val, args=(self, slaves, q)) for i in xrange(5)] 196 | for t in threads: t.start() 197 | LOGGER.debug("all threads have been started") 198 | for t in threads: t.join() 199 | LOGGER.debug("all threads have done") 200 | self.assert_(q.empty()) 201 | 202 | def testWriteSingleCoilInvalidValue(self): 203 | """Check taht an error is raised when writing a coil with an invalid value""" 204 | self.master._send(struct.pack(">HHHBBHH", 0, 0, 6, 1, modbus_tk.defines.WRITE_SINGLE_COIL, 0, 1)) 205 | response = self.master._recv() 206 | self.assertEqual(response, struct.pack(">HHHBBB", 0, 0, 3, 1, modbus_tk.defines.WRITE_SINGLE_COIL+128, 3)) 207 | 208 | if __name__ == '__main__': 209 | unittest.main(argv = sys.argv) 210 | -------------------------------------------------------------------------------- /tests/perftest_modbus_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import unittest 13 | import modbus_tk 14 | import modbus_tk.modbus_tcp as modbus_tcp 15 | import threading 16 | import struct 17 | import logging 18 | import socket 19 | import modbus_tk.utils as utils 20 | import Queue 21 | import time 22 | import sys 23 | 24 | LOGGER = modbus_tk.utils.create_logger() 25 | 26 | class TestStress(unittest.TestCase): 27 | """Test the TcpMbap class""" 28 | 29 | def setUp(self): 30 | self.server = modbus_tcp.TcpServer() 31 | 32 | def tearDown(self): 33 | self.server.stop() 34 | 35 | def testGarbageData(self): 36 | """Send Garbage data and make sure that it doesn't kill everything""" 37 | slave1 = self.server.add_slave(1) 38 | slave1.add_block("c0-100", modbus_tk.defines.COILS, 0, 100) 39 | self.server.set_verbose(True) 40 | self.server.start() 41 | time.sleep(1.0) 42 | 43 | master1 = modbus_tcp.TcpMaster() 44 | master2 = modbus_tcp.TcpMaster() 45 | master1.open() 46 | master2.open() 47 | 48 | master1._send("hello world!") 49 | 50 | result = master2.execute(1, modbus_tk.defines.WRITE_SINGLE_COIL, 0, output_value=1) 51 | self.assertEqual((0, int("ff00", 16)), result) 52 | values = slave1.get_values("c0-100", 0, 1) 53 | self.assertEqual((1,), values) 54 | 55 | master1.close() 56 | master2.close() 57 | 58 | def testSeveralClients(self): 59 | """check that the server can serve 15 clients in parallel""" 60 | 61 | masters = [modbus_tcp.TcpMaster(timeout_in_sec=5.0)] * 15 62 | 63 | slave = self.server.add_slave(1) 64 | 65 | q = Queue.Queue() 66 | 67 | slave.add_block("a", modbus_tk.defines.HOLDING_REGISTERS, 0, 100) 68 | slave.set_values("a", 0, range(100)) 69 | 70 | self.server.start() 71 | time.sleep(1.0) 72 | 73 | def read_vals(master): 74 | try: 75 | for i in xrange(100): 76 | result = master.execute(1, modbus_tk.defines.READ_HOLDING_REGISTERS, 0, 100) 77 | if result != tuple(range(100)): 78 | q.put(1) 79 | time.sleep(0.1) 80 | except Exception, msg: 81 | LOGGER.error(msg) 82 | q.put(1) 83 | 84 | threads = [] 85 | for m in masters: 86 | threads.append(threading.Thread(target=read_vals, args=(m, ))) 87 | for t in threads: t.start() 88 | LOGGER.debug("all threads have been started") 89 | for t in threads: t.join() 90 | LOGGER.debug("all threads have done") 91 | self.assert_(q.empty()) 92 | 93 | if __name__ == '__main__': 94 | unittest.main(argv = sys.argv) 95 | -------------------------------------------------------------------------------- /tests/unittest_modbus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import unittest 13 | import modbus_tk.modbus 14 | import threading 15 | import struct 16 | import logging 17 | import time 18 | import sys 19 | from modbus_tk.hooks import install_hook 20 | 21 | LOGGER = modbus_tk.utils.create_logger("udp") 22 | 23 | class TestSlaveRequestHandler(unittest.TestCase): 24 | def setUp(self): 25 | self._slave = modbus_tk.modbus.Slave(0) 26 | self._name = "toto" 27 | 28 | def tearDown(self): 29 | pass 30 | 31 | def testUnhandledFunction(self): 32 | """test that an error is sent back when using an unknown function""" 33 | func_code = 55 34 | response = struct.pack(">BB", 128+func_code, modbus_tk.defines.ILLEGAL_FUNCTION) 35 | self.assertEqual(response, self._slave.handle_request(struct.pack(">B", func_code))) 36 | self.assertEqual(response, self._slave.handle_request(struct.pack(">BHHHH", func_code, 1, 2, 3, 4))) 37 | 38 | def _read_digital_data(self, function, block_type): 39 | self._slave.add_block(self._name, block_type, 0, 128) 40 | list_of_coils = ((1, 0, 2, 1), (0, ), (1, ), [0, 1]*20, [1]*128, [1, 0, 1]*7, (1, 0, 0, 1), [1, 0]*20) 41 | starting_addresses = (0, 0, 127, 40, 0, 0, 124, 87) 42 | 43 | list_of_responses = (struct.pack(">BBB", function, 1, 13), 44 | struct.pack(">BBB", function, 1, 0), 45 | struct.pack(">BBB", function, 1, 1), 46 | struct.pack(">BBBBBBB", function, 5, 170, 170, 170, 170, 170), 47 | struct.pack(">BB", function, 16)+(struct.pack(">B", 255)*16), 48 | struct.pack(">BBBBB", function, 3, 109, 219, 22), 49 | struct.pack(">BBB", function, 1, 9), 50 | struct.pack(">BBBBBBB", function, 5, 85, 85, 85, 85, 85), 51 | ) 52 | for i in xrange(len(list_of_coils)): 53 | self._slave.set_values(self._name, starting_addresses[i], list_of_coils[i]) 54 | self.assertEqual(list_of_responses[i], self._slave.handle_request(struct.pack(">BHH", function, starting_addresses[i], len(list_of_coils[i])))) 55 | 56 | def _read_out_of_blocks(self, function, block_type): 57 | self._slave.add_block(self._name, block_type, 20, 80) 58 | list_of_ranges = ((200, 10), (0, 1), (100, 1), (100, 5), (60, 50), (10, 30)) 59 | 60 | response = struct.pack(">BB", 128+function, modbus_tk.defines.ILLEGAL_DATA_ADDRESS) 61 | 62 | for r in list_of_ranges: 63 | self.assertEqual(response, self._slave.handle_request(struct.pack(">BHH", function, r[0], r[1]))) 64 | 65 | def _read_continuous_blocks(self, function, block_type): 66 | self._slave.add_block(self._name+"1", block_type, 0, 20) 67 | self._slave.add_block(self._name+"2", block_type, 20, 80) 68 | self._slave.add_block(self._name+"3", block_type, 100, 20) 69 | 70 | list_of_ranges = ((0, 30), (10, 20), (0, 120), (80, 30)) 71 | 72 | response = struct.pack(">BB", 128+function, modbus_tk.defines.ILLEGAL_DATA_ADDRESS) 73 | 74 | for r in list_of_ranges: 75 | self.assertEqual(response, self._slave.handle_request(struct.pack(">BHH", function, r[0], r[1]))) 76 | 77 | def testHandleReadCoils(self): 78 | """test that the correct response pdu is sent when receiving a pdu for reading coils""" 79 | self._read_digital_data(modbus_tk.defines.READ_COILS, modbus_tk.defines.COILS) 80 | 81 | def testHandleReadDigitalInputs(self): 82 | """test that the correct response pdu is sent when receiving a pdu for reading discrete inputs""" 83 | self._read_digital_data(modbus_tk.defines.READ_DISCRETE_INPUTS, modbus_tk.defines.DISCRETE_INPUTS) 84 | 85 | def testHandleReadCoilsOutOfBlocks(self): 86 | """test that an error response pdu is sent when receiving a pdu for reading coils at an unknown addresses""" 87 | self._read_out_of_blocks(modbus_tk.defines.READ_COILS, modbus_tk.defines.COILS) 88 | 89 | def testHandleReadDiscreteInputsOutOfBlocks(self): 90 | """test that an error response pdu is sent when receiving a pdu for reading discrete inputs at an unknown addresses""" 91 | self._read_out_of_blocks(modbus_tk.defines.READ_DISCRETE_INPUTS, modbus_tk.defines.DISCRETE_INPUTS) 92 | 93 | def testHandleReadCoilsOnContinuousBlocks(self): 94 | """test that an error response pdu is sent when receiving a pdu for reading coils at an address shared on distinct blocks""" 95 | self._read_continuous_blocks(modbus_tk.defines.READ_COILS, modbus_tk.defines.COILS) 96 | 97 | def testHandleReadDiscreteInputsOnContinuousBlocks(self): 98 | """test that an error response pdu is sent when receiving a pdu for reading discrete inputs at an address shared on distinct blocks""" 99 | self._read_continuous_blocks(modbus_tk.defines.READ_DISCRETE_INPUTS, modbus_tk.defines.DISCRETE_INPUTS) 100 | 101 | def _make_response(self, function, regs): 102 | response = struct.pack(">BB", function, 2*len(regs)) 103 | for r in regs: 104 | response += struct.pack(">H", r) 105 | return response 106 | 107 | def _read_registers(self, function, block_type): 108 | self._slave.add_block(self._name, block_type, 0, 128) 109 | list_of_regs = ((20, 2, 19, 75, 42), (15, ), [11, 12]*20, range(125), (27, ), (1, 2, 3, 4), range(10)) 110 | starting_addresses = (0, 0, 0, 0, 127, 123, 82) 111 | 112 | for i in xrange(len(list_of_regs)): 113 | self._slave.set_values(self._name, starting_addresses[i], list_of_regs[i]) 114 | self.assertEqual(self._make_response(function, list_of_regs[i]), self._slave.handle_request(struct.pack(">BHH", function, starting_addresses[i], len(list_of_regs[i])))) 115 | 116 | def testHandleReadHoldingRegisters(self): 117 | """test that the correct response pdu is sent when receiving a pdu for reading holding registers""" 118 | self._read_registers(modbus_tk.defines.READ_HOLDING_REGISTERS, modbus_tk.defines.HOLDING_REGISTERS) 119 | 120 | def testHandleReadAnalogInputs(self): 121 | """test that the correct response pdu is sent when receiving a pdu for reading input registers""" 122 | self._read_registers(modbus_tk.defines.READ_INPUT_REGISTERS, modbus_tk.defines.ANALOG_INPUTS) 123 | 124 | def _read_too_many_registers(self, function, block_type): 125 | self._slave.add_block(self._name, block_type, 0, 128) 126 | self._slave.set_values(self._name, 0, range(128)) 127 | response = struct.pack(">BB", function+128, modbus_tk.defines.ILLEGAL_DATA_VALUE) 128 | self.assertEqual(response, self._slave.handle_request(struct.pack(">BHH", function, 0, 126))) 129 | 130 | def testHandleReadTooManyHoldingRegisters(self): 131 | """test that an error is returned when handling a pdu for reading more than 125 holding registers""" 132 | self._read_too_many_registers(modbus_tk.defines.READ_HOLDING_REGISTERS, modbus_tk.defines.HOLDING_REGISTERS) 133 | 134 | def testHandleReadTooManyAnalogInputs(self): 135 | """test that an error is returned when handling a pdu for reading more than 125 input registers""" 136 | self._read_too_many_registers(modbus_tk.defines.READ_INPUT_REGISTERS, modbus_tk.defines.ANALOG_INPUTS) 137 | 138 | def testHandleReadHoldingRegistersOutOfBlocks(self): 139 | """test that an error is returned when handling a pdu for reading out of blocks""" 140 | self._read_out_of_blocks(modbus_tk.defines.READ_HOLDING_REGISTERS, modbus_tk.defines.HOLDING_REGISTERS) 141 | 142 | def testHandleReadInputRegistersOutOfBlocks(self): 143 | """test that an error is returned when handling a pdu for reading reading out of blocks""" 144 | self._read_out_of_blocks(modbus_tk.defines.READ_INPUT_REGISTERS, modbus_tk.defines.ANALOG_INPUTS) 145 | 146 | def testHandleReadHoldingRegistersOnContinuousBlocks(self): 147 | """test that an error response pdu is sent when receiving a pdu for reading coils at an address shared on distinct blocks""" 148 | self._read_continuous_blocks(modbus_tk.defines.READ_HOLDING_REGISTERS, modbus_tk.defines.HOLDING_REGISTERS) 149 | 150 | def testHandleReadInputregistersOnContinuousBlocks(self): 151 | """test that an error response pdu is sent when receiving a pdu for reading discrete inputs at an address shared on distinct blocks""" 152 | self._read_continuous_blocks(modbus_tk.defines.READ_INPUT_REGISTERS, modbus_tk.defines.ANALOG_INPUTS) 153 | 154 | 155 | class TestSlaveBlocks(unittest.TestCase): 156 | def setUp(self): 157 | self._slave = modbus_tk.modbus.Slave(0) 158 | self._name = "toto" 159 | self._block_types = (modbus_tk.defines.COILS, modbus_tk.defines.DISCRETE_INPUTS, 160 | modbus_tk.defines.HOLDING_REGISTERS, modbus_tk.defines.ANALOG_INPUTS) 161 | 162 | def tearDown(self): 163 | pass 164 | 165 | def testAddBlock(self): 166 | """Add a block and check that it is added""" 167 | self._slave.add_block(self._name, modbus_tk.defines.COILS, 0, 100) 168 | self.assert_(self._slave._get_block(self._name)) 169 | 170 | def testRemoveBlock(self): 171 | """Add a block and remove it and make sure that it is not registred anymore""" 172 | self._slave.add_block(self._name, modbus_tk.defines.COILS, 0, 100) 173 | self.assert_(self._slave._get_block(self._name)) 174 | self._slave.remove_block(self._name) 175 | self.assertRaises(modbus_tk.modbus.MissingKeyError, self._slave._get_block, (self._name)) 176 | 177 | def testAddBlockWithSameName(self): 178 | """Add a block and make sure that adding another block with teh same name fails""" 179 | self._slave.add_block(self._name, modbus_tk.defines.COILS, 0, 100) 180 | self.assertRaises(modbus_tk.modbus.DuplicatedKeyError, self._slave.add_block, self._name, modbus_tk.defines.COILS, 100, 100) 181 | 182 | def testAddAndRemoveBlocks(self): 183 | """Add 30 blocks and remove them""" 184 | count = 30 185 | for i in xrange(count): 186 | self._slave.add_block(self._name+str(i), modbus_tk.defines.COILS, 100*i, 100) 187 | 188 | for i in xrange(count): 189 | name = self._name+str(i) 190 | self.assert_(self._slave._get_block(name)) 191 | self._slave.remove_block(name) 192 | self.assertRaises(modbus_tk.modbus.MissingKeyError, self._slave._get_block, name) 193 | 194 | def testAddBlocksOfType(self): 195 | """Add a block of each type and remove them""" 196 | for i in self._block_types: 197 | self._slave.add_block(self._name+str(i), i, 0, 100) 198 | 199 | for i in self._block_types: 200 | name = self._name+str(i) 201 | self.assert_(self._slave._get_block(name)) 202 | self._slave.remove_block(name) 203 | self.assertRaises(modbus_tk.modbus.MissingKeyError, self._slave._get_block, name) 204 | 205 | def testAddUnsupportedBlock(self): 206 | """Add a block with a wrong type""" 207 | self.assert_(5 not in self._block_types) 208 | self.assertRaises(modbus_tk.modbus.InvalidModbusBlockError, self._slave.add_block, self._name, 5, 100, 100) 209 | self.assertRaises(modbus_tk.modbus.MissingKeyError, self._slave._get_block, (self._name)) 210 | 211 | def testAddWrongAddress(self): 212 | """Add a block with a wrong addresss""" 213 | for i in self._block_types: 214 | self.assertRaises(modbus_tk.modbus.InvalidArgumentError, self._slave.add_block, self._name, i, -5, 100) 215 | self.assertRaises(modbus_tk.modbus.MissingKeyError, self._slave._get_block, (self._name)) 216 | 217 | def testAddWrongSize(self): 218 | """Add a block with a wrong size""" 219 | for i in self._block_types: 220 | self.assertRaises(modbus_tk.modbus.InvalidArgumentError, self._slave.add_block, self._name, i, 0, 0) 221 | self.assertRaises(modbus_tk.modbus.MissingKeyError, self._slave._get_block, (self._name)) 222 | self.assertRaises(modbus_tk.modbus.InvalidArgumentError, self._slave.add_block, self._name, i, 0, -10) 223 | self.assertRaises(modbus_tk.modbus.MissingKeyError, self._slave._get_block, (self._name)) 224 | 225 | def testOverlappedBlocks(self): 226 | """Add 2 blocks with overlapped ranges and check that the 2nd one is not added""" 227 | for i in self._block_types: 228 | self._slave.add_block(self._name, i, 0, 100) 229 | for j in xrange(100): 230 | self.assertRaises(modbus_tk.modbus.OverlapModbusBlockError, self._slave.add_block, self._name+"_", i, j, 100) 231 | self._slave.remove_block(self._name) 232 | 233 | def testAddContinuousBlock(self): 234 | """Add 2 continuous blocks and check that it is ok""" 235 | for i in self._block_types: 236 | self._slave.add_block(self._name, i, 0, 100) 237 | self._slave.add_block(self._name+"_", i, 100, 100) 238 | self._slave.remove_block(self._name) 239 | self._slave.remove_block(self._name+"_") 240 | 241 | def testMultiThreadedAccess(self): 242 | """test mutual access""" 243 | def add_blocks(slave, name, starting_address): 244 | slave.add_block(name, modbus_tk.defines.COILS, starting_address, 10) 245 | threads = [] 246 | for i in xrange(10): 247 | threads.append(threading.Thread(target=add_blocks, args=(self._slave, self._name+str(i), i*10))) 248 | threads[i].start() 249 | 250 | for t in threads: 251 | t.join() 252 | 253 | for i in xrange(10): 254 | self._slave.remove_block(self._name+str(i)) 255 | 256 | def testSetAndGetRegister(self): 257 | """change the value of a register and check that it is properly set""" 258 | self._slave.add_block(self._name, modbus_tk.defines.HOLDING_REGISTERS, 0, 100) 259 | self.assertEqual(self._slave.get_values(self._name, 10, 1), (0, )) 260 | self._slave.set_values(self._name, 10, 2) 261 | self.assertEqual(self._slave.get_values(self._name, 10, 1), (2, )) 262 | 263 | def testSetAndGetSeveralRegisters(self): 264 | """change the value of several registers and check that it is properly set""" 265 | self._slave.add_block(self._name, modbus_tk.defines.HOLDING_REGISTERS, 0, 100) 266 | self.assertEqual(self._slave.get_values(self._name, 10, 10), tuple([0]*10)) 267 | self._slave.set_values(self._name, 10, range(0, 10)) 268 | self.assertEqual(self._slave.get_values(self._name, 10, 10), tuple(range(10))) 269 | 270 | def testSetAndGetSeveralCoils(self): 271 | """change the value of several coils and check that it is properly set""" 272 | self._slave.add_block(self._name, modbus_tk.defines.COILS, 0, 100) 273 | self.assertEqual(self._slave.get_values(self._name, 10, 10), tuple([0]*10)) 274 | self._slave.set_values(self._name, 10, [1]*5) 275 | self.assertEqual(self._slave.get_values(self._name, 10, 10), tuple([1]*5+[0]*5)) 276 | 277 | def testSetRegisterOutOfBounds(self): 278 | """change the value of a register out of a block and check that error are raised""" 279 | self._slave.add_block(self._name, modbus_tk.defines.HOLDING_REGISTERS, 20, 80) 280 | self.assertRaises(modbus_tk.modbus.OutOfModbusBlockError, self._slave.set_values, self._name, 100, 2) 281 | self.assertRaises(modbus_tk.modbus.OutOfModbusBlockError, self._slave.set_values, self._name, 105, 2) 282 | self.assertRaises(modbus_tk.modbus.OutOfModbusBlockError, self._slave.set_values, self._name, 95, [1]*10) 283 | self.assertRaises(modbus_tk.modbus.OutOfModbusBlockError, self._slave.set_values, self._name, 0, [1]*10) 284 | self.assertRaises(modbus_tk.modbus.OutOfModbusBlockError, self._slave.set_values, self._name, 15, [1]*10) 285 | 286 | def testSetRegisterAtTheBounds(self): 287 | """change the values on limits of the block and check that it is properly set""" 288 | self._slave.add_block(self._name, modbus_tk.defines.HOLDING_REGISTERS, 20, 80) 289 | self._slave.set_values(self._name, 20, 2) 290 | self._slave.set_values(self._name, 99, 2) 291 | 292 | def testSetRegisterOnContinuousBlocks(self): 293 | """create 2 continuous blocks and check that an error is raised when accessing adress range on the 2 blocks""" 294 | self._slave.add_block(self._name, modbus_tk.defines.HOLDING_REGISTERS, 20, 80) 295 | self._slave.add_block(self._name+"_", modbus_tk.defines.HOLDING_REGISTERS, 0, 20) 296 | self.assertRaises(modbus_tk.modbus.OutOfModbusBlockError, self._slave.set_values, self._name, 15, [1]*10) 297 | 298 | def testMultiThreadedSetValues(self): 299 | """test that set and get values is thread safe""" 300 | self._slave.add_block(self._name, modbus_tk.defines.HOLDING_REGISTERS, 0, 20) 301 | 302 | all_values = [] 303 | count = 20 304 | nb_of_vals = 2 305 | 306 | def change_val(slave, name, count, nb_of_vals): 307 | for i in xrange(0, count, nb_of_vals): 308 | vals = range(i, i+nb_of_vals) 309 | slave.set_values(name, 0, vals) 310 | time.sleep(0.02) 311 | 312 | def get_val(slave, name, count, nb_of_vals, all_values): 313 | for i in xrange(0, count, nb_of_vals): 314 | for j in xrange(2): 315 | vals = slave.get_values(name, 0, nb_of_vals) 316 | all_values.append(vals) 317 | time.sleep(0.01) 318 | 319 | threads = [] 320 | threads.append(threading.Thread(target=change_val, args=(self._slave, self._name, count, nb_of_vals))) 321 | threads.append(threading.Thread(target=get_val, args=(self._slave, self._name, count, nb_of_vals, all_values))) 322 | 323 | for t in threads: 324 | t.start() 325 | 326 | for t in threads: 327 | t.join() 328 | 329 | vals = [] 330 | expected_values = [] 331 | for i in xrange(0, count, nb_of_vals): 332 | expected_values.append(tuple(range(i, i+nb_of_vals))) 333 | 334 | def avoid_duplicates(x): 335 | if x not in vals: vals.append(x) 336 | map(avoid_duplicates, all_values) 337 | 338 | self.assertEqual(vals, expected_values) 339 | 340 | class TestServer(unittest.TestCase): 341 | def setUp(self): 342 | self.server = modbus_tk.modbus.Server() 343 | 344 | def tearDown(self): 345 | pass 346 | 347 | def testInvalidSlaveId(self): 348 | """Check that an error is raised when adding a slave with a wrong id""" 349 | slaves = (-5, 0, "", 256, 5600) 350 | for s in slaves: 351 | self.assertRaises(Exception, self.server.add_slave, s) 352 | 353 | def testAddSlave(self): 354 | """Check that a slave is added correctly""" 355 | slaves = range(1, 256) 356 | for id in slaves: 357 | s = self.server.add_slave(id) 358 | self.assert_(str(s).find("modbus_tk.modbus.Slave")>0) 359 | 360 | def testAddAndGetSlave(self): 361 | """Check that a slave can be retrieved by id after added""" 362 | slaves = range(1, 248) 363 | d = {} 364 | for id in slaves: 365 | d[id] = self.server.add_slave(id) 366 | for id in slaves: 367 | s = self.server.get_slave(id) 368 | self.assert_(s is d[id]) 369 | 370 | def testErrorOnRemoveUnknownSlave(self): 371 | """Check that an error is raised when removing a slave with a wrong id""" 372 | slaves = range(0, 249) 373 | for id in slaves: 374 | self.assertRaises(Exception, self.server.remove_slave, id) 375 | 376 | def testAddAndRemove(self): 377 | """Add a slave, remove it and make sure it is not there anymore""" 378 | slaves = range(1, 248) 379 | for id in slaves: 380 | self.server.add_slave(id) 381 | for id in slaves: 382 | self.server.remove_slave(id) 383 | for id in slaves: 384 | self.assertRaises(Exception, self.server.get_slave, id) 385 | 386 | def testRemoveAllSlaves(self): 387 | """Add somes slave, remove all and make sure it there is nothing anymore""" 388 | slaves = range(1, 248) 389 | for id in slaves: 390 | self.server.add_slave(id) 391 | self.server.remove_all_slaves() 392 | for id in slaves: 393 | self.assertRaises(Exception, self.server.get_slave, id) 394 | 395 | def testHookOnSetBlockData(self): 396 | slave = self.server.add_slave(22) 397 | def setblock_hook(args): 398 | (block, slice, values) = args 399 | setblock_hook.calls += 1 400 | setblock_hook.calls = 0 401 | install_hook('modbus.ModbusBlock.setitem', setblock_hook) 402 | 403 | for block_type in (modbus_tk.defines.COILS, modbus_tk.defines.DISCRETE_INPUTS, 404 | modbus_tk.defines.HOLDING_REGISTERS, modbus_tk.defines.ANALOG_INPUTS): 405 | slave.add_block(str(block_type), block_type, 0, 20) 406 | slave.set_values(str(block_type), 0, 1) 407 | slave.set_values(str(block_type), 5, (1, 0, 1)) 408 | 409 | self.assertEquals(setblock_hook.calls, 8) 410 | 411 | if __name__ == '__main__': 412 | unittest.main(argv = sys.argv) 413 | -------------------------------------------------------------------------------- /tests/unittest_modbus_rtu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import unittest 13 | import modbus_tk 14 | import modbus_tk.modbus_rtu as modbus_rtu 15 | import threading 16 | import struct 17 | import logging 18 | import serial 19 | import sys 20 | 21 | LOGGER = modbus_tk.utils.create_logger() 22 | 23 | def crc16_alternative(data): 24 | table =("0000", "C0C1", "C181", "0140", "C301", "03C0", "0280", "C241", 25 | "C601", "06C0", "0780", "C741", "0500", "C5C1", "C481", "0440", 26 | "CC01", "0CC0", "0D80", "CD41", "0F00", "CFC1", "CE81", "0E40", 27 | "0A00", "CAC1", "CB81", "0B40", "C901", "09C0", "0880", "C841", 28 | "D801", "18C0", "1980", "D941", "1B00", "DBC1", "DA81", "1A40", 29 | "1E00", "DEC1", "DF81", "1F40", "DD01", "1DC0", "1C80", "DC41", 30 | "1400", "D4C1", "D581", "1540", "D701", "17C0", "1680", "D641", 31 | "D201", "12C0", "1380", "D341", "1100", "D1C1", "D081", "1040", 32 | "F001", "30C0", "3180", "F141", "3300", "F3C1", "F281", "3240", 33 | "3600", "F6C1", "F781", "3740", "F501", "35C0", "3480", "F441", 34 | "3C00", "FCC1", "FD81", "3D40", "FF01", "3FC0", "3E80", "FE41", 35 | "FA01", "3AC0", "3B80", "FB41", "3900", "F9C1", "F881", "3840", 36 | "2800", "E8C1", "E981", "2940", "EB01", "2BC0", "2A80", "EA41", 37 | "EE01", "2EC0", "2F80", "EF41", "2D00", "EDC1", "EC81", "2C40", 38 | "E401", "24C0", "2580", "E541", "2700", "E7C1", "E681", "2640", 39 | "2200", "E2C1", "E381", "2340", "E101", "21C0", "2080", "E041", 40 | "A001", "60C0", "6180", "A141", "6300", "A3C1", "A281", "6240", 41 | "6600", "A6C1", "A781", "6740", "A501", "65C0", "6480", "A441", 42 | "6C00", "ACC1", "AD81", "6D40", "AF01", "6FC0", "6E80", "AE41", 43 | "AA01", "6AC0", "6B80", "AB41", "6900", "A9C1", "A881", "6840", 44 | "7800", "B8C1", "B981", "7940", "BB01", "7BC0", "7A80", "BA41", 45 | "BE01", "7EC0", "7F80", "BF41", "7D00", "BDC1", "BC81", "7C40", 46 | "B401", "74C0", "7580", "B541", "7700", "B7C1", "B681", "7640", 47 | "7200", "B2C1", "B381", "7340", "B101", "71C0", "7080", "B041", 48 | "5000", "90C1", "9181", "5140", "9301", "53C0", "5280", "9241", 49 | "9601", "56C0", "5780", "9741", "5500", "95C1", "9481", "5440", 50 | "9C01", "5CC0", "5D80", "9D41", "5F00", "9FC1", "9E81", "5E40", 51 | "5A00", "9AC1", "9B81", "5B40", "9901", "59C0", "5880", "9841", 52 | "8801", "48C0", "4980", "8941", "4B00", "8BC1", "8A81", "4A40", 53 | "4E00", "8EC1", "8F81", "4F40", "8D01", "4DC0", "4C80", "8C41", 54 | "4400", "84C1", "8581", "4540", "8701", "47C0", "4680", "8641", 55 | "8201", "42C0", "4380", "8341", "4100", "81C1", "8081", "4040" ) 56 | w = int("ffff", 16) 57 | for c in data: 58 | i = (ord(c) ^ w) & 255 59 | w = (w>>8) & int("ffff",16) 60 | w = w ^ int(table[i],16) 61 | return modbus_tk.utils.swap_bytes(w) 62 | 63 | class TestCrc(unittest.TestCase): 64 | """Check that the CRC16 calculation""" 65 | def setUp(self): 66 | pass 67 | 68 | def tearDown(self): 69 | pass 70 | 71 | def testSwapBytes(self): 72 | """Check that swap_bytes is swapping bytes""" 73 | bytes_list = ("ACDC", "0010", "1000", "1975", "2002", "FAB4", "2BAD") 74 | for str_val in bytes_list: 75 | swap_str = str_val[2:4]+str_val[0:2] 76 | self.assertEqual(modbus_tk.utils.swap_bytes(int(str_val, 16)), int(swap_str, 16)) 77 | 78 | def testCrc16ReturnsAlwaysTheSame(self): 79 | """Check that the CRC16 returns the same result for the same value""" 80 | test_strings = ("hello world", "a", "12345678910111213141516", "", "modbus-tk", "www.apidev.fr") 81 | for s in test_strings: 82 | self.assertEqual(modbus_tk.utils.calculate_crc(s), modbus_tk.utils.calculate_crc(s)) 83 | 84 | def testCrc16ReturnsDifferentForDifferentStrings(self): 85 | """Check that the CRC16 returns a different value if strings are different""" 86 | test_strings = ("hello world", "a", "12345678910111213141516", "", "modbus-tk", "www.apidev.fr") 87 | for s in test_strings: 88 | self.assertNotEqual(modbus_tk.utils.calculate_crc(s), modbus_tk.utils.calculate_crc(s+"_")) 89 | 90 | def testCrc16(self): 91 | """Check that the CRC16 is generated properly""" 92 | test_strings = ("hello world", "a", "12345678910111213141516", "", "modbus-tk", "www.apidev.fr") 93 | for s in test_strings: 94 | self.assertEqual(crc16_alternative(s), modbus_tk.utils.calculate_crc(s)) 95 | 96 | def testCrc16ForAllCharValues(self): 97 | """Check that the CRC16 is generated properly for all chars""" 98 | s = "" 99 | for i in xrange(256): 100 | s += struct.pack(">B", i) 101 | self.assertEqual(crc16_alternative(s), modbus_tk.utils.calculate_crc(s)) 102 | 103 | class TestRtuQuery(unittest.TestCase): 104 | """Check that RtuQuery class""" 105 | def setUp(self): 106 | pass 107 | 108 | def tearDown(self): 109 | pass 110 | 111 | def testBuildRequest(self): 112 | """Test the string returned by building a request""" 113 | query = modbus_rtu.RtuQuery() 114 | request = query.build_request("", 0) 115 | expected = struct.pack(">B", 0) 116 | expected_crc = crc16_alternative(expected) 117 | expected += struct.pack(">H", expected_crc) 118 | self.assertEqual(expected, request) 119 | 120 | def testBuildRequestWithSlave(self): 121 | """Test the string returned by building a request with a slave""" 122 | query = modbus_rtu.RtuQuery() 123 | for i in xrange(0, 256): 124 | request = query.build_request("", i) 125 | expected = struct.pack(">B", i) 126 | expected_crc = crc16_alternative(expected) 127 | expected += struct.pack(">H", expected_crc) 128 | self.assertEqual(expected, request) 129 | 130 | def testBuildRequestWithInvalidSlave(self): 131 | """Test that an error is raised when invalid slave is passed""" 132 | query = modbus_rtu.RtuQuery() 133 | for i in ([256, -1, 312, 3541, 65536, 65656]): 134 | self.assertRaises(modbus_tk.modbus.InvalidArgumentError, query.build_request, "", i) 135 | 136 | def testBuildRequestWithPdu(self): 137 | """Test the request returned by building a request with a pdu""" 138 | query = modbus_rtu.RtuQuery() 139 | for i in xrange(247): 140 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 141 | request = query.build_request(pdu, i) 142 | expected = struct.pack(">B"+str(len(pdu))+"s", i, pdu) 143 | expected_crc = crc16_alternative(expected) 144 | expected += struct.pack(">H", expected_crc) 145 | self.assertEqual(expected, request) 146 | 147 | def testParseRespone(self): 148 | """Test that Modbus Rtu part of the response is understood""" 149 | query = modbus_rtu.RtuQuery() 150 | for i in xrange(247): 151 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 152 | request = query.build_request("a", i) 153 | response = struct.pack(">B"+str(len(pdu))+"s", i, pdu) 154 | response_crc = crc16_alternative(response) 155 | response += struct.pack(">H", response_crc) 156 | extracted = query.parse_response(response) 157 | self.assertEqual(extracted, pdu) 158 | 159 | def testParseTooShortRespone(self): 160 | """Test an error is raised if the response is too short""" 161 | query = modbus_rtu.RtuQuery() 162 | for i in xrange(3): 163 | self.assertRaises(modbus_tk.modbus.ModbusInvalidResponseError, query.parse_response, "a"*i) 164 | 165 | def testParseWrongSlaveResponse(self): 166 | """Test an error is raised if the slave id is wrong""" 167 | query = modbus_rtu.RtuQuery() 168 | pdu = "a" 169 | request = query.build_request(pdu, 5) 170 | response = struct.pack(">B"+str(len(pdu))+"s", 8, pdu) 171 | response_crc = crc16_alternative(response) 172 | response += struct.pack(">H", response_crc) 173 | self.assertRaises(modbus_tk.modbus.ModbusInvalidResponseError, query.parse_response, response) 174 | 175 | def testParseWrongCrcResponse(self): 176 | """Test an error is raised if wrong transaction id""" 177 | query = modbus_rtu.RtuQuery() 178 | pdu = "a" 179 | request = query.build_request(pdu, 5) 180 | response = struct.pack(">B"+str(len(pdu))+"s", 5, pdu) 181 | response_crc = crc16_alternative(response)+1 182 | response += struct.pack(">H", response_crc) 183 | self.assertRaises(modbus_tk.modbus.ModbusInvalidResponseError, query.parse_response, response) 184 | 185 | def testParseTooShortRequest(self): 186 | """Test an error is raised if the request is too short""" 187 | query = modbus_rtu.RtuQuery() 188 | for i in xrange(3): 189 | self.assertRaises(modbus_tk.modbus.ModbusInvalidRequestError, query.parse_request, "a"*i) 190 | 191 | def testParseRequest(self): 192 | """Test that Modbus Rtu part of the request is understood""" 193 | query = modbus_rtu.RtuQuery() 194 | i = 0 195 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 196 | request = query.build_request(pdu, i) 197 | (slave, extracted_pdu) = query.parse_request(request) 198 | self.assertEqual(extracted_pdu, pdu) 199 | self.assertEqual(slave, i) 200 | i += 1 201 | 202 | def testBuildResponse(self): 203 | """Test that the response of an request is build properly""" 204 | query = modbus_rtu.RtuQuery() 205 | i = 0 206 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 207 | request = query.build_request(pdu, i) 208 | response = query.build_response(pdu) 209 | response_pdu = query.parse_response(response) 210 | self.assertEqual(pdu, response_pdu) 211 | i += 1 212 | 213 | class TestRtuCom(unittest.TestCase): 214 | """Check rtu com settinsg are Ok""" 215 | 216 | def testCalculateT(self): 217 | """Test that the interchar is calculated ok""" 218 | baudrates = (50,75,110,134,150,200,300,600,1200,1800,2400,4800,9600, 219 | 19200,38400,57600,115200,230400,460800,500000,576000,921600, 220 | 1000000,1152000,1500000,2000000,2500000,3000000,3500000,4000000) 221 | ts = [0.22, 0.147, 0.1, 0.082, 0.073, 0.055, 0.037, 0.0183, 0.00917, 0.0061, 0.004583, 222 | 0.00229, 0.001145, 0.000572] 223 | place = [2]*3 + [3]*5 + [4]*5 + [5] 224 | missing_count = (len(baudrates)-len(ts)) 225 | ts += [0.0005]*missing_count 226 | place += [5]*missing_count 227 | for i in xrange(len(baudrates)): 228 | self.assertAlmostEqual(modbus_tk.utils.calculate_rtu_inter_char(baudrates[i]), ts[i], places=place[i]) 229 | 230 | if __name__ == '__main__': 231 | unittest.main(argv = sys.argv) 232 | -------------------------------------------------------------------------------- /tests/unittest_modbus_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import unittest 13 | import modbus_tk 14 | import modbus_tk.modbus_tcp as modbus_tcp 15 | import threading 16 | import struct 17 | import logging 18 | import sys 19 | 20 | LOGGER = modbus_tk.utils.create_logger() 21 | 22 | class TestMbap(unittest.TestCase): 23 | """Test the TcpMbap class""" 24 | def setUp(self): 25 | self.mbap1 = modbus_tcp.TcpMbap() 26 | 27 | self.mbap1.transaction_id = 1 28 | self.mbap1.protocol_id = 2 29 | self.mbap1.length = 3 30 | self.mbap1.unit_id = 4 31 | 32 | 33 | def tearDown(self): 34 | pass 35 | 36 | def testClone(self): 37 | """test the clone function makes a copy of the object""" 38 | mbap2 = modbus_tcp.TcpMbap() 39 | 40 | mbap2.clone(self.mbap1) 41 | 42 | self.assertEqual(self.mbap1.transaction_id, mbap2.transaction_id) 43 | self.assertEqual(self.mbap1.protocol_id, mbap2.protocol_id) 44 | self.assertEqual(self.mbap1.length, mbap2.length) 45 | self.assertEqual(self.mbap1.unit_id, mbap2.unit_id) 46 | 47 | self.assertNotEqual(self.mbap1, mbap2) 48 | 49 | def testCheckIds(self): 50 | """Test that the check ids pass with correct mbap""" 51 | mbap2 = modbus_tcp.TcpMbap() 52 | mbap2.transaction_id = 1 53 | mbap2.protocol_id = 2 54 | mbap2.length = 10 55 | mbap2.unit_id = 4 56 | 57 | self.mbap1.check_response(mbap2, 3-1) 58 | 59 | def testCheckIdsWrongLength(self): 60 | """Test that the check ids fails when the length is not Ok""" 61 | mbap2 = modbus_tcp.TcpMbap() 62 | mbap2.transaction_id = 1 63 | mbap2.protocol_id = 2 64 | mbap2.length = 10 65 | mbap2.unit_id = 4 66 | 67 | self.assertRaises(modbus_tcp.ModbusInvalidMbapError, self.mbap1.check_response, mbap2, 0) 68 | 69 | def testCheckIdsWrongTransactionId(self): 70 | """Test that the check ids fails when the transaction id is not Ok""" 71 | mbap2 = modbus_tcp.TcpMbap() 72 | mbap2.transaction_id = 2 73 | mbap2.protocol_id = 2 74 | mbap2.length = 10 75 | mbap2.unit_id = 4 76 | 77 | self.assertRaises(modbus_tcp.ModbusInvalidMbapError, self.mbap1.check_response, mbap2, 2) 78 | 79 | def testCheckIdsWrongProtocolId(self): 80 | """Test that the check ids fails when the transaction id is not Ok""" 81 | mbap2 = modbus_tcp.TcpMbap() 82 | mbap2.transaction_id = 1 83 | mbap2.protocol_id = 3 84 | mbap2.length = 10 85 | mbap2.unit_id = 4 86 | 87 | self.assertRaises(modbus_tcp.ModbusInvalidMbapError, self.mbap1.check_response, mbap2, 2) 88 | 89 | def testCheckIdsWrongUnitId(self): 90 | """Test that the check ids fails when the transaction id is not Ok""" 91 | mbap2 = modbus_tcp.TcpMbap() 92 | mbap2.transaction_id = 1 93 | mbap2.protocol_id = 2 94 | mbap2.length = 10 95 | mbap2.unit_id = 5 96 | 97 | self.assertRaises(modbus_tcp.ModbusInvalidMbapError, self.mbap1.check_response, mbap2, 2) 98 | 99 | def testPack(self): 100 | """Test that packing a mbap give the expected result""" 101 | self.assertEqual(self.mbap1.pack(), struct.pack(">HHHB", 1, 2, 3, 4)) 102 | 103 | def testUnpack(self): 104 | """Test that unpacking a mbap give the expected result""" 105 | mbap2 = modbus_tcp.TcpMbap() 106 | mbap2.unpack(self.mbap1.pack()) 107 | 108 | self.assertEqual(self.mbap1.transaction_id, mbap2.transaction_id) 109 | self.assertEqual(self.mbap1.protocol_id, mbap2.protocol_id) 110 | self.assertEqual(self.mbap1.length, mbap2.length) 111 | self.assertEqual(self.mbap1.unit_id, mbap2.unit_id) 112 | 113 | self.assertNotEqual(self.mbap1, mbap2) 114 | 115 | class TestTcpQuery(unittest.TestCase): 116 | def setUp(self): 117 | pass 118 | 119 | def tearDown(self): 120 | pass 121 | 122 | def testIncTrIdIsThreadSafe(self): 123 | """Check that the function in charge of increasing the transaction id is thread safe""" 124 | def inc_by(): 125 | query = modbus_tcp.TcpQuery() 126 | for i in xrange(1000): 127 | query._get_transaction_id() 128 | 129 | query = modbus_tcp.TcpQuery() 130 | tr_id_before = query._get_transaction_id() 131 | threads = [threading.Thread(target=inc_by) for thread_nr in xrange(20)] 132 | for thread in threads: thread.start() 133 | for thread in threads: thread.join() 134 | self.assertEqual(1000*20+1, query._get_transaction_id()-tr_id_before) 135 | 136 | def testCheckTrIdRollover(self): 137 | """Check that the transaction id will rollover when max valuie is reached""" 138 | query = modbus_tcp.TcpQuery() 139 | tr_id_before = query._get_transaction_id() 140 | for a in xrange(int("ffff", 16)): 141 | query._get_transaction_id() 142 | self.assertEqual(query._get_transaction_id(), tr_id_before) 143 | 144 | def testIncIdOfRequest(self): 145 | """Check that the transaction id is increased when building the request""" 146 | queries = [modbus_tcp.TcpQuery() for i in xrange(100)] 147 | 148 | for i in xrange(len(queries)): 149 | queries[i].build_request("", 0) 150 | 151 | for i in xrange(len(queries)-1): 152 | self.assertEqual(queries[i]._request_mbap.transaction_id+1, queries[i+1]._request_mbap.transaction_id) 153 | 154 | def testBuildRequest(self): 155 | """Test the mbap returned by building a request""" 156 | query = modbus_tcp.TcpQuery() 157 | request = query.build_request("", 0) 158 | self.assertEqual(struct.pack(">HHHB", query._request_mbap.transaction_id, 0, 1, 0), request) 159 | 160 | def testBuildRequestWithSlave(self): 161 | """Test the mbap returned by building a request with a slave""" 162 | query = modbus_tcp.TcpQuery() 163 | for i in xrange(0, 255): 164 | request = query.build_request("", i) 165 | self.assertEqual(struct.pack(">HHHB", query._request_mbap.transaction_id, 0, 1, i), request) 166 | 167 | def testBuildRequestWithInvalidSlave(self): 168 | """Test that an error is raised when invalid slave is passed""" 169 | query = modbus_tcp.TcpQuery() 170 | for i in [-1, 256, 257, 65536]: 171 | self.assertRaises(modbus_tk.modbus.InvalidArgumentError, query.build_request, "", i) 172 | 173 | def testBuildRequestWithPdu(self): 174 | """Test the mbap returned by building a request with a pdu""" 175 | query = modbus_tcp.TcpQuery() 176 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 177 | request = query.build_request(pdu, 0) 178 | self.assertEqual(struct.pack(">HHHB"+str(len(pdu))+"s", query._request_mbap.transaction_id, 0, len(pdu)+1, 0, pdu), request) 179 | 180 | def testParseRespone(self): 181 | """Test that Modbus TCP part of the response is understood""" 182 | query = modbus_tcp.TcpQuery() 183 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 184 | request = query.build_request("a", 0) 185 | response = struct.pack(">HHHB"+str(len(pdu))+"s", query._request_mbap.transaction_id, query._request_mbap.protocol_id, len(pdu)+1, query._request_mbap.unit_id, pdu) 186 | extracted = query.parse_response(response) 187 | self.assertEqual(extracted, pdu) 188 | 189 | def testParseTooShortRespone(self): 190 | """Test an error is raised if the response is too short""" 191 | query = modbus_tcp.TcpQuery() 192 | self.assertRaises(modbus_tk.modbus.ModbusInvalidResponseError, query.parse_response, "") 193 | self.assertRaises(modbus_tk.modbus.ModbusInvalidResponseError, query.parse_response, "a"*6) 194 | 195 | def testParseWrongSlaveResponse(self): 196 | """Test an error is raised if the slave id is wrong""" 197 | query = modbus_tcp.TcpQuery() 198 | pdu = "a" 199 | request = query.build_request(pdu, 0) 200 | response = struct.pack(">HHHB"+str(len(pdu))+"s", query._request_mbap.transaction_id, query._request_mbap.protocol_id, len(pdu)+1, query._request_mbap.unit_id+1, pdu) 201 | self.assertRaises(modbus_tk.modbus_tcp.ModbusInvalidMbapError, query.parse_response, response) 202 | 203 | def testParseWrongTransactionResponse(self): 204 | """Test an error is raised if wrong transaction id""" 205 | query = modbus_tcp.TcpQuery() 206 | pdu = "a" 207 | request = query.build_request(pdu, 0) 208 | response = struct.pack(">HHHB"+str(len(pdu))+"s", query._request_mbap.transaction_id+1, query._request_mbap.protocol_id, len(pdu)+1, query._request_mbap.unit_id, pdu) 209 | self.assertRaises(modbus_tk.modbus_tcp.ModbusInvalidMbapError, query.parse_response, response) 210 | 211 | def testParseWrongProtocolIdResponse(self): 212 | """Test an error is raised if wrong protocol id""" 213 | query = modbus_tcp.TcpQuery() 214 | pdu = "a" 215 | request = query.build_request(pdu, 0) 216 | response = struct.pack(">HHHB"+str(len(pdu))+"s", query._request_mbap.transaction_id, query._request_mbap.protocol_id+1, len(pdu)+1, query._request_mbap.unit_id, pdu) 217 | self.assertRaises(modbus_tk.modbus_tcp.ModbusInvalidMbapError, query.parse_response, response) 218 | 219 | def testParseWrongLengthResponse(self): 220 | """Test an error is raised if the length is not ok""" 221 | query = modbus_tcp.TcpQuery() 222 | pdu = "a" 223 | request = query.build_request(pdu, 0) 224 | response = struct.pack(">HHHB"+str(len(pdu))+"s", query._request_mbap.transaction_id, query._request_mbap.protocol_id+1, len(pdu), query._request_mbap.unit_id, pdu) 225 | self.assertRaises(modbus_tk.modbus_tcp.ModbusInvalidMbapError, query.parse_response, response) 226 | 227 | def testParseWrongLengthResponse(self): 228 | """Test an error is raised if the length is not ok""" 229 | query = modbus_tcp.TcpQuery() 230 | pdu = "a" 231 | request = query.build_request(pdu, 0) 232 | response = struct.pack(">HHHB"+str(len(pdu))+"s", query._request_mbap.transaction_id, query._request_mbap.protocol_id+1, len(pdu), query._request_mbap.unit_id, pdu) 233 | self.assertRaises(modbus_tk.modbus_tcp.ModbusInvalidMbapError, query.parse_response, response) 234 | 235 | def testParseTooShortRequest(self): 236 | """Test an error is raised if the request is too short""" 237 | query = modbus_tcp.TcpQuery() 238 | self.assertRaises(modbus_tk.modbus.ModbusInvalidRequestError, query.parse_request, "") 239 | self.assertRaises(modbus_tk.modbus.ModbusInvalidRequestError, query.parse_request, "a"*6) 240 | 241 | def testParseRequest(self): 242 | """Test that Modbus TCP part of the request is understood""" 243 | query = modbus_tcp.TcpQuery() 244 | i = 0 245 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 246 | request = query.build_request(pdu, i) 247 | (slave, extracted_pdu) = query.parse_request(request) 248 | self.assertEqual(extracted_pdu, pdu) 249 | self.assertEqual(slave, i) 250 | i += 1 251 | 252 | def testParseRequestInvalidLength(self): 253 | """Test that an error is raised if the length is not valid""" 254 | query = modbus_tcp.TcpQuery() 255 | i = 0 256 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 257 | request = struct.pack(">HHHB", 0, 0, (len(pdu)+2), 0) 258 | self.assertRaises(modbus_tk.modbus_tcp.ModbusInvalidMbapError, query.parse_request, request+pdu) 259 | 260 | def testBuildResponse(self): 261 | """Test that the response of a request is build properly""" 262 | query = modbus_tcp.TcpQuery() 263 | i = 0 264 | for pdu in ["", "a", "a"*127, "abcdefghi"]: 265 | request = query.build_request(pdu, i) 266 | response = query.build_response(pdu) 267 | response_pdu = query.parse_response(response) 268 | self.assertEqual(pdu, response_pdu) 269 | i += 1 270 | 271 | class TestTcpServer(unittest.TestCase): 272 | def setUp(self): pass 273 | def tearDown(self): pass 274 | 275 | def testGetRequestLength(self): 276 | """Test than _get_request_length returns the length field of request mbap""" 277 | s = modbus_tcp.TcpServer() 278 | request = struct.pack(">HHHB", 0, 0, 12, 1) 279 | self.assertEqual(s._get_request_length(request), 12) 280 | 281 | request = struct.pack(">HHH", 0, 0, 129) 282 | self.assertEqual(s._get_request_length(request), 129) 283 | 284 | def testGetRequestLengthFailsOnInvalid(self): 285 | """Test than an error is raised in _get_request_length is the length field of request mbap is not filled""" 286 | s = modbus_tcp.TcpServer() 287 | request = struct.pack(">HHB", 0, 0, 1) 288 | self.assertRaises(modbus_tk.modbus.ModbusInvalidRequestError, s._get_request_length, request) 289 | self.assertRaises(modbus_tk.modbus.ModbusInvalidRequestError, s._get_request_length, "") 290 | 291 | if __name__ == '__main__': 292 | unittest.main(argv = sys.argv) 293 | -------------------------------------------------------------------------------- /tools/zip_module.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | # 4 | # Modbus TestKit: Implementation of Modbus protocol in python 5 | # 6 | # (C)2009 - Luc Jean - luc.jean@gmail.com 7 | # (C)2009 - Apidev - http://www.apidev.fr 8 | # 9 | # This is distributed under GNU LGPL license, see license.txt 10 | # 11 | 12 | import zipfile 13 | import glob 14 | import os 15 | import sys 16 | 17 | 18 | class ZipArchive: 19 | 20 | def zip_it(self, dirName, files): 21 | dirNamePrefix = dirName+"/*" 22 | for filename in glob.glob(dirNamePrefix): 23 | if os.path.isfile(filename): 24 | exclude_me = False 25 | for exclude in self.exclude_list: 26 | if filename.find(exclude) != -1: 27 | exclude_me = True 28 | break 29 | if not exclude_me: 30 | print filename 31 | name = filename[len(self.folder)+1:] 32 | self.archive.write(filename, name, zipfile.ZIP_DEFLATED) 33 | 34 | def run(self, folder, name): 35 | self.exclude_list = (".pyc", "build", "tools", "release", ".egg-info", "dist", ".settings", ".git") 36 | self.folder = folder 37 | self.archive = zipfile.ZipFile(name+".zip", "w") 38 | os.path.walk(self.folder, ZipArchive.zip_it, self) 39 | self.archive.close() 40 | 41 | if __name__ == "__main__": 42 | arch = ZipArchive() 43 | old_dir = os.getcwd() 44 | wkdir = os.path.abspath(os.path.dirname(sys.argv[0]) + "\\..") 45 | os.chdir(wkdir + "\\tools") 46 | arch.run(wkdir, "modbus-tk") 47 | os.chdir(old_dir) 48 | print "done" 49 | --------------------------------------------------------------------------------