python-snap7 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
snap7/error.py ADDED
@@ -0,0 +1,444 @@
1
+ """
2
+ S7 error handling and exception classes.
3
+
4
+ Maps S7 error codes to Python exceptions with meaningful messages.
5
+ """
6
+
7
+ from typing import Optional, Callable, Any, Hashable
8
+ from functools import cache
9
+
10
+
11
+ class S7Error(Exception):
12
+ """Base exception for all S7 protocol errors."""
13
+
14
+ def __init__(self, message: str, error_code: Optional[int] = None):
15
+ super().__init__(message)
16
+ self.error_code = error_code
17
+
18
+
19
+ class S7ConnectionError(S7Error):
20
+ """Raised when connection to S7 device fails."""
21
+
22
+ pass
23
+
24
+
25
+ class S7ProtocolError(S7Error):
26
+ """Raised when S7 protocol communication fails."""
27
+
28
+ pass
29
+
30
+
31
+ class S7TimeoutError(S7Error):
32
+ """Raised when S7 operation times out."""
33
+
34
+ pass
35
+
36
+
37
+ class S7AuthenticationError(S7Error):
38
+ """Raised when S7 authentication fails."""
39
+
40
+ pass
41
+
42
+
43
+ # S7 client error codes
44
+ s7_client_errors = {
45
+ 0x00100000: "errNegotiatingPDU",
46
+ 0x00200000: "errCliInvalidParams",
47
+ 0x00300000: "errCliJobPending",
48
+ 0x00400000: "errCliTooManyItems",
49
+ 0x00500000: "errCliInvalidWordLen",
50
+ 0x00600000: "errCliPartialDataWritten",
51
+ 0x00700000: "errCliSizeOverPDU",
52
+ 0x00800000: "errCliInvalidPlcAnswer",
53
+ 0x00900000: "errCliAddressOutOfRange",
54
+ 0x00A00000: "errCliInvalidTransportSize",
55
+ 0x00B00000: "errCliWriteDataSizeMismatch",
56
+ 0x00C00000: "errCliItemNotAvailable",
57
+ 0x00D00000: "errCliInvalidValue",
58
+ 0x00E00000: "errCliCannotStartPLC",
59
+ 0x00F00000: "errCliAlreadyRun",
60
+ 0x01000000: "errCliCannotStopPLC",
61
+ 0x01100000: "errCliCannotCopyRamToRom",
62
+ 0x01200000: "errCliCannotCompress",
63
+ 0x01300000: "errCliAlreadyStop",
64
+ 0x01400000: "errCliFunNotAvailable",
65
+ 0x01500000: "errCliUploadSequenceFailed",
66
+ 0x01600000: "errCliInvalidDataSizeRecvd",
67
+ 0x01700000: "errCliInvalidBlockType",
68
+ 0x01800000: "errCliInvalidBlockNumber",
69
+ 0x01900000: "errCliInvalidBlockSize",
70
+ 0x01A00000: "errCliDownloadSequenceFailed",
71
+ 0x01B00000: "errCliInsertRefused",
72
+ 0x01C00000: "errCliDeleteRefused",
73
+ 0x01D00000: "errCliNeedPassword",
74
+ 0x01E00000: "errCliInvalidPassword",
75
+ 0x01F00000: "errCliNoPasswordToSetOrClear",
76
+ 0x02000000: "errCliJobTimeout",
77
+ 0x02100000: "errCliPartialDataRead",
78
+ 0x02200000: "errCliBufferTooSmall",
79
+ 0x02300000: "errCliFunctionRefused",
80
+ 0x02400000: "errCliDestroying",
81
+ 0x02500000: "errCliInvalidParamNumber",
82
+ 0x02600000: "errCliCannotChangeParam",
83
+ }
84
+
85
+ isotcp_errors = {
86
+ 0x00010000: "errIsoConnect",
87
+ 0x00020000: "errIsoDisconnect",
88
+ 0x00030000: "errIsoInvalidPDU",
89
+ 0x00040000: "errIsoInvalidDataSize",
90
+ 0x00050000: "errIsoNullPointer",
91
+ 0x00060000: "errIsoShortPacket",
92
+ 0x00070000: "errIsoTooManyFragments",
93
+ 0x00080000: "errIsoPduOverflow",
94
+ 0x00090000: "errIsoSendPacket",
95
+ 0x000A0000: "errIsoRecvPacket",
96
+ 0x000B0000: "errIsoInvalidParams",
97
+ }
98
+
99
+ tcp_errors = {
100
+ 0x00000001: "evcServerStarted",
101
+ 0x00000002: "evcServerStopped",
102
+ 0x00000004: "evcListenerCannotStart",
103
+ 0x00000008: "evcClientAdded",
104
+ 0x00000010: "evcClientRejected",
105
+ 0x00000020: "evcClientNoRoom",
106
+ 0x00000040: "evcClientException",
107
+ 0x00000080: "evcClientDisconnected",
108
+ 0x00000100: "evcClientTerminated",
109
+ 0x00000200: "evcClientsDropped",
110
+ }
111
+
112
+ s7_server_errors = {
113
+ 0x00100000: "errSrvCannotStart",
114
+ 0x00200000: "errSrvDBNullPointer",
115
+ 0x00300000: "errSrvAreaAlreadyExists",
116
+ 0x00400000: "errSrvUnknownArea",
117
+ 0x00500000: "errSrvInvalidParams",
118
+ 0x00600000: "errSrvTooManyDB",
119
+ 0x00700000: "errSrvInvalidParamNumber",
120
+ 0x00800000: "errSrvCannotChangeParam",
121
+ }
122
+
123
+ # S7 protocol-level error codes (from Wireshark S7 dissector)
124
+ # These cover USERDATA parameter errors, protocol errors, and resource errors
125
+ # that occur during real PLC communication.
126
+ # Source: https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-s7comm.c
127
+ S7_PROTOCOL_ERROR_CODES = {
128
+ 0x0000: "No error",
129
+ 0x0110: "Invalid block number",
130
+ 0x0111: "Invalid request length",
131
+ 0x0112: "Invalid parameter",
132
+ 0x0113: "Invalid block type",
133
+ 0x0114: "Block not found",
134
+ 0x0115: "Block already exists",
135
+ 0x0116: "Block is write-protected",
136
+ 0x0117: "The block/operating system update is too large",
137
+ 0x0118: "Invalid block number",
138
+ 0x0119: "Incorrect password entered",
139
+ 0x011A: "PG resource error",
140
+ 0x011B: "PLC resource error",
141
+ 0x011C: "Protocol error",
142
+ 0x011D: "Too many blocks (module-related restriction)",
143
+ 0x011E: "There is no longer a connection to the database, or S7DOS handle is invalid",
144
+ 0x011F: "Result buffer too small",
145
+ 0x0120: "End of block list",
146
+ 0x0140: "Insufficient memory available",
147
+ 0x0141: "Job cannot be processed because of a lack of resources",
148
+ 0x8001: "The requested service cannot be performed while the block is in the current status",
149
+ 0x8003: "S7 protocol error: Error occurred while transferring the block",
150
+ 0x8100: "Application, general error: Service unknown to remote module",
151
+ 0x8104: "This service is not implemented on the module or a frame error was reported",
152
+ 0x8204: "The type specification for the object is inconsistent",
153
+ 0x8205: "A copied block already exists and is not linked",
154
+ 0x8301: "Insufficient memory space or work memory on the module, or specified storage medium not accessible",
155
+ 0x8302: "Too few resources available or the processor resources are not available",
156
+ 0x8304: "No further parallel upload possible. There is a resource bottleneck",
157
+ 0x8305: "Function not available",
158
+ 0x8306: "Insufficient work memory (for copying, linking, loading AWP)",
159
+ 0x8307: "Not enough retentive work memory (for copying, linking, loading AWP)",
160
+ 0x8401: "S7 protocol error: Invalid service sequence (for example, loading or uploading a block)",
161
+ 0x8402: "Service cannot execute owing to status of the addressed object",
162
+ 0x8404: "S7 protocol: The function cannot be performed",
163
+ 0x8405: "Remote block is in DISABLE state (CFB). The function cannot be performed",
164
+ 0x8500: "S7 protocol error: Wrong frames",
165
+ 0x8503: "Alarm from the module: Service canceled prematurely",
166
+ 0x8701: "Error addressing the object on the communications partner (for example, area length error)",
167
+ 0x8702: "The requested service is not supported by the module",
168
+ 0x8703: "Access to object refused",
169
+ 0x8704: "Access error: Object damaged",
170
+ 0xD001: "Protocol error: Illegal job number",
171
+ 0xD002: "Parameter error: Illegal job variant",
172
+ 0xD003: "Parameter error: Debugging function not supported by module",
173
+ 0xD004: "Parameter error: Illegal job status",
174
+ 0xD005: "Parameter error: Illegal job termination",
175
+ 0xD006: "Parameter error: Illegal link disconnection ID",
176
+ 0xD007: "Parameter error: Illegal number of buffer elements",
177
+ 0xD008: "Parameter error: Illegal scan rate",
178
+ 0xD009: "Parameter error: Illegal number of executions",
179
+ 0xD00A: "Parameter error: Illegal trigger event",
180
+ 0xD00B: "Parameter error: Illegal trigger condition",
181
+ 0xD011: "Parameter error in path of the call environment: Block does not exist",
182
+ 0xD012: "Parameter error: Wrong address in block",
183
+ 0xD014: "Parameter error: Block being deleted/overwritten",
184
+ 0xD015: "Parameter error: Illegal tag address",
185
+ 0xD016: "Parameter error: Test jobs not possible, because of errors in user program",
186
+ 0xD017: "Parameter error: Illegal trigger number",
187
+ 0xD025: "Parameter error: Invalid path",
188
+ 0xD026: "Parameter error: Illegal access type",
189
+ 0xD027: "Parameter error: This number of data blocks is not permitted",
190
+ 0xD031: "Internal protocol error",
191
+ 0xD032: "Parameter error: Wrong result buffer length",
192
+ 0xD033: "Protocol error: Wrong job length",
193
+ 0xD03F: "Coding error: Error in parameter section (for example, reserve bytes not equal to 0)",
194
+ 0xD041: "Data error: Illegal status list ID",
195
+ 0xD042: "Data error: Illegal tag address",
196
+ 0xD043: "Data error: Referenced job not found, check job data",
197
+ 0xD044: "Data error: Illegal tag value, check job data",
198
+ 0xD045: "Data error: Exiting the ODIS control is not allowed in HOLD",
199
+ 0xD046: "Data error: Illegal measuring stage during run-time measurement",
200
+ 0xD047: "Data error: Illegal hierarchy in 'Read job list'",
201
+ 0xD048: "Data error: Illegal deletion ID in 'Delete job'",
202
+ 0xD049: "Invalid substitute ID in 'Replace job'",
203
+ 0xD04A: "Error executing 'program status'",
204
+ 0xD05F: "Coding error: Error in data section (for example, reserve bytes not equal to 0, ...)",
205
+ 0xD061: "Resource error: No memory space for job",
206
+ 0xD062: "Resource error: Job list full",
207
+ 0xD063: "Resource error: Trigger event occupied",
208
+ 0xD064: "Resource error: Not enough memory space for one result buffer element",
209
+ 0xD065: "Resource error: Not enough memory space for several result buffer elements",
210
+ 0xD066: "Resource error: The timer available for run-time measurement is occupied by another job",
211
+ 0xD067: "Resource error: Too many 'modify tag' jobs active (in particular multi-processor operation)",
212
+ 0xD081: "Function not permitted in current mode",
213
+ 0xD082: "Mode error: Cannot exit HOLD mode",
214
+ 0xD0A1: "Function not permitted in current protection level",
215
+ 0xD0A2: "Function not possible at present, because a function is running that modifies memory",
216
+ 0xD0A3: "Too many 'modify tag' jobs active on the I/O (in particular multi-processor operation)",
217
+ 0xD0A4: "'Forcing' has already been established",
218
+ 0xD0A5: "Referenced job not found",
219
+ 0xD0A6: "Job cannot be disabled/enabled",
220
+ 0xD0A7: "Job cannot be deleted, for example because it is currently being read",
221
+ 0xD0A8: "Job cannot be replaced, for example because it is currently being read or deleted",
222
+ 0xD0A9: "Job cannot be read, for example because it is currently being deleted",
223
+ 0xD0AA: "Time limit exceeded in processing operation",
224
+ 0xD0AB: "Invalid job parameters in process operation",
225
+ 0xD0AC: "Invalid job data in process operation",
226
+ 0xD0AD: "Operating mode already set",
227
+ 0xD0AE: "The job was set up over a different connection and can only be handled over this connection",
228
+ 0xD0C1: "At least one error has been detected while accessing the tag(s)",
229
+ 0xD0C2: "Change to STOP/HOLD mode",
230
+ 0xD0C3: "At least one error was detected while accessing the tag(s). Mode change to STOP/HOLD",
231
+ 0xD0C4: "Timeout during run-time measurement",
232
+ 0xD0C5: "Display of block stack inconsistent, because blocks were deleted/reloaded",
233
+ 0xD0C6: "Job was automatically deleted as the jobs it referenced have been deleted",
234
+ 0xD0C7: "The job was automatically deleted because STOP mode was exited",
235
+ 0xD0C8: "'Block status' aborted because of inconsistencies between test job and running program",
236
+ 0xD0C9: "Exit the status area by resetting OB90",
237
+ 0xD0CA: "Exiting the status range by resetting OB90 and access error reading tags before exiting",
238
+ 0xD0CB: "The output disable for the peripheral outputs has been activated again",
239
+ 0xD0CC: "The amount of data for the debugging functions is restricted by the time limit",
240
+ 0xD201: "Syntax error in block name",
241
+ 0xD202: "Syntax error in function parameters",
242
+ 0xD205: "Linked block already exists in RAM: Conditional copying is not possible",
243
+ 0xD206: "Linked block already exists in EPROM: Conditional copying is not possible",
244
+ 0xD208: "Maximum number of copied (not linked) blocks on module exceeded",
245
+ 0xD209: "(At least) one of the given blocks not found on the module",
246
+ 0xD20A: "The maximum number of blocks that can be linked with one job was exceeded",
247
+ 0xD20B: "The maximum number of blocks that can be deleted with one job was exceeded",
248
+ 0xD20C: "OB cannot be copied because the associated priority class does not exist",
249
+ 0xD20D: "SDB cannot be interpreted (for example, unknown number)",
250
+ 0xD20E: "No (further) block available",
251
+ 0xD20F: "Module-specific maximum block size exceeded",
252
+ 0xD210: "Invalid block number",
253
+ 0xD212: "Incorrect header attribute (run-time relevant)",
254
+ 0xD213: "Too many SDBs. Note the restrictions on the module being used",
255
+ 0xD216: "Invalid user program - reset module",
256
+ 0xD217: "Protection level specified in module properties not permitted",
257
+ 0xD218: "Incorrect attribute (active/passive)",
258
+ 0xD219: "Incorrect block lengths (for example, incorrect length of first section or of the whole block)",
259
+ 0xD21A: "Incorrect local data length or write-protection code faulty",
260
+ 0xD21B: "Module cannot compress or compression was interrupted early",
261
+ 0xD21D: "The volume of dynamic project data transferred is illegal",
262
+ 0xD21E: "Unable to assign parameters to a module (such as FM, CP). The system data could not be linked",
263
+ 0xD220: "Invalid programming language. Note the restrictions on the module being used",
264
+ 0xD221: "The system data for connections or routing are not valid",
265
+ 0xD222: "The system data of the global data definition contain invalid parameters",
266
+ 0xD223: "Error in instance data block for communication function block or maximum number of instance DBs exceeded",
267
+ 0xD224: "The SCAN system data block contains invalid parameters",
268
+ 0xD225: "The DP system data block contains invalid parameters",
269
+ 0xD226: "A structural error occurred in a block",
270
+ 0xD230: "A structural error occurred in a block",
271
+ 0xD231: "At least one loaded OB cannot be copied because the associated priority class does not exist",
272
+ 0xD232: "At least one block number of a loaded block is illegal",
273
+ 0xD234: "Block exists twice in the specified memory medium or in the job",
274
+ 0xD235: "The block contains an incorrect checksum",
275
+ 0xD236: "The block does not contain a checksum",
276
+ 0xD237: "You are about to load the block twice, i.e. a block with the same time stamp already exists on the CPU",
277
+ 0xD238: "At least one of the blocks specified is not a DB",
278
+ 0xD239: "At least one of the DBs specified is not available as a linked variant in the load memory",
279
+ 0xD23A: "At least one of the specified DBs is considerably different from the copied and linked variant",
280
+ 0xD240: "Coordination rules violated",
281
+ 0xD241: "The function is not permitted in the current protection level",
282
+ 0xD242: "Protection violation while processing F blocks",
283
+ 0xD250: "Update and module ID or version do not match",
284
+ 0xD251: "Incorrect sequence of operating system components",
285
+ 0xD252: "Checksum error",
286
+ 0xD253: "No executable loader available; update only possible using a memory card",
287
+ 0xD254: "Storage error in operating system",
288
+ 0xD280: "Error compiling block in S7-300 CPU",
289
+ 0xD2A1: "Another block function or a trigger on a block is active",
290
+ 0xD2A2: "A trigger is active on a block. Complete the debugging function first",
291
+ 0xD2A3: "The block is not active (linked), the block is occupied or the block is currently marked for deletion",
292
+ 0xD2A4: "The block is already being processed by another block function",
293
+ 0xD2A6: "It is not possible to save and change the user program simultaneously",
294
+ 0xD2A7: "The block has the attribute 'unlinked' or is not processed",
295
+ 0xD2A8: "An active debugging function is preventing parameters from being assigned to the CPU",
296
+ 0xD2A9: "New parameters are being assigned to the CPU",
297
+ 0xD2AA: "New parameters are currently being assigned to the modules",
298
+ 0xD2AB: "The dynamic configuration limits are currently being changed",
299
+ 0xD2AC: "A running active or deactivate assignment (SFC 12) is temporarily preventing R-KiR process",
300
+ 0xD2B0: "An error occurred while configuring in RUN (CiR)",
301
+ 0xD2C0: "The maximum number of technological objects has been exceeded",
302
+ 0xD2C1: "The same technology data block already exists on the module",
303
+ 0xD2C2: "Downloading the user program or downloading the hardware configuration is not possible",
304
+ 0xD401: "Information function unavailable",
305
+ 0xD402: "Information function unavailable",
306
+ 0xD403: "Service has already been logged on/off (Diagnostics/PMC)",
307
+ 0xD404: "Maximum number of nodes reached. No more logons possible for diagnostics/PMC",
308
+ 0xD405: "Service not supported or syntax error in function parameters",
309
+ 0xD406: "Required information currently unavailable",
310
+ 0xD407: "Diagnostics error occurred",
311
+ 0xD408: "Update aborted",
312
+ 0xD409: "Error on DP bus",
313
+ 0xD601: "Syntax error in function parameter",
314
+ 0xD602: "Incorrect password entered",
315
+ 0xD603: "The connection has already been legitimized",
316
+ 0xD604: "The connection has already been enabled",
317
+ 0xD605: "Legitimization not possible because password does not exist",
318
+ 0xD801: "At least one tag address is invalid",
319
+ 0xD802: "Specified job does not exist",
320
+ 0xD803: "Illegal job status",
321
+ 0xD804: "Illegal cycle time (illegal time base or multiple)",
322
+ 0xD805: "No more cyclic read jobs can be set up",
323
+ 0xD806: "The referenced job is in a state in which the requested function cannot be performed",
324
+ 0xD807: "Function aborted due to overload, meaning executing the read cycle takes longer than the set scan cycle time",
325
+ 0xDC01: "Date and/or time invalid",
326
+ 0xE201: "CPU is already the master",
327
+ 0xE202: "Connect and update not possible due to different user program in flash module",
328
+ 0xE203: "Connect and update not possible due to different firmware",
329
+ 0xE204: "Connect and update not possible due to different memory configuration",
330
+ 0xE205: "Connect/update aborted due to synchronization error",
331
+ 0xE206: "Connect/update denied due to coordination violation",
332
+ 0xEF01: "S7 protocol error: Error at ID2; only 00H permitted in job",
333
+ 0xEF02: "S7 protocol error: Error at ID2; set of resources does not exist",
334
+ }
335
+
336
+
337
+ def get_protocol_error_message(code: int) -> str:
338
+ """Get human-readable error message for an S7 protocol-level error code.
339
+
340
+ These are the error codes found in USERDATA parameter sections and
341
+ S7 header error_class/error_code fields. They are distinct from the
342
+ higher-level client/ISO error codes.
343
+
344
+ Args:
345
+ code: S7 protocol error code (e.g., 0xD401, 0x8104)
346
+
347
+ Returns:
348
+ Human-readable error description, or a hex-formatted unknown message.
349
+ """
350
+ return S7_PROTOCOL_ERROR_CODES.get(code, f"Unknown protocol error: {code:#06x}")
351
+
352
+
353
+ class S7StalePacketError(S7ProtocolError):
354
+ """Raised when a response has an old/stale PDU reference number."""
355
+
356
+ pass
357
+
358
+
359
+ class S7PacketLostError(S7ProtocolError):
360
+ """Raised when a response PDU reference is ahead of expected (packet loss)."""
361
+
362
+ pass
363
+
364
+
365
+ # Combined error dictionaries
366
+ client_errors = s7_client_errors.copy()
367
+ client_errors.update(isotcp_errors)
368
+ client_errors.update(tcp_errors)
369
+
370
+ server_errors = s7_server_errors.copy()
371
+ server_errors.update(isotcp_errors)
372
+ server_errors.update(tcp_errors)
373
+
374
+ # All error codes combined
375
+ S7_ERROR_CODES = {
376
+ 0x00000000: "Success",
377
+ **s7_client_errors,
378
+ **isotcp_errors,
379
+ **s7_server_errors,
380
+ }
381
+
382
+
383
+ def get_error_message(error_code: int) -> str:
384
+ """Get human-readable error message for S7 error code."""
385
+ return S7_ERROR_CODES.get(error_code, f"Unknown error: {error_code:#08x}")
386
+
387
+
388
+ @cache
389
+ def error_text(error: int, context: str = "client") -> str:
390
+ """Returns a textual explanation of a given error number.
391
+
392
+ Args:
393
+ error: an error integer
394
+ context: context in which is called from, server, client or partner
395
+
396
+ Returns:
397
+ The error message as a string.
398
+ """
399
+ errors = {"client": client_errors, "server": server_errors, "partner": client_errors}
400
+ error_dict = errors.get(context, client_errors)
401
+ return error_dict.get(error, f"Unknown error: {error:#08x}")
402
+
403
+
404
+ def check_error(code: int, context: str = "client") -> None:
405
+ """Check if the error code is set. If so, raise an appropriate exception.
406
+
407
+ Args:
408
+ code: error code number.
409
+ context: context in which is called.
410
+
411
+ Raises:
412
+ S7ConnectionError: for connection-related errors
413
+ S7TimeoutError: for timeout errors
414
+ S7ProtocolError: for protocol errors
415
+ RuntimeError: for other errors (backwards compatibility)
416
+ """
417
+ if code == 0:
418
+ return
419
+
420
+ message = error_text(code, context)
421
+
422
+ # Map to specific exception types based on error code patterns
423
+ if code in [0x00010000, 0x00020000]: # ISO connect/disconnect errors
424
+ raise S7ConnectionError(message, code)
425
+ elif code == 0x02000000: # Job timeout
426
+ raise S7TimeoutError(message, code)
427
+ elif code in isotcp_errors:
428
+ raise S7ConnectionError(message, code)
429
+ else:
430
+ # Use RuntimeError for backwards compatibility with existing code
431
+ raise RuntimeError(message)
432
+
433
+
434
+ def error_wrap(context: str) -> Callable[..., Callable[..., None]]:
435
+ """Decorator that parses an S7 error code returned by the decorated function."""
436
+
437
+ def middle(func: Callable[..., int]) -> Any:
438
+ def inner(*args: tuple[Any, ...], **kwargs: dict[Hashable, Any]) -> None:
439
+ code = func(*args, **kwargs)
440
+ check_error(code, context=context)
441
+
442
+ return inner
443
+
444
+ return middle
snap7/logo.py ADDED
@@ -0,0 +1,222 @@
1
+ """
2
+ Snap7 client used for connection to a Siemens LOGO 7/8 server.
3
+
4
+ Pure Python implementation without C library dependency.
5
+ """
6
+
7
+ import re
8
+ import struct
9
+ import logging
10
+ from typing import Optional
11
+
12
+ from .type import WordLen, Area
13
+ from .client import Client
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def parse_address(vm_address: str) -> tuple[int, WordLen]:
19
+ """
20
+ Parse VM address string to start address and word length.
21
+
22
+ Args:
23
+ vm_address: Logo VM address (e.g. "V10", "VW20", "V10.3")
24
+
25
+ Returns:
26
+ Tuple of (start_address, word_length)
27
+ """
28
+ logger.debug(f"read, vm_address:{vm_address}")
29
+ if re.match(r"V[0-9]{1,4}\.[0-7]", vm_address):
30
+ logger.info(f"read, Bit address: {vm_address}")
31
+ address = vm_address[1:].split(".")
32
+ # transform string to int
33
+ address_byte = int(address[0])
34
+ address_bit = int(address[1])
35
+ start = (address_byte * 8) + address_bit
36
+ return start, WordLen.Bit
37
+ elif re.match("V[0-9]+", vm_address):
38
+ # byte value
39
+ logger.info(f"Byte address: {vm_address}")
40
+ start = int(vm_address[1:])
41
+ return start, WordLen.Byte
42
+ elif re.match("VW[0-9]+", vm_address):
43
+ # byte value
44
+ logger.info(f"Word address: {vm_address}")
45
+ start = int(vm_address[2:])
46
+ return start, WordLen.Word
47
+ elif re.match("VD[0-9]+", vm_address):
48
+ # byte value
49
+ logger.info(f"DWord address: {vm_address}")
50
+ start = int(vm_address[2:])
51
+ return start, WordLen.DWord
52
+ else:
53
+ raise ValueError("Unknown address format")
54
+
55
+
56
+ class Logo(Client):
57
+ """
58
+ A snap7 Siemens Logo client.
59
+
60
+ There are two main comfort functions available: :func:`Logo.read` and :func:`Logo.write`.
61
+ This function offers high-level access to the VM addresses of the Siemens Logo just use the form:
62
+
63
+ Notes:
64
+ V10.3 for bit values
65
+ V10 for the complete byte
66
+ VW12 for a word (used for analog values)
67
+ For more information see examples for Siemens Logo 7 and 8
68
+ """
69
+
70
+ def __init__(self, **kwargs: object) -> None:
71
+ """
72
+ Initialize Logo client.
73
+
74
+ Args:
75
+ **kwargs: Ignored. Kept for backwards compatibility.
76
+ """
77
+ super().__init__()
78
+ self._logo_tsap_snap7: Optional[int] = None
79
+ self._logo_tsap_logo: Optional[int] = None
80
+
81
+ def connect(self, ip_address: str, tsap_snap7: int, tsap_logo: int, tcp_port: int = 102) -> "Logo":
82
+ """Connect to a Siemens LOGO server.
83
+
84
+ Notes:
85
+ Howto setup Logo communication configuration see: https://snap7.sourceforge.net/logo.html
86
+
87
+ Args:
88
+ ip_address: IP ip_address of server
89
+ tsap_snap7: TSAP SNAP7 Client (e.g. 10.00 = 0x1000)
90
+ tsap_logo: TSAP Logo Server (e.g. 20.00 = 0x2000)
91
+ tcp_port: TCP port of server
92
+
93
+ Returns:
94
+ The snap7 Logo instance
95
+ """
96
+ logger.info(f"connecting to {ip_address}:{tcp_port} tsap_snap7 {tsap_snap7} tsap_logo {tsap_logo}")
97
+
98
+ # Store TSAP values for connection
99
+ self._logo_tsap_snap7 = tsap_snap7
100
+ self._logo_tsap_logo = tsap_logo
101
+
102
+ # Set connection parameters
103
+ self.local_tsap = tsap_snap7
104
+ self.remote_tsap = tsap_logo
105
+ self.host = ip_address
106
+ self.port = tcp_port
107
+
108
+ # Connect using parent Client implementation
109
+ # For Logo, rack and slot are not used in the standard way
110
+ # but we still need to establish the connection
111
+ super().connect(ip_address, 0, 0, tcp_port)
112
+
113
+ return self
114
+
115
+ def read(self, vm_address: str) -> int:
116
+ """Reads from VM addresses of Siemens Logo.
117
+
118
+ Examples:
119
+ read("V40") / read("VW64") / read("V10.2")
120
+
121
+ Args:
122
+ vm_address: of Logo memory (e.g. V30.1, VW32, V24)
123
+
124
+ Returns:
125
+ integer
126
+ """
127
+ db_number = 1
128
+ logger.debug(f"read, vm_address:{vm_address}")
129
+ start, wordlen = parse_address(vm_address)
130
+
131
+ # Determine size based on word length
132
+ if wordlen == WordLen.Bit:
133
+ size = 1
134
+ elif wordlen == WordLen.Byte:
135
+ size = 1
136
+ elif wordlen == WordLen.Word:
137
+ size = 2
138
+ elif wordlen == WordLen.DWord:
139
+ size = 4
140
+ else:
141
+ size = 1
142
+
143
+ logger.debug(f"start:{start}, wordlen:{wordlen.name}={wordlen}, size:{size}")
144
+
145
+ # For bit access, we need to handle start address differently
146
+ if wordlen == WordLen.Bit:
147
+ # For Logo, bit access uses byte.bit notation converted to bit offset
148
+ # Read the byte containing the bit
149
+ byte_addr = start // 8
150
+ bit_offset = start % 8
151
+ data = self.read_area(Area.DB, db_number, byte_addr, 1)
152
+ # Extract the bit
153
+ result = (data[0] >> bit_offset) & 0x01
154
+ else:
155
+ # Read the appropriate number of bytes
156
+ data = self.read_area(Area.DB, db_number, start, size)
157
+
158
+ # Convert to integer based on word length
159
+ if wordlen == WordLen.Byte:
160
+ result = struct.unpack_from(">B", data)[0]
161
+ elif wordlen == WordLen.Word:
162
+ result = struct.unpack_from(">h", data)[0]
163
+ elif wordlen == WordLen.DWord:
164
+ result = struct.unpack_from(">l", data)[0]
165
+ else:
166
+ result = data[0]
167
+
168
+ return result
169
+
170
+ def write(self, vm_address: str, value: int) -> int:
171
+ """Writes to VM addresses of Siemens Logo.
172
+
173
+ Args:
174
+ vm_address: write offset
175
+ value: integer
176
+
177
+ Returns:
178
+ 0 on success
179
+
180
+ Examples:
181
+ >>> Logo().write("VW10", 200) or Logo().write("V10.3", 1)
182
+ """
183
+ db_number = 1
184
+ start, wordlen = parse_address(vm_address)
185
+
186
+ logger.debug(f"write, vm_address:{vm_address} value:{value}")
187
+
188
+ if wordlen == WordLen.Bit:
189
+ # For bit access, read-modify-write
190
+ byte_addr = start // 8
191
+ bit_offset = start % 8
192
+
193
+ # Read the current byte
194
+ current = self.read_area(Area.DB, db_number, byte_addr, 1)
195
+ byte_val = current[0]
196
+
197
+ # Modify the bit
198
+ if value > 0:
199
+ byte_val |= 1 << bit_offset # Set bit
200
+ else:
201
+ byte_val &= ~(1 << bit_offset) # Clear bit
202
+
203
+ # Write back
204
+ data = bytearray([byte_val])
205
+ self.write_area(Area.DB, db_number, byte_addr, data)
206
+
207
+ elif wordlen == WordLen.Byte:
208
+ data = bytearray(struct.pack(">B", value))
209
+ self.write_area(Area.DB, db_number, start, data)
210
+
211
+ elif wordlen == WordLen.Word:
212
+ data = bytearray(struct.pack(">h", value))
213
+ self.write_area(Area.DB, db_number, start, data)
214
+
215
+ elif wordlen == WordLen.DWord:
216
+ data = bytearray(struct.pack(">l", value))
217
+ self.write_area(Area.DB, db_number, start, data)
218
+
219
+ else:
220
+ raise ValueError(f"Unknown wordlen {wordlen}")
221
+
222
+ return 0