gijzelaerr / python-snap7

A Python wrapper for the snap7 PLC communication library
http://python-snap7.readthedocs.org/
MIT License
661 stars 246 forks source link

db_read and read_area return type #29

Closed lancel0 closed 10 years ago

lancel0 commented 10 years ago

The db_read and read_area functions use a fixed return type which is a sequence of c_int8. This is done via the wordlen_to_ctypes map.

wordlen_to_ctypes = ADict({
    S7WLBit: ctypes.c_int16,
    S7WLByte: ctypes.c_int8,
    S7WLWord: ctypes.c_int16,
    S7WLDWord: ctypes.c_int32,
    S7WLReal: ctypes.c_int32,
    S7WLCounter: ctypes.c_int16,
    S7WLTimer: ctypes.c_int16,
})

And then in the function

wordlen = snap7.snap7types.S7WLByte
type_ = snap7.snap7types.wordlen_to_ctypes[wordlen]
data = (type_ * size)()
result = self.library.Cli_ReadArea(self.pointer, area, dbnumber, start, size, wordlen, byref(data))

data is passed by reference to the library to be populated with data from the PLC

The size parameter is then used to determine how many c_int8 units to read.

It might be better to use c_uint8 as the data type so that a sequence of bytes is returned instead of signed integers. I think that the two functions are basically reading a series of bytes from a specific area starting at a given offset so returning something that is analogous to bytes instead of signed integers seems to better match the purpose.

I am not sure if the right thing to do is to modify each function that should return a sequence of bytes or to modify the wordlen_to_ctypes map so that S7WLByte points to c_uint8. Also would it then make sense that S7WLWord and S7WLDWord be changed to point at unsigned types? I haven't read through much of the library so I don't know what all of the use cases for wordlen_to_ctypes are.

Also it appears that in the Snap7 library that WordLen is not the actual word length but used an indicator of the S7 Data Type that is being read or written.

Here is an example from the Snap7 library where it converts to a WordLen value to a the size in bytes for that data type

found in s7_micro_client.cpp

switch (WordLength){
    case S7WLBit     : return 1;  // S7 sends 1 byte per bit
    case S7WLByte    : return 1;
    case S7WLChar    : return 1;
    case S7WLWord    : return 2;
    case S7WLDWord   : return 4;
    case S7WLInt     : return 2;
    case S7WLDInt    : return 4;
    case S7WLReal    : return 4;
    case S7WLCounter : return 2;
    case S7WLTimer   : return 2;
    default          : return 0;
}

Here is a modified version of read_area I have been using test against a S7-319 CPU reading REALs and INTs.

def read_area(self, area, dbnumber, start, size):
    """This is the main function to read data from a PLC.
    With it you can read DB, Inputs, Outputs, Merkers, Timers and Counters.

    :param dbnumber: The DB number, only used when area= S7AreaDB
    :param start: offset to start reading
    :param size: number of bytes to read
    """
    assert area in snap7.snap7types.areas.values()
    wordlen = snap7.snap7types.S7WLByte
    logging.debug("reading area: %s dbnumber: %s start: %s: amount %s: "
                  "wordlen: %s" % (area, dbnumber, start, size, wordlen))
    data = (c_uint8 * size)()
    result = self.library.Cli_ReadArea(self.pointer, area, dbnumber, start,
                                       size, wordlen, byref(data))
    check_error(result, context="client")
    return data

and here is how I am using it

result = client.read_area(S7AreaDB, 200, 16, 4)
bytes = ''.join([chr(x) for x in result])
real_num = struct.unpack('>f', bytes)
print(real_num)

result = client.read_area(S7AreaDB, 200, 2, 2)
bytes = ''.join([chr(x) for x in result])
int_num = struct.unpack('>h', bytes)
print(int_num)
spreeker commented 10 years ago

Thank for your research and comment lance!!

I don't think it matters much how you read out data. As long as you get the correct amount of bytes!!

BUT. looked up some official documentation

https://www.automation.siemens.com/WW/forum/guests/PostShow.aspx?HTTPS=REDIR&PageIndex=1&PostID=341187&Language=en

INT is a 16 bit value. A signed integer with a value range of -32768 to 32767

INT (Integer) 16 Decimal number signed -32768 to 32767 a then using u_int would not work or will give you wrong values if the value is actually negative.

cheers

Stephan.

lancel0 commented 10 years ago

Thanks for looking over my research Stephen.

I missed the call to bytearray at the end of the db_read function which ensures the returned data is converted to a sequence of bytes. It also explains why making the same read using area_read was failing when trying to read a REAL.

result = client.db_read(200, 16, 4)
log.debug(str(type(result)))
for x in result:
    log.debug(str(x))
bytes = ''.join([chr(x) for x in result])
real_num = struct.unpack('>f', bytes)
log.debug(real_num)

result = client.read_area(S7AreaDB, 200, 16, 4)
log.debug(str(type(result)))
for x in result:
    log.debug(str(x))
bytes = ''.join([chr(x) for x in result])
real_num = struct.unpack('>f', bytes)
log.debug(real_num)

would give the following result

DEBUG:snap7.client:db_read, db_number:200, start:16, size:4
DEBUG:root:<type 'bytearray'>
DEBUG:root:70
DEBUG:root:119
DEBUG:root:128
DEBUG:root:0
DEBUG:root:(15840.0,)
DEBUG:root:reading area: 132 dbnumber: 200 start: 16: amount 4: wordlen: 2
DEBUG:root:<class 'snap7.client.c_byte_Array_4'>
DEBUG:root:70
DEBUG:root:119
DEBUG:root:-128
DEBUG:root:0
Traceback (most recent call last):
  File "/home/llambert/Projects/python-snap7_clean/example/test.py", line 28, in <module>
    bytes = ''.join([chr(x) for x in result])
ValueError: chr() arg not in range(256)

Regarding the mapping from Step7 types (wordlen) to ctypes I was using the Siemens documentation here:

http://www.automation.siemens.com/doconweb/pdf/SINUMERIK_SINAMICS_10_2012_E/S7P.pdf?p=1

On page 589 is a table of the elementary data types and their valid values.

spreeker commented 10 years ago

I've only used read_db myself when interacting with a PLC. A fix for this problem should be trivial!

lancel0 commented 10 years ago

I have only tried to read data blocks from an actual PLC, I was using the read_area function only because I was exploring the library.

lancel0 commented 10 years ago

Disregard my commit on this one, I just saw that stephen created a pull request with a fix.

gijzelaerr commented 10 years ago

so if i understand correctly this issue can be closed?

lancel0 commented 10 years ago

I think this was fixed in this pull request :

https://github.com/gijzelaerr/python-snap7/pull/31

gijzelaerr commented 10 years ago

ok, then I close this bug report. Please reopen if the problem is not solved.

merrdoc commented 6 years ago

guys, hi. I just saw your ld pots. I wonder is there any way to read symbol names from the s7400-s71500 CPU via snap7 on raspberry pi3