pamoja.modbus

Idiomatic Modbus RTU facade.

Modbus over RS485 is what cheap industrial sensing speaks: energy meters, soil probes, water-quality transmitters, pump controllers. A ModbusClient runs whole transactions over a serial port, with the timing the serial line specification sets, and a ModbusServer is a device a ModbusLine puts on the far end of a simulated one. Underneath, each request builder here returns a complete frame with its CRC, 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. A :class:`ModbusClient` runs whole
  5transactions over a serial port, with the timing the serial line specification sets, and a
  6:class:`ModbusServer` is a device a :class:`ModbusLine` puts on the far end of a simulated
  7one. Underneath, each request builder here returns a complete frame with its CRC, and a
  8reply comes back through :func:`parse_frame` as an object that reads its own values.
  9"""
 10
 11from __future__ import annotations
 12
 13import enum
 14from typing import Sequence
 15
 16from pamoja._native import ModbusClient as _NativeClient
 17from pamoja._native import ModbusClientError
 18from pamoja._native import ModbusFrame
 19from pamoja._native import ModbusLine as _NativeLine
 20from pamoja._native import ModbusServer
 21from pamoja._native import modbus_crc16 as _crc16
 22from pamoja._native import modbus_parse_frame as _parse_frame
 23from pamoja._native import modbus_raw as _raw
 24from pamoja._native import modbus_read_coils as _read_coils
 25from pamoja._native import modbus_read_discrete_inputs as _read_discrete_inputs
 26from pamoja._native import modbus_read_holding_registers as _read_holding_registers
 27from pamoja._native import (
 28    modbus_read_holding_registers_reply as _read_holding_registers_reply,
 29)
 30from pamoja._native import modbus_read_input_registers as _read_input_registers
 31from pamoja._native import (
 32    modbus_read_input_registers_reply as _read_input_registers_reply,
 33)
 34from pamoja._native import modbus_write_multiple_coils as _write_multiple_coils
 35from pamoja._native import modbus_write_multiple_registers as _write_multiple_registers
 36from pamoja._native import modbus_write_single_coil as _write_single_coil
 37from pamoja._native import modbus_write_single_register as _write_single_register
 38from pamoja.hal import Parity, SerialPort, SerialSettings
 39
 40__all__ = [
 41    "BROADCAST",
 42    "Exception_",
 43    "Function",
 44    "ModbusClient",
 45    "ModbusClientError",
 46    "ModbusFrame",
 47    "ModbusLine",
 48    "ModbusServer",
 49    "crc16",
 50    "parse_frame",
 51    "raw",
 52    "read_coils",
 53    "read_discrete_inputs",
 54    "read_holding_registers",
 55    "read_holding_registers_reply",
 56    "read_input_registers",
 57    "read_input_registers_reply",
 58    "write_multiple_coils",
 59    "write_multiple_registers",
 60    "write_single_coil",
 61    "write_single_register",
 62]
 63
 64
 65class Function(int, enum.Enum):
 66    """The function codes this SDK names, as they appear on the wire."""
 67
 68    #: Read one or more coils (read/write bits).
 69    READ_COILS = 0x01
 70    #: Read one or more discrete inputs (read-only bits).
 71    READ_DISCRETE_INPUTS = 0x02
 72    #: Read one or more holding registers (read/write 16-bit words).
 73    READ_HOLDING_REGISTERS = 0x03
 74    #: Read one or more input registers (read-only 16-bit words).
 75    READ_INPUT_REGISTERS = 0x04
 76    #: Write a single coil.
 77    WRITE_SINGLE_COIL = 0x05
 78    #: Write a single holding register.
 79    WRITE_SINGLE_REGISTER = 0x06
 80    #: Write a contiguous block of coils.
 81    WRITE_MULTIPLE_COILS = 0x0F
 82    #: Write a contiguous block of holding registers.
 83    WRITE_MULTIPLE_REGISTERS = 0x10
 84
 85
 86class Exception_(int, enum.Enum):
 87    """The reason a device gives for refusing a request.
 88
 89    Named with a trailing underscore because ``Exception`` is a Python builtin.
 90    """
 91
 92    #: The function code is not allowed for this device.
 93    ILLEGAL_FUNCTION = 0x01
 94    #: The data address is not allowed for this device.
 95    ILLEGAL_DATA_ADDRESS = 0x02
 96    #: A value in the request is not allowed for this device.
 97    ILLEGAL_DATA_VALUE = 0x03
 98    #: The device failed while serving the request.
 99    SERVER_DEVICE_FAILURE = 0x04
100    #: The device accepted a long-running request and is still processing it.
101    ACKNOWLEDGE = 0x05
102    #: The device is busy with a long-running request; retry later.
103    SERVER_DEVICE_BUSY = 0x06
104    #: The device detected a parity error in its memory.
105    MEMORY_PARITY_ERROR = 0x08
106    #: A gateway could not route the request to the target path.
107    GATEWAY_PATH_UNAVAILABLE = 0x0A
108    #: A gateway got no response from the target device, usually one not on the network.
109    GATEWAY_TARGET_FAILED_TO_RESPOND = 0x0B
110
111
112def crc16(data: bytes) -> int:
113    """Compute the CRC-16/MODBUS that every RTU frame ends with.
114
115    :param data: The frame contents, without the trailing checksum.
116    :returns: The checksum.
117    """
118    return _crc16(bytes(data))
119
120
121def read_coils(address: int, start: int, count: int) -> bytes:
122    """Build a read-coils request (function ``0x01``).
123
124    :param address: The unit address to ask.
125    :param start: The address of the first coil.
126    :param count: How many coils to read.
127    :returns: The frame to send.
128    """
129    return _read_coils(address, start, count)
130
131
132def read_discrete_inputs(address: int, start: int, count: int) -> bytes:
133    """Build a read-discrete-inputs request (function ``0x02``).
134
135    :param address: The unit address to ask.
136    :param start: The address of the first input.
137    :param count: How many inputs to read.
138    :returns: The frame to send.
139    """
140    return _read_discrete_inputs(address, start, count)
141
142
143def read_holding_registers_reply(address: int, values: list[int]) -> bytes:
144    """Build the reply a device sends to a read-holding-registers request.
145
146    This is the answering half of :func:`read_holding_registers`, so a client can be
147    written and tested against what a device sends without a device on the line.
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_holding_registers_reply(address, values)
155
156
157def read_input_registers_reply(address: int, values: list[int]) -> bytes:
158    """Build the reply a device sends to a read-input-registers request.
159
160    :param address: The unit address the reply comes from.
161    :param values: The register values the device reports, in address order.
162    :returns: The frame the device would send.
163    :raises PamojaError: If there are no values, or more than one frame can carry.
164    """
165    return _read_input_registers_reply(address, values)
166
167
168def read_holding_registers(address: int, start: int, count: int) -> bytes:
169    """Build a read-holding-registers request (function ``0x03``).
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_holding_registers(address, start, count)
177
178
179def read_input_registers(address: int, start: int, count: int) -> bytes:
180    """Build a read-input-registers request (function ``0x04``).
181
182    :param address: The unit address to ask.
183    :param start: The address of the first register.
184    :param count: How many registers to read.
185    :returns: The frame to send.
186    """
187    return _read_input_registers(address, start, count)
188
189
190def write_single_coil(address: int, coil: int, on: bool) -> bytes:
191    """Build a write-single-coil request (function ``0x05``).
192
193    :param address: The unit address to write to.
194    :param coil: The coil address.
195    :param on: The state to write.
196    :returns: The frame to send.
197    """
198    return _write_single_coil(address, coil, on)
199
200
201def write_single_register(address: int, register: int, value: int) -> bytes:
202    """Build a write-single-register request (function ``0x06``).
203
204    :param address: The unit address to write to.
205    :param register: The register address.
206    :param value: The 16-bit value to write.
207    :returns: The frame to send.
208    """
209    return _write_single_register(address, register, value)
210
211
212def write_multiple_registers(address: int, start: int, values: Sequence[int]) -> bytes:
213    """Build a write-multiple-registers request (function ``0x10``).
214
215    :param address: The unit address to write to.
216    :param start: The address of the first register.
217    :param values: The 16-bit values, at most 123 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_registers(address, start, list(values))
222
223
224def write_multiple_coils(address: int, start: int, values: Sequence[bool]) -> bytes:
225    """Build a write-multiple-coils request (function ``0x0F``).
226
227    :param address: The unit address to write to.
228    :param start: The address of the first coil.
229    :param values: One state per coil, at most 1968 of them.
230    :returns: The frame to send.
231    :raises PamojaError: If there are no values, or more than one request carries.
232    """
233    return _write_multiple_coils(address, start, [bool(value) for value in values])
234
235
236def raw(address: int, function_code: int, data: bytes) -> bytes:
237    """Build a request from a raw function code and data.
238
239    This is the escape hatch for the function codes this SDK does not name.
240
241    :param address: The unit address to send to.
242    :param function_code: The function code byte.
243    :param data: The bytes that follow it, used verbatim.
244    :returns: The frame to send.
245    :raises PamojaError: If the data is longer than a PDU may be.
246    """
247    return _raw(address, function_code, bytes(data))
248
249
250def parse_frame(data: bytes) -> ModbusFrame:
251    """Parse a received RTU frame, verifying its CRC.
252
253    :param data: The frame as it came off the wire, checksum included.
254    :returns: The validated frame, which reads its own registers and coils.
255    :raises PamojaError: If the frame is truncated, oversized, or its CRC does not
256        match its contents.
257    """
258    return _parse_frame(bytes(data))
259
260
261#: The unit address every device acts on and none answers.
262BROADCAST = 0
263
264
265class ModbusLine:
266    """Several devices on one simulated line, as devices share an RS485 pair.
267
268    Every frame reaches all of them, the one it is addressed to answers, and each carries
269    out a broadcast write. The line shares each :class:`ModbusServer` put on it, so the
270    program's own object still reads and changes the device. :meth:`port` makes a
271    :class:`~pamoja.hal.SerialPort` with the line on its far end for a :class:`ModbusClient`
272    to poll; nothing on it waits, and the silences and timeouts of a real line are counted
273    in its ``waited_micros``.
274
275    >>> meter = ModbusServer(17)
276    >>> meter.set_holding_registers(107, [2301, 418, 0])
277    >>> line = ModbusLine().attach(meter)
278    >>> client = ModbusClient(line.port(SerialSettings(19_200, Parity.EVEN)))
279    >>> client.read_holding_registers(17, 107, 3)
280    [2301, 418, 0]
281    """
282
283    __slots__ = ("_native",)
284
285    def __init__(self) -> None:
286        """Make a line with no devices on it."""
287        self._native = _NativeLine()
288
289    def attach(self, server: ModbusServer) -> ModbusLine:
290        """Put a device on the line, which shares it.
291
292        :param server: The device.
293        :returns: This line, to put another device on.
294        """
295        self._native.attach(server)
296        return self
297
298    def __len__(self) -> int:
299        """How many devices are on the line."""
300        return len(self._native)
301
302    def port(self, settings: SerialSettings) -> SerialPort:
303        """Make a serial port with the line on its far end.
304
305        Devices put on the line later are on the port too.
306
307        :param settings: The speed and character format the line runs at.
308        :returns: The port.
309        """
310        return SerialPort(
311            self._native.port(settings.baud, Parity(settings.parity).value, settings.stop_bits)
312        )
313
314
315class ModbusClient:
316    """A Modbus RTU client on a serial line: the gateway, the master in the specification's
317    words, that sends each request and waits for its reply.
318
319    Each transaction follows the Modbus over Serial Line specification. The client leaves
320    the line silent for 3.5 characters, or 1.75 ms above 19200 baud; drops anything stale
321    waiting in the port; writes the request; and reads the reply to the length the request
322    implies, against :attr:`response_timeout`. The reply's CRC, unit, and function are
323    checked before a value is read out of it, and any failure raises
324    :class:`ModbusClientError`, whose ``kind`` says why. A write to :data:`BROADCAST` reaches
325    every device and draws no reply, so the client waits out :attr:`turnaround` instead.
326    Each call releases the interpreter while the line is busy.
327    """
328
329    __slots__ = ("_native",)
330
331    def __init__(
332        self, port: SerialPort, response_timeout: float = 1.0, turnaround: float = 0.1
333    ) -> None:
334        """Make a client on a port.
335
336        :param port: The line, opened at the speed and format the devices on it use.
337        :param response_timeout: How long to wait for a whole reply, in seconds.
338        :param turnaround: How long to leave the line quiet after a broadcast, in seconds.
339        """
340        self._native = _NativeClient(port._native)
341        self.response_timeout = response_timeout
342        self.turnaround = turnaround
343
344    @staticmethod
345    def frame_gap_nanos(settings: SerialSettings) -> int:
346        """The silence that separates two frames: 3.5 characters at the line's speed and
347        format, and a fixed 1750 microseconds above 19200 baud.
348
349        :param settings: The line's speed and character format.
350        :returns: The silence, in nanoseconds, rounded up.
351        """
352        return _NativeClient.frame_gap_nanos(
353            settings.baud, Parity(settings.parity).value, settings.stop_bits
354        )
355
356    @property
357    def response_timeout(self) -> float:
358        """How long the client waits for a whole reply, in seconds."""
359        return self._native.response_timeout_micros / 1_000_000
360
361    @response_timeout.setter
362    def response_timeout(self, seconds: float) -> None:
363        self._native.set_response_timeout_micros(_micros(seconds))
364
365    @property
366    def turnaround(self) -> float:
367        """How long the client leaves the line quiet after a broadcast, in seconds."""
368        return self._native.turnaround_micros / 1_000_000
369
370    @turnaround.setter
371    def turnaround(self, seconds: float) -> None:
372        self._native.set_turnaround_micros(_micros(seconds))
373
374    def read_coils(self, unit: int, start: int, quantity: int) -> list[bool]:
375        """Read coils, function ``0x01``.
376
377        :param unit: The device, 1 to 247.
378        :param start: The first coil's address.
379        :param quantity: How many, 1 to 2000.
380        :returns: The coils' states, in address order.
381        :raises ModbusClientError: When the transaction fails.
382        """
383        return self._native.read_coils(unit, start, quantity)
384
385    def read_discrete_inputs(self, unit: int, start: int, quantity: int) -> list[bool]:
386        """Read discrete inputs, function ``0x02``.
387
388        :param unit: The device, 1 to 247.
389        :param start: The first input's address.
390        :param quantity: How many, 1 to 2000.
391        :returns: The inputs' states, in address order.
392        :raises ModbusClientError: When the transaction fails.
393        """
394        return self._native.read_discrete_inputs(unit, start, quantity)
395
396    def read_holding_registers(self, unit: int, start: int, quantity: int) -> list[int]:
397        """Read holding registers, function ``0x03``.
398
399        :param unit: The device, 1 to 247.
400        :param start: The first register's address.
401        :param quantity: How many, 1 to 125.
402        :returns: The registers' values, in address order.
403        :raises ModbusClientError: When the transaction fails.
404        """
405        return self._native.read_holding_registers(unit, start, quantity)
406
407    def read_input_registers(self, unit: int, start: int, quantity: int) -> list[int]:
408        """Read input registers, function ``0x04``.
409
410        :param unit: The device, 1 to 247.
411        :param start: The first register's address.
412        :param quantity: How many, 1 to 125.
413        :returns: The registers' values, in address order.
414        :raises ModbusClientError: When the transaction fails.
415        """
416        return self._native.read_input_registers(unit, start, quantity)
417
418    def write_single_coil(self, unit: int, address: int, on: bool) -> None:
419        """Write one coil, function ``0x05``.
420
421        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
422        :param address: The coil's address.
423        :param on: The state to write.
424        :raises ModbusClientError: When the transaction fails.
425        """
426        self._native.write_single_coil(unit, address, bool(on))
427
428    def write_single_register(self, unit: int, address: int, value: int) -> None:
429        """Write one holding register, function ``0x06``.
430
431        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
432        :param address: The register's address.
433        :param value: The value to write.
434        :raises ModbusClientError: When the transaction fails.
435        """
436        self._native.write_single_register(unit, address, value)
437
438    def write_multiple_coils(self, unit: int, start: int, values: Sequence[bool]) -> None:
439        """Write a run of coils, function ``0x0F``.
440
441        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
442        :param start: The first coil's address.
443        :param values: The states to write, 1 to 1968 of them, in address order.
444        :raises ModbusClientError: When the transaction fails.
445        """
446        self._native.write_multiple_coils(unit, start, [bool(value) for value in values])
447
448    def write_multiple_registers(self, unit: int, start: int, values: Sequence[int]) -> None:
449        """Write a run of holding registers, function ``0x10``.
450
451        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
452        :param start: The first register's address.
453        :param values: The values to write, 1 to 123 of them, in address order.
454        :raises ModbusClientError: When the transaction fails.
455        """
456        self._native.write_multiple_registers(unit, start, list(values))
457
458
459def _micros(seconds: float) -> int:
460    if not seconds >= 0:
461        raise ValueError("a time must be zero or more seconds")
462    return round(seconds * 1_000_000)
BROADCAST = 0
class Exception_(builtins.int, enum.Enum):
 87class Exception_(int, enum.Enum):
 88    """The reason a device gives for refusing a request.
 89
 90    Named with a trailing underscore because ``Exception`` is a Python builtin.
 91    """
 92
 93    #: The function code is not allowed for this device.
 94    ILLEGAL_FUNCTION = 0x01
 95    #: The data address is not allowed for this device.
 96    ILLEGAL_DATA_ADDRESS = 0x02
 97    #: A value in the request is not allowed for this device.
 98    ILLEGAL_DATA_VALUE = 0x03
 99    #: The device failed while serving the request.
100    SERVER_DEVICE_FAILURE = 0x04
101    #: The device accepted a long-running request and is still processing it.
102    ACKNOWLEDGE = 0x05
103    #: The device is busy with a long-running request; retry later.
104    SERVER_DEVICE_BUSY = 0x06
105    #: The device detected a parity error in its memory.
106    MEMORY_PARITY_ERROR = 0x08
107    #: A gateway could not route the request to the target path.
108    GATEWAY_PATH_UNAVAILABLE = 0x0A
109    #: A gateway got no response from the target device, usually one not on the network.
110    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):
66class Function(int, enum.Enum):
67    """The function codes this SDK names, as they appear on the wire."""
68
69    #: Read one or more coils (read/write bits).
70    READ_COILS = 0x01
71    #: Read one or more discrete inputs (read-only bits).
72    READ_DISCRETE_INPUTS = 0x02
73    #: Read one or more holding registers (read/write 16-bit words).
74    READ_HOLDING_REGISTERS = 0x03
75    #: Read one or more input registers (read-only 16-bit words).
76    READ_INPUT_REGISTERS = 0x04
77    #: Write a single coil.
78    WRITE_SINGLE_COIL = 0x05
79    #: Write a single holding register.
80    WRITE_SINGLE_REGISTER = 0x06
81    #: Write a contiguous block of coils.
82    WRITE_MULTIPLE_COILS = 0x0F
83    #: Write a contiguous block of holding registers.
84    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 ModbusClient:
316class ModbusClient:
317    """A Modbus RTU client on a serial line: the gateway, the master in the specification's
318    words, that sends each request and waits for its reply.
319
320    Each transaction follows the Modbus over Serial Line specification. The client leaves
321    the line silent for 3.5 characters, or 1.75 ms above 19200 baud; drops anything stale
322    waiting in the port; writes the request; and reads the reply to the length the request
323    implies, against :attr:`response_timeout`. The reply's CRC, unit, and function are
324    checked before a value is read out of it, and any failure raises
325    :class:`ModbusClientError`, whose ``kind`` says why. A write to :data:`BROADCAST` reaches
326    every device and draws no reply, so the client waits out :attr:`turnaround` instead.
327    Each call releases the interpreter while the line is busy.
328    """
329
330    __slots__ = ("_native",)
331
332    def __init__(
333        self, port: SerialPort, response_timeout: float = 1.0, turnaround: float = 0.1
334    ) -> None:
335        """Make a client on a port.
336
337        :param port: The line, opened at the speed and format the devices on it use.
338        :param response_timeout: How long to wait for a whole reply, in seconds.
339        :param turnaround: How long to leave the line quiet after a broadcast, in seconds.
340        """
341        self._native = _NativeClient(port._native)
342        self.response_timeout = response_timeout
343        self.turnaround = turnaround
344
345    @staticmethod
346    def frame_gap_nanos(settings: SerialSettings) -> int:
347        """The silence that separates two frames: 3.5 characters at the line's speed and
348        format, and a fixed 1750 microseconds above 19200 baud.
349
350        :param settings: The line's speed and character format.
351        :returns: The silence, in nanoseconds, rounded up.
352        """
353        return _NativeClient.frame_gap_nanos(
354            settings.baud, Parity(settings.parity).value, settings.stop_bits
355        )
356
357    @property
358    def response_timeout(self) -> float:
359        """How long the client waits for a whole reply, in seconds."""
360        return self._native.response_timeout_micros / 1_000_000
361
362    @response_timeout.setter
363    def response_timeout(self, seconds: float) -> None:
364        self._native.set_response_timeout_micros(_micros(seconds))
365
366    @property
367    def turnaround(self) -> float:
368        """How long the client leaves the line quiet after a broadcast, in seconds."""
369        return self._native.turnaround_micros / 1_000_000
370
371    @turnaround.setter
372    def turnaround(self, seconds: float) -> None:
373        self._native.set_turnaround_micros(_micros(seconds))
374
375    def read_coils(self, unit: int, start: int, quantity: int) -> list[bool]:
376        """Read coils, function ``0x01``.
377
378        :param unit: The device, 1 to 247.
379        :param start: The first coil's address.
380        :param quantity: How many, 1 to 2000.
381        :returns: The coils' states, in address order.
382        :raises ModbusClientError: When the transaction fails.
383        """
384        return self._native.read_coils(unit, start, quantity)
385
386    def read_discrete_inputs(self, unit: int, start: int, quantity: int) -> list[bool]:
387        """Read discrete inputs, function ``0x02``.
388
389        :param unit: The device, 1 to 247.
390        :param start: The first input's address.
391        :param quantity: How many, 1 to 2000.
392        :returns: The inputs' states, in address order.
393        :raises ModbusClientError: When the transaction fails.
394        """
395        return self._native.read_discrete_inputs(unit, start, quantity)
396
397    def read_holding_registers(self, unit: int, start: int, quantity: int) -> list[int]:
398        """Read holding registers, function ``0x03``.
399
400        :param unit: The device, 1 to 247.
401        :param start: The first register's address.
402        :param quantity: How many, 1 to 125.
403        :returns: The registers' values, in address order.
404        :raises ModbusClientError: When the transaction fails.
405        """
406        return self._native.read_holding_registers(unit, start, quantity)
407
408    def read_input_registers(self, unit: int, start: int, quantity: int) -> list[int]:
409        """Read input registers, function ``0x04``.
410
411        :param unit: The device, 1 to 247.
412        :param start: The first register's address.
413        :param quantity: How many, 1 to 125.
414        :returns: The registers' values, in address order.
415        :raises ModbusClientError: When the transaction fails.
416        """
417        return self._native.read_input_registers(unit, start, quantity)
418
419    def write_single_coil(self, unit: int, address: int, on: bool) -> None:
420        """Write one coil, function ``0x05``.
421
422        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
423        :param address: The coil's address.
424        :param on: The state to write.
425        :raises ModbusClientError: When the transaction fails.
426        """
427        self._native.write_single_coil(unit, address, bool(on))
428
429    def write_single_register(self, unit: int, address: int, value: int) -> None:
430        """Write one holding register, function ``0x06``.
431
432        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
433        :param address: The register's address.
434        :param value: The value to write.
435        :raises ModbusClientError: When the transaction fails.
436        """
437        self._native.write_single_register(unit, address, value)
438
439    def write_multiple_coils(self, unit: int, start: int, values: Sequence[bool]) -> None:
440        """Write a run of coils, function ``0x0F``.
441
442        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
443        :param start: The first coil's address.
444        :param values: The states to write, 1 to 1968 of them, in address order.
445        :raises ModbusClientError: When the transaction fails.
446        """
447        self._native.write_multiple_coils(unit, start, [bool(value) for value in values])
448
449    def write_multiple_registers(self, unit: int, start: int, values: Sequence[int]) -> None:
450        """Write a run of holding registers, function ``0x10``.
451
452        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
453        :param start: The first register's address.
454        :param values: The values to write, 1 to 123 of them, in address order.
455        :raises ModbusClientError: When the transaction fails.
456        """
457        self._native.write_multiple_registers(unit, start, list(values))

A Modbus RTU client on a serial line: the gateway, the master in the specification's words, that sends each request and waits for its reply.

Each transaction follows the Modbus over Serial Line specification. The client leaves the line silent for 3.5 characters, or 1.75 ms above 19200 baud; drops anything stale waiting in the port; writes the request; and reads the reply to the length the request implies, against response_timeout. The reply's CRC, unit, and function are checked before a value is read out of it, and any failure raises ModbusClientError, whose kind says why. A write to BROADCAST reaches every device and draws no reply, so the client waits out turnaround instead. Each call releases the interpreter while the line is busy.

ModbusClient( port: pamoja.hal.SerialPort, response_timeout: float = 1.0, turnaround: float = 0.1)
332    def __init__(
333        self, port: SerialPort, response_timeout: float = 1.0, turnaround: float = 0.1
334    ) -> None:
335        """Make a client on a port.
336
337        :param port: The line, opened at the speed and format the devices on it use.
338        :param response_timeout: How long to wait for a whole reply, in seconds.
339        :param turnaround: How long to leave the line quiet after a broadcast, in seconds.
340        """
341        self._native = _NativeClient(port._native)
342        self.response_timeout = response_timeout
343        self.turnaround = turnaround

Make a client on a port.

Parameters
  • port: The line, opened at the speed and format the devices on it use.
  • response_timeout: How long to wait for a whole reply, in seconds.
  • turnaround: How long to leave the line quiet after a broadcast, in seconds.
response_timeout: float
357    @property
358    def response_timeout(self) -> float:
359        """How long the client waits for a whole reply, in seconds."""
360        return self._native.response_timeout_micros / 1_000_000

How long the client waits for a whole reply, in seconds.

turnaround: float
366    @property
367    def turnaround(self) -> float:
368        """How long the client leaves the line quiet after a broadcast, in seconds."""
369        return self._native.turnaround_micros / 1_000_000

How long the client leaves the line quiet after a broadcast, in seconds.

@staticmethod
def frame_gap_nanos(settings: pamoja.hal.SerialSettings) -> int:
345    @staticmethod
346    def frame_gap_nanos(settings: SerialSettings) -> int:
347        """The silence that separates two frames: 3.5 characters at the line's speed and
348        format, and a fixed 1750 microseconds above 19200 baud.
349
350        :param settings: The line's speed and character format.
351        :returns: The silence, in nanoseconds, rounded up.
352        """
353        return _NativeClient.frame_gap_nanos(
354            settings.baud, Parity(settings.parity).value, settings.stop_bits
355        )

The silence that separates two frames: 3.5 characters at the line's speed and format, and a fixed 1750 microseconds above 19200 baud.

Parameters
  • settings: The line's speed and character format. :returns: The silence, in nanoseconds, rounded up.
def read_coils(self, unit: int, start: int, quantity: int) -> list[bool]:
375    def read_coils(self, unit: int, start: int, quantity: int) -> list[bool]:
376        """Read coils, function ``0x01``.
377
378        :param unit: The device, 1 to 247.
379        :param start: The first coil's address.
380        :param quantity: How many, 1 to 2000.
381        :returns: The coils' states, in address order.
382        :raises ModbusClientError: When the transaction fails.
383        """
384        return self._native.read_coils(unit, start, quantity)

Read coils, function 0x01.

Parameters
  • unit: The device, 1 to 247.
  • start: The first coil's address.
  • quantity: How many, 1 to 2000. :returns: The coils' states, in address order.
Raises
  • ModbusClientError: When the transaction fails.
def read_discrete_inputs(self, unit: int, start: int, quantity: int) -> list[bool]:
386    def read_discrete_inputs(self, unit: int, start: int, quantity: int) -> list[bool]:
387        """Read discrete inputs, function ``0x02``.
388
389        :param unit: The device, 1 to 247.
390        :param start: The first input's address.
391        :param quantity: How many, 1 to 2000.
392        :returns: The inputs' states, in address order.
393        :raises ModbusClientError: When the transaction fails.
394        """
395        return self._native.read_discrete_inputs(unit, start, quantity)

Read discrete inputs, function 0x02.

Parameters
  • unit: The device, 1 to 247.
  • start: The first input's address.
  • quantity: How many, 1 to 2000. :returns: The inputs' states, in address order.
Raises
  • ModbusClientError: When the transaction fails.
def read_holding_registers(self, unit: int, start: int, quantity: int) -> list[int]:
397    def read_holding_registers(self, unit: int, start: int, quantity: int) -> list[int]:
398        """Read holding registers, function ``0x03``.
399
400        :param unit: The device, 1 to 247.
401        :param start: The first register's address.
402        :param quantity: How many, 1 to 125.
403        :returns: The registers' values, in address order.
404        :raises ModbusClientError: When the transaction fails.
405        """
406        return self._native.read_holding_registers(unit, start, quantity)

Read holding registers, function 0x03.

Parameters
  • unit: The device, 1 to 247.
  • start: The first register's address.
  • quantity: How many, 1 to 125. :returns: The registers' values, in address order.
Raises
  • ModbusClientError: When the transaction fails.
def read_input_registers(self, unit: int, start: int, quantity: int) -> list[int]:
408    def read_input_registers(self, unit: int, start: int, quantity: int) -> list[int]:
409        """Read input registers, function ``0x04``.
410
411        :param unit: The device, 1 to 247.
412        :param start: The first register's address.
413        :param quantity: How many, 1 to 125.
414        :returns: The registers' values, in address order.
415        :raises ModbusClientError: When the transaction fails.
416        """
417        return self._native.read_input_registers(unit, start, quantity)

Read input registers, function 0x04.

Parameters
  • unit: The device, 1 to 247.
  • start: The first register's address.
  • quantity: How many, 1 to 125. :returns: The registers' values, in address order.
Raises
  • ModbusClientError: When the transaction fails.
def write_single_coil(self, unit: int, address: int, on: bool) -> None:
419    def write_single_coil(self, unit: int, address: int, on: bool) -> None:
420        """Write one coil, function ``0x05``.
421
422        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
423        :param address: The coil's address.
424        :param on: The state to write.
425        :raises ModbusClientError: When the transaction fails.
426        """
427        self._native.write_single_coil(unit, address, bool(on))

Write one coil, function 0x05.

Parameters
  • unit: The device, 1 to 247, or BROADCAST for every device.
  • address: The coil's address.
  • on: The state to write.
Raises
  • ModbusClientError: When the transaction fails.
def write_single_register(self, unit: int, address: int, value: int) -> None:
429    def write_single_register(self, unit: int, address: int, value: int) -> None:
430        """Write one holding register, function ``0x06``.
431
432        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
433        :param address: The register's address.
434        :param value: The value to write.
435        :raises ModbusClientError: When the transaction fails.
436        """
437        self._native.write_single_register(unit, address, value)

Write one holding register, function 0x06.

Parameters
  • unit: The device, 1 to 247, or BROADCAST for every device.
  • address: The register's address.
  • value: The value to write.
Raises
  • ModbusClientError: When the transaction fails.
def write_multiple_coils(self, unit: int, start: int, values: Sequence[bool]) -> None:
439    def write_multiple_coils(self, unit: int, start: int, values: Sequence[bool]) -> None:
440        """Write a run of coils, function ``0x0F``.
441
442        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
443        :param start: The first coil's address.
444        :param values: The states to write, 1 to 1968 of them, in address order.
445        :raises ModbusClientError: When the transaction fails.
446        """
447        self._native.write_multiple_coils(unit, start, [bool(value) for value in values])

Write a run of coils, function 0x0F.

Parameters
  • unit: The device, 1 to 247, or BROADCAST for every device.
  • start: The first coil's address.
  • values: The states to write, 1 to 1968 of them, in address order.
Raises
  • ModbusClientError: When the transaction fails.
def write_multiple_registers(self, unit: int, start: int, values: Sequence[int]) -> None:
449    def write_multiple_registers(self, unit: int, start: int, values: Sequence[int]) -> None:
450        """Write a run of holding registers, function ``0x10``.
451
452        :param unit: The device, 1 to 247, or :data:`BROADCAST` for every device.
453        :param start: The first register's address.
454        :param values: The values to write, 1 to 123 of them, in address order.
455        :raises ModbusClientError: When the transaction fails.
456        """
457        self._native.write_multiple_registers(unit, start, list(values))

Write a run of holding registers, function 0x10.

Parameters
  • unit: The device, 1 to 247, or BROADCAST for every device.
  • start: The first register's address.
  • values: The values to write, 1 to 123 of them, in address order.
Raises
  • ModbusClientError: When the transaction fails.
class ModbusClientError(pamoja.PamojaError):

Raised when a Modbus transaction fails. kind names why: request, broadcast_read, port, timeout, frame, wrong_unit, wrong_function, mismatch, or exception. unit is the unit asked, and function_code, found, exception, and received carry what goes with the kind, and are None otherwise.

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.

pdu

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

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.

exception

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

class ModbusLine:
266class ModbusLine:
267    """Several devices on one simulated line, as devices share an RS485 pair.
268
269    Every frame reaches all of them, the one it is addressed to answers, and each carries
270    out a broadcast write. The line shares each :class:`ModbusServer` put on it, so the
271    program's own object still reads and changes the device. :meth:`port` makes a
272    :class:`~pamoja.hal.SerialPort` with the line on its far end for a :class:`ModbusClient`
273    to poll; nothing on it waits, and the silences and timeouts of a real line are counted
274    in its ``waited_micros``.
275
276    >>> meter = ModbusServer(17)
277    >>> meter.set_holding_registers(107, [2301, 418, 0])
278    >>> line = ModbusLine().attach(meter)
279    >>> client = ModbusClient(line.port(SerialSettings(19_200, Parity.EVEN)))
280    >>> client.read_holding_registers(17, 107, 3)
281    [2301, 418, 0]
282    """
283
284    __slots__ = ("_native",)
285
286    def __init__(self) -> None:
287        """Make a line with no devices on it."""
288        self._native = _NativeLine()
289
290    def attach(self, server: ModbusServer) -> ModbusLine:
291        """Put a device on the line, which shares it.
292
293        :param server: The device.
294        :returns: This line, to put another device on.
295        """
296        self._native.attach(server)
297        return self
298
299    def __len__(self) -> int:
300        """How many devices are on the line."""
301        return len(self._native)
302
303    def port(self, settings: SerialSettings) -> SerialPort:
304        """Make a serial port with the line on its far end.
305
306        Devices put on the line later are on the port too.
307
308        :param settings: The speed and character format the line runs at.
309        :returns: The port.
310        """
311        return SerialPort(
312            self._native.port(settings.baud, Parity(settings.parity).value, settings.stop_bits)
313        )

Several devices on one simulated line, as devices share an RS485 pair.

Every frame reaches all of them, the one it is addressed to answers, and each carries out a broadcast write. The line shares each ModbusServer put on it, so the program's own object still reads and changes the device. port() makes a ~pamoja.hal.SerialPort with the line on its far end for a ModbusClient to poll; nothing on it waits, and the silences and timeouts of a real line are counted in its waited_micros.

>>> meter = ModbusServer(17)
>>> meter.set_holding_registers(107, [2301, 418, 0])
>>> line = ModbusLine().attach(meter)
>>> client = ModbusClient(line.port(SerialSettings(19_200, Parity.EVEN)))
>>> client.read_holding_registers(17, 107, 3)
[2301, 418, 0]
ModbusLine()
286    def __init__(self) -> None:
287        """Make a line with no devices on it."""
288        self._native = _NativeLine()

Make a line with no devices on it.

def attach(self, server: ModbusServer) -> ModbusLine:
290    def attach(self, server: ModbusServer) -> ModbusLine:
291        """Put a device on the line, which shares it.
292
293        :param server: The device.
294        :returns: This line, to put another device on.
295        """
296        self._native.attach(server)
297        return self

Put a device on the line, which shares it.

Parameters
  • server: The device. :returns: This line, to put another device on.
def port(self, settings: pamoja.hal.SerialSettings) -> pamoja.hal.SerialPort:
303    def port(self, settings: SerialSettings) -> SerialPort:
304        """Make a serial port with the line on its far end.
305
306        Devices put on the line later are on the port too.
307
308        :param settings: The speed and character format the line runs at.
309        :returns: The port.
310        """
311        return SerialPort(
312            self._native.port(settings.baud, Parity(settings.parity).value, settings.stop_bits)
313        )

Make a serial port with the line on its far end.

Devices put on the line later are on the port too.

Parameters
  • settings: The speed and character format the line runs at. :returns: The port.
class ModbusServer:

A Modbus device: a unit address and the four tables it serves, coils, discrete inputs, holding registers, and input registers, each holding only the addresses it was given.

answer takes one RTU frame and returns the frame the device sends back, or None when it stays silent: the frame failed its CRC, is for another unit, or is a broadcast, whose write the device still carries out.

def set_coils(self, /, start, values):

Sets coils from an address on, adding any the device did not have.

def set_discrete_inputs(self, /, start, values):

Sets discrete inputs from an address on, adding any the device did not have.

def set_holding_registers(self, /, start, values):

Sets holding registers from an address on, adding any the device did not have.

def set_input_registers(self, /, start, values):

Sets input registers from an address on, adding any the device did not have.

def coil(self, /, address):

A coil's state, or None when the device has no coil there.

def discrete_input(self, /, address):

A discrete input's state, or None when the device has no input there.

def holding_register(self, /, address):

A holding register's value, or None when the device has no register there.

def input_register(self, /, address):

An input register's value, or None when the device has no register there.

def answer(self, /, frame):

Answers one RTU frame, as the device on the line does, or returns None when it stays silent.

unit

The device's unit address.

served

How many requests the device has carried out, broadcasts included and refusals not.

def crc16(data: bytes) -> int:
113def crc16(data: bytes) -> int:
114    """Compute the CRC-16/MODBUS that every RTU frame ends with.
115
116    :param data: The frame contents, without the trailing checksum.
117    :returns: The checksum.
118    """
119    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:
251def parse_frame(data: bytes) -> ModbusFrame:
252    """Parse a received RTU frame, verifying its CRC.
253
254    :param data: The frame as it came off the wire, checksum included.
255    :returns: The validated frame, which reads its own registers and coils.
256    :raises PamojaError: If the frame is truncated, oversized, or its CRC does not
257        match its contents.
258    """
259    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:
237def raw(address: int, function_code: int, data: bytes) -> bytes:
238    """Build a request from a raw function code and data.
239
240    This is the escape hatch for the function codes this SDK does not name.
241
242    :param address: The unit address to send to.
243    :param function_code: The function code byte.
244    :param data: The bytes that follow it, used verbatim.
245    :returns: The frame to send.
246    :raises PamojaError: If the data is longer than a PDU may be.
247    """
248    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:
122def read_coils(address: int, start: int, count: int) -> bytes:
123    """Build a read-coils request (function ``0x01``).
124
125    :param address: The unit address to ask.
126    :param start: The address of the first coil.
127    :param count: How many coils to read.
128    :returns: The frame to send.
129    """
130    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:
133def read_discrete_inputs(address: int, start: int, count: int) -> bytes:
134    """Build a read-discrete-inputs request (function ``0x02``).
135
136    :param address: The unit address to ask.
137    :param start: The address of the first input.
138    :param count: How many inputs to read.
139    :returns: The frame to send.
140    """
141    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:
169def read_holding_registers(address: int, start: int, count: int) -> bytes:
170    """Build a read-holding-registers request (function ``0x03``).
171
172    :param address: The unit address to ask.
173    :param start: The address of the first register.
174    :param count: How many registers to read.
175    :returns: The frame to send.
176    """
177    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:
144def read_holding_registers_reply(address: int, values: list[int]) -> bytes:
145    """Build the reply a device sends to a read-holding-registers request.
146
147    This is the answering half of :func:`read_holding_registers`, so a client can be
148    written and tested against what a device sends without a device on the line.
149
150    :param address: The unit address the reply comes from.
151    :param values: The register values the device reports, in address order.
152    :returns: The frame the device would send.
153    :raises PamojaError: If there are no values, or more than one frame can carry.
154    """
155    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:
180def read_input_registers(address: int, start: int, count: int) -> bytes:
181    """Build a read-input-registers request (function ``0x04``).
182
183    :param address: The unit address to ask.
184    :param start: The address of the first register.
185    :param count: How many registers to read.
186    :returns: The frame to send.
187    """
188    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:
158def read_input_registers_reply(address: int, values: list[int]) -> bytes:
159    """Build the reply a device sends to a read-input-registers request.
160
161    :param address: The unit address the reply comes from.
162    :param values: The register values the device reports, in address order.
163    :returns: The frame the device would send.
164    :raises PamojaError: If there are no values, or more than one frame can carry.
165    """
166    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:
225def write_multiple_coils(address: int, start: int, values: Sequence[bool]) -> bytes:
226    """Build a write-multiple-coils request (function ``0x0F``).
227
228    :param address: The unit address to write to.
229    :param start: The address of the first coil.
230    :param values: One state per coil, at most 1968 of them.
231    :returns: The frame to send.
232    :raises PamojaError: If there are no values, or more than one request carries.
233    """
234    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:
213def write_multiple_registers(address: int, start: int, values: Sequence[int]) -> bytes:
214    """Build a write-multiple-registers request (function ``0x10``).
215
216    :param address: The unit address to write to.
217    :param start: The address of the first register.
218    :param values: The 16-bit values, at most 123 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_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:
191def write_single_coil(address: int, coil: int, on: bool) -> bytes:
192    """Build a write-single-coil request (function ``0x05``).
193
194    :param address: The unit address to write to.
195    :param coil: The coil address.
196    :param on: The state to write.
197    :returns: The frame to send.
198    """
199    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:
202def write_single_register(address: int, register: int, value: int) -> bytes:
203    """Build a write-single-register request (function ``0x06``).
204
205    :param address: The unit address to write to.
206    :param register: The register address.
207    :param value: The 16-bit value to write.
208    :returns: The frame to send.
209    """
210    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.