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/util/setters.py ADDED
@@ -0,0 +1,733 @@
1
+ import re
2
+ import struct
3
+ from datetime import date, datetime, timedelta
4
+ from typing import Union
5
+
6
+ from .getters import get_bool
7
+
8
+
9
+ def set_bool(bytearray_: bytearray, byte_index: int, bool_index: int, value: bool) -> bytearray:
10
+ """Set boolean value on location in bytearray.
11
+
12
+ Args:
13
+ bytearray_: buffer to write to.
14
+ byte_index: byte index to write to.
15
+ bool_index: bit index to write to.
16
+ value: value to write.
17
+
18
+ Examples:
19
+ >>> buffer = bytearray([0b00000000])
20
+ >>> set_bool(buffer, 0, 0, True)
21
+ >>> buffer
22
+ bytearray(b"\\x01")
23
+ """
24
+ if value not in {0, 1, True, False}:
25
+ raise TypeError(f"Value value:{value} is not a boolean expression.")
26
+
27
+ current_value = get_bool(bytearray_, byte_index, bool_index)
28
+ index_value = 1 << bool_index
29
+
30
+ # check if bool already has correct value
31
+ if current_value == value:
32
+ return bytearray_
33
+
34
+ if value:
35
+ # make sure index_v is IN current byte
36
+ bytearray_[byte_index] += index_value
37
+ else:
38
+ # make sure index_v is NOT in current byte
39
+ bytearray_[byte_index] -= index_value
40
+ return bytearray_
41
+
42
+
43
+ def set_byte(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray:
44
+ """Set value in bytearray to byte
45
+
46
+ Args:
47
+ bytearray_: buffer to write to.
48
+ byte_index: byte index to write.
49
+ _int: value to write.
50
+
51
+ Returns:
52
+ buffer with the written value.
53
+
54
+ Examples:
55
+ >>> buffer = bytearray([0b00000000])
56
+ >>> set_byte(buffer, 0, 255)
57
+ bytearray(b"\\xFF")
58
+ """
59
+ _int = int(_int)
60
+ bytearray_[byte_index : byte_index + 1] = struct.pack("B", _int)
61
+ return bytearray_
62
+
63
+
64
+ def set_word(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray:
65
+ """Set value in bytearray to word
66
+
67
+ Notes:
68
+ Word datatype is 2 bytes long.
69
+
70
+ Args:
71
+ bytearray_: buffer to be written.
72
+ byte_index: byte index to start write from.
73
+ _int: value to write.
74
+
75
+ Return:
76
+ buffer with the written value
77
+ """
78
+ _int = int(_int)
79
+ bytearray_[byte_index : byte_index + 2] = struct.pack(">H", _int)
80
+ return bytearray_
81
+
82
+
83
+ def set_int(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray:
84
+ """Set value in bytearray to int
85
+
86
+ Notes:
87
+ An datatype `int` in the PLC consists of two `bytes`.
88
+
89
+ Args:
90
+ bytearray_: buffer to write on.
91
+ byte_index: byte index to start writing from.
92
+ _int: int value to write.
93
+
94
+ Returns:
95
+ Buffer with the written value.
96
+
97
+ Examples:
98
+ >>> data = bytearray(2)
99
+ >>> set_int(data, 0, 255)
100
+ bytearray(b'\\x00\\xff')
101
+ """
102
+ # make sure were dealing with an int
103
+ _int = int(_int)
104
+ bytearray_[byte_index : byte_index + 2] = struct.pack(">h", _int)
105
+ return bytearray_
106
+
107
+
108
+ def set_uint(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray:
109
+ """Set value in bytearray to unsigned int
110
+
111
+ Notes:
112
+ An datatype `uint` in the PLC consists of two `bytes`.
113
+
114
+ Args:
115
+ bytearray_: buffer to write on.
116
+ byte_index: byte index to start writing from.
117
+ _int: int value to write.
118
+
119
+ Returns:
120
+ Buffer with the written value.
121
+
122
+ Examples:
123
+ >>> from snap7.util import set_uint
124
+ >>> data = bytearray(2)
125
+ >>> set_uint(data, 0, 65535)
126
+ bytearray(b'\\xff\\xff')
127
+ """
128
+ # make sure were dealing with an int
129
+ _int = int(_int)
130
+ bytearray_[byte_index : byte_index + 2] = struct.pack(">H", _int)
131
+ return bytearray_
132
+
133
+
134
+ def set_real(bytearray_: bytearray, byte_index: int, real: Union[bool, str, float, int]) -> bytearray:
135
+ """Set Real value
136
+
137
+ Notes:
138
+ Datatype `real` is represented in 4 bytes in the PLC.
139
+ The packed representation uses the `IEEE 754 binary32`.
140
+
141
+ Args:
142
+ bytearray_: buffer to write to.
143
+ byte_index: byte index to start writing from.
144
+ real: value to be written.
145
+
146
+ Returns:
147
+ Buffer with the value written.
148
+
149
+ Examples:
150
+ >>> data = bytearray(4)
151
+ >>> set_real(data, 0, 123.321)
152
+ bytearray(b'B\\xf6\\xa4Z')
153
+ """
154
+ bytearray_[byte_index : byte_index + 4] = struct.pack(">f", float(real))
155
+ return bytearray_
156
+
157
+
158
+ def set_fstring(bytearray_: bytearray, byte_index: int, value: str, max_length: int) -> bytearray:
159
+ """Set space-padded fixed-length string value
160
+
161
+ Args:
162
+ bytearray_: buffer to write to.
163
+ byte_index: byte index to start writing from.
164
+ value: string to write.
165
+ max_length: maximum string length, i.e. the fixed size of the string.
166
+
167
+ Raises:
168
+ :obj:`TypeError`: if the `value` is not a :obj:`str`.
169
+ :obj:`ValueError`: if the length of the `value` is larger than the `max_size`
170
+ or 'value' contains non-ascii characters.
171
+
172
+ Examples:
173
+ >>> data = bytearray(20)
174
+ >>> set_fstring(data, 0, "hello world", 15)
175
+ >>> data
176
+ bytearray(b'hello world \x00\x00\x00\x00\x00')
177
+ """
178
+ if not value.isascii():
179
+ raise ValueError("Value contains non-ascii values.")
180
+ # FAIL HARD WHEN trying to write too much data into PLC
181
+ size = len(value)
182
+ if size > max_length:
183
+ raise ValueError(f"size {size} > max_length {max_length} {value}")
184
+
185
+ # fill array with chr integers
186
+ for i, c in enumerate(value):
187
+ bytearray_[byte_index + i] = ord(c)
188
+
189
+ # fill the rest with empty space
190
+ for r in range(len(value), max_length):
191
+ bytearray_[byte_index + r] = ord(" ")
192
+
193
+ return bytearray_
194
+
195
+
196
+ def set_string(bytearray_: bytearray, byte_index: int, value: str, max_size: int = 254) -> bytearray:
197
+ """Set string value
198
+
199
+ Args:
200
+ bytearray_: buffer to write to.
201
+ byte_index: byte index to start writing from.
202
+ value: string to write.
203
+ max_size: maximum possible string size, max. 254 as default.
204
+
205
+ Raises:
206
+ :obj:`TypeError`: if the `value` is not a :obj:`str`.
207
+ :obj:`ValueError`: if the length of the `value` is larger than the `max_size`
208
+ or 'max_size' is greater than 254 or 'value' contains ascii characters > 255.
209
+
210
+ Examples:
211
+ >>> from snap7.util import set_string
212
+ >>> data = bytearray(20)
213
+ >>> set_string(data, 0, "hello world", 254)
214
+ >>> data
215
+ bytearray(b'\\xff\\x0bhello world\\x00\\x00\\x00\\x00\\x00\\x00\\x00')
216
+ """
217
+ if not isinstance(value, str):
218
+ raise TypeError(f"Value value:{value} is not from Type string")
219
+
220
+ if max_size > 254:
221
+ raise ValueError(f"max_size: {max_size} > max. allowed 254 chars")
222
+
223
+ if any(ord(char) < 0 or ord(char) > 255 for char in value):
224
+ raise ValueError(
225
+ "Value contains ascii values > 255, which is not compatible with PLC Type STRING. "
226
+ "Check encoding of value or try set_wstring()."
227
+ )
228
+
229
+ size = len(value)
230
+ # FAIL HARD WHEN trying to write too much data into PLC
231
+ if size > max_size:
232
+ raise ValueError(f"size {size} > max_size {max_size} {value}")
233
+
234
+ # set max string size
235
+ bytearray_[byte_index] = max_size
236
+
237
+ # set len count on first position
238
+ bytearray_[byte_index + 1] = len(value)
239
+
240
+ # fill array with chr integers
241
+ for i, c in enumerate(value):
242
+ bytearray_[byte_index + 2 + i] = ord(c)
243
+
244
+ # fill the rest with empty space
245
+ for r in range(len(value), bytearray_[byte_index]):
246
+ bytearray_[byte_index + 2 + r] = ord(" ")
247
+
248
+ return bytearray_
249
+
250
+
251
+ def set_dword(bytearray_: bytearray, byte_index: int, dword: int) -> bytearray:
252
+ """Set a DWORD to the buffer.
253
+
254
+ Notes:
255
+ Datatype `dword` consists in 8 bytes in the PLC.
256
+ The maximum value posible is `4294967295`
257
+
258
+ Args:
259
+ bytearray_: buffer to write to.
260
+ byte_index: byte index from where to write.
261
+ dword: value to write.
262
+
263
+ Examples:
264
+ >>> data = bytearray(4)
265
+ >>> set_dword(data,0, 4294967295)
266
+ >>> data
267
+ bytearray(b'\\xff\\xff\\xff\\xff')
268
+ """
269
+ dword = int(dword)
270
+ bytearray_[byte_index : byte_index + 4] = struct.pack(">I", dword)
271
+ return bytearray_
272
+
273
+
274
+ def set_dint(bytearray_: bytearray, byte_index: int, dint: int) -> bytearray:
275
+ """Set value in bytearray to dint
276
+
277
+ Notes:
278
+ Datatype `dint` consists in 4 bytes in the PLC.
279
+ Maximum possible value is 2147483647.
280
+ Lower posible value is -2147483648.
281
+
282
+ Args:
283
+ bytearray_: buffer to write.
284
+ byte_index: byte index from where to start writing.
285
+ dint: double integer value
286
+
287
+ Examples:
288
+ >>> data = bytearray(4)
289
+ >>> set_dint(data, 0, 2147483647)
290
+ >>> data
291
+ bytearray(b'\\x7f\\xff\\xff\\xff')
292
+ """
293
+ dint = int(dint)
294
+ bytearray_[byte_index : byte_index + 4] = struct.pack(">i", dint)
295
+ return bytearray_
296
+
297
+
298
+ def set_udint(bytearray_: bytearray, byte_index: int, udint: int) -> bytearray:
299
+ """Set value in bytearray to unsigned dint
300
+
301
+ Notes:
302
+ Datatype `dint` consists in 4 bytes in the PLC.
303
+ Maximum possible value is 4294967295.
304
+ Minimum posible value is 0.
305
+
306
+ Args:
307
+ bytearray_: buffer to write.
308
+ byte_index: byte index from where to start writing.
309
+ udint: unsigned double integer value
310
+
311
+ Examples:
312
+ >>> data = bytearray(4)
313
+ >>> set_udint(data, 0, 4294967295)
314
+ >>> data
315
+ bytearray(b'\\xff\\xff\\xff\\xff')
316
+ """
317
+ udint = int(udint)
318
+ bytearray_[byte_index : byte_index + 4] = struct.pack(">I", udint)
319
+ return bytearray_
320
+
321
+
322
+ def set_time(bytearray_: bytearray, byte_index: int, time_string: str) -> bytearray:
323
+ """Set value in bytearray to time
324
+
325
+ Notes:
326
+ Datatype `time` consists in 4 bytes in the PLC.
327
+ Maximum possible value is T#24D_20H_31M_23S_647MS(2147483647).
328
+ Lower posible value is T#-24D_20H_31M_23S_648MS(-2147483648).
329
+
330
+ Args:
331
+ bytearray_: buffer to write.
332
+ byte_index: byte index from where to start writing.
333
+ time_string: time value in string
334
+
335
+ Examples:
336
+ >>> data = bytearray(4)
337
+
338
+ >>> set_time(data, 0, '-22:3:57:28.192')
339
+
340
+ >>> data
341
+ bytearray(b'\x8d\xda\xaf\x00')
342
+ """
343
+ sign = 1
344
+ if re.fullmatch(
345
+ r"(-?(2[0-3]|1?\d):(2[0-3]|1?\d|\d):([1-5]?\d):[1-5]?\d.\d{1,3})|"
346
+ r"(-24:(20|1?\d):(3[0-1]|[0-2]?\d):(2[0-3]|1?\d).(64[0-8]|6[0-3]\d|[0-5]\d{1,2}))|"
347
+ r"(24:(20|1?\d):(3[0-1]|[0-2]?\d):(2[0-3]|1?\d).(64[0-7]|6[0-3]\d|[0-5]\d{1,2}))",
348
+ time_string,
349
+ ):
350
+ data_list = re.split("[: .]", time_string)
351
+ days: str = data_list[0]
352
+ hours: int = int(data_list[1])
353
+ minutes: int = int(data_list[2])
354
+ seconds: int = int(data_list[3])
355
+ milli_seconds: int = int(data_list[4].ljust(3, "0"))
356
+ if re.match(r"^-\d{1,2}$", days):
357
+ sign = -1
358
+
359
+ time_int = (
360
+ (int(days) * sign * 3600 * 24 + (hours % 24) * 3600 + (minutes % 60) * 60 + seconds % 60) * 1000 + milli_seconds
361
+ ) * sign
362
+ bytes_array = time_int.to_bytes(4, byteorder="big", signed=True)
363
+ bytearray_[byte_index : byte_index + 4] = bytes_array
364
+ return bytearray_
365
+ else:
366
+ raise ValueError("time value out of range, please check the value interval")
367
+
368
+
369
+ def set_usint(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray:
370
+ """Set unsigned small int
371
+
372
+ Notes:
373
+ Datatype `usint` (Unsigned small int) consists on 1 byte in the PLC.
374
+ Maximum posible value is 255.
375
+ Lower posible value is 0.
376
+
377
+ Args:
378
+ bytearray_: buffer to write.
379
+ byte_index: byte index from where to start writing.
380
+ _int: value to write.
381
+
382
+ Returns:
383
+ Buffer with the written value.
384
+
385
+ Examples:
386
+ >>> data = bytearray(1)
387
+ >>> set_usint(data, 0, 255)
388
+ bytearray(b'\\xff')
389
+ """
390
+ _int = int(_int)
391
+ bytearray_[byte_index] = struct.pack(">B", _int)[0]
392
+ return bytearray_
393
+
394
+
395
+ def set_sint(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray:
396
+ """Set small int to the buffer.
397
+
398
+ Notes:
399
+ Datatype `sint` (Small int) consists in 1 byte in the PLC.
400
+ Maximum value posible is 127.
401
+ Lowest value posible is -128.
402
+
403
+ Args:
404
+ bytearray_: buffer to write to.
405
+ byte_index: byte index from where to start writing.
406
+ _int: value to write.
407
+
408
+ Returns:
409
+ Buffer with the written value.
410
+
411
+ Examples:
412
+ >>> data = bytearray(1)
413
+ >>> set_sint(data, 0, 127)
414
+ bytearray(b'\\x7f')
415
+ """
416
+ _int = int(_int)
417
+ bytearray_[byte_index] = struct.pack(">b", _int)[0]
418
+ return bytearray_
419
+
420
+
421
+ def set_lreal(bytearray_: bytearray, byte_index: int, lreal: float) -> bytearray:
422
+ """Set the long real
423
+
424
+ Notes:
425
+ Datatype `lreal` (long real) consists in 8 bytes in the PLC.
426
+ Negative Range: -1.7976931348623158e+308 to -2.2250738585072014e-308
427
+ Positive Range: +2.2250738585072014e-308 to +1.7976931348623158e+308
428
+ Zero: ±0
429
+
430
+ Args:
431
+ bytearray_: buffer to read from.
432
+ byte_index: byte index from where to start reading.
433
+ lreal: float value to set
434
+
435
+ Returns:
436
+ Value to write.
437
+
438
+ Examples:
439
+ write lreal value (here as example 12345.12345) to DB1.10 of a PLC
440
+ >>> data = set_lreal(data, 12345.12345)
441
+ >>> from snap7 import Client
442
+ >>> Client().db_write(db_number=1, start=10, data=data)
443
+
444
+ """
445
+ lreal = float(lreal)
446
+ struct.pack_into(">d", bytearray_, byte_index, lreal)
447
+ return bytearray_
448
+
449
+
450
+ def set_lword(bytearray_: bytearray, byte_index: int, lword: int) -> bytearray:
451
+ """Set the long word
452
+
453
+ Notes:
454
+ Datatype `lword` (long word) consists in 8 bytes in the PLC.
455
+ Maximum value is 18446744073709551615 (0xFFFFFFFFFFFFFFFF).
456
+ Minimum value is 0.
457
+
458
+ Args:
459
+ bytearray_: buffer to write to.
460
+ byte_index: byte index from where to start writing.
461
+ lword: unsigned 64-bit value to write.
462
+
463
+ Returns:
464
+ Buffer with the written value.
465
+
466
+ Examples:
467
+ >>> data = bytearray(8)
468
+ >>> set_lword(data, 0, 0xABCD)
469
+ >>> data
470
+ bytearray(b'\\x00\\x00\\x00\\x00\\x00\\x00\\xab\\xcd')
471
+ """
472
+ lword = int(lword)
473
+ bytearray_[byte_index : byte_index + 8] = struct.pack(">Q", lword)
474
+ return bytearray_
475
+
476
+
477
+ def set_char(bytearray_: bytearray, byte_index: int, chr_: str) -> bytearray:
478
+ """Set char value in a bytearray.
479
+
480
+ Notes:
481
+ Datatype `char` in the PLC is represented in 1 byte. It has to be in ASCII-format
482
+
483
+ Args:
484
+ bytearray_: buffer to write to.
485
+ byte_index: byte index from where to start writing.
486
+ chr_: `char` to write.
487
+
488
+ Returns:
489
+ Buffer with the written value.
490
+
491
+ Examples:
492
+ write `char` (here as example 'C') to DB1.10 of a PLC
493
+ >>> data = bytearray(1)
494
+ >>> set_char(data, 0, 'C')
495
+ >>> data
496
+ bytearray('0x43')
497
+ """
498
+ if not isinstance(chr_, str):
499
+ raise TypeError(f"Value value:{chr_} is not from Type string")
500
+
501
+ if len(chr_) > 1:
502
+ raise ValueError(f"size chr_ : {chr_} > 1")
503
+ elif len(chr_) < 1:
504
+ raise ValueError(f"size chr_ : {chr_} < 1")
505
+
506
+ if 0 <= ord(chr_) <= 255:
507
+ bytearray_[byte_index] = ord(chr_)
508
+ return bytearray_
509
+ else:
510
+ raise ValueError(f"chr_ : {chr_} contains ascii value > 255, which is not compatible with PLC Type CHAR.")
511
+
512
+
513
+ def set_date(bytearray_: bytearray, byte_index: int, date_: date) -> bytearray:
514
+ """Set value in bytearray to date
515
+ Notes:
516
+ Datatype `date` consists in the number of days elapsed from 1990-01-01.
517
+ It is stored as an int (2 bytes) in the PLC.
518
+ Args:
519
+ bytearray_: buffer to write.
520
+ byte_index: byte index from where to start writing.
521
+ date_: date object
522
+ Examples:
523
+ >>> data = bytearray(2)
524
+ >>> set_date(data, 0, date(2024, 3, 27))
525
+ >>> data
526
+ bytearray(b'\x30\xd8')
527
+ """
528
+ if date_ < date(1990, 1, 1):
529
+ raise ValueError("date is lower than specification allows.")
530
+ elif date_ > date(2168, 12, 31):
531
+ raise ValueError("date is higher than specification allows.")
532
+ _days = (date_ - date(1990, 1, 1)).days
533
+ bytearray_[byte_index : byte_index + 2] = struct.pack(">h", _days)
534
+ return bytearray_
535
+
536
+
537
+ def set_wchar(bytearray_: bytearray, byte_index: int, chr_: str) -> bytearray:
538
+ """Set wchar value in a bytearray.
539
+
540
+ Notes:
541
+ Datatype `wchar` in the PLC is represented in 2 bytes as UTF-16-BE.
542
+
543
+ Args:
544
+ bytearray_: buffer to write to.
545
+ byte_index: byte index from where to start writing.
546
+ chr_: single character to write.
547
+
548
+ Returns:
549
+ Buffer with the written value.
550
+
551
+ Examples:
552
+ >>> data = bytearray(2)
553
+ >>> set_wchar(data, 0, 'C')
554
+ >>> data
555
+ bytearray(b'\\x00C')
556
+ """
557
+ if not isinstance(chr_, str):
558
+ raise TypeError(f"Value value:{chr_} is not from Type string")
559
+ if len(chr_) != 1:
560
+ raise ValueError(f"Expected single character, got length {len(chr_)}")
561
+ encoded = chr_.encode("utf-16-be")
562
+ bytearray_[byte_index : byte_index + 2] = encoded
563
+ return bytearray_
564
+
565
+
566
+ def set_wstring(bytearray_: bytearray, byte_index: int, value: str, max_size: int = 16382) -> None:
567
+ """Set wstring value
568
+
569
+ Notes:
570
+ Byte 0-1: max size (number of characters, 2-byte big-endian).
571
+ Byte 2-3: current length (number of characters, 2-byte big-endian).
572
+ Byte 4+: UTF-16-BE encoded characters (2 bytes each).
573
+
574
+ Args:
575
+ bytearray_: buffer to write to.
576
+ byte_index: byte index to start writing from.
577
+ value: string to write.
578
+ max_size: maximum number of characters allowed (default 16382).
579
+
580
+ Raises:
581
+ TypeError: if the value is not a string.
582
+ ValueError: if the string is too long or max_size exceeds 16382.
583
+
584
+ Examples:
585
+ >>> data = bytearray(26)
586
+ >>> set_wstring(data, 0, "hello", 10)
587
+ """
588
+ if not isinstance(value, str):
589
+ raise TypeError(f"Value value:{value} is not from Type string")
590
+
591
+ if max_size > 16382:
592
+ raise ValueError(f"max_size: {max_size} > max. allowed 16382 chars")
593
+
594
+ size = len(value)
595
+ if size > max_size:
596
+ raise ValueError(f"size {size} > max_size {max_size}")
597
+
598
+ # set max string size (2 bytes, big-endian)
599
+ bytearray_[byte_index : byte_index + 2] = struct.pack(">H", max_size)
600
+
601
+ # set current length (2 bytes, big-endian)
602
+ bytearray_[byte_index + 2 : byte_index + 4] = struct.pack(">H", size)
603
+
604
+ # encode and write UTF-16-BE characters
605
+ encoded = value.encode("utf-16-be")
606
+ bytearray_[byte_index + 4 : byte_index + 4 + len(encoded)] = encoded
607
+
608
+
609
+ def set_tod(bytearray_: bytearray, byte_index: int, tod: timedelta) -> bytearray:
610
+ """Set TIME_OF_DAY value in bytearray.
611
+
612
+ Notes:
613
+ Datatype `TIME_OF_DAY` is stored as milliseconds since midnight in 4 bytes.
614
+ Range: 0 to 86399999 ms (00:00:00.000 to 23:59:59.999).
615
+
616
+ Args:
617
+ bytearray_: buffer to write to.
618
+ byte_index: byte index from where to start writing.
619
+ tod: timedelta representing the time of day.
620
+
621
+ Returns:
622
+ Buffer with the written value.
623
+
624
+ Examples:
625
+ >>> from datetime import timedelta
626
+ >>> data = bytearray(4)
627
+ >>> set_tod(data, 0, timedelta(hours=12, minutes=30, seconds=15, milliseconds=500))
628
+ """
629
+ if tod.days >= 1 or tod < timedelta(0):
630
+ raise ValueError("TIME_OF_DAY must be between 00:00:00.000 and 23:59:59.999")
631
+ ms = int(tod.total_seconds() * 1000)
632
+ bytearray_[byte_index : byte_index + 4] = ms.to_bytes(4, byteorder="big")
633
+ return bytearray_
634
+
635
+
636
+ def set_dtl(bytearray_: bytearray, byte_index: int, dt_: datetime) -> bytearray:
637
+ """Set DTL (Date and Time Long) value in bytearray.
638
+
639
+ Notes:
640
+ Datatype `DTL` consists of 12 bytes in the PLC:
641
+ - Bytes 0-1: Year (uint16, big-endian)
642
+ - Byte 2: Month (1-12)
643
+ - Byte 3: Day (1-31)
644
+ - Byte 4: Weekday (1=Sunday, 7=Saturday)
645
+ - Byte 5: Hour (0-23)
646
+ - Byte 6: Minute (0-59)
647
+ - Byte 7: Second (0-59)
648
+ - Bytes 8-11: Nanoseconds (uint32, big-endian)
649
+
650
+ Args:
651
+ bytearray_: buffer to write to.
652
+ byte_index: byte index from where to start writing.
653
+ dt_: datetime object to write.
654
+
655
+ Returns:
656
+ Buffer with the written value.
657
+
658
+ Examples:
659
+ >>> from datetime import datetime
660
+ >>> data = bytearray(12)
661
+ >>> set_dtl(data, 0, datetime(2024, 3, 27, 14, 30, 0))
662
+ """
663
+ if dt_ > datetime(2554, 12, 31, 23, 59, 59):
664
+ raise ValueError("date_val is higher than specification allows.")
665
+
666
+ # Year as 2-byte big-endian
667
+ bytearray_[byte_index : byte_index + 2] = struct.pack(">H", dt_.year)
668
+ bytearray_[byte_index + 2] = dt_.month
669
+ bytearray_[byte_index + 3] = dt_.day
670
+ # Weekday: isoweekday() returns 1=Monday..7=Sunday, S7 uses 1=Sunday..7=Saturday
671
+ bytearray_[byte_index + 4] = (dt_.isoweekday() % 7) + 1
672
+ bytearray_[byte_index + 5] = dt_.hour
673
+ bytearray_[byte_index + 6] = dt_.minute
674
+ bytearray_[byte_index + 7] = dt_.second
675
+ # Nanoseconds from microseconds
676
+ nanoseconds = dt_.microsecond * 1000
677
+ bytearray_[byte_index + 8 : byte_index + 12] = struct.pack(">I", nanoseconds)
678
+ return bytearray_
679
+
680
+
681
+ def set_dt(bytearray_: bytearray, byte_index: int, dt_: datetime) -> bytearray:
682
+ """Set DATE_AND_TIME value in bytearray.
683
+
684
+ Notes:
685
+ Datatype `DATE_AND_TIME` consists of 8 bytes in BCD encoding:
686
+ - Byte 0: Year (BCD, 0-99, 90-99 = 1990-1999, 0-89 = 2000-2089)
687
+ - Byte 1: Month (BCD)
688
+ - Byte 2: Day (BCD)
689
+ - Byte 3: Hour (BCD)
690
+ - Byte 4: Minute (BCD)
691
+ - Byte 5: Second (BCD)
692
+ - Byte 6-7: Milliseconds (BCD) + weekday
693
+
694
+ Args:
695
+ bytearray_: buffer to write to.
696
+ byte_index: byte index from where to start writing.
697
+ dt_: datetime object to write.
698
+
699
+ Returns:
700
+ Buffer with the written value.
701
+
702
+ Examples:
703
+ >>> from datetime import datetime
704
+ >>> data = bytearray(8)
705
+ >>> set_dt(data, 0, datetime(2020, 7, 12, 17, 32, 2, 854000))
706
+ """
707
+
708
+ def byte_to_bcd(val: int) -> int:
709
+ return ((val // 10) << 4) | (val % 10)
710
+
711
+ year = dt_.year
712
+ if year < 1990 or year > 2089:
713
+ raise ValueError("DATE_AND_TIME year must be between 1990 and 2089")
714
+
715
+ year_bcd = year - 2000 if year >= 2000 else year - 1900
716
+ bytearray_[byte_index] = byte_to_bcd(year_bcd)
717
+ bytearray_[byte_index + 1] = byte_to_bcd(dt_.month)
718
+ bytearray_[byte_index + 2] = byte_to_bcd(dt_.day)
719
+ bytearray_[byte_index + 3] = byte_to_bcd(dt_.hour)
720
+ bytearray_[byte_index + 4] = byte_to_bcd(dt_.minute)
721
+ bytearray_[byte_index + 5] = byte_to_bcd(dt_.second)
722
+
723
+ # Milliseconds: 3 BCD digits in byte 6 and upper nibble of byte 7
724
+ ms = dt_.microsecond // 1000
725
+ ms_hundreds = ms // 100
726
+ ms_tens = (ms % 100) // 10
727
+ ms_ones = ms % 10
728
+ bytearray_[byte_index + 6] = byte_to_bcd(ms_hundreds * 10 + ms_tens)
729
+ # Lower nibble of byte 7: weekday (1=Sunday..7=Saturday)
730
+ weekday = (dt_.isoweekday() % 7) + 1
731
+ bytearray_[byte_index + 7] = (ms_ones << 4) | weekday
732
+
733
+ return bytearray_