diff --git a/snap7/util.py b/snap7/util.py index 2f0b4071..4a8dd151 100644 --- a/snap7/util.py +++ b/snap7/util.py @@ -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 @@ -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 @@ -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") @@ -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: diff --git a/test/test_util.py b/test/test_util.py index 04846f04..19e747f6 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -38,6 +38,7 @@ 80 testDate DATE 82 testTod TOD 86 testDtl DTL +98 testFstring FSTRING[8] """ test_spec_indented = """ @@ -72,6 +73,7 @@ 80 testDate DATE 82 testTod TOD 86 testDtl DTL + 98 testFstring FSTRING[8] """ @@ -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) @@ -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)