Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

modbusy-1.1.0.tar.gz (19.2 kB view details)

Uploaded Source

Built Distribution

modbusy-1.1.0-py3-none-any.whl (17.5 kB view details)

Uploaded Python 3

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

Hashes for modbusy-1.1.0.tar.gz
Algorithm Hash digest
SHA256 fb6f3600f295627559b600e2b824f6f2ded9ce8015432b89d7d5c2a20be55f43
MD5 639c6d607cf7bcdd9df5b25c54198ce6
BLAKE2b-256 26bf688c839e4befae44189ad49345bc879ab0dcad4eb86e87350d2eb8d454b1

See more details on using hashes here.

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

Hashes for modbusy-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2e2f22aed088e41042851b219f90d0c44f351573b3be056fbe2e8669a3c79f76
MD5 d70108a36d03d7ef663aa3774f1db18d
BLAKE2b-256 9c8e354b3f6d433e98cef2a55979149d8deb50e128fb0ba7ae449a1e0dcdab32

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page