Skip to content

Commit fe00679

Browse files
authored
Add WSTRING support for structures (#293)
* Add WSTRING support for dict_from-bytes * Add helper function to find null-terminator in WSTRING * Add test for find_wstring_null_terminator function * Use find_wstring_null_terminator function in adsSumRead * Add WSTRING support for size_of_structure * Fix size_of_structure. Broke it in the commit before by modifying the conditions. * Add WSTRING support for bytes_from_dict * Fix bytes_from_dict Array size was not calculated correctly * Use length of bytearray * Improve performance on adding remaining bytes This may not be necessary but replacing the for loop with the extend statement speeds speeds it up by factor 5 * Fix size_of_structure for multiple fields * Add test for read/write WSTRING struct * Add test for read/write WSTRING array struct * Add Changelog entry * Fix str_len - str_len now is always the number of characters without null-terminator - n_bytes is the number of bytes and differs for STRING or WSTRING * Add tests for size_of_structure for WSTRING * Add WSTRING to test_dict_from_bytes * Add WSTRING to test_bytes_from_dict * Fix WSTRING length for bytes_from_strings * Add comments * Remove redundant comment
1 parent 8d70d1a commit fe00679

File tree

7 files changed

+316
-77
lines changed

7 files changed

+316
-77
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7-
## 3.3.10
7+
## 3.3.10 [unreleased]
88

99
### Added
10+
* [#293](https://github.com/stlehmann/pyads/pull/2939) Support WSTRINGS in structures
1011

1112
### Changed
1213
* [#292](https://github.com/stlehmann/pyads/pull/292) Improve performance of get_value_from_ctype_data for arrays

pyads/ads.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
PLCTYPE_REAL,
3232
PLCTYPE_SINT,
3333
PLCTYPE_STRING,
34+
PLCTYPE_WSTRING,
3435
PLCTYPE_TIME,
3536
PLCTYPE_TOD,
3637
PLCTYPE_UDINT,
@@ -62,7 +63,7 @@
6263
AmsAddr,
6364
SAmsNetId,
6465
)
65-
from .utils import platform_is_linux
66+
from .utils import platform_is_linux, find_wstring_null_terminator
6667

6768
# custom types
6869
StructureDef = Tuple[
@@ -259,9 +260,14 @@ def size_of_structure(structure_def: StructureDef) -> int:
259260

260261
if plc_datatype == PLCTYPE_STRING:
261262
if str_len is not None:
262-
num_of_bytes += (str_len + 1) * size
263+
num_of_bytes += (str_len + 1) * size # STRING uses 1 byte per character + null-terminator
263264
else:
264265
num_of_bytes += (PLC_DEFAULT_STRING_SIZE + 1) * size
266+
elif plc_datatype == PLCTYPE_WSTRING:
267+
if str_len is not None:
268+
num_of_bytes += 2 * (str_len + 1) * size # WSTRING uses 2 bytes per character + null-terminator
269+
else:
270+
num_of_bytes += (PLC_DEFAULT_STRING_SIZE + 1) * 2 * size
265271
elif plc_datatype not in DATATYPE_MAP:
266272
raise RuntimeError("Datatype not found")
267273
else:
@@ -306,6 +312,7 @@ def dict_from_bytes(
306312
var, plc_datatype, size = item # type: ignore
307313
str_len = None
308314
except ValueError:
315+
# str_len is the numbers of characters without null-terminator
309316
var, plc_datatype, size, str_len = item # type: ignore
310317

311318
var_array = []
@@ -319,6 +326,14 @@ def dict_from_bytes(
319326
.decode("utf-8")
320327
)
321328
index += str_len + 1
329+
elif plc_datatype == PLCTYPE_WSTRING:
330+
if str_len is None: # if no str_len is given use default size
331+
str_len = PLC_DEFAULT_STRING_SIZE
332+
n_bytes = 2 * (str_len + 1) # WSTRING uses 2 bytes per character + null-terminator
333+
a = bytearray(byte_list[index: (index + n_bytes)])
334+
null_idx = find_wstring_null_terminator(a)
335+
var_array.append(a[:null_idx].decode("utf-16-le"))
336+
index += n_bytes
322337
elif plc_datatype not in DATATYPE_MAP:
323338
raise RuntimeError("Datatype not found. Check structure definition")
324339
else:
@@ -392,12 +407,23 @@ def bytes_from_dict(
392407
str_len = PLC_DEFAULT_STRING_SIZE
393408
if size > 1:
394409
byte_list += list(var[i].encode("utf-8"))
395-
remaining_bytes = str_len + 1 - len(var[i])
410+
remaining_bytes = str_len + 1 - len(var[i]) # 1 byte a character plus null-terminator
396411
else:
397412
byte_list += list(var.encode("utf-8"))
398-
remaining_bytes = str_len + 1 - len(var)
399-
for byte in range(remaining_bytes):
400-
byte_list.append(0)
413+
remaining_bytes = str_len + 1 - len(var) # 1 byte a character plus null-terminator
414+
byte_list.extend(remaining_bytes * [0])
415+
elif plc_datatype == PLCTYPE_WSTRING:
416+
if str_len is None:
417+
str_len = PLC_DEFAULT_STRING_SIZE
418+
if size > 1:
419+
encoded = list(var[i].encode("utf-16-le"))
420+
byte_list += encoded
421+
remaining_bytes = 2 * (str_len + 1) - len(encoded) # 2 bytes a character plus null-terminator
422+
else:
423+
encoded = list(var.encode("utf-16-le"))
424+
byte_list += encoded
425+
remaining_bytes = 2 * (str_len + 1) - len(encoded) # 2 bytes a character plus null-terminator
426+
byte_list.extend(remaining_bytes * [0])
401427
elif plc_datatype not in DATATYPE_MAP:
402428
raise RuntimeError("Datatype not found. Check structure definition")
403429
else:

pyads/pyads_ex.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from contextlib import closing
1616
from functools import wraps
1717

18-
from .utils import platform_is_linux, platform_is_windows, platform_is_freebsd
18+
from .utils import platform_is_linux, platform_is_windows, platform_is_freebsd, find_wstring_null_terminator
1919
from .structs import (
2020
AmsAddr,
2121
SAmsAddr,
@@ -1005,11 +1005,8 @@ def adsSumRead(
10051005
elif data_symbols[data_name].dataType == ADST_WSTRING:
10061006
# find null-terminator 2 Bytes
10071007
a = sum_response[offset: offset + data_symbols[data_name].size]
1008-
for ix in range(1, len(a), 2):
1009-
if (a[ix-1], a[ix]) == (0, 0):
1010-
null_idx = ix - 1
1011-
break
1012-
else:
1008+
null_idx = find_wstring_null_terminator(a)
1009+
if null_idx is None:
10131010
raise ValueError("No null-terminator found in buffer")
10141011
value = bytearray(sum_response[offset: offset + null_idx]).decode("utf-16-le")
10151012
else:

pyads/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,16 @@ def decode_ads(message: bytes) -> str:
5959
ISO/IEC 8859-1 is supported.'
6060
"""
6161
return message.decode("windows-1252").strip(" \t\n\r\0")
62+
63+
64+
def find_wstring_null_terminator(data: bytearray) -> Optional[int]:
65+
"""Find null-terminator in WSTRING (UTF-16) data.
66+
67+
:return: None if no null-terminator was found, else the index of the null-terminator
68+
69+
"""
70+
for ix in range(1, len(data), 2):
71+
if (data[ix - 1], data[ix]) == (0, 0):
72+
return ix - 1
73+
else:
74+
return None

0 commit comments

Comments
 (0)