Skip to content

Commit c2c9c34

Browse files
gijzelaerrclaude
andcommitted
Add missing data type setters and fix get_lword/get_wchar bugs
New setter functions: - set_wstring: S7 WSTRING write support (UTF-16-BE, max 16382 chars) - set_wchar: single UTF-16-BE character write - set_tod: TIME_OF_DAY write (milliseconds since midnight) - set_dtl: DTL write (12-byte extended date/time) - set_dt: DATE_AND_TIME write (8-byte BCD encoded) - set_lword: 64-bit unsigned word write (was NotImplementedError) New getter implementations: - get_ltime: LTIME read (64-bit nanosecond duration, S7-1500) - get_ltod: LTOD read (64-bit nanosecond time of day, S7-1500) - get_ldt: LDT read (64-bit nanosecond datetime since epoch, S7-1500) Bug fixes: - get_lword: was reading 4 bytes instead of 8, now returns int - get_wchar: used hardcoded index 1 instead of byte_index + 1 DB Row support: - set_value now handles WSTRING, WCHAR, TOD, DTL, DATE_AND_TIME Includes 19 new tests covering all additions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 746a9b2 commit c2c9c34

5 files changed

Lines changed: 505 additions & 39 deletions

File tree

snap7/util/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
set_bool,
33
set_fstring,
44
set_string,
5+
set_wstring,
56
set_real,
67
set_dword,
78
set_udint,
@@ -11,11 +12,16 @@
1112
set_word,
1213
set_byte,
1314
set_char,
15+
set_wchar,
1416
set_usint,
1517
set_sint,
1618
set_time,
1719
set_lreal,
20+
set_lword,
1821
set_date,
22+
set_tod,
23+
set_dtl,
24+
set_dt,
1925
)
2026

2127
from .getters import (
@@ -39,6 +45,10 @@
3945
get_date,
4046
get_tod,
4147
get_lreal,
48+
get_lword,
49+
get_ltime,
50+
get_ltod,
51+
get_ldt,
4252
get_char,
4353
get_wchar,
4454
get_dtl,
@@ -60,6 +70,10 @@
6070
"get_date",
6171
"get_tod",
6272
"get_lreal",
73+
"get_lword",
74+
"get_ltime",
75+
"get_ltod",
76+
"get_ldt",
6377
"get_char",
6478
"get_wchar",
6579
"get_dtl",
@@ -72,17 +86,23 @@
7286
"set_dword",
7387
"set_date",
7488
"set_lreal",
89+
"set_lword",
7590
"set_udint",
7691
"set_dint",
7792
"set_uint",
7893
"set_int",
7994
"set_word",
8095
"set_byte",
8196
"set_char",
97+
"set_wchar",
8298
"set_usint",
8399
"set_sint",
84100
"set_time",
85101
"set_bool",
86102
"set_fstring",
87103
"set_string",
104+
"set_wstring",
105+
"set_tod",
106+
"set_dtl",
107+
"set_dt",
88108
]

snap7/util/db.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686

8787
import re
8888
from logging import getLogger
89-
from datetime import datetime, date
89+
from datetime import datetime, date, timedelta
9090
from typing import Any, Optional, Union, Iterator, Tuple, Dict, Callable
9191

9292
from snap7 import Client
@@ -96,6 +96,7 @@
9696
set_bool,
9797
set_fstring,
9898
set_string,
99+
set_wstring,
99100
set_real,
100101
set_dword,
101102
set_udint,
@@ -105,11 +106,15 @@
105106
set_word,
106107
set_byte,
107108
set_char,
109+
set_wchar,
108110
set_usint,
109111
set_sint,
110112
set_time,
111113
set_lreal,
112114
set_date,
115+
set_tod,
116+
set_dtl,
117+
set_dt,
113118
get_bool,
114119
get_fstring,
115120
get_string,
@@ -672,6 +677,14 @@ def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool,
672677
set_string(bytearray_, byte_index, value, max_size_int)
673678
return None
674679

680+
if type_.startswith("WSTRING") and isinstance(value, str):
681+
max_size = re.search(r"\d+", type_)
682+
if max_size is None:
683+
raise ValueError("Max size could not be determinate. re.search() returned None")
684+
max_size_int = int(max_size.group(0))
685+
set_wstring(bytearray_, byte_index, value, max_size_int)
686+
return None
687+
675688
if type_ == "REAL":
676689
return set_real(bytearray_, byte_index, value)
677690

@@ -681,6 +694,9 @@ def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool,
681694
if type_ == "CHAR" and isinstance(value, str):
682695
return set_char(bytearray_, byte_index, value)
683696

697+
if type_ == "WCHAR" and isinstance(value, str):
698+
return set_wchar(bytearray_, byte_index, value)
699+
684700
if isinstance(value, int):
685701
type_to_func = {
686702
"DWORD": set_dword,
@@ -702,6 +718,15 @@ def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool,
702718
if type_ == "DATE" and isinstance(value, date):
703719
return set_date(bytearray_, byte_index, value)
704720

721+
if type_ in ("TIME_OF_DAY", "TOD") and isinstance(value, timedelta):
722+
return set_tod(bytearray_, byte_index, value)
723+
724+
if type_ == "DTL" and isinstance(value, datetime):
725+
return set_dtl(bytearray_, byte_index, value)
726+
727+
if type_ == "DATE_AND_TIME" and isinstance(value, datetime):
728+
return set_dt(bytearray_, byte_index, value)
729+
705730
raise ValueError
706731

707732
def write(self, client: Client) -> None:

snap7/util/getters.py

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -515,15 +515,13 @@ def get_lreal(bytearray_: bytearray, byte_index: int) -> float:
515515
return float(struct.unpack_from(">d", bytearray_, offset=byte_index)[0])
516516

517517

518-
def get_lword(bytearray_: bytearray, byte_index: int) -> bytearray:
518+
def get_lword(bytearray_: bytearray, byte_index: int) -> int:
519519
"""Get the long word
520520
521-
THIS VALUE IS NEITHER TESTED NOR VERIFIED BY A REAL PLC AT THE MOMENT
522-
523521
Notes:
524522
Datatype `lword` (long word) consists in 8 bytes in the PLC.
525-
Maximum value posible is bytearray(b"\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF")
526-
Lowest value posible is bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00")
523+
Maximum value is 18446744073709551615 (0xFFFFFFFFFFFFFFFF).
524+
Minimum value is 0.
527525
528526
Args:
529527
bytearray_: buffer to read from.
@@ -533,15 +531,13 @@ def get_lword(bytearray_: bytearray, byte_index: int) -> bytearray:
533531
Value read.
534532
535533
Examples:
536-
read lword value (here as example 0xAB\0xCD) from DB1.10 of a PLC
537-
>>> from snap7 import Client
538-
>>> data = Client().db_read(db_number=1, start=10, size=8)
534+
>>> data = bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD")
539535
>>> get_lword(data, 0)
540-
bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD")
536+
43981
541537
"""
542-
data = bytearray_[byte_index : byte_index + 4]
543-
dword = struct.unpack(">Q", struct.pack("8B", *data))[0]
544-
return bytearray(dword)
538+
data = bytearray_[byte_index : byte_index + 8]
539+
lword: int = struct.unpack(">Q", struct.pack("8B", *data))[0]
540+
return lword
545541

546542

547543
def get_ulint(bytearray_: bytearray, byte_index: int) -> int:
@@ -591,16 +587,72 @@ def get_date(bytearray_: bytearray, byte_index: int = 0) -> date:
591587
return date_val
592588

593589

594-
def get_ltime(bytearray_: bytearray, byte_index: int) -> str:
595-
raise NotImplementedError
590+
def get_ltime(bytearray_: bytearray, byte_index: int) -> timedelta:
591+
"""Get LTIME value from bytearray.
596592
593+
Notes:
594+
Datatype `LTIME` consists of 8 bytes (64-bit signed integer) representing
595+
nanoseconds. Used in S7-1500 PLCs.
597596
598-
def get_ltod(bytearray_: bytearray, byte_index: int) -> str:
599-
raise NotImplementedError
597+
Args:
598+
bytearray_: buffer to read from.
599+
byte_index: byte index from where to start reading.
600600
601+
Returns:
602+
timedelta value.
601603
602-
def get_ldt(bytearray_: bytearray, byte_index: int) -> str:
603-
raise NotImplementedError
604+
Examples:
605+
>>> data = bytearray(8)
606+
>>> data[:] = b'\\x00\\x00\\x00\\x00\\x3b\\x9a\\xca\\x00' # 1 second in nanoseconds
607+
>>> get_ltime(data, 0)
608+
datetime.timedelta(seconds=1)
609+
"""
610+
raw = bytearray_[byte_index : byte_index + 8]
611+
nanoseconds: int = struct.unpack(">q", struct.pack("8B", *raw))[0]
612+
return timedelta(microseconds=nanoseconds // 1000)
613+
614+
615+
def get_ltod(bytearray_: bytearray, byte_index: int) -> timedelta:
616+
"""Get LTOD (Long Time of Day) value from bytearray.
617+
618+
Notes:
619+
Datatype `LTOD` consists of 8 bytes (64-bit unsigned integer) representing
620+
nanoseconds since midnight. Used in S7-1500 PLCs.
621+
Range: 0 to 86399999999999 ns.
622+
623+
Args:
624+
bytearray_: buffer to read from.
625+
byte_index: byte index from where to start reading.
626+
627+
Returns:
628+
timedelta value representing time of day.
629+
"""
630+
raw = bytearray_[byte_index : byte_index + 8]
631+
nanoseconds: int = struct.unpack(">Q", struct.pack("8B", *raw))[0]
632+
result = timedelta(microseconds=nanoseconds // 1000)
633+
if result.days >= 1:
634+
raise ValueError("LTOD value exceeds 24 hours")
635+
return result
636+
637+
638+
def get_ldt(bytearray_: bytearray, byte_index: int) -> datetime:
639+
"""Get LDT (Long Date and Time) value from bytearray.
640+
641+
Notes:
642+
Datatype `LDT` consists of 8 bytes (64-bit unsigned integer) representing
643+
nanoseconds since 1970-01-01 00:00:00 UTC. Used in S7-1500 PLCs.
644+
645+
Args:
646+
bytearray_: buffer to read from.
647+
byte_index: byte index from where to start reading.
648+
649+
Returns:
650+
datetime value.
651+
"""
652+
raw = bytearray_[byte_index : byte_index + 8]
653+
nanoseconds: int = struct.unpack(">Q", struct.pack("8B", *raw))[0]
654+
epoch = datetime(1970, 1, 1)
655+
return epoch + timedelta(microseconds=nanoseconds // 1000)
604656

605657

606658
def get_dtl(bytearray_: bytearray, byte_index: int) -> datetime:
@@ -662,7 +714,7 @@ def get_wchar(bytearray_: bytearray, byte_index: int) -> str:
662714
'C'
663715
"""
664716
if bytearray_[byte_index] == 0:
665-
return chr(bytearray_[1])
717+
return chr(bytearray_[byte_index + 1])
666718
return bytearray_[byte_index : byte_index + 2].decode("utf-16-be")
667719

668720

0 commit comments

Comments
 (0)