Fix FC17 response decode: identifier included the run indicator byte - #3017
Merged
janiversen merged 1 commit intoSep 2, 2026
Merged
Conversation
ReportDeviceIdResponse.encode() counts identifier + run indicator in the byte-count field, while decode() sliced byte-count bytes as the identifier alone. Decoding the frame the library itself encodes therefore returned an identifier one byte too long, with the run indicator appended. Slice the identifier as data[1:byte_count] and read status at data[byte_count] rather than data[-1], so a byte appended past the count is no longer mistaken for the run indicator. Guard a byte count that points outside the frame, the same shape as the FC03 response guard, so the new indexed access cannot raise a bare IndexError. The encode convention is the one calculateRtuFrameSize already assumes via rtu_byte_count_pos, so the fix is decode-side only.
There was a problem hiding this comment.
🟢 Approval recommended
The decode-side fix is consistent with the existing encode/framing assumptions, includes appropriate error handling, and is backed by targeted regression and round-trip tests.
Pull request overview
This PR fixes an FC17 (ReportDeviceIdResponse) decoding off-by-one bug where the run-indicator byte was being included in the returned identifier, making encode()/decode() asymmetric and causing client-side decoded identifiers to be polluted.
Changes:
- Align
ReportDeviceIdResponse.decode()with the existingencode()convention by slicingidentifierto exclude the run-indicator byte and reading status from the last counted byte. - Add a byte-count bounds guard in
ReportDeviceIdResponse.decode()that raisesModbusIOException(withfunction_codeattached) instead of risking anIndexError/ silent garbage decode. - Update and extend tests to assert correct field values, verify
decode(encode())round-trips, and cover invalid byte-count cases.
File summaries
| File | Description |
|---|---|
pymodbus/pdu/other_message.py |
Fix FC17 response decode slicing/indexing and add byte-count validation raising ModbusIOException. |
test/pdu/test_other_messages.py |
Strengthen FC17 tests with field assertions, round-trip coverage, and invalid byte-count exception checks. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
ReportDeviceIdResponse.encode()and.decode()disagree about what the FC17byte-count field covers.
encode()counts identifier + run indicator:decode()slices byte-count bytes as the identifier alone:So decoding the frame the library itself encodes for
identifier=b"Pymodbus",status ON (
\x09Pymodbus\xff) yieldsidentifier == b"Pymodbus\xff"— therun-indicator byte lands in the identifier. Every
client.report_device_id()against a pymodbus server returns a pollutedidentifier, reproducible on
devwith a stockModbusTcpServer+AsyncModbusTcpClienton loopback:b'Pymodbus\xff'b'Pymodbus'The encode convention is the one the rest of the library depends on:
rtu_byte_count_pos = 2meanscalculateRtuFrameSizecomputesbyte_count + 5, which only frames FC17 RTU responses correctly if the countcovers the run indicator. So the fix is decode-side only:
identifier = data[1 : byte_count](counted bytes minus the run indicator);data[byte_count]— the last counted byte — instead ofdata[-1], so a byte a device appends past the count is no longer taken forthe run indicator;
ModbusIOException, the same shape as the FC03response guard
ReadHoldingRegistersResponse.decodehas carried since Refactor PDU, add strong typing to base classes. #2438(with the
function_code=propagation from Propagate known request identity on ModbusIOException #2993), because the newdata[byte_count]access must not turn a lying byte count into a bareIndexError. This is a response decode (client side; the FC17 requestcarries no data), so the don't-raise-in-server-request-decode concern from
Validate FC23 write count against MAX_WRITE_COUNT, not MAX_READ_COUNT #3016 does not apply.
Provenance:
4de9e3c7(2011-01-31) shipped a self-consistent pair —encode()countedlen(identifier) + 2,decode()sliceddata[1:length - 1].b442af51(2011-09-06, "Finishing the remaining modbusprotocol") changed the slice to
data[1:length + 1]and leftencode()alone;the decoded identifier has been one byte too long ever since — through
0a77f6c1(2017), which movedencode()to today'slen(identifier) + 1, andthrough the PDU refactor (#2160).
Existing coverage could not catch it:
test_pdu_decodedecodes withoutasserting field values, and
test_report_device_idfeddecodea malformedframe (
b"\x03\x12\x00"— byte count 3, two bytes of payload) and thenasserted that re-encoding produces a different, 4-byte frame — the test was
pinning the asymmetry. That vector is now the well-formed
b"\x03\x12\x00\x00"and the re-encode assertion demonstrates a trueround-trip.
Two behavior changes to weigh before merging:
identifier[:-1]gets the corrected,shorter identifier.
counted byte, which is now read as the run indicator. 0x11 report_slave_id got False and lost some bytes #1600's capture is
exactly such a device — payload
07 01 02 42 00 11 01 03 23, count 7 witheight bytes after it. On
devit decodesidentifier=b'\x01\x02B\x00\x11\x01\x03', status from the uncounted trailing0x23; with this PR it decodesidentifier=b'\x01\x02B\x00\x11\x01', statusfrom
0x03. Status isFalseeither way, so for that device this change isa byte lost from the identifier, not a repair.
Spec §6.17 calls the FC17 payload "device specific" and settles neither
reading, so no decode is right for every vendor. What this PR buys is the one
invariant the library can own: pymodbus decodes its own framing convention —
the same one its RTU size math already assumes. The identifier stays a raw
value for the user to interpret, as the docstring says, and frames with
trailing bytes beyond the count still decode. If you would rather keep exotic
layouts byte-for-byte, keeping the raw payload on the PDU as an escape hatch
is an additive follow-up I am happy to write.
New tests:
(
b"\tPymodbus\xff"→b"Pymodbus", status ON);decode(encode())round-trip preserves identifier and status for bothstatus values (fails on dev: identifier grows a byte per pass);
ModbusIOException(fails on dev: silently decodes garbage).Validation, macOS / Python 3.13: