├── .gitignore ├── COPYING ├── Makefile.am ├── README.md ├── autogen.sh ├── configure.ac ├── example_configs ├── sample_rtu.conf └── sample_tcp.conf ├── man ├── Makefile.am ├── restmbmaster.8 └── restmbmaster.conf.5 ├── src ├── Makefile.am └── restmbmaster.c └── systemd └── restmbmaster@.service /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | MAINTAINERCLEANFILES = Makefile.in 2 | 3 | ACLOCAL_AMFLAGS = -I m4 4 | 5 | SUBDIRS = src man 6 | 7 | EXTRA_DIST = example_configs systemd 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### restmbmaster 2 | 3 | restmbmaster is a simple daemon that allows user to access Modbus slaves 4 | over Rest API. The slaves could be either connected over 5 | serial line (Modbus RTU protocol), or over TCP (Modbus TCP protocol). 6 | 7 | #### Examples 8 | 9 | To run connecting to Modbus TCP: 10 | 11 | ``` 12 | $ restmbmaster -c tcp://test.abc:1000 -p 8080 13 | ``` 14 | 15 | To run connecting to Modbus RTU: 16 | 17 | 18 | ``` 19 | $ restmbmaster -c rtu:/dev/ttyS0?baud=9600 -p 8080 20 | ``` 21 | 22 | To run according to the configuration from file: 23 | 24 | ``` 25 | $ restmbmaster -f myconfig.conf 26 | ``` 27 | 28 | When restmbmaster is running, one can use for example curl to communicate with Modbus slaves. 29 | In the following example, slave with address 55 is queried for the value of input register with address 10: 30 | 31 | ``` 32 | $ curl http://127.0.0.1:8080/slaves/55/input-registers/10 33 | 34 34 | ``` 35 | 36 | It is possible to query multiple registers (in sequence) at once: 37 | 38 | ``` 39 | $ curl http://127.0.0.1:8080/slaves/55/input-registers/10?count=4 40 | 34 78 234 2 41 | ``` 42 | 43 | To write new value (434) to holding register 20 the "PUT" method has to be used: 44 | 45 | ``` 46 | $ curl http://127.0.0.1:8080/slaves/55/holding-registers/20 -d "434" -H "Content-Type: text/plain" -X PUT 47 | ``` 48 | 49 | It is also possible to write to a sequence of registers (20-26): 50 | 51 | ``` 52 | $ curl http://127.0.0.1:8080/slaves/55/holding-registers/20 -d "434 48 32 92 1 0 3" -H "Content-Type: text/plain" -X PUT 53 | ``` 54 | 55 | #### Running with systemd 56 | 57 | restmbmaster is prepared to be run in multiple parallel instances to allow to access multiple buses. One just have to prepare a configuration file for each bus and ask systemd to spawn the instance. 58 | 59 | ``` 60 | $ sudo mkdir /etc/restmbmaster/ 61 | $ sudo cp /usr/share/doc/restmbmaster/example_configs/sample_tcp.conf /etc/restmbmaster/mytcpmodbus1.conf 62 | $ sudo systemctl start restmbmaster@mytcpmodbus1 63 | $ sudo systemctl status restmbmaster@mytcpmodbus1 64 | ● restmbmaster@mytcpmodbus1.service - Rest API Modbus master mytcpmodbus1 65 | Loaded: loaded (/usr/lib/systemd/system/restmbmaster@.service; static; vendor preset: disabled) 66 | Active: active (running) since Sat 2019-12-28 18:36:54 CET; 1s ago 67 | Main PID: 8337 (restmbmaster) 68 | Tasks: 1 (limit: 4915) 69 | Memory: 588.0K 70 | CGroup: /system.slice/system-restmbmaster.slice/restmbmaster@mytcpmodbus1.service 71 | └─8337 /usr/bin/restmbmaster -f /etc/restmbmaster/mytcpmodbus1.conf 72 | 73 | Dec 28 18:36:54 nanopsycho systemd[1]: Started Rest API Modbus master mytcpmodbus1. 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | autoreconf --force --install -I m4 4 | rm -Rf autom4te.cache; 5 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | # -*- Autoconf -*- 2 | # Process this file with autoconf to produce a configure script. 3 | 4 | AC_INIT([restmbmaster], [5], [jiri@resnulli.us]) 5 | AC_CONFIG_AUX_DIR([build-aux]) 6 | AC_CONFIG_MACRO_DIR([m4]) 7 | AM_INIT_AUTOMAKE([-Wall foreign subdir-objects]) 8 | m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES(yes)], []) 9 | AM_PROG_AR 10 | 11 | CFLAGS="$CFLAGS -Wall" 12 | 13 | # Checks for programs. 14 | AC_PROG_CC 15 | LT_INIT 16 | 17 | PKG_CHECK_MODULES([LIBMODBUS], [libmodbus]) 18 | PKG_CHECK_MODULES([LIBMICROHTTPD], [libmicrohttpd]) 19 | 20 | # Checks for header files. 21 | AC_CHECK_HEADERS([stdint.h stdlib.h]) 22 | 23 | # Checks for typedefs, structures, and compiler characteristics. 24 | AC_C_INLINE 25 | 26 | # Checks for library functions. 27 | AC_FUNC_MALLOC 28 | 29 | AC_CONFIG_FILES([Makefile 30 | src/Makefile \ 31 | man/Makefile]) 32 | AC_OUTPUT 33 | -------------------------------------------------------------------------------- /example_configs/sample_rtu.conf: -------------------------------------------------------------------------------- 1 | # Example Modbus RTU config 2 | 3 | # Connect to serial port ttyUSB at speed 9600 baud 4 | connect = rtu:/dev/ttyUSB0?baud=9600 5 | 6 | # Listen and expose REST API on port 8080 7 | port = 8080 8 | -------------------------------------------------------------------------------- /example_configs/sample_tcp.conf: -------------------------------------------------------------------------------- 1 | # Example Modbus TCP config 2 | 3 | # Connect to TCP on address 127.0.0.1 and port 5020 4 | connect = tcp://127.0.0.1:5020 5 | 6 | # Listen and expose REST API on port 8080 7 | port = 8080 8 | -------------------------------------------------------------------------------- /man/Makefile.am: -------------------------------------------------------------------------------- 1 | dist_man8_MANS = restmbmaster.8 2 | dist_man5_MANS = restmbmaster.conf.5 3 | -------------------------------------------------------------------------------- /man/restmbmaster.8: -------------------------------------------------------------------------------- 1 | .TH restmbmaster 8 "26 December 2019" "restmbmaster" "Rest API Modbus master" 2 | .SH NAME 3 | restmbmaster \(em Rest API gateway to Modbus slaves 4 | .SH SYNOPSIS 5 | .B restmbmaster 6 | .BI \-c " CONNECT_URI" 7 | .BI \-p " PORT" 8 | .br 9 | .B restmbmaster 10 | .BI \-f " FILE" 11 | .br 12 | .B restmbmaster 13 | .BR \-h | \-v 14 | .SH DESCRIPTION 15 | .PP 16 | restmbmaster is a simple daemon that allows user to access Modbus slaves 17 | over Rest API. The slaves could be either connected over 18 | serial line (Modbus RTU protocol), or over TCP (Modbus TCP protocol). 19 | .SH OPTIONS 20 | .TP 21 | .B "\-h, \-\-help" 22 | Print help text to console and exit. 23 | .TP 24 | .B "\-v, \-\-version" 25 | Print version information to console and exit. 26 | .TP 27 | .B "\-g, \-\-debug" 28 | Turns on debugging messages. Repeating the option increases verbosity. 29 | .TP 30 | .BI "\-c " CONNECT_URI ", \-\-connect " CONNECT_URI 31 | Specifies the target Modbus bus to connect to. Supported formats: 32 | .RS 7 33 | .PP 34 | .BR "tcp://HOSTNAME[:PORT]" 35 | .RS 7 36 | .PP 37 | Example: 38 | .BR "tcp://test.abc:1000" 39 | .PP 40 | Default PORT: 41 | .BR "502" 42 | .RE 43 | .PP 44 | .BR "rtu:DEVICEPATH[?baud=BAUDRATE]" 45 | .RS 7 46 | .PP 47 | Example: 48 | .BR "rtu:/dev/ttyS0?baud=9600" 49 | .PP 50 | Default BAUDRATE: 51 | .BR "115200" 52 | .RE 53 | .RE 54 | .TP 55 | .BI "\-p " PORT ", \-\-port " PORT 56 | Specifies TCP port on which the webserver is listening and where the Rest API is exposed. 57 | .TP 58 | .B "\-K, \-\-dontkeep" 59 | Don't keep the connection open, connect only for the time when command is processed. 60 | .TP 61 | .BI "\-f " FILE ", \-\-config " FILE 62 | Load the specified configuration file. 63 | .SH REST API 64 | .TP 65 | The API is quite simple. Only "GET" (read) and "PUT" (write) methods are supported. The "Content-Type" is always "text/plain" for both "GET" and "PUT". The path in the URL has following format: 66 | .PP 67 | .B "http://HOSTNAME:PORT/slaves/SLAVE_ADDRESS/OBJECT_FAMILY/OBJECT_ADDRESS[?count=OBJECT_COUNT]" 68 | .RS 7 69 | .PP 70 | .BR "SLAVE_ADDRESS "\(em 71 | Address of the slave to communitate with, number in range from 0 to 255. 72 | .PP 73 | .BR "OBJECT_FAMILY "\(em 74 | Is a string to identify the family of the objects. It is one of: 75 | .PP 76 | .RS 7 77 | .BR "coils "\(em 78 | To access coils (RW). 79 | .BR 80 | .PP 81 | .BR "discrete-inputs "\(em 82 | To access discrete inputs (RO). 83 | .BR 84 | .PP 85 | .BR "input-registers "\(em 86 | To access input registers (RO). 87 | .BR 88 | .PP 89 | .BR "holding-registers "\(em 90 | To access holding registers (RW). 91 | .BR 92 | .RE 93 | .PP 94 | .BR "OBJECT_ADDRESS "\(em 95 | Address of the object according to the family. It is an integer number, starting from 0. 96 | .PP 97 | .BR "OBJECT_COUNT "\(em 98 | Number of objects to read or write, starting at OBJECT_ADDRESS. It is an integer number, starting from 0. If not specified, defaults to 1. 99 | .RE 100 | .PP 101 | The content being returned by "GET" method is a value of the object as a plain integer number in the text. If values of multiple objects are requested, they are separated by a single space. Same format applies for the "PUT" method. 102 | .SH EXAMPLES 103 | .PP 104 | To run connecting to Modbus TCP: 105 | .PP 106 | .nf 107 | $ restmbmaster -c tcp://test.abc:1000 -p 8080 108 | .fi 109 | .PP 110 | To run connecting to Modbus RTU: 111 | .PP 112 | .nf 113 | $ restmbmaster -c rtu:/dev/ttyS0?baud=9600 -p 8080 114 | .fi 115 | .PP 116 | To run according to the configuration from file: 117 | .PP 118 | .nf 119 | $ restmbmaster -f myconfig.conf 120 | .PP 121 | When restmbmaster is running, one can use for example curl to communicate with Modbus slaves. 122 | In the following example, slave with address 55 is queried for the value of input register with address 10: 123 | .PP 124 | .nf 125 | $ curl http://127.0.0.1:8080/slaves/55/input-registers/10 126 | 34 127 | .fi 128 | .PP 129 | It is possible to query multiple registers (in sequence) at once: 130 | .PP 131 | .nf 132 | $ curl http://127.0.0.1:8080/slaves/55/input-registers/10?count=4 133 | 34 78 234 2 134 | .fi 135 | .PP 136 | To write new value (434) to holding register 20 the "PUT" method has to be used: 137 | .PP 138 | .nf 139 | $ curl http://127.0.0.1:8080/slaves/55/holding-registers/20 -d "434" -H "Content-Type: text/plain" -X PUT 140 | .fi 141 | .PP 142 | It is also possible to write to a sequence of registers (20-26): 143 | .PP 144 | .nf 145 | $ curl http://127.0.0.1:8080/slaves/55/holding-registers/20 -d "434 48 32 92 1 0 3" -H "Content-Type: text/plain" -X PUT 146 | .fi 147 | .SH SEE ALSO 148 | .BR restmbmaster.conf (5), 149 | .SH AUTHOR 150 | .PP 151 | Jiri Pirko is the original author and current maintainer of restmbmaster. 152 | -------------------------------------------------------------------------------- /man/restmbmaster.conf.5: -------------------------------------------------------------------------------- 1 | .TH RESTMBMASTER.CONF 5 "2019-12-28" "restmbmaster" "Rest API Modbus master configuration" 2 | .SH NAME 3 | restmbmaster.conf \(em restmbmaster configuration file 4 | .SH DESCRIPTION 5 | .TP 6 | restmbmaster uses very simple format of configuration file, each line has following format: 7 | .BI "key = " value 8 | .TP 9 | In case the option does not support value the line format is: 10 | .BI "key" 11 | .SH OPTIONS 12 | .TP 13 | .B "debug" 14 | Turns on debugging messages. Repeating the option increases verbosity. 15 | .TP 16 | .BI "connect = " CONNECT_URI 17 | Specifies the target Modbus bus to connect to. Supported formats: 18 | .RS 7 19 | .PP 20 | .BR "tcp://HOSTNAME[:PORT]" 21 | .RS 7 22 | .PP 23 | Example: 24 | .BR "tcp://test.abc:1000" 25 | .PP 26 | Default PORT: 27 | .BR "502" 28 | .RE 29 | .PP 30 | .BR "rtu:DEVICEPATH[?baud=BAUDRATE]" 31 | .RS 7 32 | .PP 33 | Example: 34 | .BR "rtu:/dev/ttyS0?baud=9600" 35 | .PP 36 | Default BAUDRATE: 37 | .BR "115200" 38 | .RE 39 | .RE 40 | .TP 41 | .BI "port = " PORT 42 | Specifies TCP port on which the webserver is listening and where the Rest API is exposed. 43 | .TP 44 | .B "dontkeep" 45 | Don't keep the connection open, connect only for the time when command is processed. 46 | .SH EXAMPLES 47 | .PP 48 | Example Modbus TCP config: 49 | .PP 50 | .nf 51 | connect = tcp://127.0.0.1:5020 52 | port = 8080 53 | .fi 54 | .PP 55 | Example Modbus RTU config: 56 | .PP 57 | .nf 58 | connect = rtu:/dev/ttyUSB0?baud=9600 59 | port = 8080 60 | .fi 61 | .PP 62 | Example Modbus RTU config with debug messages verbosity: 63 | .PP 64 | .nf 65 | connect = rtu:/dev/ttyUSB0?baud=9600 66 | port = 8080 67 | debug 68 | .fi 69 | .SH SEE ALSO 70 | .BR restmbmaster (8), 71 | .SH AUTHOR 72 | .PP 73 | Jiri Pirko is the original author and current maintainer of restmbmaster. 74 | -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | MAINTAINERCLEANFILES = Makefile.in 2 | 3 | ACLOCAL_AMFLAGS = -I m4 4 | 5 | AM_CFLAGS = -I${top_srcdir}/include 6 | 7 | restmbmaster_CFLAGS= $(LIBMODBUS_CFLAGS) $(LIBMICROHTTPD_CFLAGS) -D_GNU_SOURCE 8 | restmbmaster_LDADD = $(LIBMODBUS_LIBS) $(LIBMICROHTTPD_LIBS) 9 | 10 | bin_PROGRAMS=restmbmaster 11 | restmbmaster_SOURCES=restmbmaster.c 12 | -------------------------------------------------------------------------------- /src/restmbmaster.c: -------------------------------------------------------------------------------- 1 | /* 2 | * restmbmaster.c - Rest API gateway to Modbus slaves 3 | * Copyright (C) 2019 Jiri Pirko 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU General Public License 7 | * as published by the Free Software Foundation; either version 2 8 | * of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | static int __parse_uint(const char *str, size_t max, char **p_endptr) 38 | { 39 | bool check_end = !p_endptr; 40 | char *endptr; 41 | unsigned long int val; 42 | 43 | val = strtoul(str, &endptr, 10); 44 | if (endptr == str || !isdigit(*str) || (check_end && *endptr != '\0')) { 45 | errno = EINVAL; 46 | return -1; 47 | } 48 | if (val > max) { 49 | errno = ERANGE; 50 | return -1; 51 | } 52 | if (p_endptr) 53 | *p_endptr = endptr; 54 | return val; 55 | } 56 | 57 | static int parse_uint8(const char *str, uint8_t *dest) 58 | { 59 | int err = __parse_uint(str, UCHAR_MAX, NULL); 60 | 61 | if (err == -1) 62 | return -1; 63 | *dest = err; 64 | return 0; 65 | } 66 | 67 | static int __parse_uint16(const char *str, uint16_t *dest, char **p_endptr) 68 | { 69 | int err = __parse_uint(str, USHRT_MAX, p_endptr); 70 | 71 | if (err == -1) 72 | return -1; 73 | *dest = err; 74 | return 0; 75 | } 76 | 77 | static int parse_uint16(const char *str, uint16_t *dest) 78 | { 79 | return __parse_uint16(str, dest, NULL); 80 | } 81 | 82 | enum rmm_cmd { 83 | RMM_CMD_RUN, 84 | RMM_CMD_HELP, 85 | RMM_CMD_VERSION, 86 | }; 87 | 88 | enum rmm_modbus_connection_type { 89 | RMM_MODBUS_CONNECTION_TYPE_RTU, 90 | RMM_MODBUS_CONNECTION_TYPE_TCP, 91 | }; 92 | 93 | struct rmm { 94 | enum rmm_cmd cmd; /* run is the default */ 95 | char *argv0; 96 | char *connect_uri; 97 | uint16_t port; 98 | unsigned int debug; 99 | enum rmm_modbus_connection_type connection_type; 100 | modbus_t *mb; 101 | struct MHD_Daemon *mhd; 102 | char *page; 103 | bool mb_connected; 104 | bool mb_dontkeep; 105 | }; 106 | 107 | #define pr_dbg(rmm, args...) \ 108 | if (rmm->debug) \ 109 | fprintf(stdout, ##args) 110 | 111 | #define pr_err(args...) \ 112 | fprintf(stderr, ##args) 113 | 114 | enum rmm_modbus_obj_type { 115 | RMM_MODBUS_OBJ_TYPE_COIL, 116 | RMM_MODBUS_OBJ_TYPE_DISCRETE_INPUT, 117 | RMM_MODBUS_OBJ_TYPE_INPUT_REGISTER, 118 | RMM_MODBUS_OBJ_TYPE_HOLDING_REGISTER, 119 | }; 120 | 121 | static int rmm_modbus_obj_type_parse(const char *str, 122 | enum rmm_modbus_obj_type *obj_type) 123 | { 124 | if (!strcmp(str, "coils")) 125 | *obj_type = RMM_MODBUS_OBJ_TYPE_COIL; 126 | else if (!strcmp(str, "discrete-inputs")) 127 | *obj_type = RMM_MODBUS_OBJ_TYPE_DISCRETE_INPUT; 128 | else if (!strcmp(str, "input-registers")) 129 | *obj_type = RMM_MODBUS_OBJ_TYPE_INPUT_REGISTER; 130 | else if (!strcmp(str, "holding-registers")) 131 | *obj_type = RMM_MODBUS_OBJ_TYPE_HOLDING_REGISTER; 132 | else 133 | return -1; 134 | return 0; 135 | } 136 | 137 | #define RMM_ITEM_COUNT_MAX 128 138 | 139 | struct rmm_modbus_obj_ops { 140 | int (*get)(struct rmm *rmm, uint8_t slave_address, 141 | uint16_t item_address, uint16_t item_count, 142 | char *page, size_t page_size, unsigned int *status_code); 143 | int (*put)(struct rmm *rmm, uint8_t slave_address, 144 | uint16_t item_address, uint16_t item_count, 145 | uint16_t *item_input_vals, char *page, size_t page_size, 146 | unsigned int *status_code); 147 | }; 148 | 149 | static int rmm_page_value_put(uint16_t value, char *page, size_t page_size, 150 | unsigned int *status_code, size_t *page_offset) 151 | { 152 | int len; 153 | 154 | len = snprintf(page + *page_offset, page_size - *page_offset, 155 | "%s%u", *page_offset ? " " : "", value); 156 | if (len < 0 || page_size - *page_offset - 1 < len) { 157 | snprintf(page, page_size, "Failed to put values"); 158 | *status_code = MHD_HTTP_INTERNAL_SERVER_ERROR; 159 | return -1; 160 | } 161 | *page_offset += len; 162 | return 0; 163 | } 164 | 165 | static int rmm_modbus_coils_get(struct rmm *rmm, uint8_t slave_address, 166 | uint16_t item_address, uint16_t item_count, 167 | char *page, size_t page_size, 168 | unsigned int *status_code) 169 | { 170 | uint8_t vals[RMM_ITEM_COUNT_MAX]; 171 | bool retry_done = false; 172 | size_t page_offset = 0; 173 | int err; 174 | int i; 175 | 176 | again: 177 | modbus_set_slave(rmm->mb, slave_address); 178 | err = modbus_read_bits(rmm->mb, item_address, item_count, vals); 179 | if (err == -1) { 180 | if (!retry_done && errno == ECONNRESET) { 181 | retry_done = true; 182 | goto again; 183 | } 184 | snprintf(page, page_size, "Unable to read modbus coils: %s", 185 | modbus_strerror(errno)); 186 | *status_code = MHD_HTTP_BAD_REQUEST; 187 | return -1; 188 | } 189 | for (i = 0; i < item_count; i++) { 190 | err = rmm_page_value_put(vals[i], page, page_size, 191 | status_code, &page_offset); 192 | if (err == -1) 193 | return -1; 194 | } 195 | return 0; 196 | } 197 | 198 | static int rmm_modbus_coils_put(struct rmm *rmm, uint8_t slave_address, 199 | uint16_t item_address, uint16_t item_count, 200 | uint16_t *item_input_vals, 201 | char *page, size_t page_size, 202 | unsigned int *status_code) 203 | { 204 | uint8_t vals[RMM_ITEM_COUNT_MAX]; 205 | bool retry_done = false; 206 | int err; 207 | int i; 208 | 209 | for (i = 0; i < item_count; i++) { 210 | if (item_input_vals[i] > 1) { 211 | snprintf(page, page_size, "Wrong input value (has to be either 0 or 1)"); 212 | *status_code = MHD_HTTP_BAD_REQUEST; 213 | return -1; 214 | } 215 | vals[i] = item_input_vals[i]; 216 | } 217 | 218 | again: 219 | modbus_set_slave(rmm->mb, slave_address); 220 | err = modbus_write_bits(rmm->mb, item_address, item_count, vals); 221 | if (err == -1) { 222 | if (!retry_done && errno == ECONNRESET) { 223 | retry_done = true; 224 | goto again; 225 | } 226 | snprintf(page, page_size, "Unable to write modbus coils: %s", 227 | modbus_strerror(errno)); 228 | *status_code = MHD_HTTP_BAD_REQUEST; 229 | return -1; 230 | } 231 | return 0; 232 | } 233 | 234 | static int rmm_modbus_discrete_inputs_get(struct rmm *rmm, 235 | uint8_t slave_address, 236 | uint16_t item_address, 237 | uint16_t item_count, 238 | char *page, size_t page_size, 239 | unsigned int *status_code) 240 | { 241 | uint8_t vals[RMM_ITEM_COUNT_MAX]; 242 | bool retry_done = false; 243 | size_t page_offset = 0; 244 | int err; 245 | int i; 246 | 247 | again: 248 | modbus_set_slave(rmm->mb, slave_address); 249 | err = modbus_read_input_bits(rmm->mb, item_address, item_count, vals); 250 | if (err == -1) { 251 | if (!retry_done && errno == ECONNRESET) { 252 | retry_done = true; 253 | goto again; 254 | } 255 | snprintf(page, page_size, "Unable to read modbus discrete inputs: %s", 256 | modbus_strerror(errno)); 257 | *status_code = MHD_HTTP_BAD_REQUEST; 258 | return -1; 259 | } 260 | for (i = 0; i < item_count; i++) { 261 | err = rmm_page_value_put(vals[i], page, page_size, 262 | status_code, &page_offset); 263 | if (err == -1) 264 | return -1; 265 | } 266 | return 0; 267 | } 268 | 269 | static int rmm_modbus_input_registers_get(struct rmm *rmm, 270 | uint8_t slave_address, 271 | uint16_t item_address, 272 | uint16_t item_count, 273 | char *page, size_t page_size, 274 | unsigned int *status_code) 275 | { 276 | uint16_t regs[RMM_ITEM_COUNT_MAX]; 277 | bool retry_done = false; 278 | size_t page_offset = 0; 279 | int err; 280 | int i; 281 | 282 | again: 283 | modbus_set_slave(rmm->mb, slave_address); 284 | err = modbus_read_input_registers(rmm->mb, item_address, 285 | item_count, regs); 286 | if (err == -1) { 287 | if (!retry_done && errno == ECONNRESET) { 288 | retry_done = true; 289 | goto again; 290 | } 291 | snprintf(page, page_size, "Unable to read modbus input registers: %s", 292 | modbus_strerror(errno)); 293 | *status_code = MHD_HTTP_BAD_REQUEST; 294 | return -1; 295 | } 296 | for (i = 0; i < item_count; i++) { 297 | err = rmm_page_value_put(regs[i], page, page_size, 298 | status_code, &page_offset); 299 | if (err == -1) 300 | return -1; 301 | } 302 | return 0; 303 | } 304 | 305 | static int rmm_modbus_holding_registers_get(struct rmm *rmm, 306 | uint8_t slave_address, 307 | uint16_t item_address, 308 | uint16_t item_count, 309 | char *page, size_t page_size, 310 | unsigned int *status_code) 311 | { 312 | uint16_t regs[RMM_ITEM_COUNT_MAX]; 313 | bool retry_done = false; 314 | size_t page_offset = 0; 315 | int err; 316 | int i; 317 | 318 | again: 319 | modbus_set_slave(rmm->mb, slave_address); 320 | err = modbus_read_registers(rmm->mb, item_address, item_count, regs); 321 | if (err == -1) { 322 | if (!retry_done && errno == ECONNRESET) { 323 | retry_done = true; 324 | goto again; 325 | } 326 | snprintf(page, page_size, "Unable to read modbus holding registers: %s", 327 | modbus_strerror(errno)); 328 | *status_code = MHD_HTTP_BAD_REQUEST; 329 | return -1; 330 | } 331 | for (i = 0; i < item_count; i++) { 332 | err = rmm_page_value_put(regs[i], page, page_size, 333 | status_code, &page_offset); 334 | if (err == -1) 335 | return -1; 336 | } 337 | return 0; 338 | } 339 | 340 | static int rmm_modbus_holding_registers_put(struct rmm *rmm, 341 | uint8_t slave_address, 342 | uint16_t item_address, 343 | uint16_t item_count, 344 | uint16_t *item_input_vals, 345 | char *page, size_t page_size, 346 | unsigned int *status_code) 347 | { 348 | bool retry_done = false; 349 | int err; 350 | 351 | again: 352 | modbus_set_slave(rmm->mb, slave_address); 353 | err = modbus_write_registers(rmm->mb, item_address, item_count, item_input_vals); 354 | if (err == -1) { 355 | if (!retry_done && errno == ECONNRESET) { 356 | retry_done = true; 357 | goto again; 358 | } 359 | snprintf(page, page_size, "Unable to write modbus holding registers: %s", 360 | modbus_strerror(errno)); 361 | *status_code = MHD_HTTP_BAD_REQUEST; 362 | return -1; 363 | } 364 | return 0; 365 | } 366 | 367 | static const struct rmm_modbus_obj_ops rmm_modbus_obj_ops[] = { 368 | [RMM_MODBUS_OBJ_TYPE_COIL] = { 369 | .get = rmm_modbus_coils_get, 370 | .put = rmm_modbus_coils_put, 371 | }, 372 | [RMM_MODBUS_OBJ_TYPE_DISCRETE_INPUT] = { 373 | .get = rmm_modbus_discrete_inputs_get, 374 | }, 375 | [RMM_MODBUS_OBJ_TYPE_INPUT_REGISTER] = { 376 | .get = rmm_modbus_input_registers_get, 377 | }, 378 | [RMM_MODBUS_OBJ_TYPE_HOLDING_REGISTER] = { 379 | .get = rmm_modbus_holding_registers_get, 380 | .put = rmm_modbus_holding_registers_put, 381 | }, 382 | }; 383 | 384 | static const char *rmm_next_slash(char **pos) 385 | { 386 | char *slash, *str = *pos; 387 | 388 | if (!*pos) 389 | return NULL; 390 | 391 | slash = strchr(str, '/'); 392 | if (slash) { 393 | slash[0] = '\0'; 394 | *pos = slash + 1; 395 | } else { 396 | *pos = NULL; 397 | } 398 | return str; 399 | } 400 | 401 | /* 402 | * /slaves/SLAVE_ADDRESS/coils/INDEX[?count=NUMBER_OF_ITEMS] 403 | * PUT - success 204 (MHD_HTTP_NO_CONTENT) 404 | * error 400 (MHD_HTTP_BAD_REQUEST) 405 | * GET - success 200 (MHD_HTTP_OK) 406 | * 407 | * wrong path 404 (MHD_HTTP_NOT_FOUND) 408 | * method not supported 405 (MHD_HTTP_METHOD_NOT_ALLOWED) 409 | * 410 | */ 411 | 412 | #define RMM_PAGE_SIZE (128 * 8) 413 | #define RMM_URL_MAX 64 414 | 415 | struct rmm_post_context { 416 | char buf[RMM_PAGE_SIZE]; 417 | size_t offset; 418 | }; 419 | 420 | static void 421 | rmm_request_completed_callback(void *cls, struct MHD_Connection *connection, 422 | void **con_cls, 423 | enum MHD_RequestTerminationCode toe) 424 | { 425 | struct rmm_post_context *post_context = *con_cls; 426 | 427 | if (!post_context) 428 | return; 429 | free(post_context); 430 | } 431 | 432 | static enum MHD_Result rmm_ahcb(void *cls, struct MHD_Connection *connection, 433 | const char *_url, const char *method, 434 | const char *version, const char *upload_data, 435 | size_t *upload_data_size, void **ptr) 436 | { 437 | const struct rmm_modbus_obj_ops *modbus_obj_ops; 438 | struct rmm_post_context *post_context = *ptr; 439 | uint16_t item_input_vals[RMM_ITEM_COUNT_MAX]; 440 | enum rmm_modbus_obj_type obj_type; 441 | bool unexpected_method = false; 442 | struct MHD_Response *response; 443 | unsigned int status_code; 444 | const char *input = NULL; 445 | uint16_t item_count = 1; 446 | uint16_t item_address; 447 | uint8_t slave_address; 448 | struct rmm *rmm = cls; 449 | char *page = rmm->page; 450 | char url[RMM_URL_MAX]; 451 | char *pos = url; 452 | const char *str; 453 | int err; 454 | 455 | memset(page, 0, RMM_PAGE_SIZE); 456 | 457 | if (!strcmp(method, MHD_HTTP_METHOD_PUT)) { 458 | if (!post_context) { 459 | str = MHD_lookup_connection_value(connection, 460 | MHD_HEADER_KIND, 461 | MHD_HTTP_HEADER_CONTENT_TYPE); 462 | if (str && strcmp(str, "text/plain")) { 463 | snprintf(page, RMM_PAGE_SIZE, "Wrong content type, expected \"text/plain\""); 464 | status_code = MHD_HTTP_BAD_REQUEST; 465 | goto response; 466 | } 467 | 468 | post_context = calloc(1, sizeof(*post_context)); 469 | if (!post_context) { 470 | fprintf(stderr, "Failed to allocate POST context: %s\n", 471 | strerror(errno)); 472 | status_code = MHD_HTTP_INTERNAL_SERVER_ERROR; 473 | goto response; 474 | } 475 | *ptr = post_context; 476 | return MHD_YES; 477 | } else { 478 | size_t size = *upload_data_size; 479 | 480 | if (size) { 481 | if (size + post_context->offset > 482 | sizeof(post_context->buf)) 483 | return MHD_NO; 484 | memcpy(post_context->buf + post_context->offset, 485 | upload_data, size); 486 | post_context->offset += size; 487 | *upload_data_size = 0; 488 | return MHD_YES; 489 | } 490 | input = post_context->buf; 491 | } 492 | } 493 | if (!strcmp(method, MHD_HTTP_METHOD_GET) && *upload_data_size) 494 | return MHD_NO; 495 | 496 | if (strlen(_url) + 1 > sizeof(url)) 497 | goto wrong_path; 498 | 499 | strcpy(url, _url); 500 | 501 | str = rmm_next_slash(&pos); 502 | if (!str || strlen(str)) 503 | goto wrong_path; 504 | 505 | str = rmm_next_slash(&pos); 506 | if (!str || !strlen(str)) 507 | goto wrong_path; 508 | 509 | if (strcmp(str, "slaves")) 510 | goto wrong_path; 511 | 512 | str = rmm_next_slash(&pos); 513 | if (!str || !strlen(str)) 514 | goto wrong_path; 515 | 516 | err = parse_uint8(str, &slave_address); 517 | if (err == -1) { 518 | snprintf(page, RMM_PAGE_SIZE, "Failed to parse slave address: %s", 519 | strerror(errno)); 520 | status_code = MHD_HTTP_BAD_REQUEST; 521 | goto response; 522 | } 523 | 524 | str = rmm_next_slash(&pos); 525 | if (!str) 526 | goto wrong_path; 527 | 528 | err = rmm_modbus_obj_type_parse(str, &obj_type); 529 | if (err == -1) 530 | goto wrong_path; 531 | modbus_obj_ops = &rmm_modbus_obj_ops[obj_type]; 532 | 533 | str = rmm_next_slash(&pos); 534 | if (!str || !strlen(str)) 535 | goto wrong_path; 536 | 537 | err = parse_uint16(str, &item_address); 538 | if (err == -1) { 539 | snprintf(page, RMM_PAGE_SIZE, "Failed to parse item address: %s", 540 | strerror(errno)); 541 | status_code = MHD_HTTP_BAD_REQUEST; 542 | goto response; 543 | } 544 | 545 | /* Nothing else expected in the URL */ 546 | str = rmm_next_slash(&pos); 547 | if (str) 548 | goto wrong_path; 549 | 550 | if (!strcmp(method, MHD_HTTP_METHOD_PUT)) { 551 | const char *pos = input; 552 | char *endptr; 553 | 554 | item_count = 0; 555 | do { 556 | err = __parse_uint16(pos, 557 | &item_input_vals[item_count++], 558 | &endptr); 559 | if (err) { 560 | snprintf(page, RMM_PAGE_SIZE, "Failed to parse input values: %s", 561 | strerror(errno)); 562 | status_code = MHD_HTTP_BAD_REQUEST; 563 | goto response; 564 | 565 | } 566 | pos = endptr + 1; 567 | 568 | } while (*endptr != '\0'); 569 | } 570 | 571 | str = MHD_lookup_connection_value(connection, 572 | MHD_GET_ARGUMENT_KIND, "count"); 573 | if (str) { 574 | uint16_t arg_item_count; 575 | 576 | err = parse_uint16(str, &arg_item_count); 577 | if (err == -1) { 578 | snprintf(page, RMM_PAGE_SIZE, "Failed to parse count arg: %s", 579 | strerror(errno)); 580 | status_code = MHD_HTTP_BAD_REQUEST; 581 | goto response; 582 | } 583 | if (arg_item_count > RMM_ITEM_COUNT_MAX) { 584 | snprintf(page, RMM_PAGE_SIZE, "Count is bigger than max"); 585 | status_code = MHD_HTTP_BAD_REQUEST; 586 | goto response; 587 | } 588 | if (!strcmp(method, MHD_HTTP_METHOD_PUT) && 589 | item_count != arg_item_count) { 590 | snprintf(page, RMM_PAGE_SIZE, "Count arg is not in sync with number of input values"); 591 | status_code = MHD_HTTP_BAD_REQUEST; 592 | goto response; 593 | } 594 | item_count = arg_item_count; 595 | } 596 | 597 | if (!rmm->mb_connected) { 598 | err = modbus_connect(rmm->mb); 599 | if (err == -1) { 600 | snprintf(page, RMM_PAGE_SIZE, "Unable to connect to modbus"); 601 | status_code = MHD_HTTP_BAD_REQUEST; 602 | goto response; 603 | } else { 604 | rmm->mb_connected = true; 605 | } 606 | } 607 | 608 | if (!strcmp(method, MHD_HTTP_METHOD_GET)) { 609 | err = modbus_obj_ops->get(rmm, slave_address, item_address, 610 | item_count, page, RMM_PAGE_SIZE, &status_code); 611 | } else if (!strcmp(method, MHD_HTTP_METHOD_PUT)) { 612 | if (!modbus_obj_ops->put) 613 | goto unexpected_method; 614 | err = modbus_obj_ops->put(rmm, slave_address, item_address, 615 | item_count, item_input_vals, 616 | page, RMM_PAGE_SIZE, &status_code); 617 | } else { 618 | unexpected_method = true; 619 | } 620 | 621 | if (rmm->mb_dontkeep) { 622 | modbus_close(rmm->mb); 623 | rmm->mb_connected = false; 624 | } 625 | 626 | if (unexpected_method) 627 | goto unexpected_method; 628 | 629 | if (!err) 630 | status_code = MHD_HTTP_OK; 631 | 632 | response: 633 | response = MHD_create_response_from_buffer(strlen(page), page, 634 | MHD_RESPMEM_PERSISTENT); 635 | if (!response) 636 | return MHD_NO; 637 | 638 | if (MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, 639 | "text/plain") == MHD_NO) 640 | return MHD_NO; 641 | 642 | err = MHD_queue_response(connection, status_code, response); 643 | MHD_destroy_response(response); 644 | return err; 645 | 646 | wrong_path: 647 | page[0] = '\0'; 648 | status_code = MHD_HTTP_NOT_FOUND; 649 | goto response; 650 | 651 | unexpected_method: 652 | snprintf(page, RMM_PAGE_SIZE, "Allow: GET%s", 653 | modbus_obj_ops->put ? ", PUT" : ""); 654 | status_code = MHD_HTTP_METHOD_NOT_ALLOWED; 655 | goto response; 656 | } 657 | 658 | static int rmm_main_loop_run(struct rmm *rmm) 659 | { 660 | MHD_UNSIGNED_LONG_LONG mhd_timeout; 661 | struct timeval tv, *tvp; 662 | fd_set fds[3]; 663 | int fdmax = 0; 664 | int i; 665 | 666 | again: 667 | for (i = 0; i < 3; i++) 668 | FD_ZERO(&fds[i]); 669 | fdmax = 0; 670 | if (MHD_get_fdset(rmm->mhd, &fds[0], &fds[1], &fds[2], &fdmax) == 671 | MHD_NO) { 672 | pr_err("Unable to get webserver fdset\n"); 673 | return -1; 674 | } 675 | 676 | if (MHD_get_timeout(rmm->mhd, &mhd_timeout) == MHD_YES) { 677 | tv.tv_sec = mhd_timeout / 1000; 678 | tv.tv_usec = (mhd_timeout - tv.tv_sec * 1000) * 1000; 679 | tvp = &tv; 680 | } else { 681 | tvp = NULL; 682 | } 683 | 684 | while (select(fdmax + 1, &fds[0], &fds[1], &fds[2], tvp) < 0) { 685 | if (errno == EINTR) 686 | continue; 687 | pr_err("Select failed\n"); 688 | return -1; 689 | } 690 | MHD_run_from_select(rmm->mhd, &fds[0], &fds[1], &fds[2]); 691 | 692 | goto again; 693 | } 694 | 695 | static int rmm_webserver_init(struct rmm *rmm) 696 | { 697 | rmm->mhd = MHD_start_daemon(MHD_USE_ERROR_LOG, rmm->port, NULL, NULL, 698 | &rmm_ahcb, rmm, 699 | MHD_OPTION_NOTIFY_COMPLETED, 700 | &rmm_request_completed_callback, NULL, 701 | MHD_OPTION_END); 702 | if (!rmm->mhd) { 703 | pr_err("Unable to start webserver\n"); 704 | return -1; 705 | } 706 | return 0; 707 | } 708 | 709 | static void rmm_webserver_fini(struct rmm *rmm) 710 | { 711 | MHD_stop_daemon(rmm->mhd); 712 | } 713 | 714 | static int host_to_ip(char *host, char *ip) 715 | { 716 | struct addrinfo hints = { 717 | .ai_family = AF_INET, 718 | }; 719 | struct sockaddr_in sa_in; 720 | struct addrinfo *result; 721 | int err; 722 | 723 | err = getaddrinfo(host, NULL, &hints, &result); 724 | if (err) { 725 | pr_err("Unable to resolve hostname: %s\n", gai_strerror(err)); 726 | return -1; 727 | } 728 | memcpy(&sa_in, result->ai_addr, sizeof(sa_in)); 729 | freeaddrinfo(result); 730 | 731 | if (!inet_ntop(AF_INET, &sa_in.sin_addr, ip, INET_ADDRSTRLEN)) { 732 | pr_err("Unable to convert address to string\n"); 733 | return -1; 734 | } 735 | 736 | return 0; 737 | } 738 | 739 | #define RMM_MODBUS_RTU_PREFIX "rtu:" 740 | #define RMM_MODBUS_RTU_BAUD_DEFAULT 115200 741 | #define RMM_MODBUS_TCP_PREFIX "tcp://" 742 | 743 | static int rmm_modbus_init(struct rmm *rmm) 744 | { 745 | int err; 746 | 747 | if (!strncmp(rmm->connect_uri, RMM_MODBUS_RTU_PREFIX, 748 | strlen(RMM_MODBUS_RTU_PREFIX))) { 749 | int baud = RMM_MODBUS_RTU_BAUD_DEFAULT; 750 | char *device, *sep; 751 | 752 | rmm->connection_type = RMM_MODBUS_CONNECTION_TYPE_RTU; 753 | device = rmm->connect_uri + strlen(RMM_MODBUS_RTU_PREFIX); 754 | sep = strchr(device, '?'); 755 | if (sep) { 756 | err = sscanf(sep, "?baud=%u", &baud); 757 | if (err != 1) { 758 | pr_err("Failed to parse RTU parameters\n"); 759 | return -1; 760 | } 761 | sep[0] = '\0'; 762 | } 763 | pr_dbg(rmm, "RTU, %s, %u\n", device, baud); 764 | rmm->mb = modbus_new_rtu(device, baud, 'N', 8, 1); 765 | } else if (!strncmp(rmm->connect_uri, RMM_MODBUS_TCP_PREFIX, 766 | strlen(RMM_MODBUS_TCP_PREFIX))) { 767 | int port = MODBUS_TCP_DEFAULT_PORT; 768 | char ip[INET_ADDRSTRLEN]; 769 | char *host, *sep; 770 | 771 | rmm->connection_type = RMM_MODBUS_CONNECTION_TYPE_TCP; 772 | host = rmm->connect_uri + strlen(RMM_MODBUS_TCP_PREFIX); 773 | sep = strchr(host, ':'); 774 | if (sep) { 775 | err = sscanf(sep, ":%u", &port); 776 | if (err != 1) { 777 | pr_err("Failed to parse TCP port\n"); 778 | return -1; 779 | } 780 | sep[0] = '\0'; 781 | } 782 | err = host_to_ip(host, ip); 783 | if (err == -1) 784 | return -1; 785 | pr_dbg(rmm, "TCP, %s, %u\n", ip, port); 786 | rmm->mb = modbus_new_tcp(ip, port); 787 | } else { 788 | pr_err("Unsupported target type\n"); 789 | return -1; 790 | } 791 | if (!rmm->mb) { 792 | pr_err("Unable to allocate libmodbus context: %s\n", 793 | modbus_strerror(errno)); 794 | return -1; 795 | } 796 | 797 | err = modbus_set_error_recovery(rmm->mb, MODBUS_ERROR_RECOVERY_LINK | 798 | MODBUS_ERROR_RECOVERY_PROTOCOL); 799 | if (err == -1) { 800 | pr_err("Unable set error recovery: %s\n", 801 | modbus_strerror(errno)); 802 | modbus_free(rmm->mb); 803 | return -1; 804 | } 805 | 806 | err = modbus_connect(rmm->mb); 807 | if (err == -1) { 808 | pr_err("Unable to connect to modbus: %s\n", 809 | modbus_strerror(errno)); 810 | } else if (rmm->mb_dontkeep) { 811 | modbus_close(rmm->mb); 812 | } else { 813 | rmm->mb_connected = true; 814 | } 815 | 816 | return 0; 817 | } 818 | 819 | static void rmm_modbus_fini(struct rmm *rmm) 820 | { 821 | modbus_close(rmm->mb); 822 | modbus_free(rmm->mb); 823 | } 824 | 825 | static char *ident_from_argv0(struct rmm *rmm, char *argv0) 826 | { 827 | char *p; 828 | 829 | if ((p = strrchr(argv0, '/'))) 830 | return p + 1; 831 | return argv0; 832 | } 833 | 834 | static int parse_port(const char *str, uint16_t *port) 835 | { 836 | int err; 837 | 838 | err = parse_uint16(str, port); 839 | if (err == -1) { 840 | if (errno == EINVAL) 841 | pr_err("Port is garbage\n"); 842 | else if (errno == ERANGE) 843 | pr_err("Port number is outside value range\n"); 844 | return -1; 845 | } 846 | return 0; 847 | } 848 | 849 | static bool stronlyblanks(const char *str) 850 | { 851 | int i; 852 | 853 | for (i = 0; i < strlen(str); i++) 854 | if (!isblank(str[i])) 855 | return false; 856 | return true; 857 | } 858 | 859 | static char *strtrim(char *str) 860 | { 861 | int i; 862 | 863 | for (i = strlen(str) - 1; i >= 0; i--) { 864 | if (!isblank(str[i])) 865 | break; 866 | str[i] = '\0'; 867 | } 868 | for (i = 0; i < strlen(str); i++) 869 | if (!isblank(str[i])) 870 | return &str[i]; 871 | return NULL; 872 | } 873 | 874 | static int rmm_parse_config(struct rmm *rmm, const char *path) 875 | { 876 | char *pos, *key, *val; 877 | char buffer[128]; 878 | int linecnt = 0; 879 | char *rpath; 880 | FILE *f; 881 | int err; 882 | 883 | rpath = realpath(optarg, NULL); 884 | if (!rpath) { 885 | pr_err("Failed to get absolute path of \"%s\": %s\n", 886 | path, strerror(errno)); 887 | return -1; 888 | } 889 | f = fopen(rpath, "r"); 890 | if (!f) { 891 | pr_err("Failed to open config file \"%s\": %s\n", 892 | rpath, strerror(errno)); 893 | return -1; 894 | } 895 | 896 | while (fgets(buffer, sizeof(buffer), f)) { 897 | buffer[strlen(buffer) - 1] = '\0'; 898 | linecnt++; 899 | pos = strchr(buffer, '#'); 900 | if (pos) 901 | *pos = '\0'; 902 | if (stronlyblanks(buffer)) 903 | continue; 904 | pos = strchr(buffer, '='); 905 | if (pos) { 906 | *pos = '\0'; 907 | val = pos + 1; 908 | val = strtrim(val); 909 | } else { 910 | val = NULL; 911 | } 912 | key = buffer; 913 | key = strtrim(key); 914 | 915 | if (!strcmp(key, "connect")) { 916 | if (!val) 917 | goto err_value_missing; 918 | free(rmm->connect_uri); 919 | rmm->connect_uri = strdup(val); 920 | } else if (!strcmp(key, "port")) { 921 | if (!val) 922 | goto err_value_missing; 923 | err = parse_port(val, &rmm->port); 924 | if (err) 925 | goto err_out; 926 | } else if (!strcmp(key, "debug")) { 927 | if (val) 928 | goto err_value_should_not_be_there; 929 | rmm->debug++; 930 | } else if (!strcmp(key, "dontkeep")) { 931 | if (val) 932 | goto err_value_should_not_be_there; 933 | rmm->mb_dontkeep = true; 934 | } else { 935 | pr_err("Config, line %d: Key \"%s\" is unknown.\n", 936 | linecnt, key); 937 | goto err_out; 938 | } 939 | } 940 | 941 | fclose(f); 942 | return 0; 943 | 944 | err_value_missing: 945 | pr_err("Config, line %d: Key \"%s\" requires a value.\n", 946 | linecnt, key); 947 | goto err_out; 948 | err_value_should_not_be_there: 949 | pr_err("Config, line %d: Key \"%s\" does not allow value.\n", 950 | linecnt, key); 951 | err_out: 952 | fclose(f); 953 | return -1; 954 | } 955 | 956 | static int rmm_parse_cmdline(struct rmm *rmm, int argc, char *argv[]) 957 | { 958 | static const struct option long_options[] = { 959 | { "help", no_argument, NULL, 'h' }, 960 | { "version", no_argument, NULL, 'v' }, 961 | { "connect", required_argument, NULL, 'c' }, 962 | { "port", required_argument, NULL, 'p' }, 963 | { "debug", no_argument, NULL, 'd' }, 964 | { "config", required_argument, NULL, 'f' }, 965 | { "dontkeep", no_argument, NULL, 'K' }, 966 | { NULL, 0, NULL, 0 } 967 | }; 968 | int opt; 969 | int err; 970 | 971 | rmm->argv0 = ident_from_argv0(rmm, argv[0]); 972 | 973 | while ((opt = getopt_long(argc, argv, "hvc:p:df:K", 974 | long_options, NULL)) >= 0) { 975 | 976 | switch(opt) { 977 | case 'h': 978 | rmm->cmd = RMM_CMD_HELP; 979 | break; 980 | case 'v': 981 | rmm->cmd = RMM_CMD_VERSION; 982 | break; 983 | case 'c': 984 | free(rmm->connect_uri); 985 | rmm->connect_uri = strdup(optarg); 986 | break; 987 | case 'p': 988 | err = parse_port(optarg, &rmm->port); 989 | if (err) 990 | return -1; 991 | break; 992 | case 'd': 993 | rmm->debug++; 994 | break; 995 | case 'f': 996 | err = rmm_parse_config(rmm, optarg); 997 | if (err) 998 | return -1; 999 | break; 1000 | case 'K': 1001 | rmm->mb_dontkeep = true; 1002 | break; 1003 | default: 1004 | return -1; 1005 | } 1006 | } 1007 | 1008 | if (optind < argc) { 1009 | pr_err("Too many arguments\n"); 1010 | return -1; 1011 | } 1012 | 1013 | return 0; 1014 | } 1015 | 1016 | static void rmm_print_help(struct rmm *rmm) 1017 | { 1018 | printf( 1019 | "%s [options]\n" 1020 | " -h --help Show this help\n" 1021 | " -v --version Show version\n" 1022 | " -d --debug Increase verbosity\n" 1023 | " -c --connect=CONNECT_URI Modbus target to connect to. Supported formats:\n" 1024 | " tcp://HOSTNAME[:PORT]\n" 1025 | " (e.g tcp://test.abc:1000)\n" 1026 | " Default PORT is 502\n" 1027 | " rtu:DEVICEPATH[?baud=BAUDRATE]\n" 1028 | " (e.g. rtu:/dev/ttyS0?baud=9600\n" 1029 | " Default BAUDRATE is 115200\n" 1030 | " -p --port=PORT Port on which the webserver is listening\n" 1031 | " -f --config=FILE Load the specified configuration file\n" 1032 | " -K --dontkeep Don't keep connection open\n", 1033 | rmm->argv0); 1034 | } 1035 | 1036 | static int rmm_check_config(struct rmm *rmm) 1037 | { 1038 | if (!rmm->connect_uri) { 1039 | pr_err("Connect URI is unspecified\n"); 1040 | return -1; 1041 | } 1042 | if (!rmm->port) { 1043 | pr_err("Webserver port is undefined\n"); 1044 | return -1; 1045 | } 1046 | return 0; 1047 | } 1048 | 1049 | static struct rmm *rmm_alloc(void) 1050 | { 1051 | struct rmm *rmm = calloc(1, sizeof(struct rmm)); 1052 | 1053 | if (!rmm) 1054 | return NULL; 1055 | rmm->page = malloc(RMM_PAGE_SIZE); 1056 | if (!rmm->page) { 1057 | free(rmm); 1058 | return NULL; 1059 | } 1060 | return rmm; 1061 | } 1062 | 1063 | static void rmm_free(struct rmm *rmm) 1064 | { 1065 | free(rmm->connect_uri); 1066 | free(rmm->page); 1067 | free(rmm); 1068 | } 1069 | 1070 | int main(int argc, char *argv[]) 1071 | { 1072 | struct rmm *rmm; 1073 | int err; 1074 | 1075 | rmm = rmm_alloc(); 1076 | if (!rmm) { 1077 | pr_err("Unable to allocate rmm context\n"); 1078 | return EXIT_FAILURE; 1079 | } 1080 | 1081 | err = rmm_parse_cmdline(rmm, argc, argv); 1082 | if (err) { 1083 | err = EXIT_FAILURE; 1084 | goto rmm_free; 1085 | } 1086 | 1087 | switch (rmm->cmd) { 1088 | case RMM_CMD_HELP: 1089 | rmm_print_help(rmm); 1090 | err = EXIT_SUCCESS; 1091 | goto rmm_free; 1092 | case RMM_CMD_VERSION: 1093 | printf("%s "PACKAGE_VERSION"\n", rmm->argv0); 1094 | err = EXIT_SUCCESS; 1095 | goto rmm_free; 1096 | case RMM_CMD_RUN: 1097 | break; 1098 | } 1099 | 1100 | err = rmm_check_config(rmm); 1101 | if (err) { 1102 | rmm_print_help(rmm); 1103 | err = EXIT_FAILURE; 1104 | goto rmm_free; 1105 | } 1106 | 1107 | err = rmm_modbus_init(rmm); 1108 | if (err) { 1109 | err = EXIT_FAILURE; 1110 | goto rmm_free; 1111 | } 1112 | 1113 | err = rmm_webserver_init(rmm); 1114 | if (err) { 1115 | err = EXIT_FAILURE; 1116 | goto rmm_modbus_fini; 1117 | } 1118 | 1119 | err = rmm_main_loop_run(rmm); 1120 | err = err ? EXIT_FAILURE : EXIT_SUCCESS; 1121 | 1122 | rmm_webserver_fini(rmm); 1123 | rmm_modbus_fini: 1124 | rmm_modbus_fini(rmm); 1125 | rmm_free: 1126 | rmm_free(rmm); 1127 | return err; 1128 | } 1129 | -------------------------------------------------------------------------------- /systemd/restmbmaster@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rest API Modbus master %I 3 | 4 | [Service] 5 | ExecStart=/usr/bin/restmbmaster -f /etc/restmbmaster/%i.conf 6 | Restart=on-failure 7 | RestartPreventExitStatus=1 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | --------------------------------------------------------------------------------