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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
BROADCASTfor every device. - address: The coil's address.
- on: The state to write.
Raises
- ModbusClientError: When the transaction fails.
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
BROADCASTfor every device. - address: The register's address.
- value: The value to write.
Raises
- ModbusClientError: When the transaction fails.
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
BROADCASTfor 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.
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
BROADCASTfor 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.
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.
A received Modbus RTU frame whose CRC has been verified.
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]
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.
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.
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.
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.
Sets coils from an address on, adding any the device did not have.
Sets discrete inputs from an address on, adding any the device did not have.
Sets holding registers from an address on, adding any the device did not have.
Sets input registers from an address on, adding any the device did not have.
A discrete input's state, or None when the device has no input there.
A holding register's value, or None when the device has no register there.
An input register's value, or None when the device has no register there.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.