bnjmnp / pysoem

Cython wrapper for the Simple Open EtherCAT Master Library
MIT License
101 stars 37 forks source link

how `send_processdata()` and `receive_processdata()` work? #41

Open zthcool opened 3 years ago

zthcool commented 3 years ago

Hi @bnjmnp : This is my first time contact with pysoem and soem,In the past few days,I have built communication with the driver (Panasonic),and make it run to a position successful with sdo_read() and sdo_write().But the basic_example.py and orther issues is all about send_processdata() and receive_processdata().and I found the command is in while cycle,how do I communicate with driver when the cycle began?and what's the different between sdo_read()sdo_write() written only and send_processdata()receive_processdata() used? Maybe my question is junior,but it's helpful for me. Thank you for taking your time.

bnjmnp commented 3 years ago

Hi @zthcool,

send_processdata() and receive_processdata() take care of what we could call PDO communication.

But let's start with SDO communication. You already used SDO communication to access objects within the object dictionary. The objects are addressed via index and subindex. SDO communication is based on the CoE protocol which is defined on top of the EtherCAT protocol. SDO stands for service data object, what means it is mainly used for setup and configuration. In SDO communication you have a request and a reply, thus you can have error information in the reply, for example if an invalid value is written to an object. SDO communication is not very deterministic, for example if you command a new position for your driver with it, you don't exactly know when the motor is starting to move. For some applications this might be sufficient, but EtherCAT is all about real time, where you can have very exact timing. For this PDO communication comes into play. PDO communication is running cyclically at a fixed cycle time. On wikipedia there is a nice animation that demonstrates the cycle of the PDO frame. When in the animation the frame leaves the master send_processdata() was called and receive_processdata() is called to collect the returning frame. To have a stable cycle time, we put the call to send- and receive_processdata into a parallel (high prioritized) thread. In the context of the main function we update the data send to the devices, or read out the data the a device put into the frame. Usually the update of the PDO data in the main should be sychronized to the call of send_processdata() somehow, but this is not done the the examples. The structure of the PDO data of a device is defined by some special CoE objects (0x1C12+0x16xx and 0x1C13+0x1Axx), for some devices the structure can be change (by changing the special objects) during the setup phase. And in the end PDO data itself are CoE objects, but in PDO communication the objects are accessed much faster. This is why you can control the position via SDO.

Sounds all complicated but once you get it, it is not that complicated at all.

zthcool commented 3 years ago

@bnjmnp thank you very much about the explanation,I really appreciate.Here is my understanding and application. For example,SDO just like I got an address about a shop,I can get what I want from that. PDO is something like train and station,send_processdata() and receive_processdata is start to run the train, the data can get in and get out on their station.But which data should up and down is what I am confusing.After searched,I think the map from SDO to PDO has decided which data should up or down on the station. With the understanding above,I modified and run the basic_example.py,I added

`

    if self._master.state == pysoem.OP_STATE:
        output_data = OutputPdo()
        output_data.modes_of_operation = modes_of_operation['Profile velocity mode']
        self._master.slaves[0].output = bytes(output_data)
        print('mode op display:',
              bin(int.from_bytes(self._master.slaves[0].sdo_read(operation_mode_disp[0], operation_mode_disp[1]), 'little')))
        print(binascii.b2a_hex(self._master.slaves[0].sdo_read(operation_mode_disp[0], operation_mode_disp[1])))

        output_data.target_velocity = 500
        print("target velocity set as:", output_data.target_velocity)
        for control_cmd in [6, 7, 15]:
            print('control_cmd:', control_cmd)
            output_data.controlword = control_cmd
            self._master.slaves[0].output = bytes(output_data)
            self._master.send_processdata()
            self._master.receive_processdata(1_0000)
            time.sleep(0.01)
            # bin(int.from_bytes(r_data, 'little')
            print('controlword:', bin(int.from_bytes(self._master.slaves[0].sdo_read(ctrl_word[0], ctrl_word[1]), 'little')))
            print('statusword:', bin(int.from_bytes(self._master.slaves[0].sdo_read(statusword[0], statusword[1]), 'little')))

`

the code in the _pdo_update_loop().this code and related definitions is copied from #30,other definitions as follows: operation_mode_disp is define as operation_mode_disp = [0x6061, 0x00, 8, 'SINT'], ctrl_word is define as ctrl_word = [0x6040, 0x00, 16, 'UINT']. statusword is define as statusword = [0x6041, 0x00, 16, 'UNIT']. Here is the print:

mode op display: 0b1
b'01'
target velocity set as: 500
control_cmd: 6
controlword: 0b11000000011
statusword: 0b11001110000
control_cmd: 7
controlword: 0b11100000011
statusword: 0b11001110000
control_cmd: 15
controlword: 0b111100000011
statusword: 0b11001110000

It seems like the PDO is not working,all the output is failed,the master.state is 8,so It's in op mode. By analyzing the above results,I think the map is not configured,so the train is run but don't know which data should up and down.Then I noticed the el1259_setup() function.Is this function define the map?and self._master.config_map() is to write the map? If my understanding is wrong, Could you please correct it ,and show some example about how to select map and custom map? Thank you again.

zthcool commented 3 years ago

Here is the code I am trying

import sys
import struct
import time
import threading
import ctypes
from collections import namedtuple
import binascii
import pysoem

statusword = [0x6041, 0x00, 16, 'UNIT']
operation_mode_disp = [0x6061, 0x00, 8, 'SINT']
ctrl_word = [0x6040, 0x00, 16, 'UINT']

class InputPdo(ctypes.Structure):
    _pack_ = 1
    _fields_ = [
        ('modes_of_operation_display', ctypes.c_int8),
        ('statusword', ctypes.c_int32),
        ('position_demand_value', ctypes.c_int32),
        ('position_actual_value', ctypes.c_int32),
        ('velocity_demand_value', ctypes.c_int32),
        ('velocity_actual_value', ctypes.c_int32),
        ('torque_demand_value', ctypes.c_int32),
        ('torque_actual_value', ctypes.c_int32),
        ('digital_input', ctypes.c_int32)
    ]

class OutputPdo(ctypes.Structure):
    _pack_ = 1
    _fields_ = [
        ('modes_of_operation', ctypes.c_int8),
        ('controlword', ctypes.c_int16),
        ('target_position', ctypes.c_int32),
        ('target_velocity', ctypes.c_int32),
        ('target_torque', ctypes.c_int32),
        ('digital_output', ctypes.c_int32)
    ]

modes_of_operation = {
    'No mode': 0,
    'Profile position mode': 1,
    'Profile velocity mode': 3,
    'Homing mode': 6,
    'Cyclic synchronous position mode': 8,
    'Cyclic synchronous velocity mode': 9,
    'Cyclic synchronous torque mode': 10,
}

class BasicExample:
    BECKHOFF_VENDOR_ID = 0x0000066F
    MADLT15BF_PRODUCT_CODE = 0x613C0005
    MADLT25BF_PRODUCT_CODE = 0x613C0006
    # EK1100_PRODUCT_CODE = 0x044c2c52
    # EL3002_PRODUCT_CODE = 0x0bba3052
    # EL1259_PRODUCT_CODE = 0x04eb3052

    def __init__(self, ifname):
        self._ifname = ifname
        self._pd_thread_stop_event = threading.Event()
        self._ch_thread_stop_event = threading.Event()
        self._actual_wkc = 0
        self._master = pysoem.Master()
        self._master.in_op = False
        self._master.do_check_state = False
        SlaveSet = namedtuple('SlaveSet', 'name product_code config_func')
        # self._expected_slave_layout = {0: SlaveSet('EK1100', self.EK1100_PRODUCT_CODE, None),
        #                                1: SlaveSet('EL3002', self.EL3002_PRODUCT_CODE, None),
        #                                2: SlaveSet('EL1259', self.EL1259_PRODUCT_CODE, self.el1259_setup)}
        self._expected_slave_layout = {0: SlaveSet('MADLT25BF', self.MADLT25BF_PRODUCT_CODE, self.mbdlt25bf_setup),
                                       1: SlaveSet('MADLT15BF', self.MADLT15BF_PRODUCT_CODE, None),}

    def mbdlt25bf_setup(self, slave_pos):
        slave = self._master.slaves[slave_pos]
        rx_map_obj = [0x1600]
        rx_map_obj_bytes = struct.pack(
            'Bx' + ''.join(['H' for i in range(len(rx_map_obj))]), len(rx_map_obj), *rx_map_obj)
        print("rx_map_obj_bytes:", rx_map_obj_bytes)
        slave.sdo_write(0x1c12, 0, rx_map_obj_bytes, True)
        slave.dc_sync(1, 10000000)

    def el1259_setup(self, slave_pos):
        slave = self._master.slaves[slave_pos]

        slave.sdo_write(0x8001, 2, struct.pack('B', 1))

        rx_map_obj = [0x1603,
                      0x1607,
                      0x160B,
                      0x160F,
                      0x1611,
                      0x1617,
                      0x161B,
                      0x161F,
                      0x1620,
                      0x1621,
                      0x1622,
                      0x1623,
                      0x1624,
                      0x1625,
                      0x1626,
                      0x1627]
        rx_map_obj_bytes = struct.pack(
            'Bx' + ''.join(['H' for i in range(len(rx_map_obj))]), len(rx_map_obj), *rx_map_obj)
        slave.sdo_write(0x1c12, 0, rx_map_obj_bytes, True)

        slave.dc_sync(1, 10000000)

    def _processdata_thread(self):
        while not self._pd_thread_stop_event.is_set():
            self._master.send_processdata()
            self._actual_wkc = self._master.receive_processdata(10000)
            if not self._actual_wkc == self._master.expected_wkc:
                print('incorrect wkc')
            time.sleep(0.01)

    def _pdo_update_loop(self):

        # 实现的是切换从机的引脚输出,如果有 LED 灯,看到的效果是每隔 1s 点亮一次
        self._master.in_op = True

        output_len = len(self._master.slaves[0].output)
        print("self._master.slaves[0].output:", self._master.slaves[0].output)
        print("output_len:", output_len)
        tmp = bytearray([0 for i in range(output_len)])

        if self._master.state == pysoem.OP_STATE:
            output_data = OutputPdo()
            output_data.modes_of_operation = modes_of_operation['Profile velocity mode']
            self._master.slaves[0].output = bytes(output_data)
            print('mode op display:',
                  bin(int.from_bytes(self._master.slaves[0].sdo_read(operation_mode_disp[0], operation_mode_disp[1]), 'little')))
            print(binascii.b2a_hex(self._master.slaves[0].sdo_read(operation_mode_disp[0], operation_mode_disp[1])))

            output_data.target_velocity = 500
            print("target velocity set as:", output_data.target_velocity)
            for control_cmd in [6, 7, 15]:
                print('control_cmd:', control_cmd)
                output_data.controlword = control_cmd
                self._master.slaves[0].output = bytes(output_data)
                self._master.send_processdata()
                self._master.receive_processdata(1_0000)
                time.sleep(0.01)
                # bin(int.from_bytes(r_data, 'little')
                print('controlword:', bin(int.from_bytes(self._master.slaves[0].sdo_read(ctrl_word[0], ctrl_word[1]), 'little')))
                print('statusword:', bin(int.from_bytes(self._master.slaves[0].sdo_read(statusword[0], statusword[1]), 'little')))

        toggle = True
        try:
            while 1:
                if toggle:
                    tmp[0] = 0x00
                else:
                    tmp[0] = 0x02
                self._master.slaves[0].output = bytes(tmp)
                print("bytes(tmp):", bytes(tmp))
                toggle ^= True  # 取反,如果上一次的值是 True ,则这次的值为 False

                time.sleep(1)

        except KeyboardInterrupt:
            # ctrl-C abort handling
            print('stopped')

    def run(self):

        # 打开网卡地址
        self._master.open(self._ifname)

        # 检查是否存在从机
        if not self._master.config_init() > 0:
            self._master.close()
            raise BasicExampleError('no slave found')

        for i, slave in enumerate(self._master.slaves):  # 枚举从机
            # 判断从机的制造商和产品码是否符合
            if not ((slave.man == self.BECKHOFF_VENDOR_ID) and
                    (slave.id == self._expected_slave_layout[i].product_code)):
                print("slave.man:", slave.man)
                print("self.BECKHOFF_VENDOR_ID:", self.BECKHOFF_VENDOR_ID)
                print("slave.id:", slave.id)
                print("self._expected_slave_layout[i].product_code:", self._expected_slave_layout[i].product_code)
                self._master.close()
                raise BasicExampleError('unexpected slave layout')
            # 读取从机的配置
            slave.config_func = self._expected_slave_layout[i].config_func
            slave.is_lost = False
        print('-01_master.state:', self._master.state)
        # 在IO映射中映射所有从属PDO
        self._master.config_map()
        print('00_master.state:', self._master.state)
        # 检查从机实际状态是否在 SAFEOP_STATE 状态下
        if self._master.state_check(pysoem.SAFEOP_STATE, 50000) != pysoem.SAFEOP_STATE:
            self._master.close()
            raise BasicExampleError('not all slaves reached SAFEOP state')

        print('01_master.state:', self._master.state)
        # 可用于检查所有从属设备是否处于工作状态,或为所有从属设备请求新状态。
        self._master.state = pysoem.OP_STATE

        # 状态监测进程,实时监测所有从机是否在 op 状态,如果不是则打印出检测结果
        check_thread = threading.Thread(target=self._check_thread)
        check_thread.start()
        # 过程数据处理进程 ========================= ?? 2021-10-13不明白是如何处理数据的 ?? ===============
        proc_thread = threading.Thread(target=self._processdata_thread)
        proc_thread.start()

        # send one valid process data to make outputs in slaves happy
        self._master.send_processdata()
        self._master.receive_processdata(2000)
        # request OP state for all slaves

        # 写入所有从机状态
        self._master.write_state()
        print('02_master.state:', self._master.state)
        # 先将所有从机状态标志符号设置为 false,通过后续检测是否所有从机真的处于 op 状态,再将标识符置为 true
        all_slaves_reached_op_state = False
        for i in range(40):
            self._master.state_check(pysoem.OP_STATE, 50000)
            if self._master.state == pysoem.OP_STATE:
                all_slaves_reached_op_state = True
                break
        print('03_master.state:', self._master.state)

        # 如果所有的从机状态均在 op 则主进程开始 pdo 循环
        if all_slaves_reached_op_state:
            self._pdo_update_loop()

        self._pd_thread_stop_event.set()
        self._ch_thread_stop_event.set()
        proc_thread.join()
        check_thread.join()
        self._master.state = pysoem.INIT_STATE
        # request INIT state for all slaves
        self._master.write_state()
        self._master.close()

        if not all_slaves_reached_op_state:
            raise BasicExampleError('not all slaves reached OP state')

    @staticmethod
    def _check_slave(slave, pos):
        if slave.state == (pysoem.SAFEOP_STATE + pysoem.STATE_ERROR):
            print(
                'ERROR : slave {} is in SAFE_OP + ERROR, attempting ack.'.format(pos))
            slave.state = pysoem.SAFEOP_STATE + pysoem.STATE_ACK
            slave.write_state()
        elif slave.state == pysoem.SAFEOP_STATE:
            print(
                'WARNING : slave {} is in SAFE_OP, try change to OPERATIONAL.'.format(pos))
            slave.state = pysoem.OP_STATE
            slave.write_state()
        elif slave.state > pysoem.NONE_STATE:
            if slave.reconfig():
                slave.is_lost = False
                print('MESSAGE : slave {} reconfigured'.format(pos))
        elif not slave.is_lost:
            slave.state_check(pysoem.OP_STATE)
            if slave.state == pysoem.NONE_STATE:
                slave.is_lost = True
                print('ERROR : slave {} lost'.format(pos))
        if slave.is_lost:
            if slave.state == pysoem.NONE_STATE:
                if slave.recover():
                    slave.is_lost = False
                    print(
                        'MESSAGE : slave {} recovered'.format(pos))
            else:
                slave.is_lost = False
                print('MESSAGE : slave {} found'.format(pos))

    def _check_thread(self):
        # master.expected_wkc: 计算预期的工作计数器(Calculates the expected Working Counter)
        while not self._ch_thread_stop_event.is_set():
            # 如果在op状态并且实际的工作计数器小于期望的计数器或者正在检查状态
            if self._master.in_op and ((self._actual_wkc < self._master.expected_wkc) or self._master.do_check_state):
                self._master.do_check_state = False
                # 读取所有从机的状态,返回找到的最低的状态
                self._master.read_state()
                #
                for i, slave in enumerate(self._master.slaves):  # 枚举所有的从站
                    # 如果从站的状态不在 op 状态,就检查是否在其他状态,并根据状态打印检测的结果
                    if slave.state != pysoem.OP_STATE:
                        self._master.do_check_state = True
                        BasicExample._check_slave(slave, i)
                # 如果没有触发状态检测,则打印所有从机均在 op 状态
                if not self._master.do_check_state:
                    print('OK : all slaves resumed OPERATIONAL.')
            time.sleep(0.01)

class BasicExampleError(Exception):
    def __init__(self, message):
        super(BasicExampleError, self).__init__(message)
        self.message = message

if __name__ == '__main__':

    print('basic_example started')

    ifname = '\\Device\\NPF_{34D52289-95B1-43E5-9D94-FCCD0AB38F92}'

    try:
        BasicExample(ifname).run()
    except BasicExampleError as expt:
        print('basic_example failed: ' + expt.message)
        sys.exit(1)

When I set the mbdlt25bf_setup() function I got error as follows:

basic_example started
-01_master.state: 0
rx_map_obj_bytes: b'\x01\x00\x00\x16'
Traceback (most recent call last):
  File "D:/Users/XH/PycharmProjects/pyserial/pysoem_Panasonic_A6_main.py", line 485, in <module>
    BasicExample(ifname).run()
  File "D:/Users/XH/PycharmProjects/pyserial/pysoem_Panasonic_A6_main.py", line 337, in run
    self._master.config_map()
  File "pysoem\pysoem.pyx", line 220, in pysoem.pysoem.CdefMaster.config_map
  File "pysoem\pysoem.pyx", line 964, in pysoem.pysoem._xPO2SOconfig
  File "D:/Users/XH/PycharmProjects/pyserial/pysoem_Panasonic_A6_main.py", line 226, in mbdlt25bf_setup
    slave.sdo_write(0x1c12, 1, rx_map_obj_bytes, True)
  File "pysoem\pysoem.pyx", line 608, in pysoem.pysoem.CdefSlave.sdo_write
  File "pysoem\pysoem.pyx", line 732, in pysoem.pysoem.CdefSlave._raise_exception
pysoem.pysoem.PacketError: (1, 1)
bnjmnp commented 3 years ago

I'm a little bit lost with all your replies. I would suggest you to start with the default PDO configuration. So don't provide a config function (e.g. mbdlt25bf_setup) at all. Just find the default PDO configuration in the ESI file of your drive and align it with the definition of the InputPdo and OutputPdo classes. Here I tried to explain where to find the default PDO configuration: #30