Skip to content

FYI: Multi-read optimization integrated into upstream python-snap7#1

Open
gijzelaerr wants to merge 78 commits intoQuakeString:masterfrom
gijzelaerr:master
Open

FYI: Multi-read optimization integrated into upstream python-snap7#1
gijzelaerr wants to merge 78 commits intoQuakeString:masterfrom
gijzelaerr:master

Conversation

@gijzelaerr
Copy link
Copy Markdown

Hi! This is not a code PR — just a heads-up since your repo has issues disabled.

We noticed your multi-variable read optimizer (sort-merge-packetize pipeline) and were impressed by the approach. We've implemented a similar optimization in upstream python-snap7, inspired by your work:

Our PR: gijzelaerr#641

Our implementation follows the same core idea (sort → merge → packetize → extract), with some differences:

  • No parallel dispatch — sequential for now to avoid PLC compatibility concerns with pipelined PDUs
  • No auto-tuning via SZLMaxConnections describes TCP slots, not in-flight PDU tolerance
  • Configurable max_gap via client.multi_read_max_gap
  • Clean cache class (_OptimizationPlan) instead of fields on Client
  • Server-side multi-item support for end-to-end testing

If you're interested, we'd welcome your review or any contributions back to upstream. If there are features from your fork we missed, feel free to open issues or PRs on gijzelaerr/python-snap7.

Thanks for the inspiration!

dependabot bot and others added 30 commits March 18, 2026 09:16
Bumps the all-dependencies group with 4 updates: [ruff](https://github.com/astral-sh/ruff), [tox](https://github.com/tox-dev/tox), [tox-uv](https://github.com/tox-dev/tox-uv) and [uv](https://github.com/astral-sh/uv).


Updates `ruff` from 0.15.5 to 0.15.6
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.15.5...0.15.6)

Updates `tox` from 4.49.0 to 4.49.1
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](tox-dev/tox@4.49.0...4.49.1)

Updates `tox-uv` from 1.33.1 to 1.33.4
- [Release notes](https://github.com/tox-dev/tox-uv/releases)
- [Commits](tox-dev/tox-uv@1.33.1...1.33.4)

Updates `uv` from 0.10.9 to 0.10.10
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](astral-sh/uv@0.10.9...0.10.10)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: tox
  dependency-version: 4.49.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: tox-uv
  dependency-version: 1.33.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: uv
  dependency-version: 0.10.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Strengthen messaging around the pure Python rewrite: emphasize that 3.0
completely breaks with the C snap7 shared library wrapper approach, highlight
improved portability and easier installation, and add clear guidance for
reporting issues and falling back to pre-3.0 releases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The library no longer wraps the C snap7 library as of 3.0, but the name
is kept for backwards compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add coverage badge and coverage threshold

- Replace artifact upload with codecov/codecov-action@v5 in test workflow
- Add codecov badge to README.rst
- Add coverage threshold of 80% in pyproject.toml and codecov.yml

Closes #619

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Lower coverage threshold to 75% to match current coverage

Current coverage is 78% — set threshold to 75% to provide a safety net
without failing CI. Can be raised as coverage improves.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add project badges to README

Add PyPI version, Python versions, license, CI status, and Read the
Docs badges alongside the existing Codecov badge for a professional
landing page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Don't fail CI when Codecov upload fails

Codecov v5 requires a token for protected branches, causing
"Token required because branch is protected" errors that block CI.
Coverage upload is best-effort, not a gate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add 55 tests covering PDU building/parsing, lifecycle management,
send/recv buffers, parameters, and dual-partner data exchange via
socket pairs.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The setter functions already worked with memoryview at runtime (using
direct struct.pack() slice assignment), but the type annotations only
accepted bytearray. This caused mypy errors when passing memoryview
objects from ctypes buffers.

- Add Buffer type alias (Union[bytearray, memoryview]) to setters and getters
- Update all function signatures to accept Buffer
- Fix .decode() calls in getters to use bytes() for memoryview compat
- Add 17 memoryview compatibility tests

Credit: LuTiFlekSSer for identifying the memoryview compatibility issue.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…rs (#644)

Adds 32 new tests exercising server-side protocol handlers through
real client-server communication: block list/info/upload/download,
SZL reads, clock get/set, PLC control (stop/start/compress), and
error scenarios for unregistered areas and nonexistent blocks.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Tests parse_address() for all address types (V, VW, VD, V.bit) and
invalid inputs, plus integration tests for Logo read/write against
the built-in server covering byte, word, dword, and bit operations
including boundary values and read-modify-write bit logic.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tests to improve coverage from 78% to ~85%

Add test suites for untested code paths that don't require a real PLC:
- error.py: error routing, check_error(), error_wrap() decorator
- connection.py: socket mocking, COTP parsing, exception paths
- server/__main__.py: CLI entrypoint test
- s7protocol.py: response parser tests with crafted PDUs
- util/db.py: DB/Row type conversions, dict-like interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix test_server_cli.py when click is not installed

Use pytest.importorskip to gracefully skip CLI tests when click
(an optional dependency) is not available in the test environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Revert "Revert async client commits from master"

This reverts commit 746a9b2.

* Fix pre-commit issues: unused imports, hex casing, mypy types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: add pytest-asyncio dep, ruff formatting, async docs

- Add pytest-asyncio to test dependencies in pyproject.toml
- Apply ruff format to async_client.py (struct.pack arg formatting)
- Add AsyncClient documentation (doc/API/async_client.rst)
- Add async_client to Sphinx toctree
- Add async example to README.rst

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix AsyncClient.get_connected to also check connection.connected

Address PR review comment: verify the underlying Connection object
reports as connected, not just the client-level flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add S7CommPlus protocol scaffolding for S7-1200/1500 support

Adds the snap7.s7commplus package as a foundation for future S7CommPlus
protocol support, targeting all S7-1200/1500 PLCs (V1/V2/V3/TLS).

Includes:
- Protocol constants (opcodes, function codes, data types, element IDs)
- VLQ encoding/decoding (Variable-Length Quantity, the S7CommPlus wire format)
- Codec for frame headers, request/response headers, and typed values
- Connection skeleton with multi-version support (V1/V2/V3/TLS)
- Client stub with symbolic variable access API
- 86 passing tests for VLQ and codec modules

The wire protocol (VLQ, data types, object model) is the same across all
protocol versions -- only the session authentication layer differs. The
protocol version is auto-detected from the PLC's CreateObject response.

Reference: thomas-v2/S7CommPlusDriver (C#, LGPL-3.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S7CommPlus server emulator, async client, and integration tests

Server emulator (snap7/s7commplus/server.py):
- Full PLC memory model with thread-safe data blocks
- Named variable registration with type metadata
- Handles CreateObject/DeleteObject session management
- Handles Explore (browse registered DBs and variables)
- Handles GetMultiVariables/SetMultiVariables (read/write)
- Multi-client support (threaded)
- CPU state management

Async client (snap7/s7commplus/async_client.py):
- asyncio-based S7CommPlus client with Lock for concurrent safety
- Same API as sync client: db_read, db_write, db_read_multi, explore
- Native COTP/TPKT transport using asyncio streams

Updated sync client and connection to be functional for V1:
- CreateObject/DeleteObject session lifecycle
- Send/receive with S7CommPlus framing over COTP/TPKT
- db_read, db_write, db_read_multi operations

Integration tests (25 new tests):
- Server unit tests (data blocks, variables, CPU state)
- Sync client <-> server: connect, read, write, multi-read, explore
- Async client <-> server: connect, read, write, concurrent reads
- Data persistence across client sessions
- Multiple concurrent clients with unique sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up security-focused wording in S7CommPlus docstrings

Reframe protocol version descriptions around interoperability rather
than security vulnerabilities. Remove CVE references and replace
implementation-specific language with neutral terminology.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: remove pytest-asyncio dependency, fix formatting

Rewrite async tests to use asyncio.run() instead of @pytest.mark.asyncio
since pytest-asyncio is not a project dependency. Also apply ruff
formatting fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add pytest-asyncio dependency and use native async tests

Add pytest-asyncio to test dependencies and set asyncio_mode=auto.
Restore async test methods with @pytest.mark.asyncio instead of
asyncio.run() wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI and add S7CommPlus end-to-end tests

Fix ruff formatting violations and mypy type errors in S7CommPlus code
that caused pre-commit CI to fail. Add end-to-end test suite for
validating S7CommPlus against a real S7-1200/1500 PLC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Enhance S7CommPlus connection with variable-length TSAP support and async client improvements

Support bytes-type remote TSAP (e.g. "SIMATIC-ROOT-HMI") in ISOTCPConnection,
extend S7CommPlus protocol handling, and improve async client and server emulator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add extensive debug logging to S7CommPlus protocol stack for real PLC diagnostics

Adds hex dumps and detailed parsing at every protocol layer (ISO-TCP,
S7CommPlus connection, client) plus 6 new diagnostic e2e tests that
probe different payload formats and function codes against real hardware.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix S7CommPlus wire format for real PLC compatibility

Rewrite client payload encoding/decoding to use the correct S7CommPlus
protocol format with ItemAddress structures (SymbolCrc, AccessArea,
AccessSubArea, LIDs), ObjectQualifier, and proper PValue response
parsing. Previously the client used a simplified custom format that
only worked with the emulated server, causing ERROR2 responses from
real S7-1200/1500 PLCs.

- client.py: Correct GetMultiVariables/SetMultiVariables request format
- async_client.py: Reuse corrected payload builders from client.py
- codec.py: Add ItemAddress, ObjectQualifier, PValue encode/decode
- protocol.py: Add Ids constants (DB_ACCESS_AREA_BASE, etc.)
- server.py: Update to parse/generate the corrected wire format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix S7CommPlus LID byte offsets to use 1-based addressing

S7CommPlus protocol uses 1-based LID byte offsets, but the client was
sending 0-based offsets. This caused real PLCs to reject all db_read
and db_write requests. Also fixes lint issues in e2e test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S7CommPlus session setup and legacy S7 fallback for data operations

Implement the missing SetMultiVariables session handshake step that echoes
ServerSessionVersion (attr 306) back to the PLC after CreateObject. Without
this, PLCs reject data operations with ERROR2 (0x05A9).

For PLCs that don't provide ServerSessionVersion or don't support S7CommPlus
data operations, the client transparently falls back to the legacy S7 protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Potential fix for code scanning alert no. 9: Binding a socket to all network interfaces

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* Add CLI tools for direct PLC interaction (read, write, dump, info)

Expand the snap7 CLI beyond snap7-server with subcommands for reading,
writing, dumping, and inspecting PLCs directly from the terminal.

Closes #621

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: skip CLI tests when click is not installed

Uses pytest.importorskip("click") so test collection doesn't fail
in CI environments that only install the test dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename CLI entry point from snap7 to s7, add discover hook

Renames the CLI command from `snap7` to `s7` for a cleaner interface.
Adds a hook to auto-register `s7 discover` when the discovery module
is available, so both PRs compose cleanly when merged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds db_read_*/db_write_* convenience methods for bool, byte, int, uint,
word, dint, udint, dword, real, lreal, string, and wstring types.
Closes #617

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds 70 tests validating TPKT, COTP, and S7 protocol encoding against
the specification at byte level.
Closes #620

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add examples cookbook and troubleshooting documentation

Based on analysis of 312 issues and discussions, the two biggest
sources of user confusion are S7-1200/1500 configuration and data
type handling. These new pages address the most common questions.

examples.rst: rack/slot reference table, PLC address mapping guide,
complete data types cookbook (BOOL through DATE_AND_TIME), memory
areas, analog I/O, multi-variable read, and server setup for testing.

troubleshooting.rst: error message reference table, S7-1200/1500
TIA Portal configuration steps, connection recovery patterns, timeout
configuration, thread safety, and protocol limitations FAQ.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add PLC support matrix documentation

Add a new page documenting which Siemens PLC families are supported,
their protocol capabilities, PUT/GET configuration requirements, and
alternatives for unsupported PLCs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Cleanup: consolidate tests, fix docs, remove README async section

README:
- Remove async support section (unnecessary on landing page)

Documentation:
- Add S7CommPlus API docs with experimental warning
- Add experimental warning to AsyncClient docs
- Update PLC support matrix for S7CommPlus V1/V2 status

Test consolidation (no test logic changed):
- Merge test_server_coverage.py into test_server.py
- Merge test_partner_coverage.py into test_partner.py
- Merge test_logo_coverage.py into test_logo_client.py
- Merge test_db_coverage.py into test_util.py
- Rename test_s7protocol_coverage.py to test_s7protocol.py

Mypy fixes:
- Widen Row.set_value type to accept date/datetime/timedelta
- Add type annotations in test_s7protocol.py, test_partner.py,
  test_connection.py, test_async_client.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ruff formatting in test_util.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Improve S7CommPlus test coverage and fix Codecov upload

Add 154 new unit tests covering codec decoders, PValue parsing for all
data types, payload builders/parsers, connection response parsing, and
client error paths. S7CommPlus coverage rises from 77% to 87%, with
codec.py reaching 100%. Also add CODECOV_TOKEN to the workflow to fix
silent upload failures on protected branches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Cleanup: consolidate tests, fix docs, remove README async section

README:
- Remove async support section (unnecessary on landing page)

Documentation:
- Add S7CommPlus API docs with experimental warning
- Add experimental warning to AsyncClient docs
- Update PLC support matrix for S7CommPlus V1/V2 status

Test consolidation (no test logic changed):
- Merge test_server_coverage.py into test_server.py
- Merge test_partner_coverage.py into test_partner.py
- Merge test_logo_coverage.py into test_logo_client.py
- Merge test_db_coverage.py into test_util.py
- Rename test_s7protocol_coverage.py to test_s7protocol.py

Mypy fixes:
- Widen Row.set_value type to accept date/datetime/timedelta
- Add type annotations in test_s7protocol.py, test_partner.py,
  test_connection.py, test_async_client.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ruff formatting in test_util.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Improve S7CommPlus test coverage and fix Codecov upload

Add 154 new unit tests covering codec decoders, PValue parsing for all
data types, payload builders/parsers, connection response parsing, and
client error paths. S7CommPlus coverage rises from 77% to 87%, with
codec.py reaching 100%. Also add CODECOV_TOKEN to the workflow to fix
silent upload failures on protected branches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restructure docs into logical sections

Split the monolithic examples.rst and troubleshooting.rst into focused,
topic-based pages and group them under clear sections in the toctree:
Getting Started, User Guide, Troubleshooting, Development, API Reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Bumps the all-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [pytest-cov](https://github.com/pytest-dev/pytest-cov) | `7.0.0` | `7.1.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.15.4` | `0.15.7` |
| [tox](https://github.com/tox-dev/tox) | `4.46.3` | `4.50.3` |
| [tox-uv](https://github.com/tox-dev/tox-uv) | `1.33.0` | `1.33.4` |
| [uv](https://github.com/astral-sh/uv) | `0.10.6` | `0.10.12` |


Updates `pytest-cov` from 7.0.0 to 7.1.0
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](pytest-dev/pytest-cov@v7.0.0...v7.1.0)

Updates `ruff` from 0.15.4 to 0.15.7
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.15.4...0.15.7)

Updates `tox` from 4.46.3 to 4.50.3
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](tox-dev/tox@4.46.3...4.50.3)

Updates `tox-uv` from 1.33.0 to 1.33.4
- [Release notes](https://github.com/tox-dev/tox-uv/releases)
- [Commits](tox-dev/tox-uv@1.33.0...1.33.4)

Updates `uv` from 0.10.6 to 0.10.12
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](astral-sh/uv@0.10.6...0.10.12)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: ruff
  dependency-version: 0.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: tox
  dependency-version: 4.50.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: tox-uv
  dependency-version: 1.33.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: uv
  dependency-version: 0.10.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Add property-based tests with Hypothesis, fix bugs found

Add 48 property-based tests using Hypothesis covering:
- Roundtrip tests for all getter/setter pairs (integers, floats, strings, dates)
- Roundtrip tests for S7 data type encode/decode
- TPKT/COTP frame structure validation
- S7 PDU structure validation
- Fuzz tests for robustness against malformed input

Bugs found and fixed:
- set_date: used signed int16 (>h) for days offset, overflows for dates
  after ~2079. Fixed to unsigned int16 (>H).
- set_tod: used float arithmetic (total_seconds() * 1000) causing precision
  loss. Fixed to use integer arithmetic on timedelta components.

Known issue documented (not fixed):
- wstring get/set uses character count but UTF-16-BE surrogate pairs for
  supplementary characters (codepoint > 0xFFFF) need 4 bytes per character.

Closes #629

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reject supplementary Unicode characters in set_wstring

PLCs only support BMP characters (U+0000–U+FFFF) in WSTRING. Characters
above U+FFFF require UTF-16 surrogate pairs (4 bytes) but the WSTRING
length field counts 2-byte code units, causing data corruption.

Validate input and raise ValueError for non-BMP characters, matching
the reference PLC implementation behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix get_dtl to read full 4-byte nanosecond field and clean up fuzz test

get_dtl was reading only byte 8 as a raw integer for microseconds,
but the DTL format stores nanoseconds as a 4-byte big-endian uint32
in bytes 8-11. This fix reads the full field and converts ns to us.
Also removes unnecessary KeyError catch from PDU fuzz test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove codecov.yml configuration
- Remove coverage upload step from CI workflow
- Remove Codecov badge from README
- Keep local coverage reporting (--cov-report=term)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add get_ulint and get_lint to snap7.util exports

These functions existed in snap7.util.getters but were not re-exported
from snap7.util, making them harder to discover and use.

Closes #651

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Export missing getters and remove unimplemented get_array stub

- Add get_ulint, get_lint, and get_date_time_object to snap7.util exports
- Remove get_array stub (unimplemented since 2024, raises NotImplementedError)
- Clean up unused NoReturn import

Closes #651

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix docstring indentation in get_lint and get_ulint

The example return values had extra indentation that caused Sphinx to
fail with "Unexpected indentation" errors when building docs with -W.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add S7CommPlus protocol scaffolding for S7-1200/1500 support

Adds the snap7.s7commplus package as a foundation for future S7CommPlus
protocol support, targeting all S7-1200/1500 PLCs (V1/V2/V3/TLS).

Includes:
- Protocol constants (opcodes, function codes, data types, element IDs)
- VLQ encoding/decoding (Variable-Length Quantity, the S7CommPlus wire format)
- Codec for frame headers, request/response headers, and typed values
- Connection skeleton with multi-version support (V1/V2/V3/TLS)
- Client stub with symbolic variable access API
- 86 passing tests for VLQ and codec modules

The wire protocol (VLQ, data types, object model) is the same across all
protocol versions -- only the session authentication layer differs. The
protocol version is auto-detected from the PLC's CreateObject response.

Reference: thomas-v2/S7CommPlusDriver (C#, LGPL-3.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S7CommPlus server emulator, async client, and integration tests

Server emulator (snap7/s7commplus/server.py):
- Full PLC memory model with thread-safe data blocks
- Named variable registration with type metadata
- Handles CreateObject/DeleteObject session management
- Handles Explore (browse registered DBs and variables)
- Handles GetMultiVariables/SetMultiVariables (read/write)
- Multi-client support (threaded)
- CPU state management

Async client (snap7/s7commplus/async_client.py):
- asyncio-based S7CommPlus client with Lock for concurrent safety
- Same API as sync client: db_read, db_write, db_read_multi, explore
- Native COTP/TPKT transport using asyncio streams

Updated sync client and connection to be functional for V1:
- CreateObject/DeleteObject session lifecycle
- Send/receive with S7CommPlus framing over COTP/TPKT
- db_read, db_write, db_read_multi operations

Integration tests (25 new tests):
- Server unit tests (data blocks, variables, CPU state)
- Sync client <-> server: connect, read, write, multi-read, explore
- Async client <-> server: connect, read, write, concurrent reads
- Data persistence across client sessions
- Multiple concurrent clients with unique sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up security-focused wording in S7CommPlus docstrings

Reframe protocol version descriptions around interoperability rather
than security vulnerabilities. Remove CVE references and replace
implementation-specific language with neutral terminology.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: remove pytest-asyncio dependency, fix formatting

Rewrite async tests to use asyncio.run() instead of @pytest.mark.asyncio
since pytest-asyncio is not a project dependency. Also apply ruff
formatting fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add pytest-asyncio dependency and use native async tests

Add pytest-asyncio to test dependencies and set asyncio_mode=auto.
Restore async test methods with @pytest.mark.asyncio instead of
asyncio.run() wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI and add S7CommPlus end-to-end tests

Fix ruff formatting violations and mypy type errors in S7CommPlus code
that caused pre-commit CI to fail. Add end-to-end test suite for
validating S7CommPlus against a real S7-1200/1500 PLC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Enhance S7CommPlus connection with variable-length TSAP support and async client improvements

Support bytes-type remote TSAP (e.g. "SIMATIC-ROOT-HMI") in ISOTCPConnection,
extend S7CommPlus protocol handling, and improve async client and server emulator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add extensive debug logging to S7CommPlus protocol stack for real PLC diagnostics

Adds hex dumps and detailed parsing at every protocol layer (ISO-TCP,
S7CommPlus connection, client) plus 6 new diagnostic e2e tests that
probe different payload formats and function codes against real hardware.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix S7CommPlus wire format for real PLC compatibility

Rewrite client payload encoding/decoding to use the correct S7CommPlus
protocol format with ItemAddress structures (SymbolCrc, AccessArea,
AccessSubArea, LIDs), ObjectQualifier, and proper PValue response
parsing. Previously the client used a simplified custom format that
only worked with the emulated server, causing ERROR2 responses from
real S7-1200/1500 PLCs.

- client.py: Correct GetMultiVariables/SetMultiVariables request format
- async_client.py: Reuse corrected payload builders from client.py
- codec.py: Add ItemAddress, ObjectQualifier, PValue encode/decode
- protocol.py: Add Ids constants (DB_ACCESS_AREA_BASE, etc.)
- server.py: Update to parse/generate the corrected wire format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix S7CommPlus LID byte offsets to use 1-based addressing

S7CommPlus protocol uses 1-based LID byte offsets, but the client was
sending 0-based offsets. This caused real PLCs to reject all db_read
and db_write requests. Also fixes lint issues in e2e test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S7CommPlus session setup and legacy S7 fallback for data operations

Implement the missing SetMultiVariables session handshake step that echoes
ServerSessionVersion (attr 306) back to the PLC after CreateObject. Without
this, PLCs reject data operations with ERROR2 (0x05A9).

For PLCs that don't provide ServerSessionVersion or don't support S7CommPlus
data operations, the client transparently falls back to the legacy S7 protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Potential fix for code scanning alert no. 9: Binding a socket to all network interfaces

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Add S7CommPlus protocol scaffolding for S7-1200/1500 support

Adds the snap7.s7commplus package as a foundation for future S7CommPlus
protocol support, targeting all S7-1200/1500 PLCs (V1/V2/V3/TLS).

Includes:
- Protocol constants (opcodes, function codes, data types, element IDs)
- VLQ encoding/decoding (Variable-Length Quantity, the S7CommPlus wire format)
- Codec for frame headers, request/response headers, and typed values
- Connection skeleton with multi-version support (V1/V2/V3/TLS)
- Client stub with symbolic variable access API
- 86 passing tests for VLQ and codec modules

The wire protocol (VLQ, data types, object model) is the same across all
protocol versions -- only the session authentication layer differs. The
protocol version is auto-detected from the PLC's CreateObject response.

Reference: thomas-v2/S7CommPlusDriver (C#, LGPL-3.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S7CommPlus server emulator, async client, and integration tests

Server emulator (snap7/s7commplus/server.py):
- Full PLC memory model with thread-safe data blocks
- Named variable registration with type metadata
- Handles CreateObject/DeleteObject session management
- Handles Explore (browse registered DBs and variables)
- Handles GetMultiVariables/SetMultiVariables (read/write)
- Multi-client support (threaded)
- CPU state management

Async client (snap7/s7commplus/async_client.py):
- asyncio-based S7CommPlus client with Lock for concurrent safety
- Same API as sync client: db_read, db_write, db_read_multi, explore
- Native COTP/TPKT transport using asyncio streams

Updated sync client and connection to be functional for V1:
- CreateObject/DeleteObject session lifecycle
- Send/receive with S7CommPlus framing over COTP/TPKT
- db_read, db_write, db_read_multi operations

Integration tests (25 new tests):
- Server unit tests (data blocks, variables, CPU state)
- Sync client <-> server: connect, read, write, multi-read, explore
- Async client <-> server: connect, read, write, concurrent reads
- Data persistence across client sessions
- Multiple concurrent clients with unique sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up security-focused wording in S7CommPlus docstrings

Reframe protocol version descriptions around interoperability rather
than security vulnerabilities. Remove CVE references and replace
implementation-specific language with neutral terminology.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: remove pytest-asyncio dependency, fix formatting

Rewrite async tests to use asyncio.run() instead of @pytest.mark.asyncio
since pytest-asyncio is not a project dependency. Also apply ruff
formatting fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add pytest-asyncio dependency and use native async tests

Add pytest-asyncio to test dependencies and set asyncio_mode=auto.
Restore async test methods with @pytest.mark.asyncio instead of
asyncio.run() wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI and add S7CommPlus end-to-end tests

Fix ruff formatting violations and mypy type errors in S7CommPlus code
that caused pre-commit CI to fail. Add end-to-end test suite for
validating S7CommPlus against a real S7-1200/1500 PLC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Enhance S7CommPlus connection with variable-length TSAP support and async client improvements

Support bytes-type remote TSAP (e.g. "SIMATIC-ROOT-HMI") in ISOTCPConnection,
extend S7CommPlus protocol handling, and improve async client and server emulator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add extensive debug logging to S7CommPlus protocol stack for real PLC diagnostics

Adds hex dumps and detailed parsing at every protocol layer (ISO-TCP,
S7CommPlus connection, client) plus 6 new diagnostic e2e tests that
probe different payload formats and function codes against real hardware.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix S7CommPlus wire format for real PLC compatibility

Rewrite client payload encoding/decoding to use the correct S7CommPlus
protocol format with ItemAddress structures (SymbolCrc, AccessArea,
AccessSubArea, LIDs), ObjectQualifier, and proper PValue response
parsing. Previously the client used a simplified custom format that
only worked with the emulated server, causing ERROR2 responses from
real S7-1200/1500 PLCs.

- client.py: Correct GetMultiVariables/SetMultiVariables request format
- async_client.py: Reuse corrected payload builders from client.py
- codec.py: Add ItemAddress, ObjectQualifier, PValue encode/decode
- protocol.py: Add Ids constants (DB_ACCESS_AREA_BASE, etc.)
- server.py: Update to parse/generate the corrected wire format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix S7CommPlus LID byte offsets to use 1-based addressing

S7CommPlus protocol uses 1-based LID byte offsets, but the client was
sending 0-based offsets. This caused real PLCs to reject all db_read
and db_write requests. Also fixes lint issues in e2e test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S7CommPlus session setup and legacy S7 fallback for data operations

Implement the missing SetMultiVariables session handshake step that echoes
ServerSessionVersion (attr 306) back to the PLC after CreateObject. Without
this, PLCs reject data operations with ERROR2 (0x05A9).

For PLCs that don't provide ServerSessionVersion or don't support S7CommPlus
data operations, the client transparently falls back to the legacy S7 protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S7CommPlus V2 protocol support (TLS + IntegrityId)

Implements V2 protocol support for S7-1200/1500 PLCs with modern firmware:
- TLS 1.3 activation between InitSSL and CreateObject
- OMS exporter secret extraction for legitimation key derivation
- Dual IntegrityId counters (read vs write) in V2 PDU headers
- Password legitimation module (legacy SHA-1 XOR + new AES-256-CBC)
- V2 server emulator with TLS and IntegrityId tracking
- 39 new tests covering all V2 components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Complete V2 legitimation with cryptography optional dependency

- Add cryptography as optional dep: pip install python-snap7[s7commplus]
- Implement connection.authenticate() with challenge/response flow
- Wire up legitimation in client.connect(password=...)
- Auto-detect legacy (SHA-1 XOR) vs new (AES-256-CBC) auth mode
- Add legitimation protocol methods: get challenge, send response
- 46 tests now (was 39), including AES roundtrip verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: skip V2 crypto tests when cryptography not installed

- Skip TestBuildNewResponse when cryptography package is unavailable
  (CI only installs [test] extras, not [s7commplus])
- Fix extra blank line in protocol.py that failed ruff format check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Install s7commplus extras in CI to test cryptography-dependent code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* Add PROFINET DCP network discovery

Wraps pnio-dcp library for discovering Siemens PLCs on the local network.
Includes Device dataclass, discover() and identify() functions, and CLI entry point.
Closes #622

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Export discover_command for s7 CLI integration

Exports a reusable click command (discover_command) that the s7 CLI
can auto-register as `s7 discover`. Removes the standalone snap7-scan
entry point in favor of the unified s7 CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…635)

Adds configurable heartbeat probing, automatic reconnection with
exponential backoff and jitter, and disconnect/reconnect callbacks.
Closes #626, closes #627

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- source-build.yml: uv venv + uv pip install build + python -m build → uv build
- publish-pypi.yml: pip install build + python -m build → uv build
- publish-test-pypi.yml: same
- test.yml: uv venv + uv pip install → uv sync --extra
- doc.yml: uv venv + uv pip install → uv sync --extra

uv venvs don't include pip, so workflows using bare pip or
python -m build (which needs pip internally) break. Use uv build
for building and uv sync for dependency installation.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Add auto-reconnect/heartbeat section to connection-issues.rst
- Update limitations.rst: discovery is available via PROFINET DCP,
  S7CommPlus V1/V2 are now supported
- Update plc-support.rst: V2 is functional (not "in development"),
  fix V2 encryption to TLS 1.3, update protocol overview
- Add V2 TLS connection and password authentication docs to
  API/s7commplus.rst
- Add LINT/ULINT data type sections to reading-writing.rst

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tests for CLI discover, legitimation failures, async client, and heartbeat

New test file covering identified coverage gaps:
- CLI discover command: help, devices found/not found, timeout, import error
- Legitimation failure paths: not connected, no TLS, no OMS secret, edge cases
- S7CommPlus async client: connect, read, write, context manager, properties
- Heartbeat concurrency: rapid reads and writes during active heartbeat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused threading import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add TLS support to S7CommPlus async client

Implement TLS 1.3 for S7CommPlusAsyncClient, bringing feature parity
with the sync S7CommPlusConnection for V2 protocol connections:

- Add use_tls, tls_cert, tls_key, tls_ca parameters to connect()
- Implement _activate_tls() using asyncio start_tls() for in-place
  transport upgrade
- Add authenticate() method with full legitimation support (legacy
  SHA-1 XOR and new AES-256-CBC modes)
- Add V2 post-connection checks (require TLS, enable IntegrityId)
- Reset TLS state on disconnect
- Fix TLS 1.3 cipher configuration for Python 3.13+ compatibility
  (use set_ciphersuites instead of set_ciphers for TLS 1.3)

The cipher fix also applies to the sync connection.py to prevent the
same issue on Python 3.14+.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ruff formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
gijzelaerr and others added 30 commits April 3, 2026 13:42
Update all documentation, examples, and source docstrings to present the
s7 package as the recommended entry point for all PLC models, with snap7
kept for backwards compatibility. Add s7.util module re-exporting
snap7.util so users never need to import snap7 directly.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Bumps the all-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [hypothesis](https://github.com/HypothesisWorks/hypothesis) | `6.151.10` | `6.151.11` |
| [mypy](https://github.com/python/mypy) | `1.19.1` | `1.20.0` |
| [types-setuptools](https://github.com/python/typeshed) | `82.0.0.20260210` | `82.0.0.20260402` |
| [ruff](https://github.com/astral-sh/ruff) | `0.15.8` | `0.15.9` |
| [tox](https://github.com/tox-dev/tox) | `4.51.0` | `4.52.0` |
| [tox-uv](https://github.com/tox-dev/tox-uv) | `1.33.4` | `1.34.0` |
| [uv](https://github.com/astral-sh/uv) | `0.11.2` | `0.11.3` |
| [click](https://github.com/pallets/click) | `8.3.1` | `8.3.2` |


Updates `hypothesis` from 6.151.10 to 6.151.11
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](HypothesisWorks/hypothesis@hypothesis-python-6.151.10...hypothesis-python-6.151.11)

Updates `mypy` from 1.19.1 to 1.20.0
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](python/mypy@v1.19.1...v1.20.0)

Updates `types-setuptools` from 82.0.0.20260210 to 82.0.0.20260402
- [Commits](https://github.com/python/typeshed/commits)

Updates `ruff` from 0.15.8 to 0.15.9
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.15.8...0.15.9)

Updates `tox` from 4.51.0 to 4.52.0
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](tox-dev/tox@4.51.0...4.52.0)

Updates `tox-uv` from 1.33.4 to 1.34.0
- [Release notes](https://github.com/tox-dev/tox-uv/releases)
- [Commits](tox-dev/tox-uv@1.33.4...1.34.0)

Updates `uv` from 0.11.2 to 0.11.3
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](astral-sh/uv@0.11.2...0.11.3)

Updates `click` from 8.3.1 to 8.3.2
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst)
- [Commits](pallets/click@8.3.1...8.3.2)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.151.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: mypy
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: types-setuptools
  dependency-version: 82.0.0.20260402
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: ruff
  dependency-version: 0.15.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: tox
  dependency-version: 4.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: tox-uv
  dependency-version: 1.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: uv
  dependency-version: 0.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: click
  dependency-version: 8.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Fix partner S7 Communication Setup and bsend/brecv PDU format

The partner module was only completing the COTP handshake but skipping
the S7 Communication Setup negotiation, causing real PLCs to stay in
"awaiting connection" status. Additionally, the bsend/brecv PDUs used
a minimal custom format instead of proper S7 USERDATA PDUs with R-ID,
making them incompatible with real Siemens PLCs.

Changes:
- Add S7 Communication Setup after COTP connect (active mode)
- Handle incoming COTP CR and S7 Setup for passive mode
- Rewrite partner data PDU to use S7 USERDATA format (type 0x07) with
  push function group (0x06), proper parameter section, and R-ID
- Rewrite partner ACK PDU to use S7 USERDATA response format
- Add r_id attribute for bsend/brecv matching
- Update tests for new PDU format, add R-ID coverage

Fixes #668

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Partner to s7 package for drop-in snap7 compatibility

Re-exports snap7.Partner and PartnerStatus from s7, so users can
do `from s7 import Partner` just like Client, AsyncClient, and Server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ruff format in partner.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix bsend PDU format: use subfunction 0x06 and compact parameter header

The bsend data push was using subfunction 0x01 (push notification) instead
of 0x06 (BSend), causing the PLC to reject the PDU with error 0x8404.
Also fix the parameter sub-length from 0x08 to 0x04 for requests (no
data_unit_ref/last_data_unit/error_code needed), and add error code
checking in the ACK parser.

Fixes #668

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix BSend PDU format to match PBC protocol expected by S7-1500

The BSend USERDATA PDU was rejected by real PLCs (error 0x8104 "object
does not exist") because the parameter section used request semantics
instead of the PBC push format. Changes:

- Method byte: 0x12 (push) instead of 0x11 (request)
- Type/Group byte: 0x06 (push|PBC) instead of 0x46 (request|PBC)
- Sub-length: 0x08 to include dur/ldu/error_code fields
- Add variable specification block (12 06 82 41 ...) with payload length
- Add PBC prefix (12 00) before payload in data section
- Parse and strip PBC prefix in _parse_partner_data_pdu for incoming data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix BSend PDU to match PBC format verified against real S7-1500

Based on feedback from real PLC testing (issue #668), the PDU format
needed further corrections:

- Type/Group byte: 0x46 (request|PBC) was correct after all
- Subfunction: 0x01 (not 0x06)
- Sequence number in parameter: always 0 for PBC
- R-ID moved from parameter to data section variable specification
- Data section format: var_spec (12 06 13 00) + R-ID + payload_len + data
- Parser updated to strip 10-byte PBC var spec prefix from incoming data

This format was confirmed working against a real S7-1500 PLC by the
issue reporter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Implement async receive path for Partner

Add the missing async receive functionality so partners can receive
data non-blocking, matching the existing async send pattern:

- as_b_recv(): start async receive in background listener thread
- wait_as_b_recv_completion(timeout): block until data arrives
- check_as_b_recv_completion(): poll for completion (enhanced with
  error state reporting)
- _recv_listener(): background thread that monitors the socket for
  incoming data, parses the PDU, sends ACK, and queues the result

Also implemented:
- set_recv_callback(callback): register callback for incoming data
  (was a no-op stub)
- set_send_callback(callback): register callback for send completion
  (was a no-op stub)
- Thread-safe I/O via _io_lock to coordinate between async send and
  receive threads on the shared socket

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition in wait_as_b_recv_completion

The recv listener thread could complete before wait_as_b_recv_completion
was called, causing a spurious RuntimeError. Track whether an async recv
was started so the wait method returns the result instead of raising
when the listener already finished.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ACK to echo PDU reference and match PLC expected format

Based on real PLC testing feedback (issue #668):

- ACK must echo the PDU reference from the incoming data PDU, not use
  a new sequence number — the PLC needs this to correlate the response
- Remove R-ID from ACK parameter section (not needed)
- Add 4-byte data section to ACK (return code 0x0a, transport 0x00)
- _parse_partner_data_pdu now returns (payload, r_id, pdu_ref) tuple,
  extracting R-ID and PDU reference from the variable specification
  block and S7 header respectively

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix BSend/BRecv socket contention and expose recv R-ID

Based on real PLC testing feedback (issue #668): BSend timed out when
the async recv listener was active because both competed for the socket
without proper coordination.

Fixes:
- Use RLock instead of Lock so b_send/b_recv can be called both
  directly and from the async processor without deadlock
- Add _io_lock to b_send() around send+receive_ack
- Add _io_lock to b_recv() around receive+send_ack
- Move socket timeout change inside the lock in _recv_listener to
  prevent the timeout from affecting concurrent operations

New features:
- recv_timeout attribute: configurable socket timeout (seconds) for
  the async receive listener (default 0.2)
- get_recv_r_id(): returns the R-ID from the last received PDU
- _recv_r_id stored on both sync and async receive paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Bumps [uv](https://github.com/astral-sh/uv) from 0.11.3 to 0.11.6.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](astral-sh/uv@0.11.3...0.11.6)

---
updated-dependencies:
- dependency-name: uv
  dependency-version: 0.11.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.6 to 46.0.7.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@46.0.6...46.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
- Merge s7commplus.rst + async_client.rst into client.rst so Client,
  Server, and Partner all follow the same pattern: recommended (s7)
  first, then legacy (snap7) below
- Add s7.Partner to partner.rst (was only showing snap7.Partner)
- Group low-level modules (connection, s7protocol, datatypes, discovery)
  under a separate "Internals" section in the sidebar
- Keep user-facing modules (client, server, partner, logo, util, type)
  in the main API Reference section

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bumps the all-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [pytest](https://github.com/pytest-dev/pytest) | `9.0.2` | `9.0.3` |
| [hypothesis](https://github.com/HypothesisWorks/hypothesis) | `6.151.11` | `6.151.13` |
| [mypy](https://github.com/python/mypy) | `1.20.0` | `1.20.1` |
| [types-setuptools](https://github.com/python/typeshed) | `82.0.0.20260402` | `82.0.0.20260408` |
| [ruff](https://github.com/astral-sh/ruff) | `0.15.9` | `0.15.10` |
| [tox](https://github.com/tox-dev/tox) | `4.52.0` | `4.52.1` |
| [tox-uv](https://github.com/tox-dev/tox-uv) | `1.34.0` | `1.35.1` |
| [rich](https://github.com/Textualize/rich) | `14.3.3` | `15.0.0` |


Updates `pytest` from 9.0.2 to 9.0.3
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](pytest-dev/pytest@9.0.2...9.0.3)

Updates `hypothesis` from 6.151.11 to 6.151.13
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](HypothesisWorks/hypothesis@hypothesis-python-6.151.11...hypothesis-python-6.151.13)

Updates `mypy` from 1.20.0 to 1.20.1
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](python/mypy@v1.20.0...v1.20.1)

Updates `types-setuptools` from 82.0.0.20260402 to 82.0.0.20260408
- [Commits](https://github.com/python/typeshed/commits)

Updates `ruff` from 0.15.9 to 0.15.10
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.15.9...0.15.10)

Updates `tox` from 4.52.0 to 4.52.1
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](tox-dev/tox@4.52.0...4.52.1)

Updates `tox-uv` from 1.34.0 to 1.35.1
- [Release notes](https://github.com/tox-dev/tox-uv/releases)
- [Commits](tox-dev/tox-uv@1.34.0...1.35.1)

Updates `rich` from 14.3.3 to 15.0.0
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](Textualize/rich@v14.3.3...v15.0.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: hypothesis
  dependency-version: 6.151.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: mypy
  dependency-version: 1.20.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: types-setuptools
  dependency-version: 82.0.0.20260408
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: ruff
  dependency-version: 0.15.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: tox
  dependency-version: 4.52.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: tox-uv
  dependency-version: 1.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: rich
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](pytest-dev/pytest@9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Automatically squash-merges Dependabot PRs once all CI checks pass.
Uses GitHub's native auto-merge feature so branch protection rules
are still respected.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add symbolic addressing via SymbolTable for read/write by tag name

Closes #616

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix issues in symbolic addressing: maps, docstring, source detection

- Move getter/setter dispatch maps to module-level constants to avoid
  rebuilding them on every read/write call
- Move set_lword from _INT_SETTER_MAP to _SIMPLE_SETTER_MAP so its
  value is not cast through int() redundantly
- Fix read_many docstring to honestly describe individual reads instead
  of claiming batched/grouped behavior
- Extract _read_source() helper for from_csv/from_json that checks for
  newlines before falling back to filesystem path detection, preventing
  inline content from accidentally matching an existing file
- Document get_word type annotation mismatch (returns int at runtime
  despite bytearray annotation in getters.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add S7 routing support for multi-subnet PLC access

Implement routing parameters in the COTP Connection Request PDU so
clients can reach PLCs behind a gateway on another subnet.  The new
ISOTCPConnection.set_routing() method appends subnet ID (0xC6) and
routing TSAP (0xC7) parameters to the CR, and Client.connect_routed()
provides a high-level entry point that mirrors connect() but accepts
gateway and destination rack/slot/subnet.

Closes #615

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ruff format for routing debug log line

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Mark routing support as experimental

Add experimental warnings to connect_routed, set_routing, and
documentation so users know the API may change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Set TCP_NODELAY and SO_KEEPALIVE on all S7 sockets

Eliminates 100-150ms per-exchange latency caused by Nagle's algorithm
interacting with TCP delayed ACKs. S7 is a request/response protocol
that sends complete PDUs per sendall() call, so Nagle only adds delay.

TCP_NODELAY is set on all client-initiated sockets (snap7/connection.py,
which is also used by s7/connection.py via ISOTCPConnection) and on all
accepted sockets in snap7/server, snap7/partner, and s7/_s7commplus_server.

SO_KEEPALIVE is also enabled to detect dead connections during idle periods.

Closes #673

https://claude.ai/code/session_01CkAzrsFA7oZHZL7h2JEavb

* Configure SO_KEEPALIVE timing to detect failures in ~90s

Without explicit timing parameters, SO_KEEPALIVE uses the OS default
of ~2 hours idle before the first probe fires (Linux: tcp_keepalive_time=7200),
making it practically useless for PLC connections.

Sets TCP_KEEPIDLE=60, TCP_KEEPINTVL=10, TCP_KEEPCNT=3 on platforms that
support them (Linux, macOS 10.15+), reducing dead-connection detection to
~90s of idle. Windows keeps OS defaults, which are already more aggressive
than Linux defaults.

https://claude.ai/code/session_01CkAzrsFA7oZHZL7h2JEavb

* Move s7 package example to unreleased 4.0 section in README

The Quick Start previously showed s7.Client as the recommended API,
but pip install python-snap7 delivers v3.0 which only has the snap7
package. A user following the README would get an ImportError.

- Quick Start now shows only the stable snap7 API (v3.0)
- Renamed "Version 3.1 (unreleased)" to "Version 4.0 (unreleased)"
- Added an explicit rst note block warning that 4.0 is not yet on PyPI
- Moved the s7.Client example and s7CommPlus description into the 4.0 section

https://claude.ai/code/session_01CkAzrsFA7oZHZL7h2JEavb

* Fix ruff format: extra trailing space in comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
…679)

Add operations that S7CommPlusDriver (C#) implements but we were missing:

- read_area/write_area for controller memory areas (M, I, Q, counters,
  timers) — previously only DB access was supported via S7CommPlus
- explore(explore_id) — browse specific objects, not just root
- set_plc_operating_state(state) — start/stop PLC via INVOKE function

Both sync and async clients updated. Request/response builders are
module-level functions shared between both clients.

Note: Link operations (ADD_LINK, REMOVE_LINK, GET_LINK) and sequencing
(BEGIN_SEQUENCE, END_SEQUENCE) are defined in protocol.py but not
implemented by S7CommPlusDriver either — these are rarely used features.

Reference: thomas-v2/S7CommPlusDriver (C#, LGPL-3.0)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add multi-variable read optimizer for minimal PDU-packed S7 exchanges

Introduces a read optimization pipeline that merges scattered read requests
into contiguous blocks and packs them into minimal PDU exchanges, reducing
round-trips when reading multiple variables.

- snap7/optimizer.py: Pure-logic optimization with sort, merge, packetize,
  and extract_results functions
- snap7/s7protocol.py: build_multi_read_request and extract_multi_read_data
  methods for multi-item S7 READ_AREA PDUs
- snap7/server/__init__.py: Server-side multi-item read support
- snap7/client.py: read_multi_vars now uses optimizer for 2+ dict items,
  with plan caching for repeated layouts
- tests/test_optimizer.py: 23 tests covering unit and integration scenarios

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix multi-read optimizer: cache mutation, dead code, and usability issues

- Fix cache mutation bug: deep-copy cached ReadPackets before assigning
  buffers so repeated calls don't corrupt cached state
- Remove dead _map_area_int method
- Replace per-call set comprehension with module-level _VALID_AREA_VALUES
  frozenset for Area validation
- Fix O(n^2) byte concatenation in build_multi_read_request using list
  and b"".join()
- Clear optimizer cache on disconnect
- Add use_optimizer attribute (default True) to allow opting out

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Mark read optimizer as experimental

Add experimental warnings to optimizer module, client docstring, and
documentation so users know the API may change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add parallel dispatch to read optimizer

Fire multiple PDU requests back-to-back on the same TCP connection and
collect responses by sequence number, reducing round-trip overhead when
reading many scattered variables.

- data_available() on ISOTCPConnection: select()-based readable check
- _send_receive_parallel(): pipelining multiple S7 requests
- _auto_tune_parallel(): set max_parallel based on negotiated PDU size
- _execute_packets_parallel/sequential: dispatch strategies
- Comprehensive optimizer documentation page

Inspired by QuakeString/python-snap7-optimized fork.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix heartbeat race condition with RLock on _send_receive

The heartbeat thread holds _reconnect_lock while calling get_cpu_state(),
which calls _send_receive(). Adding the lock to _send_receive() caused a
deadlock with threading.Lock. Switch to RLock (reentrant) so the same
thread can acquire the lock multiple times without deadlocking.

This also protects _send_receive_parallel() with the lock to prevent
conflicts between parallel dispatch and heartbeat probes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix ruff format: collapse short method signatures to single line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add structured logging with PLC connection context

New snap7.log module with:
- PLCLoggerAdapter: injects plc_host, plc_rack, plc_slot, plc_protocol
  into every log record and prefixes messages with [host R{rack}/S{slot}]
- OperationLogger: context manager that logs operation timing at DEBUG
  level (e.g. "db_read db=1 start=0 size=4 (2.3ms)")
- JSONFormatter: single-line JSON output for structured log aggregation
  (ELK, Datadog, etc.)

Integration:
- Client.__init__ creates self.logger as PLCLoggerAdapter
- Client.connect updates context with host/rack/slot/protocol
- db_read uses OperationLogger for timing

Backward compatible: standard logging.getLogger("snap7") still works.
The adapter adds context fields without changing the logger hierarchy.

Closes #618.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix Python 3.10 compatibility: LoggerAdapter is not subscriptable

logging.LoggerAdapter[logging.Logger] requires Python 3.11+.
Use the non-generic form for 3.10 compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…687)

Implements features from issues #681, #682, #685, #686:

S7CommPlus EXPLORE-based operations (experimental):
- list_datablocks(): enumerate all DBs via EXPLORE, with legacy
  fallback to list_blocks_of_type (#686)
- browse(): walk PLC symbol table via EXPLORE to get variable names,
  DB numbers, and offsets — returns data for SymbolTable (#681)
- Structured EXPLORE request builder with attribute filters
- Response parsers for datablock info and field layout

SymbolTable enhancements (experimental):
- from_tia_xml(): parse TIA Portal DB source XML exports (#682)
- from_browse(): create SymbolTable from live PLC browse results (#681)

Protocol IDs added for EXPLORE, subscriptions, alarms:
- NATIVE_THE_PLC_PROGRAM_RID, OBJECT_VARIABLE_TYPE_NAME,
  BLOCK_BLOCK_NUMBER, CLASS_SUBSCRIPTION, alarm subscription IDs

s7.Client unified methods:
- list_datablocks() with S7CommPlus/legacy fallback
- browse() for live symbol resolution
- explore(explore_id) with specific object browsing

PLC clock (#685) already works via __getattr__ delegation to
snap7.Client.get_plc_datetime/set_plc_datetime.

Alarm subscriptions (#683) and data change subscriptions (#684) have
protocol IDs defined but full implementation requires real PLC testing
of the subscription CREATE_OBJECT flow.

All features marked experimental in docstrings.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive cleanup for the 4.0 release:

README:
- 3.0 (current release) section first with snap7 examples
- 4.0 (unreleased) section with s7 examples and S7CommPlus headline
- Clear note that pip install gives 3.0, not 4.0
- Experimental features listed separately (optimizer, routing, symbols)
- Changelog with all 4.0 features

Documentation:
- Update limitations.rst: V3 supported, V4 is the gap
- Update plc-support.rst: S7-1500 FW 3.x+ now fully supported
- Add symbols.rst doc page for SymbolTable (experimental)
- Add symbols to API Reference section in index.rst

s7 package:
- Export SymbolTable from s7.__init__ so users can do `from s7 import SymbolTable`

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements #683 (diagnostic buffer) and #684 (data subscriptions):

Diagnostic buffer (#683):
- read_diagnostic_buffer() on snap7.Client: reads SZL 0x00A0, parses
  20-byte entries with BCD timestamps into structured dicts
- Exposed through s7.Client.read_diagnostic_buffer() via legacy fallback

Data change subscriptions (#684, experimental):
- create_subscription(items, cycle_ms): builds a CREATE_OBJECT request
  with subscription-class attributes (cycle time, credit limit,
  reference list) — PLC pushes updates for monitored variables
- delete_subscription(subscription_id): sends DELETE_OBJECT to clean up
- Exposed through s7.Client with S7CommPlus requirement check

All subscription methods marked experimental. The subscription protocol
is modeled after S7CommPlusDriver's alarm subscription pattern, adapted
for data variable monitoring.

Closes #683, closes #684.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
32 tests for s7.Client and s7.Server using the built-in emulators:

- Legacy: connect, db_read, db_write, db_read_multi, list_datablocks,
  context manager, delegated methods, diagnostic buffer
- S7CommPlus: connect, db_read, db_write, db_read_multi, explore,
  list_datablocks, browse, browse-to-SymbolTable, auto-detection
- Server: context manager, register_db/raw_db, get_db, properties
- Guard tests: browse/explore/subscription require S7CommPlus

Bug fixes:
- s7/client.py: legacy connection optional when S7CommPlus explicit
- s7/_s7commplus_client.py: EXPLORE parser handles WSTRING (0x15),
  UDINT (0x04) datatypes, skips return code VLQ, tracks nesting depth
  to avoid child objects overwriting parent DB name/number
- s7/_s7commplus_server.py: EXPLORE response uses standard S7CommPlus
  IDs (Ids.OBJECT_VARIABLE_TYPE_NAME, Ids.BLOCK_BLOCK_NUMBER) with
  proper flags+datatype encoding

Coverage: s7/client.py 21%→88%, s7/_s7commplus_client.py 40%→67%,
total 78%→81%. mypy clean (0 errors across 79 files).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix in structure 'S7SZL' function '__str__'

Fix in structure "S7SZL" function "__str__", previusly gave error "AttributeError: 'S7SZL' object has no attribute 'S7SZHeader'"

* Fixed function get_cpu_info

Fixed function get_cpu_info, it returned all the field empty.
Now tested working on s7-300 and s7-1500, same output as the old python-snap7 2.1.1.

* Fixed function get_cpu_info

Fixed function get_cpu_info, it returned all the field empty.
Now tested working on s7-300 and s7-1500, same output as the old python-snap7 2.1.1.
… state (#693)

* Add missing setters, optimized read_many, array helpers

Missing data type setters:
- set_lint, set_ulint: 64-bit signed/unsigned integer
- set_ltime: nanosecond timedelta (LTIME)
- set_ltod: nanosecond time-of-day (LTOD)
- set_ldt: nanosecond epoch datetime (LDT)
- All exported from snap7.util and registered in SymbolTable dispatch

Optimized SymbolTable.read_many():
- Now uses read_multi_vars for batched reads (via optimizer when enabled)
  instead of reading each tag individually
- Falls back to single read for single-tag requests

Array read/write helpers:
- db_read_array(db, start, count, fmt): read N values using struct format
- db_write_array(db, start, values, fmt): pack and write N values
- Supports any struct format (REAL, INT, DINT, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add S7CommPlus CPU state, block transfer, and routing

S7CommPlus operations (experimental):
- get_cpu_state(): read CPU state via EXPLORE on CPU exec unit object
- upload_block(block_type, block_number): read program block via
  GET_VAR_SUBSTREAMED, with legacy full_upload fallback
- download_block(block_type, block_number, data): write program block
  via SET_VAR_SUBSTREAMED, with legacy download fallback

All exposed through s7.Client with S7CommPlus/legacy fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR #692 corrected get_cpu_info field offsets to match real S7-300/1500
SZL responses. The server emulator was using a simplified sequential
layout that didn't match. Update the server to place fields at the
correct offsets (ASName@6, ModuleName@40, Copyright@108,
SerialNumber@142, ModuleTypeName@176).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_symbols.py: use isinstance check for ValueType union
- server/__init__.py: avoid bytes/bytearray type mismatch in SZL list

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ix (#696)

Documentation:
- Add logging doc page (API/log.rst) to index
- Update CHANGES.md with all recent features (block transfer, array
  helpers, setters, cpu_info fix)
- Add s7 examples (s7_basic.py, s7_symbols.py, s7_server.py)

Optimizer fix:
- Exclude counter (0x1C) and timer (0x1D) areas from byte-range
  merging — they use element-based addressing

SymbolTable:
- Accept both snap7.Client and s7.Client (use Any type hint)

Property-based tests (Hypothesis):
- Round-trip tests for all 20 getter/setter pairs
- Found and fixed float precision bug in set_ltime/set_ltod/set_ldt
  (use integer arithmetic instead of float multiplication)

Multi-client stress tests:
- Concurrent reads from 4 clients
- Concurrent writer + reader on same DB
- Rapid connect/disconnect cycles

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Unified Tag API replacing SymbolTable

Replace the homegrown SymbolTable class with a simpler Tag-based API
using PLC4X / Siemens STEP7 address syntax that's familiar to every
TIA Portal user.

New API:
- snap7.tags.Tag dataclass: area, db_number, byte_offset, bit, datatype,
  count, name
- Tag.from_string("DB1.DBX0.0:BOOL") — full PLC4X parser
- load_csv / load_json / load_tia_xml — return dict[str, Tag]
- Client.read_tag(tag_or_str) — read typed value
- Client.write_tag(tag_or_str, value) — write typed value
- Client.read_tags([tag, ...]) — batch read via optimizer

Address syntax supported:
- DB1.DBX0.0:BOOL, DB1.DBB10:BYTE, DB1.DBW10:INT, DB1.DBD10:REAL
- DB1:10:INT (short form), DB1:10:STRING[20], DB1:10:REAL[5] (array)
- M10.5:BOOL, MW20:WORD (Merker)
- I0.0:BOOL, Q0.0:BOOL (I/O)
- Leading %% optional

Removed:
- snap7/util/symbols.py (SymbolTable, TagAddress)
- tests/test_symbols.py
- doc/API/symbols.rst

SymbolTable was experimental, unreleased, and its homegrown dict syntax
was not aligned with industry convention. The new Tag string syntax
matches what TIA Portal watch tables use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add symbolic (LID-based) access for S7-1200/1500 optimized DBs

S7-1200/1500 DBs with "Optimized block access" enabled (the TIA Portal
V13+ default) do not use fixed byte offsets — the PLC relocates
variables internally between downloads. Symbolic access navigates the
PLC's symbol tree using LIDs (Local IDs) instead.

Tag extensions:
- access_sequence: list[int] — LID path through the symbol tree
- symbol_crc: int — layout version validation (0 = skip)
- is_symbolic property — True when access_sequence is set
- Tag.from_access_string("8A0E0001.A", "REAL") classmethod using the
  S7CommPlusDriver dot-separated hex format (AccessArea.LID.LID...)

S7CommPlus client:
- read_symbolic(access_area, lids, symbol_crc) — GetMultiVariables with
  LID-based ItemAddress
- write_symbolic(...) — SetMultiVariables equivalent
- Module-level _build_symbolic_read/write_payload helpers

s7.Client routing:
- read_tag/write_tag detect Tag.is_symbolic and route to S7CommPlus
  symbolic access; classic byte-offset tags continue to use legacy
- read_tags falls back to sequential when any tag is symbolic
  (batching symbolic reads via optimizer is future work)
- snap7.Client.read_tag raises NotImplementedError for symbolic tags
  (legacy S7 has no symbolic support)

Experimental — the wire implementation follows S7CommPlusDriver but
has not been validated against a real optimized-DB PLC yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Extract LIDs from browse() for symbolic access

Complete the symbolic access loop: browse() now captures the LID (from
the object RID in EXPLORE responses) and symbol_crc for each variable.
These can be fed into Tag objects for optimized-block access.

New:
- snap7.tags.from_browse(variables) — converts browse() results to
  dict[str, Tag], producing symbolic Tags when LIDs are present
- _parse_explore_fields now captures "lid" and "symbol_crc" keys

Workflow for optimized DBs:

    variables = client.browse()
    tags = from_browse(variables)
    value = client.read_tag(tags["Motor.Speed"])  # symbolic access

Still experimental — the CRC handling in particular needs real PLC
validation. If browse doesn't provide a CRC, the Tag uses CRC=0
(skip check) which the PLC may or may not accept depending on config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Apply ruff format

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces PLC4XTag and NodeS7Tag as subtypes of Tag, each with their
own .parse() classmethod and __str__ that round-trips to the source
dialect. Adds parse_tag(s, *, strict=True) as a dispatcher that
autodetects dialect from syntax markers (',' -> nodeS7, ':TYPE' -> PLC4X).

Why: lets users of pyS7 / Node-RED bring their existing addressing
conventions (DB1,X0.0, M7.1, IW22) into ha-s7 and other integrations
without learning PLC4X syntax, while keeping PLC4X as the default in
docs and examples. Subtypes make the source dialect visible in types
and debuggers; isinstance(tag, Tag) still works internally.

Tag.from_string stays as an alias for PLC4XTag.parse for backwards
compatibility. Canonical protocol-layer code continues to work with
the base Tag type.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix async get_cpu_info SZL offsets to match sync

Discussion #700 reports that AsyncClient.get_cpu_info() returns empty
fields against a real PLC. Root cause: PR #692 fixed the offsets in
the sync Client (0,32,56,80,106,130 → 6,30,40,64,108,134,142,166,
176,208 with the correct field ordering) and #694 updated the server
emulator to match, but async_client.py was never touched and kept
the pre-#692 layout.

Ports the same offsets to the async client so sync/async produce
identical output against both real PLCs and the server emulator.

Strengthens the async test to assert specific field values (matching
the sync test) instead of just checking attribute presence — the
prior smoke test passed even with all-empty fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Extract SZL and block parsers shared by sync and async clients

Follow-on to the async get_cpu_info fix: the root cause was that
parsing logic for SZL records (and the block-info dict→struct copy)
lived inline in both Client and AsyncClient, so a fix to one side
silently drifted from the other.

- New snap7/szl.py module with parse_{cpu_info,cp_info,order_code,
  protection}_szl(szl) helpers. Both clients call them; offsets and
  field definitions exist in exactly one place.
- Two new non-SZL converters live alongside the existing protocol
  parsers in s7protocol.py (build_blocks_list_from_dict,
  build_block_info_from_dict) exposed as S7Protocol.parse_list_blocks
  / S7Protocol.parse_get_block_info. The clients' dict→struct copies
  disappear.
- Strengthened async tests for list_blocks, get_cp_info,
  get_order_code, get_protection, get_block_info — the previous
  tests only did hasattr() checks and so could not catch a value
  regression. They now assert the same concrete values the sync
  suite asserts.

Net: clients shrink by ~280 lines, parsing surface is unified, and
the async tests carry the same safety net as sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

The post-merge CI for #701 hit Errno 98 ("Address already in use") on
ubuntu-24.04 / py3.14 inside test_server_context_manager. Root cause:
tests picked ports via random.randint over narrow ranges (5k-20k
ports) with no collision check, so two concurrent server starts could
grab the same port, and re-runs on the same runner hit TIME_WAIT
lingers.

Fix: bind a throwaway socket to port 0, read the OS-assigned
ephemeral port, close, and pass it to the server. The ephemeral pool
is tens of thousands wide and the OS guarantees the port is free at
the moment of pick — much smaller collision window than a 1-in-5000
random draw. snap7.server already sets SO_REUSEADDR so TIME_WAIT
lingers don't bite either.

One helper (`get_free_tcp_port`) lives in tests/conftest.py and is
reused by test_s7_unified, test_optimizer, and test_stress. Added
tests/__init__.py so `from .conftest import ...` works.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps the all-dependencies group with 4 updates: [hypothesis](https://github.com/HypothesisWorks/hypothesis), [ruff](https://github.com/astral-sh/ruff), [tox](https://github.com/tox-dev/tox) and [uv](https://github.com/astral-sh/uv).


Updates `hypothesis` from 6.151.13 to 6.152.1
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](HypothesisWorks/hypothesis@hypothesis-python-6.151.13...hypothesis-python-6.152.1)

Updates `ruff` from 0.15.10 to 0.15.11
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.15.10...0.15.11)

Updates `tox` from 4.52.1 to 4.53.0
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](tox-dev/tox@4.52.1...4.53.0)

Updates `uv` from 0.11.6 to 0.11.7
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](astral-sh/uv@0.11.6...0.11.7)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.152.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: ruff
  dependency-version: 0.15.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
- dependency-name: tox
  dependency-version: 4.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-dependencies
- dependency-name: uv
  dependency-version: 0.11.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oup (#698)

Bumps the all-actions group with 1 update: [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact).


Updates `actions/upload-pages-artifact` from 4 to 5
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](actions/upload-pages-artifact@v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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