Easy library for writing Modbus slaves
Project description
Modbusy - Modbus for mortals
Modbusy simplifies the creation of Modbus slaves. It is a Python library providing a simple interface to reduce boilerplate code when interfacing with uModbus. It requires only rudimentary knowledge of the Modbus protocol.
Additional helper methods are also provided to further accelerate development.
Example
The following example uses the modbusy.tcp_app()
helper to create a slave
that provides access to a single signed, 32-bit integer.
Execute the script using the following command:
python modbusy_example.py --address 127.0.0.1 --port 5020 12345
modbusy_example.py
import contextlib
import click
import modbusy
# The order of decorators is important
@modbusy.tcp_app()
@click.argument('value', type=int) # require a command-line argument to initialize value
@contextlib.contextmanager
def slave(app, value) -> None:
'''Modbus emulator to expose a single signed 32-bit integer.'''
@app.register(0, modbusy.INT32)
def read_value(for_write):
return value
@read_value.setter
def write_value(new_value):
nonlocal value
value = new_value
yield
if __name__ == '__main__':
slave()
Initialization should occur before the yield
and cleanup should occur after.
Description
Modbusy is 100% compatible with uModbus and can be used in existing code to simplify and extend it. Values are exposed on the slave by decorating getter and setter functions/methods. Function codes, address spaces, and address ranges are automatically determined based on the base address and the type registered for the getter and on whether or not a setter is provided.
Address spaces
BOOL
types are considered Modbus coils and use one address space. All other
Modbusy types are treated as Modbus registers and use another address space.
Extends uModbus
Modbusy is designed to work with an already exceptional Modbus library:
uModbus. The patch_server()
function is designed to add the register()
decorator and the update_defaults()
method to the server object returned by
either umodbus.server.tcp.get_server()
or umodbus.server.serial.get_server()
.
def patch_server(server, slave_id: int = 1, byteorder: str = 'big', mixed: bool = False): # returns server
...
It may also be passed default slave ID and endianness (byte order and mixed
mode). This may be updated later using update_defaults()
.
def update_defaults(self, slave_id: int = None, byteorder: str = None, mixed: bool = None) -> None:
...
TCP server example
from socketserver import TCPServer
from umodbus.server.tcp import RequestHandler, get_server
import modbusy
TCPServer.allow_reuse_address = True
app = get_server(TCPServer, ('localhost', 502), RequestHandler)
modbusy.patch_server(app, slave_id=2, byteorder='little', mixed=True)
Registering getters and setters
The register()
decorator is used to define points that are exposed by the
slave. It is a high-level wrapper around uModbus's route()
decorator.
def register(self, address: Union[int, Iterable[int]], kind: RegisterType, *,
slave_id: int = None, byteorder: str = None, mixed: bool = None) -> None:
...
The address parameter should typically be the base Modbus address for the value. The full address range will be computed based on the size of the value defined by the kind parameter, but it may be overridden by passing a list. If not provided, the slave_id, byteorder, and mixed parameters will use the server defaults. The available operations (function codes) are defined by the kind.
@app.register(100, modbusy.UINT64)
def read_value(for_write):
return value
The register()
decorator is used to define getters. The wrapped function is
also used when performing a partial update of a value (i.e., updating a word).
In that case, the for_write parameter will be True, and may be used to
suppress calculations that might normally occur on a read. Use the setter()
decorator of the read function to decorate the write function, similar to how
the built-in property()
decorator works.
@read_value.setter
def write_value(new_value):
value = new_value
If the second word of the value is being written, read_value(True)
is
called, bytes 3 and 4 are updated, and the new value is passed to
write_value()
. Writes are atomic for a single TCP request.
Read/write object attributes
The attribute()
helper method is used to make object attributes accessible
via Modbus without a bunch of boilerplate code.
class Settings:
knob = 123
flag = False
app.register(104, modbusy.INT32).attribute(Settings, 'knob', writable=False)
app.register(10, modbusy.BOOL).attribute(Settings, 'flag')
The writable parameter defaults to True. It may also be a callable that can override the default behavior. For instance, if writes should be allowed on read-only values without an error, then writable could be passed a no-op.
app.register(104, modbusy.INT32).attribute(Settings, 'knob', writable=lambda _: None)
Predefined types
BOOL
:
A boolean type implemented as a bit value or Modbus coil
INT16
:
Signed, 16-bit integer
INT32
:
Signed, 32-bit integer
INT64
:
Signed, 64-bit integer
INT128
:
Signed, 128-bit integer
UINT16
:
Usigned, 16-bit integer
UINT32
:
Usigned, 32-bit integer
UINT64
:
Usigned, 64-bit integer
UINT128
:
Usigned, 128-bit integer
FLOAT
:
4-byte, floating-point integer
FLOAT
:
8-byte, floating-point integer
String(N)
:
N-byte string (address is rounded up to a 2-byte boundary)
Extending types
Custom integers may be created using modbusy.Integer
. All modbusy types
subclass modbusy.RegisterType
. Additional types may be created by
subclassing modbusy.RegisterType
as well.
Address validation
uModbus and, by extension, modbusy, do no validation of addresses to prevent
overlap. The assert_unque_addresses()
helper can perform that validation.
modbusy.assert_unique_addresses(app)
Easy application creation
modbus.tcp_app()
is a helper function to avoid writing boilerplate code for
parsing arguments and instantiating and running a TCP slave server. It
decorates a Python context manager, typically created using the
contextlib.contextmanager
decorator, with optional click command-line
parsing. See the example at the top of this file for an example.
Creating a slave from a CSV file
A Modbus slave can be easily created from a CSV time series file.
sample_pv_1min.csv
utc_timestamp,active_power_total,dc_voltage,dc_unclipped_power
2018-03-01 00:00:00,71431,686.505555555555,72056
2018-03-01 00:01:00,70676,687.975925925926,71287
2018-03-01 00:02:00,69835,687.888888888889,70436
2018-03-01 00:05:00,67641,687.964814814815,68207
2018-03-01 00:06:00,67039,688.537037037037,67603
2018-03-01 00:07:00,66345,688.925925925926,66898
2018-03-01 00:08:00,65831,689.492592592593,66375
2018-03-01 00:10:00,64436,690.283333333333,64957
...
pv.yaml
registers:
- address: 0
column: active_power_total
type: float
trigger: yes
- address: 2
column: dc_voltage
type: float
- address: 4
column: dc_unclipped_power
type: float
- address: 60
value: 123
writable: yes
- address: 100
type: string[19]
column: utc_timestamp
Given the CSV data file and the YAML configuration file above, a slave can be started using the following command:
python -m modbusy.csvslave --address 127.0.0.1 --port 5020 pv.yaml sample_pv_1min.csv
When the first register, at address 0, is read, it will cause the next row in the CSV file to be read because it is a trigger register. After the entire file is read, it will start back at the beginning looping infinitely.
See the source code for more information. Check out the validate()
function
for the schema describing the YAML configuration.
License
Modbusy is licensed under a BSD 3-clause license found here.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file modbusy-1.1.0.tar.gz
.
File metadata
- Download URL: modbusy-1.1.0.tar.gz
- Upload date:
- Size: 19.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.4.2 importlib_metadata/4.4.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.0 CPython/3.9.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | fb6f3600f295627559b600e2b824f6f2ded9ce8015432b89d7d5c2a20be55f43 |
|
MD5 | 639c6d607cf7bcdd9df5b25c54198ce6 |
|
BLAKE2b-256 | 26bf688c839e4befae44189ad49345bc879ab0dcad4eb86e87350d2eb8d454b1 |
File details
Details for the file modbusy-1.1.0-py3-none-any.whl
.
File metadata
- Download URL: modbusy-1.1.0-py3-none-any.whl
- Upload date:
- Size: 17.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.4.2 importlib_metadata/4.4.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.0 CPython/3.9.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2e2f22aed088e41042851b219f90d0c44f351573b3be056fbe2e8669a3c79f76 |
|
MD5 | d70108a36d03d7ef663aa3774f1db18d |
|
BLAKE2b-256 | 9c8e354b3f6d433e98cef2a55979149d8deb50e128fb0ba7ae449a1e0dcdab32 |