bnjmnp / pysoem

Cython wrapper for the Simple Open EtherCAT Master Library
MIT License
95 stars 36 forks source link

Help needed with reading SDO & Uploading SDO map #121

Closed Michaelicious closed 7 months ago

Michaelicious commented 9 months ago

I'm having 2 issues:

  1. Reading/Writing SDO
  2. Uploading XML file with SDO map / Reading object dictionary.

I'm trying to read/write SDO's. can find a documentation on the proper way to initialize a network.

master = ecMaster()
con_thread = master.ecConnection().join()    # @threaded ---- > self.device_count = self.init_config()
print(master.device_count > 0)
if master.device_count > 0 :
    # s2 = master.slaves[0].dc_sync(True, 1000000)
    s1 = master.config_dc()
    s2 = master.config_map()
    print(s1,',',s2)
    od = master.slaves[0].od

raises SdoInfoError: Sdo List Info read failed due to od = master.slaves[0].od

Without calling OD, Sometimes master.config_map() raises ConfigMapError: [MailboxError(1, 19, 'Unknown'), MailboxError(1, 19, 'Unknown'), MailboxError(1, 19, 'Unknown'), ...

Hard reboot of the slave solves the ConfigMapError, and I'm able to read an SDO as following:

print(f'6040 = {sdo}')
sdo = master.slaves[0].sdo_read(0x6060, 0)
print(f'6040 decode\n {sdo_decode}')
sdo_decode = struct.unpack('<' + 'I' * (len(sdo) // 4), sdo)
# using sdo_decode = struct.unpack('I', sdo)  is raising error: unpack requires a buffer of 4 bytes

output:

6040 sdo = 
b'\xd82\xa2Y\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\xcb\x1e\xfe\xb0\x00\x00\x000\xee\xe5Y\xa0\x02\x00\x00@eWV\xa0\x02\x00\x00\xd0HK\xed\xff\x7f\x00\x00\xd82\xa2Y\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\xcb\x1e\xfe\xb0\x00\x00\x000\xee\xe5Y\xa0\x02\x00\x00\xc0EpX\xa0\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\xabR\xa0\x02\x00\x00@0\xa2Y\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xabR\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x04\xa0Y\xa0\x02\x00\x00\xd1XK\xed\xff\x7f\x00\x00@\x03\xabR\xa0\x02\x00\x00\x00"@\x00\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x7f\x00\x000\xbc\x1bY\xa0\x02\x00\x00\xbb\xc0\x94o\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[\xf0\xea\xea\xff\x7f\x00\x00p\xca\x1e\xfe\xb0\x00\x00\x00p\x9b\xe9Y\xa0\x02\x00\x00\x90\xbb\x94o\xff\x7f\x00\x00\x18\x99\xf5Y\xa0\x02\x00\x00'

6040 decode:
 (1503802072, 672, 0, 0, 4263431008, 176, 1508240944, 672, 1448568128, 672, 3981134032, 32767, 1503802072, 672, 0, 0, 4263431008, 176, 1508240944, 672, 1483752896, 672, 1, 0, 1386938624, 672, 1503801408, 672, 0, 0, 0, 0, 1386938368, 672, 0, 0, 1503659104, 672, 3981138129, 32767, 1386939200, 672, 4203008, 672, 0, 0, 0, 32767, 1494989872, 672, 1872019643, 32767, 0, 0, 3941265499, 32767, 4263430768, 176, 1508481904, 672, 1872018320, 32767, 1509267736, 672)

so sdo_decode is after converting from bytes. BUT ITS AN ETHERNET FRAME.

  1. How to extract the actual SDO data? is that the proper way of handling it ?
  2. What's master.send_processdata() and master.recieve_processdata() ? how to use them ?
  3. And what's up with the 4 bit error ? how do I verify I decoded the frame correctly ?
  4. Is there a way to upload an SDO map XML file ?
  5. how to properly write_sdo() in my case ? (since none of the examples addresses the 4 bit buffer error )

I'm stuck on it for days now, I would love some brief explanation on setting up an eCat network properly. Help would be very helpful.

bnjmnp commented 9 months ago

I'm trying to read/write SDO's. can find a documentation on the proper way to initialize a network.

One (proper) way of initializing a network is showcased in the examples/basic_example.py, look for the run() function in there. The cause for the SdoInfoError, I don't understand for now, but maybe you don't need it at all. Did you try to add a little delay (via time.sleep()) between config_dc() and config_map() to get rid of the ConfigMapError?

Coming to the sdo_read(), you probably need to do the unpack like this:

sdo_decode = struct.unpack('b', sdo)

Object 0x6060 is probably just a two byte integer (SINT) therefore you need the b as format character. (You also mixed up you code with 0x6040 a bit) Did you also read through the Reading and Writing CoE Objects section in the readthedocs documentation?

  1. What's master.send_processdata() and master.recieve_processdata() ? how to use them ?

master.send_processdata() is used to send the PDO EtherCAT frame (that contains output PDO data) into the network, and master.recieve_processdata() is used to wait for frame to return (and it copies the input PDO data into a buffer). Check out this video https://youtu.be/z2OagcHG-UU?si=0f4zRq4R60QrrOFI, master.send_processdata() makes the "telegram" leave the master, and master.recieve_processdata() collects the retuning "telegram".

  1. And what's up with the 4 bit error ? how do I verify I decoded the frame correctly ?

This is about the 4 byte error, we might have already solved with the b as format character?

  1. Is there a way to upload an SDO map XML file ?

No. But I admit it would be nice if reading-in the object directory from the XML file would be possible, to reduce the trouble converting bytes returned by sdo_read() to a equivalent Python data type.

If you are about to operate a servo drive, check out #89 there I point to other issues that should contain valuable information and reference code.

Michaelicious commented 9 months ago

Thank you for your quick response, you helped me a lot. SdoInfoError error was due to my slave wasn't working with CIA 402 apparently. once I switched to other type of slave, it worked and i got the slave.od

Another question, how could I verify connection if I'm working in a ring topography ? (Master will be connected to slave 's 1 input, and slave's N output ). My only plan currently is to open both interfaces, doing dc_config() and hope it will get it by itself - verifying with wireshark. Is there something i've missed ? @bnjmnp

Michaelicious commented 8 months ago

Sharing in case it will help anyone. for helping out with SDO decoding, based on read_sdo_info.py, i've created custom read/write functions on a customSlave class.

from ecat_helpers import _convert_to_binary, _convert_from_binary
from ethercat_helpers import _convert_to_binary, _convert_from_binary

class customSlave():
    def __init__(self,slave, slave_pos = None):
        self.my_slave = slave
        self.slave_pos = slave_pos      #Probably not needed
        self.my_od = {}

    def _getObjectDictionary(self,try_count=0): 
        self.my_od = {}
        try:
            temp_od = self.my_slave.od
            print('OD Found')
        except pysoem.SdoInfoError as e:
            if try_count < 10:
                print(f'SdoInfoError {try_count}:\t{e}')
                try_count += 1
                self._getObjectDictionary(try_count)
        else:
            print(self.my_slave.name+'Creating OD')
            for obj in temp_od:
                self.my_od[obj.index] = obj                    
            print(self.my_slave.name+'OD Created')

    def _sdo_data_from_od(self,index,subindex):
        if not self.my_od:
            self._getObjectDictionary()

        coe_obj = self.my_od[index]

        if coe_obj.entries :
            data_type = coe_obj.entries[subindex].data_type
            bit_length = coe_obj.entries[subindex].bit_length
        else :
            data_type = coe_obj.data_type
            bit_length = coe_obj.bit_length

        return data_type, bit_length

    def readSDO(self,index,subindex):   # Read & Decode SDO from slave
        sdo = self.my_slave.sdo_read(index,subindex)
        # data from dictionary for decoding
        data_type, bit_length = self._sdo_data_from_od(index,subindex)
        return _convert_from_binary(sdo,data_type,bit_length)

    def writeSDO(self,index,subindex,value):    #  Decode & Write SDO to slave
        # data from dictionary for decoding
        data_type, bit_length = self._sdo_data_from_od(index,subindex)
        # data to binary
        binary_data = _convert_to_binary(value,data_type,bit_length)
        # Write SDO
        try: 
            self.my_slave.sdo_write(index,subindex,binary_data)
        except pysoem.SdoError: print('SDO write error')
        else: print(f'SDO ({index},{subindex}) wrote {value} successfuly')

than I'm using following functions from ethercat_helpers.py to make read & write more easy.

import ctypes

def _choose_ctypes_object(data_type, bitsize):
    """ Returns a ctypes object based on the data type and bitsize from slave.od
        dataType  Ctype object
            1       only 2 sync object subindexes 32
            2       INT8
            3       INT16
            4       DINT / INT32
            5       UINT8
            6       UINT16
            7       UINT32
            8       None has been found
            9       String / c_char_p
            32      has enteries
            42      has enteries
"""
    data_type = int(data_type)
    bitsize = int(bitsize)
    match (data_type, bitsize):
        case (2, 8):            # INT8
            return ctypes.c_int8
        case (3, 16):           # INT16
            return ctypes.c_int16
        case (4, 32):           # DINT / INT32
            return ctypes.c_int32
        case (5, 8):            # UINT8
            return ctypes.c_uint8
        case (6, 16):           # UINT16
            return ctypes.c_uint16
        case (7, 32):           # UDINT
            return ctypes.c_uint32
        case (9, _):            # VISIBLE_STRING
            return ctypes.c_char_p
        case _:
            raise ValueError("Unsupported data type or bitsize")  

def _convert_from_binary(binary_data, data_type, bitsize):
    ctypes_type = _choose_ctypes_object(data_type, bitsize)
    if (data_type != 9): data = ctypes_type.from_buffer_copy(binary_data).value
    else: data = ctypes_type(binary_data).value.decode() # Decode getting rid of b'...'
    return data

def _convert_to_binary(data, data_type, bitsize):
    ctypes_type = _choose_ctypes_object(data_type, bitsize)
    binary_data = bytes(ctypes_type(data))
    return binary_data