Conversation
Add snap7.AsyncClient that wraps the synchronous Client using asyncio.to_thread() for all blocking I/O operations. This allows using python-snap7 in async applications without blocking the event loop. All public Client methods are wrapped: I/O-bound methods (connect, read, write, etc.) are async, while local-only methods (get_param, set_param, error_text, etc.) remain synchronous. AsyncClient supports async context managers via __aenter__/__aexit__. Fixes #164 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nikteliy
left a comment
There was a problem hiding this comment.
I’m not sure this would work without a lock.
What happens if I run two tasks without awaiting them?
asyncio.create_task(client.read_data(1, 0, 1))
asyncio.create_task(client.read_data(2, 0, 2))
Replace the asyncio.to_thread() approach (PR #589) with native async I/O: - Extract BaseISOTCPConnection with shared TPKT/COTP packet logic - Add AsyncISOTCPConnection using asyncio.open_connection/streams - Add AsyncClient with asyncio.Lock() serializing send/receive cycles - Export AsyncClient from snap7.__init__ The lock ensures concurrent coroutines (asyncio.gather) never interleave on the same TCP socket, fixing the issue raised by @nikteliy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@nikteliy You were absolutely right — sorry about that, Claude hallucinated and built its own thing with I've now properly implemented it: native Thanks for catching this! |
|
Closing this PR — the approach has been reworked from scratch in #593 with a native async implementation inspired by @nikteliy's python-s7comm. The new PR uses |
Summary
Adds a native
AsyncClientfor async S7 communication usingasynciostreams. This replaces the previousasyncio.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
asyncio.open_connection()withStreamReader/StreamWriterinstead 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, makingasyncio.gather()safe.ClientMixinfor shared logic: 14 pure-computation methods (parameter management, error text, area mapping, etc.) are shared betweenClientandAsyncClientvia a mixin insnap7/client_base.py, eliminating code duplication.S7Protocolunchanged: The protocol layer is pure computation (struct pack/unpack) — no I/O, no async needed.New files
snap7/async_client.py—AsyncISOTCPConnection(async transport) andAsyncClient(full-featured async S7 client)snap7/client_base.py—ClientMixinwith shared pure-computation methodstests/test_async_client.py— 26 tests covering connection, DB read/write, area read/write, concurrent safety, multi-var operations, and synchronous helpersFeature parity
AsyncClientsupports the same operations as the syncClient:connect/disconnect/get_connecteddb_read/db_write/db_getread_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_writeread_multi_vars/write_multi_varslist_blocks/list_blocks_of_type/get_block_infoget_cpu_state/get_cpu_info/get_pdu_lengthupload/full_upload/downloadread_szl/get_plc_datetime/set_plc_datetimeplc_hot_start/plc_cold_start/plc_stopget_cp_info/get_order_code/get_protectionasync withcontext managerTest plan
asyncio.gather()with multiple reads, mixed read/write, and 10 concurrent reads