|
16 | 16 | from .connection import TPDUSize |
17 | 17 | from .s7protocol import S7Protocol, get_return_code_description |
18 | 18 | from .datatypes import S7Area, S7WordLen |
19 | | -from .error import S7Error, S7ConnectionError, S7ProtocolError, S7StalePacketError, S7TimeoutError |
| 19 | +from .error import S7Error, S7ConnectionError, S7ProtocolError, S7TimeoutError |
20 | 20 |
|
21 | 21 |
|
22 | 22 | logger = logging.getLogger(__name__) |
@@ -295,6 +295,7 @@ def __init__(self) -> None: |
295 | 295 | self.local_tsap = 0x0100 |
296 | 296 | self.remote_tsap = 0x0102 |
297 | 297 | self.connection_type = 1 # PG |
| 298 | + self.session_password: Optional[str] = None |
298 | 299 |
|
299 | 300 | self._exec_time = 0 |
300 | 301 | self._last_error = 0 |
@@ -324,24 +325,38 @@ async def _send_receive(self, request: bytes, max_stale_retries: int = 3) -> dic |
324 | 325 |
|
325 | 326 | The lock ensures that concurrent coroutines never interleave |
326 | 327 | send/receive on the same TCP socket. |
| 328 | +
|
| 329 | + Unlike the sync client, we do NOT use protocol.validate_pdu_reference() |
| 330 | + because the protocol's shared sequence counter can be incremented by |
| 331 | + a concurrent coroutine between request building and lock acquisition. |
| 332 | + Instead, we extract the expected sequence directly from the request |
| 333 | + bytes (S7 header bytes 4-5). |
327 | 334 | """ |
328 | 335 | conn = self._get_connection() |
329 | 336 |
|
| 337 | + # Extract the sequence number we embedded in this request's S7 header. |
| 338 | + # S7 header: 0x32 | pdu_type | reserved(2) | sequence(2) | ... |
| 339 | + expected_seq = struct.unpack(">H", request[4:6])[0] |
| 340 | + |
330 | 341 | async with self._lock: |
331 | 342 | await conn.send_data(request) |
332 | 343 |
|
333 | 344 | for attempt in range(max_stale_retries + 1): |
334 | 345 | response_data = await conn.receive_data() |
335 | 346 | response = self.protocol.parse_response(response_data) |
336 | 347 |
|
337 | | - try: |
338 | | - self.protocol.validate_pdu_reference(response["sequence"]) |
| 348 | + resp_seq = response.get("sequence", 0) |
| 349 | + if resp_seq == expected_seq: |
339 | 350 | return response |
340 | | - except S7StalePacketError: |
341 | | - if attempt < max_stale_retries: |
342 | | - logger.warning(f"Stale packet (attempt {attempt + 1}/{max_stale_retries}), retrying receive") |
343 | | - continue |
344 | | - raise S7ProtocolError(f"Max stale packet retries ({max_stale_retries}) exceeded") |
| 351 | + |
| 352 | + # Stale packet — response is for an older request |
| 353 | + if attempt < max_stale_retries: |
| 354 | + logger.warning( |
| 355 | + f"Stale packet: expected seq {expected_seq}, got {resp_seq} " |
| 356 | + f"(attempt {attempt + 1}/{max_stale_retries}), retrying receive" |
| 357 | + ) |
| 358 | + continue |
| 359 | + raise S7ProtocolError(f"Max stale packet retries ({max_stale_retries}) exceeded") |
345 | 360 |
|
346 | 361 | raise S7ProtocolError("Failed to receive valid response") # Should not reach here |
347 | 362 |
|
@@ -1215,6 +1230,54 @@ def get_last_error(self) -> int: |
1215 | 1230 | """Get last error code.""" |
1216 | 1231 | return self._last_error |
1217 | 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 | + |
1218 | 1281 | def set_connection_params(self, address: str, local_tsap: int, remote_tsap: int) -> None: |
1219 | 1282 | """Set connection parameters.""" |
1220 | 1283 | self.host = address |
|
0 commit comments