Repository navigation
Conversation
An exception response carries a two byte PDU: the function code with bit 7
set, followed by the exception code. ModbusTcpProtocol framed it from the MBAP
length field instead, which some devices get wrong.
A Marstek Venus D (Control/EMS v150) answers a read of an unimplemented
register with a nine byte frame whose length field reads 4 where the protocol
requires 3:
18 58 00 00 00 04 01 83 03 register 39000 -> exception 3
1F C8 00 00 00 04 01 83 02 register 45000 -> exception 2
The parser then waits for a tenth byte that is never sent, so the request
times out on a device that in fact answered within about 100 ms. Worse, the
nine bytes stay in the buffer: the next response supplies the missing byte,
both frames are discarded as an unmatched transaction id, and the parser
resynchronises. One rejected register therefore costs two failed reads.
An exception has a fixed size, so it no longer needs the declared length.
Devices that declare it correctly are unaffected, and the change is confined
to frames whose function code has bit 7 set.
A frame padded to match an oversized field is deliberately not handled: the
surplus byte cannot be distinguished from the start of the next response
without waiting, and the existing resync recovers from it. No such device is
known.
Adds three tests: an exception with an oversized declared length is delivered,
the response following it survives, and a correctly declared exception keeps
working. The first two fail without this change.
Owner
|
Thank you for this PR. I reviewed it, but don't think it's a good idea to start adding all sorts of workarounds for quirks of devices that have a faulty Modbus implementation. The current behavior is standard-compliment and it should stay that way. If you need to support something like that, it is better to just create your own transport and use that instead. The architecture of this library was done in such a way that it's very easy to swap out the transport in the client. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed Changes
I've got a Marstek Venus D (EMS v150) that sends a broken exception frame, and tmodbus can't read it. When you read a register the device doesn't implement, it answers with 9 bytes but the MBAP length field says 4, where it should say 3:
So
7 + (length - 1)comes out as 10 and the parser sits there waiting for a byte that never arrives, until the read times out. The device isn't slow, it answers in about 100ms.The part that cost me more time is what happens next. Those 9 bytes stay in the buffer, so the following response donates its first byte to them, both frames get thrown away as an unmatched transaction id, and the parser resyncs. One bad register costs you two reads:
An exception PDU is always two bytes anyway, so the length field isn't needed for it. That's the whole fix: if the function code has bit 7 set, use 9. Devices that get the field right don't notice any difference.
I left padded frames alone on purpose. If a device ever padded to match its own wrong length, you can't tell that extra byte from the start of the next response without waiting for it, and the resync already handles that case. Haven't seen a device do it.
Added three tests, two of them fail without the change. Full suite passes, ruff and mypy are clean.
I also checked the firmware: the exception branch writes a constant 4 while sending 9 bytes, and the same firmware gets the success path right with
quantity * 2 + 3. I'll report it to Marstek, but I wouldn't hold my breath - I sent them a report about another Modbus bug a while back and never heard anything. Either way the library shouldn't trust that field.Related Issues
None.