Skip to content

feat(ppi): add S7-200 serial transport - #824

Open
gijzelaerr wants to merge 4 commits into
masterfrom
feat/614-ppi-transport
Open

feat(ppi): add S7-200 serial transport#824
gijzelaerr wants to merge 4 commits into
masterfrom
feat/614-ppi-transport

Conversation

@gijzelaerr

@gijzelaerr gijzelaerr commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • add SD1, SD2, SD3, and short-confirmation PPI frame codecs
  • add an optional pyserial transport implementing the S7-200 request, acknowledgement, poll, and response exchange
  • add a PPI client with S7 PDU negotiation and read/write support for S, SM, AI, AQ, I, Q, M, V, counter, and timer areas
  • document installation and usage, and cover framing, retries, area mappings, and client behavior with tests

Initial scope

This is the first point-to-point PC-master/single-slave milestone for #614. Multi-master token passing and PPI-over-TCP are intentionally deferred until protocol traces or suitable hardware are available. The implementation is marked experimental and still needs validation against a real S7-200 PLC.

The wire exchange and area mappings follow the mature libnodave implementation:
https://github.com/netdata-be/libnodave/blob/master/nodave.c#L4077-L4220

The branch now includes current master through a merge commit.

Validation

  • uv run --frozen pre-commit run --all-files
  • uv run --frozen --extra test --extra s7commplus --extra ppi pytest -q (1799 passed, 82 skipped)
  • focused PPI/protocol/datatype tests (129 passed)
  • uv build --no-sources

Fixes #614

@gijzelaerr
gijzelaerr requested review from spreeker and removed request for spreeker September 1, 2026 17:07
@gijzelaerr

Copy link
Copy Markdown
Owner Author

@Poseidonas You recently contributed protocol/server fixes and careful regression coverage. Could you give this PPI transport implementation a code and test review?

@Poseidonas

Copy link
Copy Markdown
Contributor

Had a look. The framing and the client layer hold up well under abuse: I fuzzed decode_frame for 900k mutated frames across the four delimiters and got no exception outside S7ProtocolError, so the length, checksum and delimiter checks are doing their job. Round trips, the address bounds on read_area/write_area, the 246-byte PDU cap and the E5 polling flow all behave as documented when I drive them through a scripted serial port.

One thing I would raise before this lands.

Line corruption is not retried, only timeouts are. exchange wraps the acknowledgement read in except S7ConnectionError, so a silent slave is retried. Everything decode_frame rejects raises S7ProtocolError, which propagates straight out of the retry loop, and the poll loop has no except at all. Driving the transport with retries=3:

line condition request attempts
slave never answers 3
stray byte before the E5 1
corrupted checksum in the response 1
corrupted byte in the payload 1
broken end delimiter 1

The last row is the one that bothers me: a single flipped bit on a 9600 8E1 line ends the exchange, and in the case where noise is followed by a perfectly good frame the good frame is never read, because _read_frame has already raised on the first byte. On RS-485 that is the failure retries exists to absorb. Treating a decode failure as retryable, or hunting for the next start delimiter before giving up, would make the retry count mean what it says.

Two smaller notes, neither introduced here.

build_item_spec has a lower bound on start but no upper one, so an address at or above 2097152 wraps silently: start=2097152 and start=0 produce the same 000000 on the wire, and start=2**31 surfaces a raw struct.error rather than an S7 error. That is shared code and predates this PR, but write_area is a new public entry point to it and already validates count and the data length, so a bound on start would fit naturally alongside them. A silently misdirected write is an unpleasant failure mode on a PLC.

PPITransport.exchange takes a lock, which reads as a promise of thread safety, but the sequence counter is advanced in build_*_request before the lock and validated in PPIClient._exchange after it. Six concurrent read_area calls on one client gave me five spurious S7StalePacketError. I checked and Client._send_receive behaves the same way, so this is not something this PR introduces, and it may well be out of scope, but the lock does invite the assumption.

What I could not check is the wire dialogue itself. I have no S7-200, so the SD2/E5/SD1/SD2 sequence, the 0x6C request control and the 0x5C/0x7C poll toggle are taken on trust from libnodave; everything above is from running the code, not from hardware.

Co-Authored-By: Codex <noreply@openai.com>
@gijzelaerr

Copy link
Copy Markdown
Owner Author

Addressed the serial robustness findings in 992cbba.

  • acknowledgement decode failures now participate in the configured retry loop;
  • corrupt frames encountered while polling are skipped within the bounded poll window, allowing a following valid frame to be consumed;
  • byte-addressed areas reject offsets above the 21-bit byte range before they can wrap into the 24-bit wire address (timers/counters retain their full 24-bit item-index range);
  • request construction, transport exchange, parsing, and sequence validation now share one client lock, so concurrent calls cannot advance the sequence outside the serialized exchange.

Added regression coverage for corrupt acknowledgements, noise before a valid response, and unencodable write/read addresses. Validation: 1,804 passed, 82 skipped; full pre-commit suite and source/wheel build pass.

@gijzelaerr gijzelaerr added this to the 4.1 milestone Sep 8, 2026
@Poseidonas Poseidonas moved this to Waiting on review in Industrial open source Sep 8, 2026
@gijzelaerr

Copy link
Copy Markdown
Owner Author

@yangyachao you recently tested python-snap7 against a real S7-200 SMART setup in #765. Before asking you to test this PR: does that PLC expose a physical serial/PPI interface, and do you have an RS-485 adapter available?\n\nThis PR implements serial PPI, so Ethernet/port-102 testing would not validate the missing wire path. A quick yes/no on hardware availability is all we need for now.

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.

PPI (Point-to-Point Interface) protocol support

2 participants