├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── pylibmodbus ├── __init__.py ├── modbus_core.py ├── modbus_rtu.py └── modbus_tcp.py ├── pyproject.toml ├── setup.cfg └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .DS_Store 3 | .vscode 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # CFFI cache 27 | __pycache__/ 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Stéphane Raimbault 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Webstack SARL nor the names of its 12 | contributors may be to endorse or promote products derived from this 13 | software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | .PHONY: install 3 | install: 4 | pip install . 5 | 6 | .ONESHELL: 7 | .PHONY: build 8 | build: install 9 | python -m pip install --upgrade build twine 10 | python -m build 11 | 12 | .ONESHELL: 13 | .PHONY: publish 14 | publish: build 15 | python -m twine upload dist/* 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pylibmodbus 2 | 3 | [![PyPI version](https://badge.fury.io/py/pylibmodbus.svg)](https://badge.fury.io/py/pylibmodbus) 4 | 5 | Python Interface for libmodbus written with CFFI. 6 | This libmodbus wrapper is compatible with Python 2 and Python 3. 7 | 8 | This wrapper is only compatible with libmodbus v3.1.2 and above. 9 | 10 | Required packages: 11 | 12 | - python-dev and libffi-dev 13 | - libmodbus and libmodbus-dev 14 | 15 | Licensed under BSD 3-Clause (see LICENSE file for details). 16 | 17 | ## Installation 18 | 19 | The package `pylibmodbus' is available from Pypi but you must install libmodbus 20 | before using is (see for details). 21 | 22 | Example for Debian: 23 | 24 | ```shell 25 | apt-get install libmodbus 26 | pip install pylibmodbus 27 | ``` 28 | 29 | ## Tests 30 | 31 | Before running the test suite, you need to launch a TCP server. 32 | You can use the server provided by libmodbus in `tests` directory: 33 | 34 | ```shell 35 | ./tests/bandwidth-server-many-up 36 | ``` 37 | 38 | Once this server is running, you can launch the Python tests with: 39 | 40 | ```shell 41 | python -m tests 42 | ``` 43 | -------------------------------------------------------------------------------- /pylibmodbus/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Stéphane Raimbault 2 | # 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | 5 | from .modbus_core import get_float, set_float, cast_to_int16, cast_to_int32 6 | from .modbus_tcp import ModbusTcp 7 | from .modbus_rtu import ModbusRtu 8 | -------------------------------------------------------------------------------- /pylibmodbus/modbus_core.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Stéphane Raimbault 2 | # 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | 5 | from cffi import FFI 6 | 7 | ffi = FFI() 8 | ffi.cdef( 9 | """ 10 | typedef struct _modbus modbus_t; 11 | int modbus_connect(modbus_t *ctx); 12 | int modbus_set_slave(modbus_t *ctx, int slave); 13 | void modbus_get_response_timeout(modbus_t *ctx, uint32_t *to_sec, uint32_t *to_usec); 14 | void modbus_set_response_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec); 15 | void modbus_close(modbus_t *ctx); 16 | int modbus_flush(modbus_t *ctx); 17 | const char *modbus_strerror(int errnum); 18 | 19 | int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); 20 | int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); 21 | int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); 22 | int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); 23 | int modbus_write_bit(modbus_t *ctx, int coil_addr, int status); 24 | int modbus_write_register(modbus_t *ctx, int reg_addr, int value); 25 | int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *data); 26 | int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *data); 27 | int modbus_write_and_read_registers(modbus_t *ctx, int write_addr, int write_nb, const uint16_t *src, int read_addr, int read_nb, uint16_t *dest); 28 | 29 | float modbus_get_float(const uint16_t *src); 30 | void modbus_set_float(float f, uint16_t *dest); 31 | 32 | modbus_t* modbus_new_tcp(const char *ip_address, int port); 33 | modbus_t* modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit); 34 | """ 35 | ) 36 | C = ffi.dlopen("modbus") 37 | 38 | 39 | def get_float(data): 40 | return C.modbus_get_float(data) 41 | 42 | 43 | def set_float(value, data): 44 | C.modbus_set_float(value, data) 45 | 46 | 47 | def cast_to_int16(data): 48 | return int(ffi.cast("int16_t", data)) 49 | 50 | 51 | def cast_to_int32(data): 52 | return int(ffi.cast("int32_t", data)) 53 | 54 | 55 | class ModbusException(Exception): 56 | pass 57 | 58 | 59 | class ModbusCore(object): 60 | def _run(self, func, *args): 61 | rc = func(self.ctx, *args) 62 | if rc == -1: 63 | raise Exception(ffi.string(C.modbus_strerror(ffi.errno))) 64 | 65 | def connect(self): 66 | return self._run(C.modbus_connect) 67 | 68 | def set_slave(self, slave): 69 | return self._run(C.modbus_set_slave, slave) 70 | 71 | def get_response_timeout(self): 72 | sec = ffi.new("uint32_t*") 73 | usec = ffi.new("uint32_t*") 74 | self._run(C.modbus_get_response_timeout, sec, usec) 75 | return sec[0] + (usec[0] / 1000000) 76 | 77 | def set_response_timeout(self, seconds): 78 | sec = int(seconds) 79 | usec = int((seconds - sec) * 1000000) 80 | self._run(C.modbus_set_response_timeout, sec, usec) 81 | 82 | def close(self): 83 | C.modbus_close(self.ctx) 84 | 85 | def flush(self): 86 | C.modbus_flush(self.ctx) 87 | 88 | def read_bits(self, addr, nb): 89 | dest = ffi.new("uint8_t[]", nb) 90 | self._run(C.modbus_read_bits, addr, nb, dest) 91 | return dest 92 | 93 | def read_input_bits(self, addr, nb): 94 | dest = ffi.new("uint8_t[]", nb) 95 | self._run(C.modbus_read_input_bits, addr, nb, dest) 96 | return dest 97 | 98 | def read_registers(self, addr, nb): 99 | dest = ffi.new("uint16_t[]", nb) 100 | self._run(C.modbus_read_registers, addr, nb, dest) 101 | return dest 102 | 103 | def read_input_registers(self, addr, nb): 104 | dest = ffi.new("uint16_t[]", nb) 105 | self._run(C.modbus_read_input_registers, addr, nb, dest) 106 | return dest 107 | 108 | def write_bit(self, addr, status): 109 | # int 110 | self._run(C.modbus_write_bit, addr, status) 111 | 112 | def write_register(self, addr, value): 113 | # int 114 | self._run(C.modbus_write_register, addr, value) 115 | 116 | def write_bits(self, addr, nb, data): 117 | # const uint8_t* 118 | nb = len(data) 119 | self._run(C.modbus_write_bits, addr, nb, data) 120 | 121 | def write_registers(self, addr, data): 122 | # const uint16_t* 123 | nb = len(data) 124 | self._run(C.modbus_write_registers, addr, nb, data) 125 | 126 | def write_and_read_registers(self, write_addr, data, read_addr, read_nb): 127 | # const uint16_t* 128 | dest = ffi.new("uint16_t[]", read_nb) 129 | self._run( 130 | C.modbus_write_and_read_registers, 131 | write_addr, 132 | len(data), 133 | data, 134 | read_addr, 135 | read_nb, 136 | dest, 137 | ) 138 | return dest 139 | -------------------------------------------------------------------------------- /pylibmodbus/modbus_rtu.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Stéphane Raimbault 2 | # 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | 5 | from .modbus_core import C, ModbusCore 6 | 7 | 8 | class ModbusRtu(ModbusCore): 9 | def __init__( 10 | self, device="/dev/ttyS0", baud=19200, parity="N", data_bit=8, stop_bit=1 11 | ): 12 | self.ctx = C.modbus_new_rtu(device, baud, parity, data_bit, stop_bit) 13 | -------------------------------------------------------------------------------- /pylibmodbus/modbus_tcp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Stéphane Raimbault 2 | # 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | 5 | from .modbus_core import C, ModbusCore 6 | 7 | 8 | class ModbusTcp(ModbusCore): 9 | def __init__(self, ip="127.0.0.1", port=502): 10 | self.ctx = C.modbus_new_tcp(ip.encode(), port) 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.black] 9 | target-version = ["py39"] 10 | line_length = 88 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pylibmodbus 3 | version = 0.6.2 4 | author = Stéphane Raimbault 5 | author_email = stephane.raimbault@gmail.com 6 | description = Python wrapper for libmodbus 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = http://github.com/stephane/pylibmodbus 10 | project_urls = 11 | Bug Tracker = https://github.com/stephane/pylibmodbus/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: BSD License 15 | Operating System :: OS Independent 16 | Intended Audience :: Developers 17 | Natural Language :: English 18 | 19 | [options] 20 | packages = find: 21 | python_requires = >=3.7 22 | install_requires = 23 | cffi>=1.15.1 24 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) Stéphane Raimbault 3 | 4 | """ 5 | Launch the program `tests/bandwidth-server-many-up` from libmodbus 6 | before running the test suite with: python -m tests 7 | """ 8 | import unittest 9 | 10 | import pylibmodbus 11 | 12 | 13 | class ModbusTcpTest(unittest.TestCase): 14 | def setUp(self): 15 | self.mb = pylibmodbus.ModbusTcp("127.0.0.1", 1502) 16 | self.mb.connect() 17 | 18 | def tearDown(self): 19 | self.mb.close() 20 | 21 | def test_get_set_timeout(self): 22 | old_response_timeout = self.mb.get_response_timeout() 23 | self.mb.set_response_timeout(old_response_timeout + 1) 24 | 25 | new_response_timeout = self.mb.get_response_timeout() 26 | self.assertEqual(new_response_timeout, old_response_timeout + 1) 27 | 28 | def test_read_and_write(self): 29 | nb = 5 30 | 31 | # Write [0, 0, 0, 0, 0] 32 | write_data = [0] * nb 33 | self.mb.write_registers(0, write_data) 34 | 35 | # Read 36 | read_data = self.mb.read_registers(0, nb) 37 | self.assertListEqual(write_data, list(read_data)) 38 | 39 | # Write [0, 1, 2, 3, 4] 40 | write_data = list(range(nb)) 41 | self.mb.write_registers(0, write_data) 42 | 43 | # Read 44 | read_data = self.mb.read_registers(0, nb) 45 | self.assertListEqual(write_data, list(read_data)) 46 | 47 | def test_write_and_read_registers(self): 48 | write_data = list(range(5)) 49 | # Write 5 registers and read 3 from address 2 50 | read_data = self.mb.write_and_read_registers(0, write_data, 2, 3) 51 | self.assertListEqual(list(read_data), write_data[2:]) 52 | 53 | 54 | class ModbusDataTest(unittest.TestCase): 55 | def test_set_get_float(self): 56 | UT_REAL = 916.540649 57 | data = [0x229A, 0x4465] 58 | self.assertAlmostEqual(pylibmodbus.get_float(data), UT_REAL, places=6) 59 | 60 | pylibmodbus.set_float(UT_REAL, data) 61 | self.assertAlmostEqual(pylibmodbus.get_float(data), UT_REAL, places=6) 62 | 63 | def test_cast_signed_integers(self): 64 | MAX_UINT16 = 65535 65 | MAX_UINT32 = 4294967295 66 | 67 | # 0 to 32767 -32768 to - 1 68 | self.assertEqual(pylibmodbus.cast_to_int16(0), 0) 69 | self.assertEqual(pylibmodbus.cast_to_int16(MAX_UINT16), -1) 70 | self.assertEqual(pylibmodbus.cast_to_int16(MAX_UINT16 / 2), 32767) 71 | self.assertEqual(pylibmodbus.cast_to_int16((MAX_UINT16 / 2) + 1), -32768) 72 | 73 | # Idem for 32 bits 74 | self.assertEqual(pylibmodbus.cast_to_int32(0), 0) 75 | self.assertEqual(pylibmodbus.cast_to_int32(MAX_UINT32), -1) 76 | self.assertEqual(pylibmodbus.cast_to_int32(MAX_UINT32 / 2), MAX_UINT32 // 2) 77 | self.assertEqual( 78 | pylibmodbus.cast_to_int32((MAX_UINT32 / 2) + 1), -((MAX_UINT32 // 2) + 1) 79 | ) 80 | 81 | 82 | if __name__ == "__main__": 83 | unittest.main() 84 | --------------------------------------------------------------------------------