|
9 | 9 |
|
10 | 10 | from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient |
11 | 11 | from pymodbus.exceptions import ConnectionException |
12 | | -from pymodbus.framer import FramerType |
| 12 | +from pymodbus.framer import FramerType, FramerAscii, FramerRTU |
13 | 13 | from pymodbus.logging import Log |
14 | | -from pymodbus.pdu import ModbusPDU |
| 14 | +from pymodbus.pdu import ModbusPDU, DecodePDU |
15 | 15 | from pymodbus.transport import CommParams, CommType |
16 | 16 |
|
17 | 17 |
|
@@ -318,3 +318,79 @@ def __repr__(self): |
318 | 318 | f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, " |
319 | 319 | f"framer={self.framer}, timeout={self.comm_params.timeout_connect}>" |
320 | 320 | ) |
| 321 | + |
| 322 | + |
| 323 | +class ModbusFrameGenerator(ModbusSerialClient): |
| 324 | + """A class to generate and parse Modbus frames without actual communication. |
| 325 | + |
| 326 | + This class provides a way to generate Modbus frames and parse responses |
| 327 | + without actual serial communication. |
| 328 | +
|
| 329 | + :param framer: The framer to use (RTU or ASCII) |
| 330 | + :param slave: Default slave address |
| 331 | + |
| 332 | + Example:: |
| 333 | + from pymodbus.client import ModbusFrameGenerator |
| 334 | + from pymodbus.framer import FramerType |
| 335 | + |
| 336 | + generator = ModbusFrameGenerator(framer=FramerType.RTU, slave=1) |
| 337 | + request = generator.read_coils(1, 10) |
| 338 | + print(f"Generated frame: {request.hex()}") |
| 339 | + """ |
| 340 | + |
| 341 | + def __init__( |
| 342 | + self, |
| 343 | + framer: FramerType = FramerType.RTU, |
| 344 | + slave: int = 0x00, |
| 345 | + ) -> None: |
| 346 | + """Initialize the frame generator.""" |
| 347 | + if framer not in [FramerType.ASCII, FramerType.RTU]: |
| 348 | + raise TypeError("Only FramerType RTU/ASCII allowed.") |
| 349 | + super().__init__( |
| 350 | + port="DUMMY", |
| 351 | + framer=framer, |
| 352 | + ) |
| 353 | + self.slave = slave |
| 354 | + self.socket = None # Prevent actual serial operations |
| 355 | + self.comm_params.timeout_connect = 0 # Prevent retries |
| 356 | + self.retries = 0 # Disable retries |
| 357 | + |
| 358 | + def build_request(self, request: ModbusPDU) -> bytes: |
| 359 | + """Build a complete frame from a request PDU. |
| 360 | + |
| 361 | + :param request: The request PDU to frame |
| 362 | + :returns: The framed request |
| 363 | + """ |
| 364 | + request.dev_id = self.slave |
| 365 | + return self.framer.buildFrame(request) |
| 366 | + |
| 367 | + def execute(self, no_response_expected: bool, request: ModbusPDU) -> bytes: |
| 368 | + """Override execute to only generate frame without sending.""" |
| 369 | + return self.build_request(request) |
| 370 | + |
| 371 | + def send(self, request: bytes, addr: tuple | None = None) -> int: |
| 372 | + """Don't actually send, just return the length of the request.""" |
| 373 | + return len(request) if request else 0 |
| 374 | + |
| 375 | + def recv(self, size: int | None) -> bytes: |
| 376 | + """Dummy receive - always returns empty bytes.""" |
| 377 | + return b"" |
| 378 | + |
| 379 | + def close(self): |
| 380 | + """Dummy close - does nothing.""" |
| 381 | + pass |
| 382 | + |
| 383 | + def is_socket_open(self) -> bool: |
| 384 | + """Always returns False since we're not actually connecting.""" |
| 385 | + return False |
| 386 | + |
| 387 | + def parse_response(self, response: bytes) -> ModbusPDU: |
| 388 | + """Parse a response frame into a PDU. |
| 389 | + |
| 390 | + :param response: The response frame to parse |
| 391 | + :returns: The decoded PDU |
| 392 | + """ |
| 393 | + used_len, pdu = self.framer.processIncomingFrame(response) |
| 394 | + if not pdu: |
| 395 | + raise ValueError("Failed to decode response") |
| 396 | + return pdu |
0 commit comments