Skip to content

fix(transport): frame TCP exception responses by their fixed size - #146

Closed
sphings79 wants to merge 1 commit into
wlcrs:mainfrom
sphings79:fix/exception-frame-declared-length
Closed

sphings79 wants to merge 1 commit into
wlcrs:mainfrom
sphings79:fix/exception-frame-declared-length

Conversation

@sphings79

Copy link
Copy Markdown

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:

18 58 00 00 00 04 01 83 03    reg 39000 -> exception 3
1F C8 00 00 00 04 01 83 02    reg 45000 -> exception 2

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:

1) good register     -> 150    (0.01s)
2) bad register      -> None   (3.05s)
   Received unexpected response with Transaction ID: 2.
   Discarding bytes: 00 02 00 00 00 04 01 83 03 00
3) good, same conn   -> None   (3.05s)   lost
4) good again        -> 150    (0.05s)

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.

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.
@wlcrs

wlcrs commented Sep 20, 2026

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

@wlcrs wlcrs closed this Oct 6, 2026
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