Skip to content

Commit

Permalink
Add WSTRING support for structures (#293)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stlehmann authored Dec 14, 2021
1 parent 8d70d1a commit fe00679
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 77 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ All notable changes to this project will be documented in this file.

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

## 3.3.10
## 3.3.10 [unreleased]

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

### Changed
* [#292](https://github.com/stlehmann/pyads/pull/292) Improve performance of get_value_from_ctype_data for arrays
Expand Down
38 changes: 32 additions & 6 deletions pyads/ads.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
PLCTYPE_REAL,
PLCTYPE_SINT,
PLCTYPE_STRING,
PLCTYPE_WSTRING,
PLCTYPE_TIME,
PLCTYPE_TOD,
PLCTYPE_UDINT,
Expand Down Expand Up @@ -62,7 +63,7 @@
AmsAddr,
SAmsNetId,
)
from .utils import platform_is_linux
from .utils import platform_is_linux, find_wstring_null_terminator

# custom types
StructureDef = Tuple[
Expand Down Expand Up @@ -259,9 +260,14 @@ def size_of_structure(structure_def: StructureDef) -> int:

if plc_datatype == PLCTYPE_STRING:
if str_len is not None:
num_of_bytes += (str_len + 1) * size
num_of_bytes += (str_len + 1) * size # STRING uses 1 byte per character + null-terminator
else:
num_of_bytes += (PLC_DEFAULT_STRING_SIZE + 1) * size
elif plc_datatype == PLCTYPE_WSTRING:
if str_len is not None:
num_of_bytes += 2 * (str_len + 1) * size # WSTRING uses 2 bytes per character + null-terminator
else:
num_of_bytes += (PLC_DEFAULT_STRING_SIZE + 1) * 2 * size
elif plc_datatype not in DATATYPE_MAP:
raise RuntimeError("Datatype not found")
else:
Expand Down Expand Up @@ -306,6 +312,7 @@ def dict_from_bytes(
var, plc_datatype, size = item # type: ignore
str_len = None
except ValueError:
# str_len is the numbers of characters without null-terminator
var, plc_datatype, size, str_len = item # type: ignore

var_array = []
Expand All @@ -319,6 +326,14 @@ def dict_from_bytes(
.decode("utf-8")
)
index += str_len + 1
elif plc_datatype == PLCTYPE_WSTRING:
if str_len is None: # if no str_len is given use default size
str_len = PLC_DEFAULT_STRING_SIZE
n_bytes = 2 * (str_len + 1) # WSTRING uses 2 bytes per character + null-terminator
a = bytearray(byte_list[index: (index + n_bytes)])
null_idx = find_wstring_null_terminator(a)
var_array.append(a[:null_idx].decode("utf-16-le"))
index += n_bytes
elif plc_datatype not in DATATYPE_MAP:
raise RuntimeError("Datatype not found. Check structure definition")
else:
Expand Down Expand Up @@ -392,12 +407,23 @@ def bytes_from_dict(
str_len = PLC_DEFAULT_STRING_SIZE
if size > 1:
byte_list += list(var[i].encode("utf-8"))
remaining_bytes = str_len + 1 - len(var[i])
remaining_bytes = str_len + 1 - len(var[i]) # 1 byte a character plus null-terminator
else:
byte_list += list(var.encode("utf-8"))
remaining_bytes = str_len + 1 - len(var)
for byte in range(remaining_bytes):
byte_list.append(0)
remaining_bytes = str_len + 1 - len(var) # 1 byte a character plus null-terminator
byte_list.extend(remaining_bytes * [0])
elif plc_datatype == PLCTYPE_WSTRING:
if str_len is None:
str_len = PLC_DEFAULT_STRING_SIZE
if size > 1:
encoded = list(var[i].encode("utf-16-le"))
byte_list += encoded
remaining_bytes = 2 * (str_len + 1) - len(encoded) # 2 bytes a character plus null-terminator
else:
encoded = list(var.encode("utf-16-le"))
byte_list += encoded
remaining_bytes = 2 * (str_len + 1) - len(encoded) # 2 bytes a character plus null-terminator
byte_list.extend(remaining_bytes * [0])
elif plc_datatype not in DATATYPE_MAP:
raise RuntimeError("Datatype not found. Check structure definition")
else:
Expand Down
9 changes: 3 additions & 6 deletions pyads/pyads_ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from contextlib import closing
from functools import wraps

from .utils import platform_is_linux, platform_is_windows, platform_is_freebsd
from .utils import platform_is_linux, platform_is_windows, platform_is_freebsd, find_wstring_null_terminator
from .structs import (
AmsAddr,
SAmsAddr,
Expand Down Expand Up @@ -1005,11 +1005,8 @@ def adsSumRead(
elif data_symbols[data_name].dataType == ADST_WSTRING:
# find null-terminator 2 Bytes
a = sum_response[offset: offset + data_symbols[data_name].size]
for ix in range(1, len(a), 2):
if (a[ix-1], a[ix]) == (0, 0):
null_idx = ix - 1
break
else:
null_idx = find_wstring_null_terminator(a)
if null_idx is None:
raise ValueError("No null-terminator found in buffer")
value = bytearray(sum_response[offset: offset + null_idx]).decode("utf-16-le")
else:
Expand Down
13 changes: 13 additions & 0 deletions pyads/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,16 @@ def decode_ads(message: bytes) -> str:
ISO/IEC 8859-1 is supported.'
"""
return message.decode("windows-1252").strip(" \t\n\r\0")


def find_wstring_null_terminator(data: bytearray) -> Optional[int]:
"""Find null-terminator in WSTRING (UTF-16) data.
:return: None if no null-terminator was found, else the index of the null-terminator
"""
for ix in range(1, len(data), 2):
if (data[ix - 1], data[ix]) == (0, 0):
return ix - 1
else:
return None
Loading

0 comments on commit fe00679

Please sign in to comment.