Skip to content

Commit 14eb635

Browse files
gijzelaerrclaude
andcommitted
Extract shared pure-computation methods into ClientMixin
Moves 14 identical methods (get_pdu_length, get_exec_time, get_last_error, error_text, get_pg_block_info, set_connection_params, set_connection_type, set_session_password, clear_session_password, get_param, set_param, _max_read_size, _max_write_size, _map_area) into a shared ClientMixin base class, eliminating ~340 lines of duplication between Client and AsyncClient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a3f5be0 commit 14eb635

3 files changed

Lines changed: 271 additions & 343 deletions

File tree

snap7/async_client.py

Lines changed: 16 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,27 @@
99
import logging
1010
import struct
1111
import time
12-
from typing import List, Any, Optional, Tuple, Type, Union
12+
from typing import List, Any, Optional, Tuple, Type
1313
from types import TracebackType
1414
from datetime import datetime
1515

1616
from .connection import TPDUSize
1717
from .s7protocol import S7Protocol, get_return_code_description
18-
from .datatypes import S7Area, S7WordLen
18+
from .datatypes import S7WordLen
1919
from .error import S7Error, S7ConnectionError, S7ProtocolError, S7TimeoutError
20+
from .client_base import ClientMixin
21+
from .type import (
22+
Area,
23+
Block,
24+
BlocksList,
25+
S7CpuInfo,
26+
TS7BlockInfo,
27+
S7CpInfo,
28+
S7OrderCode,
29+
S7Protection,
30+
S7SZL,
31+
Parameter,
32+
)
2033

2134

2235
logger = logging.getLogger(__name__)
@@ -250,22 +263,8 @@ async def __aexit__(
250263
) -> None:
251264
await self.disconnect()
252265

253-
from .type import (
254-
Area,
255-
Block,
256-
BlocksList,
257-
S7CpuInfo,
258-
TS7BlockInfo,
259-
S7CpInfo,
260-
S7OrderCode,
261-
S7Protection,
262-
S7SZL,
263-
WordLen,
264-
Parameter,
265-
)
266-
267266

268-
class AsyncClient:
267+
class AsyncClient(ClientMixin):
269268
"""
270269
Native async S7 client implementation.
271270
@@ -1214,102 +1213,6 @@ async def ct_write(self, start: int, size: int, data: bytearray) -> int:
12141213
raise ValueError(f"Data length {len(data)} doesn't match size {size * 2}")
12151214
return await self.write_area(Area.CT, 0, start, data)
12161215

1217-
# ---------------------------------------------------------------
1218-
# Synchronous helpers (no I/O)
1219-
# ---------------------------------------------------------------
1220-
1221-
def get_pdu_length(self) -> int:
1222-
"""Get negotiated PDU length."""
1223-
return self.pdu_length
1224-
1225-
def get_exec_time(self) -> int:
1226-
"""Get last operation execution time in milliseconds."""
1227-
return self._exec_time
1228-
1229-
def get_last_error(self) -> int:
1230-
"""Get last error code."""
1231-
return self._last_error
1232-
1233-
def error_text(self, error_code: int) -> str:
1234-
"""Get error text for error code."""
1235-
error_texts = {
1236-
0: "OK",
1237-
0x0001: "Invalid resource",
1238-
0x0002: "Invalid handle",
1239-
0x0003: "Not connected",
1240-
0x0004: "Connection error",
1241-
0x0005: "Data error",
1242-
0x0006: "Timeout",
1243-
0x0007: "Function not supported",
1244-
0x0008: "Invalid PDU size",
1245-
0x0009: "Invalid PLC answer",
1246-
0x000A: "Invalid CPU state",
1247-
0x01E00000: "CPU : Invalid password",
1248-
0x00D00000: "CPU : Invalid value supplied",
1249-
0x02600000: "CLI : Cannot change this param now",
1250-
}
1251-
return error_texts.get(error_code, f"Unknown error: {error_code}")
1252-
1253-
def get_pg_block_info(self, data: bytearray) -> TS7BlockInfo:
1254-
"""Get block info from raw block data."""
1255-
block_info = TS7BlockInfo()
1256-
1257-
if len(data) >= 36:
1258-
block_info.BlkType = data[5]
1259-
block_info.BlkNumber = struct.unpack(">H", data[6:8])[0]
1260-
block_info.BlkLang = data[4]
1261-
block_info.MC7Size = struct.unpack(">I", data[8:12])[0]
1262-
block_info.LoadSize = struct.unpack(">I", data[12:16])[0]
1263-
block_info.SBBLength = struct.unpack(">I", data[28:32])[0]
1264-
block_info.CheckSum = struct.unpack(">H", data[32:34])[0]
1265-
block_info.Version = data[34]
1266-
block_info.CodeDate = b"2019/06/27"
1267-
block_info.IntfDate = b"2019/06/27"
1268-
1269-
return block_info
1270-
1271-
def set_session_password(self, password: str) -> int:
1272-
"""Set session password."""
1273-
self.session_password = password
1274-
return 0
1275-
1276-
def clear_session_password(self) -> int:
1277-
"""Clear session password."""
1278-
self.session_password = None
1279-
return 0
1280-
1281-
def set_connection_params(self, address: str, local_tsap: int, remote_tsap: int) -> None:
1282-
"""Set connection parameters."""
1283-
self.host = address
1284-
self.local_tsap = local_tsap
1285-
self.remote_tsap = remote_tsap
1286-
1287-
def set_connection_type(self, connection_type: int) -> None:
1288-
"""Set connection type (1=PG, 2=OP, 3=S7Basic)."""
1289-
self.connection_type = connection_type
1290-
1291-
def get_param(self, param: Parameter) -> int:
1292-
"""Get client parameter."""
1293-
non_client = [
1294-
Parameter.LocalPort, Parameter.WorkInterval, Parameter.MaxClients,
1295-
Parameter.BSendTimeout, Parameter.BRecvTimeout,
1296-
Parameter.RecoveryTime, Parameter.KeepAliveTime,
1297-
]
1298-
if param in non_client:
1299-
raise RuntimeError(f"Parameter {param} not valid for client")
1300-
if param == Parameter.SrcTSap:
1301-
return self.local_tsap
1302-
return self._params.get(param, 0)
1303-
1304-
def set_param(self, param: Parameter, value: int) -> int:
1305-
"""Set client parameter."""
1306-
if param == Parameter.RemotePort and self.connected:
1307-
raise RuntimeError("Cannot change RemotePort while connected")
1308-
if param == Parameter.PDURequest:
1309-
self.pdu_length = value
1310-
self._params[param] = value
1311-
return 0
1312-
13131216
# ---------------------------------------------------------------
13141217
# Internal helpers
13151218
# ---------------------------------------------------------------
@@ -1328,24 +1231,6 @@ async def _setup_communication(self) -> None:
13281231
self._params[Parameter.PDURequest] = self.pdu_length
13291232
logger.info(f"Negotiated PDU length: {self.pdu_length}")
13301233

1331-
def _max_read_size(self) -> int:
1332-
"""Maximum payload bytes for a single read request."""
1333-
return self.pdu_length - 18
1334-
1335-
def _max_write_size(self) -> int:
1336-
"""Maximum payload bytes for a single write request."""
1337-
return self.pdu_length - 35
1338-
1339-
def _map_area(self, area: Area) -> S7Area:
1340-
"""Map library area enum to native S7 area."""
1341-
area_mapping = {
1342-
Area.PE: S7Area.PE, Area.PA: S7Area.PA, Area.MK: S7Area.MK,
1343-
Area.DB: S7Area.DB, Area.CT: S7Area.CT, Area.TM: S7Area.TM,
1344-
}
1345-
if area not in area_mapping:
1346-
raise S7ProtocolError(f"Unsupported area: {area}")
1347-
return area_mapping[area]
1348-
13491234
# ---------------------------------------------------------------
13501235
# Context manager
13511236
# ---------------------------------------------------------------

0 commit comments

Comments
 (0)