Package: modbusy
Modbusy - Modbus for mortalsModbusy 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.ExampleThe following example uses themodbusy.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 12345modbusy_example.pyimportcontextlibimportclickimportmodbusy# The order of decorators is important@modbusy.tcp_app()@click.argument('value',type=int)# require a command-line argument to initialize value@contextlib.contextmanagerdefslave(app,value)->None:'''Modbus emulator to expose a single signed 32-bit integer.'''@app.register(0,modbusy.INT32)defread_value(for_write):returnvalue@read_value.setterdefwrite_value(new_value):nonlocalvaluevalue=new_valueyieldif__name__=='__main__':slave()Initialization should occur before theyieldand cleanup should occur after.DescriptionModbusy 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 spacesBOOLtypes are considered Modbus coils and use one address space. All other
Modbusy types are treated as Modbus registers and use another address space.Extends uModbusModbusy is designed to work with an already exceptional Modbus library:
uModbus.  Thepatch_server()function is designed to add theregister()decorator and theupdate_defaults()method to the server object returned by
eitherumodbus.server.tcp.get_server()orumodbus.server.serial.get_server().defpatch_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 usingupdate_defaults().defupdate_defaults(self,slave_id:int=None,byteorder:str=None,mixed:bool=None)->None:...TCP server examplefromsocketserverimportTCPServerfromumodbus.server.tcpimportRequestHandler,get_serverimportmodbusyTCPServer.allow_reuse_address=Trueapp=get_server(TCPServer,('localhost',502),RequestHandler)modbusy.patch_server(app,slave_id=2,byteorder='little',mixed=True)Registering getters and settersTheregister()decorator is used to define points that are exposed by the
slave. It is a high-level wrapper around uModbus'sroute()decorator.defregister(self,address:Union[int,Iterable[int]],kind:RegisterType,*,slave_id:int=None,byteorder:str=None,mixed:bool=None)->None:...Theaddressparameter 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 thekindparameter, but it may be overridden by passing a list.
If not provided, theslave_id,byteorder, andmixedparameters will use
the server defaults. The available operations (function codes) are defined by
thekind.@app.register(100,modbusy.UINT64)defread_value(for_write):returnvalueTheregister()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, thefor_writeparameter will be True, and may be used to
suppress calculations that might normally occur on a read. Use thesetter()decorator of the read function to decorate the write function, similar to how
the built-inproperty()decorator works.@read_value.setterdefwrite_value(new_value):value=new_valueIf 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 towrite_value(). Writes are atomic for a single TCP request.Read/write object attributesTheattribute()helper method is used to make object attributes accessible
via Modbus without a bunch of boilerplate code.classSettings:knob=123flag=Falseapp.register(104,modbusy.INT32).attribute(Settings,'knob',writable=False)app.register(10,modbusy.BOOL).attribute(Settings,'flag')Thewritableparameter 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, thenwritablecould be passed a no-op.app.register(104,modbusy.INT32).attribute(Settings,'knob',writable=lambda_:None)Predefined typesBOOL:
A boolean type implemented as a bit value or Modbus coilINT16:
Signed, 16-bit integerINT32:
Signed, 32-bit integerINT64:
Signed, 64-bit integerINT128:
Signed, 128-bit integerUINT16:
Usigned, 16-bit integerUINT32:
Usigned, 32-bit integerUINT64:
Usigned, 64-bit integerUINT128:
Usigned, 128-bit integerFLOAT:
4-byte, floating-point integerFLOAT:
8-byte, floating-point integerString(N):
N-byte string (address is rounded up to a 2-byte boundary)Extending typesCustom integers may be created usingmodbusy.Integer. All modbusy types
subclassmodbusy.RegisterType. Additional types may be created by
subclassingmodbusy.RegisterTypeas well.Address validationuModbus and, by extension, modbusy, do no validation of addresses to prevent
overlap. Theassert_unque_addresses()helper can perform that validation.modbusy.assert_unique_addresses(app)Easy application creationmodbus.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 thecontextlib.contextmanagerdecorator, with optional click command-line
parsing. See the example at the top of this file for an example.Creating a slave from a CSV fileA Modbus slave can be easily created from a CSV time series file.sample_pv_1min.csvutc_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.yamlregisters:-address:0column:active_power_totaltype:floattrigger:yes-address:2column:dc_voltagetype:float-address:4column:dc_unclipped_powertype:float-address:60value:123writable:yes-address:100type:string[19]column:utc_timestampGiven 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.csvWhen 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 thevalidate()function
for the schema describing the YAML configuration.LicenseModbusy is licensed under a BSD 3-clause license foundhere.