pymodbus-dev / pymodbus

A full modbus protocol written in python
Other
2.16k stars 889 forks source link

ModbusProtocol throws AttributeError : 'Serial' object has no attribute 'startswith' #2181

Closed Hizaak closed 1 month ago

Hizaak commented 2 months ago

Versions

Pymodbus Specific

Description

I'm trying to emulate a Modbus Server on RTU, to be able to query dumb adresses after. It seems to be do-able according to PyModbus documentation

Code and Logs

Here is the source code :

import asyncio
from pymodbus.server import StartAsyncSerialServer
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.transaction import ModbusRtuFramer
import serial

# Configuration Modbus
port = "/dev/pts/8"
stopbits = 1
bytesize = 8
parity = serial.PARITY_NONE
baudrate = 9600

async def run_server():
    # Configuration du contexte du serveur Modbus
    store = ModbusSlaveContext(
        di=ModbusSequentialDataBlock.create(),
        co=ModbusSequentialDataBlock.create(),
        hr=ModbusSequentialDataBlock.create(),
        ir=ModbusSequentialDataBlock.create())
    context = ModbusServerContext(slaves=store, single=True)

    # Configuration de la communication série
    serial_context = serial.serial_for_url(
        port,
        baudrate=baudrate,
        bytesize=bytesize,
        parity=parity,
        stopbits=stopbits,
        timeout=0.5)

    # Démarrage du serveur Modbus
    await StartAsyncSerialServer(context, framer=ModbusRtuFramer, port=serial_context)

if __name__ == "__main__":
    asyncio.run(run_server())

Here is the error :

Traceback (most recent call last):
  File "/home/amaurice/Bureau/inviseo/inviseobox-test-server/main.py", line 46, in <module>
    asyncio.run(run_server())
  File "/usr/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/base_events.py", line 654, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/amaurice/Bureau/inviseo/inviseobox-test-server/main.py", line 38, in run_server
    await StartAsyncSerialServer(
  File "/home/amaurice/Bureau/inviseo/inviseobox-test-server/venv/lib/python3.11/site-packages/pymodbus/server/async_io.py", line 695, in StartAsyncSerialServer
    server = ModbusSerialServer(
             ^^^^^^^^^^^^^^^^^^^
  File "/home/amaurice/Bureau/inviseo/inviseobox-test-server/venv/lib/python3.11/site-packages/pymodbus/server/async_io.py", line 524, in __init__
    super().__init__(
  File "/home/amaurice/Bureau/inviseo/inviseobox-test-server/venv/lib/python3.11/site-packages/pymodbus/server/async_io.py", line 256, in __init__
    super().__init__(
  File "/home/amaurice/Bureau/inviseo/inviseobox-test-server/venv/lib/python3.11/site-packages/pymodbus/transport/transport.py", line 185, in __init__
    and host.startswith("socket")
        ^^^^^^^^^^^^^^^
AttributeError: 'Serial' object has no attribute 'startswith'

I digged in the source code, and found this (commented interesting points) :

class ModbusProtocol(asyncio.BaseProtocol):
    """Protocol layer including transport."""

    def __init__(
        self,
        params: CommParams,
        is_server: bool,
    ) -> None:
        """Initialize a transport instance.

        :param params: parameter dataclass
        :param is_server: true if object act as a server (listen/connect)
        """
        self.comm_params = params.copy()
        self.is_server = is_server
        self.is_closing = False

        self.transport: asyncio.BaseTransport = None  # type: ignore[assignment]
        self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
        self.recv_buffer: bytes = b""
        self.call_create: Callable[[], Coroutine[Any, Any, Any]] = None  # type: ignore[assignment]
        if self.is_server:
            self.active_connections: dict[str, ModbusProtocol] = {}
        else:
            self.listener: ModbusProtocol | None = None
            self.unique_id: str = str(id(self))
            self.reconnect_task: asyncio.Task | None = None
            self.reconnect_delay_current = 0.0
            self.sent_buffer: bytes = b""

        # ModbusProtocol specific setup
        if self.is_server:
            if self.comm_params.source_address is not None:
                host = self.comm_params.source_address[0]
                port = int(self.comm_params.source_address[1])
# "port" : Serial<id=0x7e51af3e0dc0, open=True>(port='/dev/pts/8', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=0.5, xonxoff=False, rtscts=False, dsrdtr=False)
            else:
                # This behaviour isn't quite right.
                # It listens on any IPv4 address rather than the more natural default of any address (v6 or v4).
                host = "0.0.0.0" # Any IPv4 host
                port = 0 # Server will select an ephemeral port for itself
        else:
            host = self.comm_params.host
            port = int(self.comm_params.port)
        if self.comm_params.comm_type == CommType.SERIAL and NULLMODEM_HOST in host:
# Doesn't match
            host, port = NULLMODEM_HOST, int(host[9:].split(":")[1])
        if host == NULLMODEM_HOST:
# Doesn't match neither
            self.call_create = partial(self.create_nullmodem, port)
            return
        if (
            self.comm_params.comm_type == CommType.SERIAL
            and self.is_server
            and host.startswith("socket")
# "port" : Serial<id=0x7e51af3e0dc0, open=True>(port='/dev/pts/8', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=0.5, xonxoff=False, rtscts=False, dsrdtr=False)
# Here : AttributeError: 'Serial' object has no attribute 'startswith'
        ):
            # format is "socket://<host>:port"
            self.comm_params.comm_type = CommType.TCP
            parts = host.split(":")
            host, port = parts[1][2:], int(parts[2])
        self.init_setup_connect_listen(host, port)

How to reproduce ?

sudo socat -d -d pty,raw,echo=0 pty,raw,echo=0

It will give you something like this :

2024/04/26 20:55:00 socat[2561893] N PTY is /dev/pts/8
2024/04/26 20:55:00 socat[2561893] N PTY is /dev/pts/10
2024/04/26 20:55:00 socat[2561893] N starting data transfer loop with FDs [5,5] and [7,7]

Use the first /dev/pts/X as "port".

janiversen commented 2 months ago

Your call to startAsyncSerialServer is wrong please see our examples and/or documentation

You need to give a framer not a serial object.

github-actions[bot] commented 1 month ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] commented 1 month ago

This issue was closed because it has been stalled for 5 days with no activity.