Skip to content

Commit

Permalink
add support to read/write space-padded fixed-length strings (#378)
Browse files Browse the repository at this point in the history
these strings are not part of the official s7comm specification, but
they are commonly found in real-world systems. they simply consist of a
byte array that contains characters forming a string, padded on the
right with spaces (ascii 32) when the string length is shorter than the
array.

this commit adds support for reading and writing, as well as usage in
tables (snap7.util.DB), and unit tests.
  • Loading branch information
vmsh0 authored Aug 3, 2022
1 parent 080affa commit 38d6576
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 5 deletions.
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

0 comments on commit 38d6576

Please sign in to comment.