Skip to content

Migrate from pyserial and in-tree async serial transport to serialx#2929

Draft
puddly wants to merge 1 commit intopymodbus-dev:devfrom
puddly:puddly/migrate-to-serialx
Draft

Migrate from pyserial and in-tree async serial transport to serialx#2929
puddly wants to merge 1 commit intopymodbus-dev:devfrom
puddly:puddly/migrate-to-serialx

Conversation

@puddly
Copy link
Copy Markdown

@puddly puddly commented Apr 29, 2026

Implementation of #2928.

This PR migrates from pyserial to serialx. As part of the change, I've dropped the in-tree asyncio adapter in favor of serialx's native serial code. serialx does not use polling on any platform or for either API so I've removed a few tests that referenced it.

Two major bugfixes:

  1. The in-tree async serial transport called Serial.close() within the event loop. This is a blocking operation for the socket:// URI scheme and can block almost indefinitely with os.close() on Linux, especially at low baudrates. For code running in a shared event loop (like in Home Assistant), this will cause issues. Only call connection_lost after the serial port is actually closed home-assistant-libs/pyserial-asyncio-fast#16 has more context.
  2. Windows support no longer relies on any sort of polling. Serialx uses overlapped IO on Windows and hooks into the proactor event loop for async.

Unrelated, but I think a codepaths using await asyncio.sleep(0.1) may be due to a race between opening a transport and connection_made being called.

Copy link
Copy Markdown
Collaborator

@janiversen janiversen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be very fair and based on experience, that the tests pass is good and like sufficient for the async code.

But for the sync code, we have over time had so many issues stemming from real life devices, that we must be very careful to throw out that code. Your code removes all the wait for data as well as inter byte timeouts which scares me.

Comment thread pymodbus/client/serial.py
# Buffer size for a single `read()` syscall when the caller didn't specify how many
# bytes to read. Larger than any Modbus frame, so one syscall returns whatever's
# currently in the kernel buffer without truncation.
DEFAULT_RECV_SIZE = 4096
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way too big !! this must be synchronized with the cutoff limit in transport.py. The cutoff was added to close huge frames as an attack vector.

Comment thread pymodbus/client/serial.py
# except serial.SerialException as msg:
# pyserial raises undocumented exceptions like termios
except Exception as msg: # pylint: disable=broad-exception-caught
self.socket.open()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the inter_byte_timeout ? this is used to detect broken frames.

Comment thread pymodbus/client/serial.py
self._wait_for_data()
result = self.socket.read(size)
return result
return self.socket.read(size if size else DEFAULT_RECV_SIZE)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to ignore the whole idea of waiting for data, something which is important with low BPS.

This can return 0, which meaning the wait for data must be handled at the calling level, which is not acceptable....all serial special handling must be done at this level.

)

@mock.patch("serial.Serial")
@mock.patch("serialx.serial_for_url")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks as if, this is not testing a serial line, but the socket part. If correct the tests should be duplicated to ensure it works for both.

Same goes for the next tests.


self.serial_write = ( # pylint: disable=attribute-defined-outside-init
client.transport.sync_serial.write
client.transport.serial.write
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.transport provides to different serial implementations, a sync and an async, not sure if this change just combined them.

Comment thread pyproject.toml
"ruff>=0.15.0",
"twine>=6.2.0",
"types-Pygments",
"types-pyserial",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cannot be true, does serialx not have a typed API ???

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants