Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow configuring pyserial hardware RS485 settings #2460

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

jameshilliard
Copy link
Contributor

Loosely based on #2205 but is designed to configure the native rs485 serial port settings via ioctl's rather than using the weird RS485 subclass that appears to try to implement some sort of software based rs485 support.

Copy link
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.

Not a formal review, just a fast comment, but a very important one.

@@ -98,6 +102,9 @@ class CommParams:
parity: str = ''
stopbits: int = -1

# RS485
rs485_settings: RS485Settings | None = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

This fails is serial is not installed.

Pymodbus must work without serial installed.

@jameshilliard jameshilliard force-pushed the pyserial-rs485 branch 2 times, most recently from bda50b7 to 8cf52c6 Compare November 19, 2024 23:44
@yawor
Copy link

yawor commented Nov 19, 2024

Great idea but still has one of the issues from #2205 - this may not work properly when used as async. The way pymodbus implements the async side of the library may cause multiple calls to write in the underlying Serial instance for a single modbus call to send whole modbus frame. This may have the effect that the RTS pin is going to toggle (once or multiple times) in the middle of modbus frame.
I'm not sure whether this affects also sync code or not (probably not).
There are many things this may depend on, like specific hardware, OS settings, size of the modbus frame. Probably small modbus frames that set or read a single register won't be affected, but longer ones may and it may cause hard to debug issues, which may be detected only by connecting oscilloscope or logic state analyser to the serial pins.
Also the RS485 subclass has been added to the pyserial for a reason. The ioctl is only supported on Linux. Also not all hardware may support such mode. On some SBCs, for example, the hardware RTS pin may not be even exposed to be used by the rs485 transceiver.

@jameshilliard jameshilliard force-pushed the pyserial-rs485 branch 2 times, most recently from 0936647 to c704a8f Compare November 19, 2024 23:50
@jameshilliard
Copy link
Contributor Author

Also the RS485 subclass has been added to the pyserial for a reason.

It seems problematic to support the userspace sleep techniques that is using, probably best to defer that for now.

The ioctl is only supported on Linux.

It's seems there's some sort of windows support as well here that this should work with.

@yawor
Copy link

yawor commented Nov 20, 2024

Also the RS485 subclass has been added to the pyserial for a reason.

It seems problematic to support the userspace sleep techniques that is using, probably best to defer that for now

Yes, that's true. It uses time.sleep, which is really bad in async code. I didn't mean to use that class, but why it's been added. And it's because sometimes there's a need to control the RTS pin from the software instead of leaving it to the hardware implementation.

Also like I've described in my previous comment, the hardware RTS control may not be desired here anyway, as it is highly OS/hardware dependant. It may cause invalid RTS states during frame transmission. The RTS control should be implemented in the pymodbus directly, as it knows when the frame starts and where it ends (the underlying Serial instance is not aware of that, it just gets some bytes to write, which may not even be a complete frame at once).
Also software control gives one more option. You don't really need to control RTS pin. If the hardware doesn't expose RTS pin but has GPIO pins you can control, it would be possible to control one of the GPIO pins as a TX/RX select for RS485 transceiver.

Unfortunately I haven't had time to come with my own proposal in form of the PR :(.

@jameshilliard
Copy link
Contributor Author

Also like I've described in my previous comment, the hardware RTS control may not be desired here anyway, as it is highly OS/hardware dependant.

Aren't there cases where settings rs485 hardware options like this that would potentially be useful though?

@yawor
Copy link

yawor commented Nov 20, 2024

They are useful, but the main issue I see with going with this implementation is uncertainty of the RTS behaviour while frame is being sent. It's because neither pymodbus, not pyserial is in full control of the write operation in async, as it's the asyncio loop that schedules writes: when and how much data to write. You don't have any guarantees that the frame is going to be written with a single Serial.write. And if it's not in a single write, then the hardware may toggle the RTS pin because from the hardware standpoint it finished writing some data it was requested to write. The hardware is not aware when the frame really ends.
As I've mentioned earlier, the only fully modbus frame aware code is in the pymodbus.

@jameshilliard
Copy link
Contributor Author

It's because neither pymodbus, not pyserial is in full control of the write operation in async, as it's the asyncio loop that schedules writes: when and how much data to write.

AFAIU it's really the kernel and lower level hardware/drivers that determine the actual execution of these sort of operations with userspace largely delegating to the lower level layers. I think there's also a 4096 byte write buffer in the kernel for serial ports.

Additionally pymodbus should never write more than one modbus frame at a time before waiting for a response from my understanding with some limited exceptions like broadcasts(due to a transaction execution lock, and for our projects we also implement a priority queue with additional locking on top of this for scheduling high priority modbus operations ahead of low priority operations). I would expect in practice that modbus frames would be effectively written in their entirety to the kernel buffer since they are much smaller than the kernel buffer.

Also the event loop in general will execute any CPU bound code paths until competition(like generating a modbus frame) unless waiting for an IO bound operation to complete(i.e. like a serial file descriptor read/write, although in practice likely the event loop will only wait on a read due to kernel write buffering).

@janiversen
Copy link
Collaborator

janiversen commented Nov 20, 2024

The async loop also cuts cpu bound methods, the only guarantee is that it does not cut a python statement due to the python global lock.

@janiversen
Copy link
Collaborator

janiversen commented Nov 20, 2024

We really need to define the use case....in theory it sounds good, but most usb rs485 converters handle the dtr, so does the rs485 piggy back I use on my rp4, so maybe the most generic solution is to document the problem and suggest to use a converter that does it automatically.

The current PR, does not warn when an OS does not support setting RS485.

Calling send directly, means blocking, which is bad when the app uses multiple clients.

@jameshilliard
Copy link
Contributor Author

We really need to define the use case....in theory it sounds good, but most usb rs485 converters handle the dtr, so does the rs485 piggy back I use on my rp4, so maybe the most generic solution is to document the problem and suggest to use a converter that does it automatically.

I think the use case is the same as for pyserial, to apply the rs485 configuration settings to the port.

The current PR, does not warn when an OS does not support setting RS485.

In at least some cases pyserial will AFAIU, IMO best to just leave it delegated to pyserial as it's going to be a bit tricky to cover all cases.

@janiversen
Copy link
Collaborator

I think the use case is the same as for pyserial, to apply the rs485 configuration settings to the port.

That sounds more like the solution, why do you need to apply rs485 settings to the port, when most usb converters and piggy backs handles it automatically ?

I think you are right that it is very complicated to get right without severe side effects, hence my suggestion to update the documentation instead.

@janiversen
Copy link
Collaborator

Just for fun I tried it on one of my mac minis with a M2 Cpu....no errors until open was called, then it broke, with an illegal action on port.

Seems the usb converter I used, creates/uses a pseudo port, which clearly does not like ioctl......this is all something we have to document with this PR, in order to avoid issues.

@janiversen
Copy link
Collaborator

An alternative solution, for those who need this speciality, is to adapt socat and do whatever they need done with the serial port that way pymodbus stays clean, and users have even more freedom.

@jameshilliard
Copy link
Contributor Author

That sounds more like the solution, why do you need to apply rs485 settings to the port, when most usb converters and piggy backs handles it automatically ?

Just for any ports that actually need it configured.

Seems the usb converter I used, creates/uses a pseudo port, which clearly does not like ioctl......this is all something we have to document with this PR, in order to avoid issues.

AFAIU it's fairly driver/chipset specific even for native serial ports. Where should I add docs for the option?

An alternative solution, for those who need this speciality, is to adapt socat and do whatever they need done with the serial port that way pymodbus stays clean, and users have even more freedom.

That seems quite complicated. I mean this is just a pass-through option(like the various other pyserial params) that's forwarded to pyserial by pymodbus so it shoudn't cause much in the way of maintainability issues.

@janiversen
Copy link
Collaborator

janiversen commented Nov 20, 2024

AFAIU it's fairly driver/chipset specific even for native serial ports. Where should I add docs for the option?

it needs to go into the client/server documentation at least....but it is NOT about documenting the option, but about how to see if works, and if not what are probably causes.

Please be aware I am far from convinced it is a complexity we want to have in pymodbus.

That seems quite complicated. I mean this is just a pass-through option(like the various other pyserial params) that's forwarded to pyserial by pymodbus so it shoudn't cause much in the way of maintainability issues.

Why is that complicated ? I do it frequently to test specific protocol problems.

It is a lot more than just a pass through the option makes pymodbus responsible for potential problems from a user pow. And that is what I want to avoid.

If the pull request is turned into something where e.g, @yawor nods and says it a stable and good solution, I would be more convinced.

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.

3 participants