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 get_time and set_time in util #308

Merged
merged 3 commits into from
Sep 8, 2021
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
103 changes: 92 additions & 11 deletions snap7/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,8 +586,85 @@ def get_dt(bytearray_: bytearray, byte_index: int) -> str:
return date_and_time


def get_time(bytearray_: bytearray, byte_index: int) -> str:
"""Get time value from bytearray.

Notes:
Datatype `time` consists in 4 bytes in the PLC.
Maximum possible value is T#24D_20H_31M_23S_647MS(2147483647).
Lower posible value is T#-24D_20H_31M_23S_648MS(-2147483648).

Args:
bytearray_: buffer to read.
byte_index: byte index from where to start reading.

Returns:
Value read.

Examples:
>>> import struct
>>> data = bytearray(4)
>>> data[:] = struct.pack(">i", 2147483647)
>>> snap7.util.get_time(data, 0)
'24:20:31:23:647'
"""
data_bytearray = bytearray_[byte_index:byte_index + 4]
bits = 32
sign = 1
byte_str = data_bytearray.hex()
val = int(byte_str, 16)
if (val & (1 << (bits - 1))) != 0:
sign = -1 # if sign bit is set e.g., 8bit: 128-255
val = val - (1 << bits) # compute negative value
val = val * sign

milli_seconds = val % 1000
seconds = val // 1000
minutes = seconds // 60
hours = minutes // 60
days = hours // 24
time_str = str(days * sign) + ":" + str(hours % 24) + ":" + str(minutes % 60) + ":" + str(seconds % 60) + "." + str(milli_seconds)
Copy link
Contributor

@swamper123 swamper123 Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This string concatination style still works, but is a bit outdated (but not wrong). An f-string equivalent would be sth. like this:

time_str = f"{days * sign!s}):{hours % 24!s}:{minutes % 60}:{seconds % 60!s}.{milli_seconds!s}")

return time_str


def set_time(bytearray_: bytearray, byte_index: int, time_string: str) -> bytearray:
"""Set value in bytearray to time

Notes:
Datatype `time` consists in 4 bytes in the PLC.
Maximum possible value is T#24D_20H_31M_23S_647MS(2147483647).
Lower posible value is T#-24D_20H_31M_23S_648MS(-2147483648).

Args:
bytearray_: buffer to write.
byte_index: byte index from where to start writing.
time_string: time value in string

Examples:
>>> data = bytearray(4)
>>> snap7.util.set_dint(data, 0, '-22:3:57:28.192')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo. Wrong method.

>>> data
bytearray(b'\x8d\xda\xaf\x00')
"""
import re
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re is already imported at line 88. This line is not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some smaller style things in my opinion.
A question I would have is, what happens if sb. sets a bigger/false time_string greater than 4 Bytes. Would it break succesfuly?

Good catch. I didn't notice that issue. I will modify this and make a new pull request.

sign = 1
bits = 32
data_list = re.split('[: .]', time_string)
days, hours, minutes, seconds, milli_seconds = [int(x) for x in data_list]
if days < 0:
sign = -1
time_int = ((days * sign * 3600 * 24 + (hours % 24) * 3600 + (minutes % 60) * 60 + seconds % 60) * 1000 + milli_seconds) * sign
if sign < 0:
time_int = (1 << bits) + time_int
formatstring = '{:0%ib}' % bits
byte_hex = hex(int(formatstring.format(time_int), 2)).split('x')[1]
bytes_array = bytes.fromhex(byte_hex)
Comment on lines +659 to +661
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these conversions necessary? Int has a method .to_bytes() that does the same. Or it might be better to use a struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You approach is more elegant. Thank you.

bytearray_[byte_index:byte_index + 4] = bytes_array
return bytearray_


def set_usint(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray:
"""set unsigned small int
"""Set unsigned small int

Notes:
Datatype `usint` (Unsigned small int) consists on 1 byte in the PLC.
Expand Down Expand Up @@ -749,7 +826,8 @@ class DB:

def __init__(self, db_number: int, bytearray_: bytearray,
specification: str, row_size: int, size: int, id_field: Optional[str] = None,
db_offset: Optional[int] = 0, layout_offset: Optional[int] = 0, row_offset: Optional[int] = 0, area: Optional[Areas] = Areas.DB):
db_offset: Optional[int] = 0, layout_offset: Optional[int] = 0, row_offset: Optional[int] = 0,
area: Optional[Areas] = Areas.DB):
""" Creates a new instance of the `Row` class.

Args:
Expand Down Expand Up @@ -844,14 +922,14 @@ class DB_Row:
_specification: OrderedDict = OrderedDict() # row specification

def __init__(
self,
bytearray_: bytearray,
_specification: str,
row_size: Optional[int] = 0,
db_offset: int = 0,
layout_offset: int = 0,
row_offset: Optional[int] = 0,
area: Optional[Areas] = Areas.DB
self,
bytearray_: bytearray,
_specification: str,
row_size: Optional[int] = 0,
db_offset: int = 0,
layout_offset: int = 0,
row_offset: Optional[int] = 0,
area: Optional[Areas] = Areas.DB
):
"""Creates a new instance of the `DB_Row` class.

Expand Down Expand Up @@ -1012,7 +1090,7 @@ def get_value(self, byte_index: Union[str, int], type_: str) -> Union[ValueError
# add these three not implemented data typ to avoid
# 'Unable to get repr for class<snap7.util.DB_ROW>' error
elif type_ == 'TIME':
return 'read TIME not implemented'
return get_time(bytearray_, byte_index)

elif type_ == 'DATE':
return 'read DATE not implemented'
Expand Down Expand Up @@ -1081,6 +1159,9 @@ def set_value(self, byte_index: Union[str, int], type: str, value: Union[bool, s
if type == 'SINT' and isinstance(value, int):
return set_sint(bytearray_, byte_index, value)

if type == 'TIME' and isinstance(value, str):
return set_time(bytearray_, byte_index, value)

raise ValueError

def write(self, client: Client) -> None:
Expand Down
23 changes: 21 additions & 2 deletions test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@
21 testint2 INT
23 testDint DINT
27 testWord WORD
29 tests5time S5TIME
29 testS5time S5TIME
31 testdateandtime DATE_AND_TIME
43 testusint0 USINT
44 testsint0 SINT
46 testTime TIME
"""

test_spec_indented = """
Expand Down Expand Up @@ -70,11 +71,13 @@
# https://support.industry.siemens.com/cs/document/36479/date_and_time-format-bei-s7-?dti=0&lc=de-DE
254, 254, 254, 254, 254, 127, # test small int
128, # test set byte
143, 255, 255, 255 # test time
])

_new_bytearray = bytearray(100)
_new_bytearray[41:41 + 1] = struct.pack("B", 128) # byte_index=41, value=128, bytes=1
_new_bytearray[42:42 + 1] = struct.pack("B", 255) # byte_index=41, value=255, bytes=1
_new_bytearray[43:43 + 4] = struct.pack("I", 286331153) # byte_index=43, value=286331153(T#3D_7H_32M_11S_153MS), bytes=4


class TestS7util(unittest.TestCase):
Expand All @@ -100,7 +103,7 @@ def test_get_s5time(self):

row = util.DB_Row(test_array, test_spec, layout_offset=4)

self.assertEqual(row['tests5time'], '0:00:00.100000')
self.assertEqual(row['testS5time'], '0:00:00.100000')

def test_get_dt(self):
"""
Expand All @@ -112,6 +115,22 @@ def test_get_dt(self):

self.assertEqual(row['testdateandtime'], '2020-07-12T17:32:02.854000')

def test_get_time(self):
"""
TIME extraction from bytearray
"""
test_array = bytearray(_bytearray)

row = util.DB_Row(test_array, test_spec, layout_offset=4)

self.assertEqual(row['testTime'], '-21:17:57:28.193')

def test_set_time(self):
test_array = bytearray(_new_bytearray)
util.set_time(test_array, 43, '3:7:32:11.153')
byte_ = util.get_time(test_array, 43)
self.assertEqual(byte_, '3:7:32:11.153')

def test_get_string(self):
"""
Text extraction from string from bytearray
Expand Down