Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to read/write space-padded fixed-length strings #378

Merged
merged 1 commit into from
Aug 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion snap7/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,44 @@ def get_real(bytearray_: bytearray, byte_index: int) -> float:
return real


def set_fstring(bytearray_: bytearray, byte_index: int, value: str, max_length: int):
"""Set space-padded fixed-length string value

Args:
bytearray_: buffer to write to.
byte_index: byte index to start writing from.
value: string to write.
max_length: maximum string length, i.e. the fixed size of the string.

Raises:
:obj:`TypeError`: if the `value` is not a :obj:`str`.
:obj:`ValueError`: if the length of the `value` is larger than the `max_size`
or 'value' contains non-ascii characters.

Examples:
>>> data = bytearray(20)
>>> snap7.util.set_fstring(data, 0, "hello world", 15)
>>> data
bytearray(b'hello world \x00\x00\x00\x00\x00')
"""
if not value.isascii():
raise ValueError("Value contains non-ascii values.")
# FAIL HARD WHEN trying to write too much data into PLC
size = len(value)
if size > max_length:
raise ValueError(f'size {size} > max_length {max_length} {value}')

i = 0

# fill array which chr integers
for i, c in enumerate(value):
bytearray_[byte_index + i] = ord(c)

# fill the rest with empty space
for r in range(i + 1, max_length):
bytearray_[byte_index + r] = ord(' ')


def set_string(bytearray_: bytearray, byte_index: int, value: str, max_size: int = 255):
"""Set string value

Expand Down Expand Up @@ -461,6 +499,37 @@ def set_string(bytearray_: bytearray, byte_index: int, value: str, max_size: int
bytearray_[byte_index + 2 + r] = ord(' ')


def get_fstring(bytearray_: bytearray, byte_index: int, max_length: int, remove_padding: bool = True) -> str:
"""Parse space-padded fixed-length string from bytearray

Notes:
This function supports fixed-length ASCII strings, right-padded with spaces.

Args:
bytearray_: buffer from where to get the string.
byte_index: byte index from where to start reading.
max_length: the maximum length of the string.
remove_padding: whether to remove the right-padding.

Returns:
String value.

Examples:
>>> data = [ord(letter) for letter in "hello world "]
>>> snap7.util.get_fstring(data, 0, 15)
'hello world'
>>> snap7.util.get_fstring(data, 0, 15, remove_padding=false)
'hello world '
"""
data = map(chr, bytearray_[byte_index:byte_index + max_length])
string = "".join(data)

if remove_padding:
return string.rstrip(' ')
else:
return string


def get_string(bytearray_: bytearray, byte_index: int) -> str:
"""Parse string from bytearray

Expand Down Expand Up @@ -1532,7 +1601,12 @@ def get_value(self, byte_index: Union[str, int], type_: str) -> Union[ValueError
# first 4 bytes are used by db
byte_index = self.get_offset(byte_index)

if type_.startswith('STRING'):
if type_.startswith('FSTRING'):
max_size = re.search(r'\d+', type_)
if max_size is None:
raise ValueError("Max size could not be determinate. re.search() returned None")
return get_fstring(bytearray_, byte_index, int(max_size[0]))
elif type_.startswith('STRING'):
max_size = re.search(r'\d+', type_)
if max_size is None:
raise ValueError("Max size could not be determinate. re.search() returned None")
Expand Down Expand Up @@ -1594,6 +1668,14 @@ def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool,

byte_index = self.get_offset(byte_index)

if type_.startswith('FSTRING') and isinstance(value, str):
max_size = re.search(r'\d+', type_)
if max_size is None:
raise ValueError("Max size could not be determinate. re.search() returned None")
max_size_grouped = max_size.group(0)
max_size_int = int(max_size_grouped)
return set_fstring(bytearray_, byte_index, value, max_size_int)

if type_.startswith('STRING') and isinstance(value, str):
max_size = re.search(r'\d+', type_)
if max_size is None:
Expand Down
45 changes: 41 additions & 4 deletions test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
80 testDate DATE
82 testTod TOD
86 testDtl DTL
98 testFstring FSTRING[8]
"""

test_spec_indented = """
Expand Down Expand Up @@ -72,6 +73,7 @@
80 testDate DATE
82 testTod TOD
86 testDtl DTL
98 testFstring FSTRING[8]
"""


Expand All @@ -95,14 +97,15 @@
143, 255, 255, 255, # test time
254, # test byte 0xFE
48, 57, # test uint 12345
7, 91, 205, 21, # test udint 123456789
7, 91, 205, 21, # test udint 123456789
65, 157, 111, 52, 84, 126, 107, 117, # test lreal 123456789.123456789
65, # test char A
3, 169, # test wchar Ω
0, 4, 0, 4, 3, 169, 0, ord('s'), 0, ord('t'), 0, 196, # test wstring Ω s t Ä
45, 235, # test date 09.03.2022
2, 179, 41, 128, # test tod 12:34:56
7, 230, 3, 9, 4, 12, 34, 45, 0, 0, 0, 0 # test dtl 09.03.2022 12:34:56
45, 235, # test date 09.03.2022
2, 179, 41, 128, # test tod 12:34:56
7, 230, 3, 9, 4, 12, 34, 45, 0, 0, 0, 0, # test dtl 09.03.2022 12:34:56
116, 101, 115, 116, 32, 32, 32, 32 # test fstring 'test '
])

_new_bytearray = bytearray(100)
Expand Down Expand Up @@ -232,6 +235,40 @@ def test_write_string(self):
except ValueError:
pass

def test_get_fstring(self):
data = [ord(letter) for letter in "hello world "]
self.assertEqual(util.get_fstring(data, 0, 15), 'hello world')
self.assertEqual(util.get_fstring(data, 0, 15, remove_padding=False), 'hello world ')

def test_get_fstring_name(self):
test_array = bytearray(_bytearray)
row = util.DB_Row(test_array, test_spec, layout_offset=4)
value = row['testFstring']
self.assertEqual(value, 'test')

def test_get_fstring_index(self):
test_array = bytearray(_bytearray)
row = util.DB_Row(test_array, test_spec, layout_offset=4)
value = row.get_value(98, 'FSTRING[8]') # get value
self.assertEqual(value, 'test')

def test_set_fstring(self):
data = bytearray(20)
util.set_fstring(data, 0, "hello world", 15)
self.assertEqual(data, bytearray(b'hello world \x00\x00\x00\x00\x00'))

def test_set_fstring_name(self):
test_array = bytearray(_bytearray)
row = util.DB_Row(test_array, test_spec, layout_offset=4)
row['testFstring'] = 'TSET'
self.assertEqual(row['testFstring'], 'TSET')

def test_set_fstring_index(self):
test_array = bytearray(_bytearray)
row = util.DB_Row(test_array, test_spec, layout_offset=4)
row.set_value(98, 'FSTRING[8]', 'TSET')
self.assertEqual(row['testFstring'], 'TSET')

def test_get_int(self):
test_array = bytearray(_bytearray)
row = util.DB_Row(test_array, test_spec, layout_offset=4)
Expand Down