Skip to content

Commit 38d6576

Browse files
authored
add support to read/write space-padded fixed-length strings (#378)
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.
1 parent 080affa commit 38d6576

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

snap7/util.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,44 @@ def get_real(bytearray_: bytearray, byte_index: int) -> float:
411411
return real
412412

413413

414+
def set_fstring(bytearray_: bytearray, byte_index: int, value: str, max_length: int):
415+
"""Set space-padded fixed-length string value
416+
417+
Args:
418+
bytearray_: buffer to write to.
419+
byte_index: byte index to start writing from.
420+
value: string to write.
421+
max_length: maximum string length, i.e. the fixed size of the string.
422+
423+
Raises:
424+
:obj:`TypeError`: if the `value` is not a :obj:`str`.
425+
:obj:`ValueError`: if the length of the `value` is larger than the `max_size`
426+
or 'value' contains non-ascii characters.
427+
428+
Examples:
429+
>>> data = bytearray(20)
430+
>>> snap7.util.set_fstring(data, 0, "hello world", 15)
431+
>>> data
432+
bytearray(b'hello world \x00\x00\x00\x00\x00')
433+
"""
434+
if not value.isascii():
435+
raise ValueError("Value contains non-ascii values.")
436+
# FAIL HARD WHEN trying to write too much data into PLC
437+
size = len(value)
438+
if size > max_length:
439+
raise ValueError(f'size {size} > max_length {max_length} {value}')
440+
441+
i = 0
442+
443+
# fill array which chr integers
444+
for i, c in enumerate(value):
445+
bytearray_[byte_index + i] = ord(c)
446+
447+
# fill the rest with empty space
448+
for r in range(i + 1, max_length):
449+
bytearray_[byte_index + r] = ord(' ')
450+
451+
414452
def set_string(bytearray_: bytearray, byte_index: int, value: str, max_size: int = 255):
415453
"""Set string value
416454
@@ -461,6 +499,37 @@ def set_string(bytearray_: bytearray, byte_index: int, value: str, max_size: int
461499
bytearray_[byte_index + 2 + r] = ord(' ')
462500

463501

502+
def get_fstring(bytearray_: bytearray, byte_index: int, max_length: int, remove_padding: bool = True) -> str:
503+
"""Parse space-padded fixed-length string from bytearray
504+
505+
Notes:
506+
This function supports fixed-length ASCII strings, right-padded with spaces.
507+
508+
Args:
509+
bytearray_: buffer from where to get the string.
510+
byte_index: byte index from where to start reading.
511+
max_length: the maximum length of the string.
512+
remove_padding: whether to remove the right-padding.
513+
514+
Returns:
515+
String value.
516+
517+
Examples:
518+
>>> data = [ord(letter) for letter in "hello world "]
519+
>>> snap7.util.get_fstring(data, 0, 15)
520+
'hello world'
521+
>>> snap7.util.get_fstring(data, 0, 15, remove_padding=false)
522+
'hello world '
523+
"""
524+
data = map(chr, bytearray_[byte_index:byte_index + max_length])
525+
string = "".join(data)
526+
527+
if remove_padding:
528+
return string.rstrip(' ')
529+
else:
530+
return string
531+
532+
464533
def get_string(bytearray_: bytearray, byte_index: int) -> str:
465534
"""Parse string from bytearray
466535
@@ -1532,7 +1601,12 @@ def get_value(self, byte_index: Union[str, int], type_: str) -> Union[ValueError
15321601
# first 4 bytes are used by db
15331602
byte_index = self.get_offset(byte_index)
15341603

1535-
if type_.startswith('STRING'):
1604+
if type_.startswith('FSTRING'):
1605+
max_size = re.search(r'\d+', type_)
1606+
if max_size is None:
1607+
raise ValueError("Max size could not be determinate. re.search() returned None")
1608+
return get_fstring(bytearray_, byte_index, int(max_size[0]))
1609+
elif type_.startswith('STRING'):
15361610
max_size = re.search(r'\d+', type_)
15371611
if max_size is None:
15381612
raise ValueError("Max size could not be determinate. re.search() returned None")
@@ -1594,6 +1668,14 @@ def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool,
15941668

15951669
byte_index = self.get_offset(byte_index)
15961670

1671+
if type_.startswith('FSTRING') and isinstance(value, str):
1672+
max_size = re.search(r'\d+', type_)
1673+
if max_size is None:
1674+
raise ValueError("Max size could not be determinate. re.search() returned None")
1675+
max_size_grouped = max_size.group(0)
1676+
max_size_int = int(max_size_grouped)
1677+
return set_fstring(bytearray_, byte_index, value, max_size_int)
1678+
15971679
if type_.startswith('STRING') and isinstance(value, str):
15981680
max_size = re.search(r'\d+', type_)
15991681
if max_size is None:

test/test_util.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
80 testDate DATE
3939
82 testTod TOD
4040
86 testDtl DTL
41+
98 testFstring FSTRING[8]
4142
"""
4243

4344
test_spec_indented = """
@@ -72,6 +73,7 @@
7273
80 testDate DATE
7374
82 testTod TOD
7475
86 testDtl DTL
76+
98 testFstring FSTRING[8]
7577
"""
7678

7779

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

108111
_new_bytearray = bytearray(100)
@@ -232,6 +235,40 @@ def test_write_string(self):
232235
except ValueError:
233236
pass
234237

238+
def test_get_fstring(self):
239+
data = [ord(letter) for letter in "hello world "]
240+
self.assertEqual(util.get_fstring(data, 0, 15), 'hello world')
241+
self.assertEqual(util.get_fstring(data, 0, 15, remove_padding=False), 'hello world ')
242+
243+
def test_get_fstring_name(self):
244+
test_array = bytearray(_bytearray)
245+
row = util.DB_Row(test_array, test_spec, layout_offset=4)
246+
value = row['testFstring']
247+
self.assertEqual(value, 'test')
248+
249+
def test_get_fstring_index(self):
250+
test_array = bytearray(_bytearray)
251+
row = util.DB_Row(test_array, test_spec, layout_offset=4)
252+
value = row.get_value(98, 'FSTRING[8]') # get value
253+
self.assertEqual(value, 'test')
254+
255+
def test_set_fstring(self):
256+
data = bytearray(20)
257+
util.set_fstring(data, 0, "hello world", 15)
258+
self.assertEqual(data, bytearray(b'hello world \x00\x00\x00\x00\x00'))
259+
260+
def test_set_fstring_name(self):
261+
test_array = bytearray(_bytearray)
262+
row = util.DB_Row(test_array, test_spec, layout_offset=4)
263+
row['testFstring'] = 'TSET'
264+
self.assertEqual(row['testFstring'], 'TSET')
265+
266+
def test_set_fstring_index(self):
267+
test_array = bytearray(_bytearray)
268+
row = util.DB_Row(test_array, test_spec, layout_offset=4)
269+
row.set_value(98, 'FSTRING[8]', 'TSET')
270+
self.assertEqual(row['testFstring'], 'TSET')
271+
235272
def test_get_int(self):
236273
test_array = bytearray(_bytearray)
237274
row = util.DB_Row(test_array, test_spec, layout_offset=4)

0 commit comments

Comments
 (0)