Skip to content

Add native AsyncClient with asyncio support#593

Open
gijzelaerr wants to merge 4 commits intomasterfrom
async-client
Open

Add native AsyncClient with asyncio support#593
gijzelaerr wants to merge 4 commits intomasterfrom
async-client

Conversation

@gijzelaerr
Copy link
Owner

Summary

Adds a native AsyncClient for async S7 communication using asyncio streams. This replaces the previous asyncio.to_thread() approach (which was not safe for concurrent use) with proper non-blocking I/O, following the pattern from python-s7comm.

Key design decisions

  • Native async I/O: Uses asyncio.open_connection() with StreamReader/StreamWriter instead of blocking sockets wrapped in threads. The event loop is never blocked.
  • asyncio.Lock() in _send_receive(): S7 is request-response, so each send+receive cycle must be atomic. The lock serializes concurrent coroutines, making asyncio.gather() safe.
  • Request-embedded sequence validation: The async client extracts the expected PDU sequence number directly from the request bytes (S7 header offset 4-5) rather than relying on the shared protocol counter, avoiding race conditions when multiple coroutines build requests concurrently.
  • ClientMixin for shared logic: 14 pure-computation methods (parameter management, error text, area mapping, etc.) are shared between Client and AsyncClient via a mixin in snap7/client_base.py, eliminating code duplication.
  • S7Protocol unchanged: The protocol layer is pure computation (struct pack/unpack) — no I/O, no async needed.

New files

  • snap7/async_client.pyAsyncISOTCPConnection (async transport) and AsyncClient (full-featured async S7 client)
  • snap7/client_base.pyClientMixin with shared pure-computation methods
  • tests/test_async_client.py — 26 tests covering connection, DB read/write, area read/write, concurrent safety, multi-var operations, and synchronous helpers

Feature parity

AsyncClient supports the same operations as the sync Client:

  • connect / disconnect / get_connected
  • db_read / db_write / db_get
  • read_area / write_area (with chunked transfers for large payloads)
  • ab_read / ab_write, eb_read / eb_write, mb_read / mb_write, tm_read / tm_write, ct_read / ct_write
  • read_multi_vars / write_multi_vars
  • list_blocks / list_blocks_of_type / get_block_info
  • get_cpu_state / get_cpu_info / get_pdu_length
  • upload / full_upload / download
  • read_szl / get_plc_datetime / set_plc_datetime
  • plc_hot_start / plc_cold_start / plc_stop
  • get_cp_info / get_order_code / get_protection
  • Parameter management, session passwords, connection params
  • async with context manager

Test plan

  • All 454 existing tests pass (428 sync + 26 new async)
  • Concurrent safety validated: asyncio.gather() with multiple reads, mixed read/write, and 10 concurrent reads
  • Ruff formatting checks pass

🤖 Generated with Claude Code

gijzelaerr and others added 2 commits February 28, 2026 10:55
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
@gijzelaerr gijzelaerr added this to the 3.0 milestone Feb 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant