pamoja.modbus

Idiomatic Modbus RTU facade.

Modbus over RS485 is what cheap industrial sensing speaks: energy meters, soil probes, water-quality transmitters, pump controllers. Each request builder here returns a complete frame with its CRC, ready to write to a port, and a reply comes back through parse_frame() as an object that reads its own values.

  1"""Idiomatic Modbus RTU facade.
  2
  3Modbus over RS485 is what cheap industrial sensing speaks: energy meters, soil
  4probes, water-quality transmitters, pump controllers. Each request builder here
  5returns a complete frame with its CRC, ready to write to a port, and a reply comes
  6back through :func:`parse_frame` as an object that reads its own values.
  7"""
  8
  9from __future__ import annotations
 10
 11import enum
 12from typing import Sequence
 13
 14from pamoja._native import ModbusFrame
 15from pamoja._native import modbus_crc16 as _crc16
 16from pamoja._native import modbus_parse_frame as _parse_frame
 17from pamoja._native import modbus_raw as _raw
 18from pamoja._native import modbus_read_coils as _read_coils
 19from pamoja._native import modbus_read_discrete_inputs as _read_discrete_inputs
 20from pamoja._native import modbus_read_holding_registers as _read_holding_registers
 21from pamoja._native import (
 22    modbus_read_holding_registers_reply as _read_holding_registers_reply,
 23)
 24from pamoja._native import modbus_read_input_registers as _read_input_registers
 25from pamoja._native import (
 26    modbus_read_input_registers_reply as _read_input_registers_reply,
 27)
 28from pamoja._native import modbus_write_multiple_coils as _write_multiple_coils
 29from pamoja._native import modbus_write_multiple_registers as _write_multiple_registers
 30from pamoja._native import modbus_write_single_coil as _write_single_coil
 31from pamoja._native import modbus_write_single_register as _write_single_register
 32
 33__all__ = [
 34    "Exception_",
 35    "Function",
 36    "ModbusFrame",
 37    "crc16",
 38    "parse_frame",
 39    "raw",
 40    "read_coils",
 41    "read_discrete_inputs",
 42    "read_holding_registers",
 43    "read_holding_registers_reply",
 44    "read_input_registers",
 45    "read_input_registers_reply",
 46    "write_multiple_coils",
 47    "write_multiple_registers",
 48    "write_single_coil",
 49    "write_single_register",
 50]
 51
 52
 53class Function(int, enum.Enum):
 54    """The function codes this SDK names, as they appear on the wire."""
 55
 56    #: Read one or more coils (read/write bits).
 57    READ_COILS = 0x01
 58    #: Read one or more discrete inputs (read-only bits).
 59    READ_DISCRETE_INPUTS = 0x02
 60    #: Read one or more holding registers (read/write 16-bit words).
 61    READ_HOLDING_REGISTERS = 0x03
 62    #: Read one or more input registers (read-only 16-bit words).
 63    READ_INPUT_REGISTERS = 0x04
 64    #: Write a single coil.
 65    WRITE_SINGLE_COIL = 0x05
 66    #: Write a single holding register.
 67    WRITE_SINGLE_REGISTER = 0x06
 68    #: Write a contiguous block of coils.
 69    WRITE_MULTIPLE_COILS = 0x0F
 70    #: Write a contiguous block of holding registers.
 71    WRITE_MULTIPLE_REGISTERS = 0x10
 72
 73
 74class Exception_(int, enum.Enum):
 75    """The reason a device gives for refusing a request.
 76
 77    Named with a trailing underscore because ``Exception`` is a Python builtin.
 78    """
 79
 80    #: The function code is not allowed for this device.
 81    ILLEGAL_FUNCTION = 0x01
 82    #: The data address is not allowed for this device.
 83    ILLEGAL_DATA_ADDRESS = 0x02
 84    #: A value in the request is not allowed for this device.
 85    ILLEGAL_DATA_VALUE = 0x03
 86    #: The device failed while serving the request.
 87    SERVER_DEVICE_FAILURE = 0x04
 88    #: The device accepted a long-running request and is still processing it.
 89    ACKNOWLEDGE = 0x05
 90    #: The device is busy with a long-running request; retry later.
 91    SERVER_DEVICE_BUSY = 0x06
 92    #: The device detected a parity error in its memory.
 93    MEMORY_PARITY_ERROR = 0x08
 94    #: A gateway could not route the request to the target path.
 95    GATEWAY_PATH_UNAVAILABLE = 0x0A
 96    #: A gateway reached the target device but got no response.
 97    GATEWAY_TARGET_FAILED_TO_RESPOND = 0x0B
 98
 99
100def crc16(data: bytes) -> int:
101    """Compute the CRC-16/MODBUS that every RTU frame ends with.
102
103    :param data: The frame contents, without the trailing checksum.
104    :returns: The checksum.
105    """
106    return _crc16(bytes(data))
107
108
109def read_coils(address: int, start: int, count: int) -> bytes:
110    """Build a read-coils request (function ``0x01``).
111
112    :param address: The unit address to ask.
113    :param start: The address of the first coil.
114    :param count: How many coils to read.
115    :returns: The frame to send.
116    """
117    return _read_coils(address, start, count)
118
119
120def read_discrete_inputs(address: int, start: int, count: int) -> bytes:
121    """Build a read-discrete-inputs request (function ``0x02``).
122
123    :param address: The unit address to ask.
124    :param start: The address of the first input.
125    :param count: How many inputs to read.
126    :returns: The frame to send.
127    """
128    return _read_discrete_inputs(address, start, count)
129
130
131def read_holding_registers_reply(address: int, values: list[int]) -> bytes:
132    """Build the reply a device sends to a read-holding-registers request.
133
134    This is the answering half of :func:`read_holding_registers`, so a client can be
135    written and tested against what a device sends without a device on the line.
136
137    :param address: The unit address the reply comes from.
138    :param values: The register values the device reports, in address order.
139    :returns: The frame the device would send.
140    :raises PamojaError: If there are no values, or more than one frame can carry.
141    """
142    return _read_holding_registers_reply(address, values)
143
144
145def read_input_registers_reply(address: int, values: list[int]) -> bytes:
146    """Build the reply a device sends to a read-input-registers request.
147
148    :param address: The unit address the reply comes from.
149    :param values: The register values the device reports, in address order.
150    :returns: The frame the device would send.
151    :raises PamojaError: If there are no values, or more than one frame can carry.
152    """
153    return _read_input_registers_reply(address, values)
154
155
156def read_holding_registers(address: int, start: int, count: int) -> bytes:
157    """Build a read-holding-registers request (function ``0x03``).
158
159    :param address: The unit address to ask.
160    :param start: The address of the first register.
161    :param count: How many registers to read.
162    :returns: The frame to send.
163    """
164    return _read_holding_registers(address, start, count)
165
166
167def read_input_registers(address: int, start: int, count: int) -> bytes:
168    """Build a read-input-registers request (function ``0x04``).
169
170    :param address: The unit address to ask.
171    :param start: The address of the first register.
172    :param count: How many registers to read.
173    :returns: The frame to send.
174    """
175    return _read_input_registers(address, start, count)
176
177
178def write_single_coil(address: int, coil: int, on: bool) -> bytes:
179    """Build a write-single-coil request (function ``0x05``).
180
181    :param address: The unit address to write to.
182    :param coil: The coil address.
183    :param on: The state to write.
184    :returns: The frame to send.
185    """
186    return _write_single_coil(address, coil, on)
187
188
189def write_single_register(address: int, register: int, value: int) -> bytes:
190    """Build a write-single-register request (function ``0x06``).
191
192    :param address: The unit address to write to.
193    :param register: The register address.
194    :param value: The 16-bit value to write.
195    :returns: The frame to send.
196    """
197    return _write_single_register(address, register, value)
198
199
200def write_multiple_registers(address: int, start: int, values: Sequence[int]) -> bytes:
201    """Build a write-multiple-registers request (function ``0x10``).
202
203    :param address: The unit address to write to.
204    :param start: The address of the first register.
205    :param values: The 16-bit values, at most 123 of them.
206    :returns: The frame to send.
207    :raises PamojaError: If there are no values, or more than one request carries.
208    """
209    return _write_multiple_registers(address, start, list(values))
210
211
212def write_multiple_coils(address: int, start: int, values: Sequence[bool]) -> bytes:
213    """Build a write-multiple-coils request (function ``0x0F``).
214
215    :param address: The unit address to write to.
216    :param start: The address of the first coil.
217    :param values: One state per coil, at most 1968 of them.
218    :returns: The frame to send.
219    :raises PamojaError: If there are no values, or more than one request carries.
220    """
221    return _write_multiple_coils(address, start, [bool(value) for value in values])
222
223
224def raw(address: int, function_code: int, data: bytes) -> bytes:
225    """Build a request from a raw function code and data.
226
227    This is the escape hatch for the function codes this SDK does not name.
228
229    :param address: The unit address to send to.
230    :param function_code: The function code byte.
231    :param data: The bytes that follow it, used verbatim.
232    :returns: The frame to send.
233    :raises PamojaError: If the data is longer than a PDU may be.
234    """
235    return _raw(address, function_code, bytes(data))
236
237
238def parse_frame(data: bytes) -> ModbusFrame:
239    """Parse a received RTU frame, verifying its CRC.
240
241    :param data: The frame as it came off the wire, checksum included.
242    :returns: The validated frame, which reads its own registers and coils.
243    :raises PamojaError: If the frame is truncated, oversized, or its CRC does not
244        match its contents.
245    """
246    return _parse_frame(bytes(data))
class Exception_(builtins.int, enum.Enum):
75class Exception_(int, enum.Enum):
76    """The reason a device gives for refusing a request.
77
78    Named with a trailing underscore because ``Exception`` is a Python builtin.
79    """
80
81    #: The function code is not allowed for this device.
82    ILLEGAL_FUNCTION = 0x01
83    #: The data address is not allowed for this device.
84    ILLEGAL_DATA_ADDRESS = 0x02
85    #: A value in the request is not allowed for this device.
86    ILLEGAL_DATA_VALUE = 0x03
87    #: The device failed while serving the request.
88    SERVER_DEVICE_FAILURE = 0x04
89    #: The device accepted a long-running request and is still processing it.
90    ACKNOWLEDGE = 0x05
91    #: The device is busy with a long-running request; retry later.
92    SERVER_DEVICE_BUSY = 0x06
93    #: The device detected a parity error in its memory.
94    MEMORY_PARITY_ERROR = 0x08
95    #: A gateway could not route the request to the target path.
96    GATEWAY_PATH_UNAVAILABLE = 0x0A
97    #: A gateway reached the target device but got no response.
98    GATEWAY_TARGET_FAILED_TO_RESPOND = 0x0B

The reason a device gives for refusing a request.

Named with a trailing underscore because Exception is a Python builtin.

ILLEGAL_FUNCTION = <Exception_.ILLEGAL_FUNCTION: 1>
ILLEGAL_DATA_ADDRESS = <Exception_.ILLEGAL_DATA_ADDRESS: 2>
ILLEGAL_DATA_VALUE = <Exception_.ILLEGAL_DATA_VALUE: 3>
SERVER_DEVICE_FAILURE = <Exception_.SERVER_DEVICE_FAILURE: 4>
ACKNOWLEDGE = <Exception_.ACKNOWLEDGE: 5>
SERVER_DEVICE_BUSY = <Exception_.SERVER_DEVICE_BUSY: 6>
MEMORY_PARITY_ERROR = <Exception_.MEMORY_PARITY_ERROR: 8>
GATEWAY_PATH_UNAVAILABLE = <Exception_.GATEWAY_PATH_UNAVAILABLE: 10>
GATEWAY_TARGET_FAILED_TO_RESPOND = <Exception_.GATEWAY_TARGET_FAILED_TO_RESPOND: 11>
class Function(builtins.int, enum.Enum):
54class Function(int, enum.Enum):
55    """The function codes this SDK names, as they appear on the wire."""
56
57    #: Read one or more coils (read/write bits).
58    READ_COILS = 0x01
59    #: Read one or more discrete inputs (read-only bits).
60    READ_DISCRETE_INPUTS = 0x02
61    #: Read one or more holding registers (read/write 16-bit words).
62    READ_HOLDING_REGISTERS = 0x03
63    #: Read one or more input registers (read-only 16-bit words).
64    READ_INPUT_REGISTERS = 0x04
65    #: Write a single coil.
66    WRITE_SINGLE_COIL = 0x05
67    #: Write a single holding register.
68    WRITE_SINGLE_REGISTER = 0x06
69    #: Write a contiguous block of coils.
70    WRITE_MULTIPLE_COILS = 0x0F
71    #: Write a contiguous block of holding registers.
72    WRITE_MULTIPLE_REGISTERS = 0x10

The function codes this SDK names, as they appear on the wire.

READ_COILS = <Function.READ_COILS: 1>
READ_DISCRETE_INPUTS = <Function.READ_DISCRETE_INPUTS: 2>
READ_HOLDING_REGISTERS = <Function.READ_HOLDING_REGISTERS: 3>
READ_INPUT_REGISTERS = <Function.READ_INPUT_REGISTERS: 4>
WRITE_SINGLE_COIL = <Function.WRITE_SINGLE_COIL: 5>
WRITE_SINGLE_REGISTER = <Function.WRITE_SINGLE_REGISTER: 6>
WRITE_MULTIPLE_COILS = <Function.WRITE_MULTIPLE_COILS: 15>
WRITE_MULTIPLE_REGISTERS = <Function.WRITE_MULTIPLE_REGISTERS: 16>
class ModbusFrame:

A received Modbus RTU frame whose CRC has been verified.

def registers(self, /):

Reads the 16-bit registers out of a read-registers reply.

def coils(self, /, count):

Reads count coils or discrete inputs out of a read-bits reply.

exception

The exception code a device reported, or None when the frame is not an exception response.

function_code

The function code. An exception response carries the request's code with its high bit set, as it appeared on the wire.

address

The unit (slave) address the frame is addressed to or came from.

pdu

The protocol data unit: the function code and its data, without the address or the CRC.

def crc16(data: bytes) -> int:
101def crc16(data: bytes) -> int:
102    """Compute the CRC-16/MODBUS that every RTU frame ends with.
103
104    :param data: The frame contents, without the trailing checksum.
105    :returns: The checksum.
106    """
107    return _crc16(bytes(data))

Compute the CRC-16/MODBUS that every RTU frame ends with.

Parameters
  • data: The frame contents, without the trailing checksum. :returns: The checksum.
def parse_frame(data: bytes) -> ModbusFrame:
239def parse_frame(data: bytes) -> ModbusFrame:
240    """Parse a received RTU frame, verifying its CRC.
241
242    :param data: The frame as it came off the wire, checksum included.
243    :returns: The validated frame, which reads its own registers and coils.
244    :raises PamojaError: If the frame is truncated, oversized, or its CRC does not
245        match its contents.
246    """
247    return _parse_frame(bytes(data))

Parse a received RTU frame, verifying its CRC.

Parameters
  • data: The frame as it came off the wire, checksum included. :returns: The validated frame, which reads its own registers and coils.
Raises
  • PamojaError: If the frame is truncated, oversized, or its CRC does not match its contents.
def raw(address: int, function_code: int, data: bytes) -> bytes:
225def raw(address: int, function_code: int, data: bytes) -> bytes:
226    """Build a request from a raw function code and data.
227
228    This is the escape hatch for the function codes this SDK does not name.
229
230    :param address: The unit address to send to.
231    :param function_code: The function code byte.
232    :param data: The bytes that follow it, used verbatim.
233    :returns: The frame to send.
234    :raises PamojaError: If the data is longer than a PDU may be.
235    """
236    return _raw(address, function_code, bytes(data))

Build a request from a raw function code and data.

This is the escape hatch for the function codes this SDK does not name.

Parameters
  • address: The unit address to send to.
  • function_code: The function code byte.
  • data: The bytes that follow it, used verbatim. :returns: The frame to send.
Raises
  • PamojaError: If the data is longer than a PDU may be.
def read_coils(address: int, start: int, count: int) -> bytes:
110def read_coils(address: int, start: int, count: int) -> bytes:
111    """Build a read-coils request (function ``0x01``).
112
113    :param address: The unit address to ask.
114    :param start: The address of the first coil.
115    :param count: How many coils to read.
116    :returns: The frame to send.
117    """
118    return _read_coils(address, start, count)

Build a read-coils request (function 0x01).

Parameters
  • address: The unit address to ask.
  • start: The address of the first coil.
  • count: How many coils to read. :returns: The frame to send.
def read_discrete_inputs(address: int, start: int, count: int) -> bytes:
121def read_discrete_inputs(address: int, start: int, count: int) -> bytes:
122    """Build a read-discrete-inputs request (function ``0x02``).
123
124    :param address: The unit address to ask.
125    :param start: The address of the first input.
126    :param count: How many inputs to read.
127    :returns: The frame to send.
128    """
129    return _read_discrete_inputs(address, start, count)

Build a read-discrete-inputs request (function 0x02).

Parameters
  • address: The unit address to ask.
  • start: The address of the first input.
  • count: How many inputs to read. :returns: The frame to send.
def read_holding_registers(address: int, start: int, count: int) -> bytes:
157def read_holding_registers(address: int, start: int, count: int) -> bytes:
158    """Build a read-holding-registers request (function ``0x03``).
159
160    :param address: The unit address to ask.
161    :param start: The address of the first register.
162    :param count: How many registers to read.
163    :returns: The frame to send.
164    """
165    return _read_holding_registers(address, start, count)

Build a read-holding-registers request (function 0x03).

Parameters
  • address: The unit address to ask.
  • start: The address of the first register.
  • count: How many registers to read. :returns: The frame to send.
def read_holding_registers_reply(address: int, values: list[int]) -> bytes:
132def read_holding_registers_reply(address: int, values: list[int]) -> bytes:
133    """Build the reply a device sends to a read-holding-registers request.
134
135    This is the answering half of :func:`read_holding_registers`, so a client can be
136    written and tested against what a device sends without a device on the line.
137
138    :param address: The unit address the reply comes from.
139    :param values: The register values the device reports, in address order.
140    :returns: The frame the device would send.
141    :raises PamojaError: If there are no values, or more than one frame can carry.
142    """
143    return _read_holding_registers_reply(address, values)

Build the reply a device sends to a read-holding-registers request.

This is the answering half of read_holding_registers(), so a client can be written and tested against what a device sends without a device on the line.

Parameters
  • address: The unit address the reply comes from.
  • values: The register values the device reports, in address order. :returns: The frame the device would send.
Raises
  • PamojaError: If there are no values, or more than one frame can carry.
def read_input_registers(address: int, start: int, count: int) -> bytes:
168def read_input_registers(address: int, start: int, count: int) -> bytes:
169    """Build a read-input-registers request (function ``0x04``).
170
171    :param address: The unit address to ask.
172    :param start: The address of the first register.
173    :param count: How many registers to read.
174    :returns: The frame to send.
175    """
176    return _read_input_registers(address, start, count)

Build a read-input-registers request (function 0x04).

Parameters
  • address: The unit address to ask.
  • start: The address of the first register.
  • count: How many registers to read. :returns: The frame to send.
def read_input_registers_reply(address: int, values: list[int]) -> bytes:
146def read_input_registers_reply(address: int, values: list[int]) -> bytes:
147    """Build the reply a device sends to a read-input-registers request.
148
149    :param address: The unit address the reply comes from.
150    :param values: The register values the device reports, in address order.
151    :returns: The frame the device would send.
152    :raises PamojaError: If there are no values, or more than one frame can carry.
153    """
154    return _read_input_registers_reply(address, values)

Build the reply a device sends to a read-input-registers request.

Parameters
  • address: The unit address the reply comes from.
  • values: The register values the device reports, in address order. :returns: The frame the device would send.
Raises
  • PamojaError: If there are no values, or more than one frame can carry.
def write_multiple_coils(address: int, start: int, values: Sequence[bool]) -> bytes:
213def write_multiple_coils(address: int, start: int, values: Sequence[bool]) -> bytes:
214    """Build a write-multiple-coils request (function ``0x0F``).
215
216    :param address: The unit address to write to.
217    :param start: The address of the first coil.
218    :param values: One state per coil, at most 1968 of them.
219    :returns: The frame to send.
220    :raises PamojaError: If there are no values, or more than one request carries.
221    """
222    return _write_multiple_coils(address, start, [bool(value) for value in values])

Build a write-multiple-coils request (function 0x0F).

Parameters
  • address: The unit address to write to.
  • start: The address of the first coil.
  • values: One state per coil, at most 1968 of them. :returns: The frame to send.
Raises
  • PamojaError: If there are no values, or more than one request carries.
def write_multiple_registers(address: int, start: int, values: Sequence[int]) -> bytes:
201def write_multiple_registers(address: int, start: int, values: Sequence[int]) -> bytes:
202    """Build a write-multiple-registers request (function ``0x10``).
203
204    :param address: The unit address to write to.
205    :param start: The address of the first register.
206    :param values: The 16-bit values, at most 123 of them.
207    :returns: The frame to send.
208    :raises PamojaError: If there are no values, or more than one request carries.
209    """
210    return _write_multiple_registers(address, start, list(values))

Build a write-multiple-registers request (function 0x10).

Parameters
  • address: The unit address to write to.
  • start: The address of the first register.
  • values: The 16-bit values, at most 123 of them. :returns: The frame to send.
Raises
  • PamojaError: If there are no values, or more than one request carries.
def write_single_coil(address: int, coil: int, on: bool) -> bytes:
179def write_single_coil(address: int, coil: int, on: bool) -> bytes:
180    """Build a write-single-coil request (function ``0x05``).
181
182    :param address: The unit address to write to.
183    :param coil: The coil address.
184    :param on: The state to write.
185    :returns: The frame to send.
186    """
187    return _write_single_coil(address, coil, on)

Build a write-single-coil request (function 0x05).

Parameters
  • address: The unit address to write to.
  • coil: The coil address.
  • on: The state to write. :returns: The frame to send.
def write_single_register(address: int, register: int, value: int) -> bytes:
190def write_single_register(address: int, register: int, value: int) -> bytes:
191    """Build a write-single-register request (function ``0x06``).
192
193    :param address: The unit address to write to.
194    :param register: The register address.
195    :param value: The 16-bit value to write.
196    :returns: The frame to send.
197    """
198    return _write_single_register(address, register, value)

Build a write-single-register request (function 0x06).

Parameters
  • address: The unit address to write to.
  • register: The register address.
  • value: The 16-bit value to write. :returns: The frame to send.