Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

kuka_experimental/kuka_eki_hw_interface for KRC2 #196

Closed
Levaru opened this issue Sep 20, 2021 · 6 comments
Closed

kuka_experimental/kuka_eki_hw_interface for KRC2 #196

Levaru opened this issue Sep 20, 2021 · 6 comments

Comments

@Levaru
Copy link

Levaru commented Sep 20, 2021

Hi,
we have loaned a robot to test our software but we were only able to get an old one with the KRC2 controller.
I successfully installed the KUKA Ethernet KRL Interface software (Version 1.2) but encountered problems when trying to implement kuka_experimental/kuka_eki_hw_interface.

It seems that the kuka_eki_hw_interface was written with KUKA.Ethernet KRL 2.x which has a different xml structure and commands compared to 1.2.

Here are some things that are different in 1.2:

  • functions don't return eki_status but rather an integer
  • function names begin with EKX instead of EKI
  • EKI_INIT, EKI_Clear and EKI_CheckBuffer is missing
  • The structure of EkiHwInterface.xml is divided into EkiHwInterface.xml, EkiHwInterface+.xml and XmlApiConfig.xml

I'm really lost concerning the last point. I was able to rename most of the functions but I'm not sure how to write those three files. I'm guessing that I'll also have to rewrite some of the source code for the kuka_eki_hw_interface ROS-node to make it compatible with the old version.

Does someone have any pointers on how to properly adjust the files for EKI V1.2?

@gavanderhoorn
Copy link
Member

I'm (personally) not aware of anyone having performed this migration/port.

You could see whether any of the forks contain the changes you need.

@Levaru
Copy link
Author

Levaru commented Sep 28, 2021

I managed to port it but it required some workarounds. The two most important changes are:

  1. Changing the socket in the ROS-Program from a UDP-Client to a TCP-Server. I'm actually not sure if TCP is absolutely required but that's what the documentation says.
  2. The 1.2 version doesn't have the EKI_CheckBuffer() function, so I had to implement a command counter that gets updated once the roboter reads the next command. Here I'm also not sure if this is the correct solution, because I get some very choppy/laggy movement.

Another thing that really confuses me are the EKX_Get***Element() functions. Parameter 1 of these functions has the following description:

INTEGER
Determines whether the next value or the current
value is to be read from the buffer
INT=0: the next value is read (First In First Out)
INT=1: the current value is read (Last In First Out)

At first I had this set to 0 since I wouldn't want to miss any commands from the buffer right? This led to some strange movement behaviour, where the robot would oscillate during the movement by moving forward and then back a little bit, before executing the rest of the command normally. Some other times the robot would move 80% of the trajectory, then got back to the first 20%, then again forward to 80% and would repeat this multiple times, before finally reaching the target. After setting the parameter to 1 I didn't encounter this anymore, aside from some very choppy/laggy movement.

Here are my changes, could somebody please make a sanity check?

EkiHwInterface.xml

<Elements>
  <Element Tag="RobotCommand" Type="STRUCTTAG" Stacksize="5" />
    <Element Tag="RobotCommand.ID" Type="INTEGER" Stacksize="5" />
    <Element Tag="RobotCommand.Pos" Type="STRUCTTAG" Stacksize="5" />
      <Element Tag="RobotCommand.Pos.A1" Type="REAL" Stacksize="5" />
      <Element Tag="RobotCommand.Pos.A2" Type="REAL" Stacksize="5" />
      <Element Tag="RobotCommand.Pos.A3" Type="REAL" Stacksize="5" />
      <Element Tag="RobotCommand.Pos.A4" Type="REAL" Stacksize="5" />
      <Element Tag="RobotCommand.Pos.A5" Type="REAL" Stacksize="5" />
      <Element Tag="RobotCommand.Pos.A6" Type="REAL" Stacksize="5" />
</Elements>

EkiHwInterface+.xml

<RobotState>
  <Pos A1="" A2="" A3="" A4="" A5="" A6="">
  </Pos>
  <Vel A1="" A2="" A3="" A4="" A5="" A6="">
  </Vel>
  <Eff A1="" A2="" A3="" A4="" A5="" A6="">
  </Eff>
  <RobotCommand ID="" Size="5">
  </RobotCommand>
</RobotState>

XmlApiConfig.xml

<?xml version="1.0"?>
<XmlApiConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="XMLCommunicationSetup.xsd">
  <XmlApiParam InitOnce="false"/>
  <Channel SensorName="EkiHwInterface" SensorType="EkiHwInterface">
    <TCP_IP IP="192.168.16.98" Port="54601" Route="true" MapPort="54601"/>
  </Channel>
</XmlApiConfig>

kuka_eki_hw_interface.src

&ACCESS RVP
&REL 5
def  kuka_eki_hw_interface()
  ; Declarations
  decl axis joint_pos_tgt
  decl int elements_read

  ; INI
  bas(#initmov, 0)  ; Basic initialization of axes

  eki_hw_iface_init()
  
  joint_pos_tgt = $axis_act
  ptp joint_pos_tgt

  ; Loop forever
  $advance = 5
  LOOP
    elements_read = eki_hw_iface_get(joint_pos_tgt)  ; Get new command from buffer if present
    ptp joint_pos_tgt c_ptp                          ; PTP to most recent commanded position
  ENDLOOP
end


def eki_hw_iface_init()
  decl int ekx_ret
  id_count = 0
  ; Setup interrupts
  ; Interrupt 15 - Connection cleanup on client disconnect
  global interrupt decl 15 when $flag[1]==false do eki_hw_iface_reset()
  interrupt on 15
  ; Interrupt 16 - Timer interrupt for periodic state transmission
  global interrupt decl 16 when $timer_flag[1]==true do eki_hw_iface_send()
  interrupt on 16
  wait sec 0.012          ; Wait for next interpolation cycle
  $timer[1] = -200        ; Time in [ms] before first interrupt call
  $timer_stop[1] = false  ; Start timer 1

  ; Create and open EKI interface
  ; ekx_ret = eki_init("EkiHwInterface")
  ekx_ret = EKX_Open("EkiHwInterface")
  EKX_HandleError(ekx_ret)

  $flag[1] = true
end


def eki_hw_iface_send()
  decl int ekx_ret   
  decl real vel_percent      
  
  if $flag[1] then  ; If connection alive
    ; Load state values into xml structure
    
    ; position
    ekx_ret = EKX_WriteReal("EkiHwInterface.RobotState.Pos.A1", $axis_act.a1)
    EKX_HandleError(ekx_ret)
    ekx_ret = EKX_WriteReal("EkiHwInterface.RobotState.Pos.A2", $axis_act.a2)
    EKX_HandleError(ekx_ret)
    ekx_ret = EKX_WriteReal("EkiHwInterface.RobotState.Pos.A3", $axis_act.a3)
    EKX_HandleError(ekx_ret)
    ekx_ret = EKX_WriteReal("EkiHwInterface.RobotState.Pos.A4", $axis_act.a4)    
    EKX_HandleError(ekx_ret)
    ekx_ret = EKX_WriteReal("EkiHwInterface.RobotState.Pos.A5", $axis_act.a5)
    EKX_HandleError(ekx_ret)
    ekx_ret = EKX_WriteReal("EkiHwInterface.RobotState.Pos.A6", $axis_act.a6)
    EKX_HandleError(ekx_ret)
    
    ekx_ret = EKX_WriteInteger("EkiHwInterface.RobotState.RobotCommand.ID", id_count)
    EKX_HandleError(ekx_ret)

    ; Send xml structure
    if $flag[1] then  ; Make sure connection hasn't died while updating xml structure
      ekx_ret = EKX_Send("EkiHwInterface.RobotState")
      EKX_HandleError(ekx_ret)
    endif
  endif

  ; Set timer for next interrupt [ms]
  $timer[1] = -10  ; ~10 ms for above send + 10 ms interrupt timer -> ~50 Hz state transmission
end

; eki_hw_iface_get
; Tries to read most recent element from buffer. q left unchanged if empty.
; Returns number of elements read.
deffct int eki_hw_iface_get(joint_pos_cmd :out)
  decl bool ekx_ret
  decl axis joint_pos_cmd
  decl bool is_new 
  decl int rec_id

  ekx_ret = EKX_WaitForSensorData(1, "EkiHwInterface.RobotCommand.ID", 10000)
  ekx_ret = EKX_GetIntegerElement(1, "EkiHwInterface.RobotCommand.ID", rec_id, is_new)
  
  ekx_ret = EKX_GetRealElement(1, "EkiHwInterface.RobotCommand.Pos.A1", joint_pos_cmd.a1, is_new)
  ekx_ret = EKX_GetRealElement(1, "EkiHwInterface.RobotCommand.Pos.A2", joint_pos_cmd.a2, is_new)
  ekx_ret = EKX_GetRealElement(1, "EkiHwInterface.RobotCommand.Pos.A3", joint_pos_cmd.a3, is_new)
  ekx_ret = EKX_GetRealElement(1, "EkiHwInterface.RobotCommand.Pos.A4", joint_pos_cmd.a4, is_new)
  ekx_ret = EKX_GetRealElement(1, "EkiHwInterface.RobotCommand.Pos.A5", joint_pos_cmd.a5, is_new)
  ekx_ret = EKX_GetRealElement(1, "EkiHwInterface.RobotCommand.Pos.A6", joint_pos_cmd.a6, is_new)
  
  id_count = rec_id

  return 1
endfct


def eki_hw_iface_reset()
  decl int ekx_ret_int
  decl bool ekx_ret_bool

  ekx_ret_bool = EKX_Close("EkiHwInterface")
  ekx_ret_int = EKX_Open("EkiHwInterface")
  EKX_HandleError(ekx_ret_int)
  $flag[1] = true
end

kuka_eki_hw_interface.cpp

namespace kuka_eki_hw_interface
{

    KukaEkiHardwareInterface::KukaEkiHardwareInterface() 
    : old_joint_position_(n_dof_, 0.0)
    , joint_position_(n_dof_, 0.0)
    , joint_velocity_(n_dof_, 0.0)
    , joint_effort_(n_dof_, 0.0)
    , joint_position_command_(n_dof_, 0.0)
    , joint_names_(n_dof_)
    , deadline_(ios_)
    , eki_cmd_id_count_(0)
    , eki_cmd_buff_len_(0)
    , eki_server_socket_(ios_)
    , eki_acceptor_(ios_, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0))
    {

    }

    bool KukaEkiHardwareInterface::eki_read_state(std::vector<double> &joint_position,
                                                  std::vector<double> &joint_velocity,
                                                  std::vector<double> &joint_effort,
                                                  int &cmd_buff_len)
    {
        static boost::array<char, 2048> in_buffer;
        boost::asio::streambuf st_buffer;

        // Read socket buffer (with timeout)
        deadline_.expires_from_now(boost::posix_time::seconds(eki_read_state_timeout_));  // set deadline
        boost::system::error_code ec = boost::asio::error::would_block;
        
        size_t len = 0;
        eki_server_socket_.async_receive(boost::asio::buffer(in_buffer), boost::bind(&KukaEkiHardwareInterface::eki_handle_receive, _1, _2, &ec, &len));
        do ios_.run_one(); while (ec == boost::asio::error::would_block);

        if (ec)
            return false;

        // Update joint positions from XML packet (if received)
        if (len == 0)
            return false;

        // Parse XML
        TiXmlDocument xml_in;
        in_buffer[len] = '\0';  // null-terminate data buffer for parsing (expects c-string)
        xml_in.Parse(in_buffer.data());
        
        TiXmlElement* robot_state = xml_in.FirstChildElement("RobotState");
        if (!robot_state)
            return false;
        TiXmlElement* pos = robot_state->FirstChildElement("Pos");
        TiXmlElement* vel = robot_state->FirstChildElement("Vel");
        TiXmlElement* eff = robot_state->FirstChildElement("Eff");
        TiXmlElement* robot_command = robot_state->FirstChildElement("RobotCommand");
        if (!pos || !vel || !eff || !robot_command)
            return false;
            
        // Extract axis positions
        double joint_pos;  // [deg]
        double joint_vel;  // [%max]
        double joint_eff;  // [Nm]
        char axis_name[] = "A1";
        for (int i = 0; i < n_dof_; ++i)
        {
            pos->Attribute(axis_name, &joint_pos);
            joint_position[i] = angles::from_degrees(joint_pos);  // convert deg to rad
            vel->Attribute(axis_name, &joint_vel);
            joint_velocity[i] = joint_vel;
            eff->Attribute(axis_name, &joint_eff);
            joint_effort[i] = joint_eff;

            axis_name[1]++;
        }

        // Extract last command id that was received by the robot 
        int id_count;
        robot_command->Attribute("ID", &id_count);

        bool wrap_around = id_count > eki_cmd_id_count_;
        if(!wrap_around)
            cmd_buff_len = eki_cmd_id_count_ - id_count;
        else
            cmd_buff_len = (INT32_MAX - id_count) + eki_cmd_id_count_;

        return true;
    }


    bool KukaEkiHardwareInterface::eki_write_command(const std::vector<double> &joint_position_command)
    {
        if( (eki_cmd_id_count_ + 1) > INT32_MAX )
            eki_cmd_id_count_ = 0;
        else
            ++eki_cmd_id_count_;

        TiXmlDocument xml_out;
        TiXmlElement* robot_command = new TiXmlElement("RobotCommand");
        TiXmlElement* pos = new TiXmlElement("Pos");
        TiXmlText* empty_text = new TiXmlText("");
        robot_command->LinkEndChild(pos);
        pos->LinkEndChild(empty_text);   // force <Pos></Pos> format (vs <Pos />)
        char axis_name[] = "A1";

        robot_command->SetAttribute("ID", std::to_string(eki_cmd_id_count_));

        for (int i = 0; i < n_dof_; ++i)
        {
            pos->SetAttribute(axis_name, std::to_string(angles::to_degrees(joint_position_command[i])).c_str());
            axis_name[1]++;
        }
        xml_out.LinkEndChild(robot_command);

        TiXmlPrinter xml_printer;
        xml_printer.SetStreamPrinting();  // no linebreaks
        xml_out.Accept(&xml_printer);

        size_t len = eki_server_socket_.send(boost::asio::buffer(xml_printer.CStr(), xml_printer.Size()));

        return true;
    }

    void KukaEkiHardwareInterface::start()
    {
        ROS_INFO_NAMED("kuka_eki_hw_interface", "Starting Kuka EKI hardware interface...");

        // Start client
        ROS_INFO_NAMED("kuka_eki_hw_interface", "... connecting to robot's EKI server...");
        eki_server_endpoint_ = boost::asio::ip::tcp::endpoint(boost::asio::ip::address_v4::any(), std::stoi(eki_server_port_));

        ROS_WARN_STREAM("IP: " << eki_server_endpoint_.address().to_string()); 
        ROS_WARN_STREAM("PORT: " << eki_server_endpoint_.port());

        eki_acceptor_ = boost::asio::ip::tcp::acceptor(ios_, eki_server_endpoint_.protocol());

        boost::system::error_code ec;
        
        eki_acceptor_.bind(eki_server_endpoint_, ec);
        while(ec.value() == 98)
        {
            ROS_WARN_STREAM("[ERROR] Code: " << ec.value() << " MSG: " << ec.message());
            ROS_WARN("Retrying...");
            eki_acceptor_.bind(eki_server_endpoint_, ec);
            ros::Duration(0.1).sleep();
        }
        if(ec != 0)
        {
            ROS_WARN_STREAM("[ERROR] Code: " << ec.value() << " MSG: " << ec.message());
            throw boost::system::system_error(ec ? ec : boost::asio::error::operation_aborted);
        }
        ROS_WARN("Connected!");
        eki_acceptor_.listen(1);

        eki_acceptor_.accept(eki_server_socket_, eki_server_endpoint_);
        
        eki_acceptor_.close();

        // Start persistent actor to check for eki_read_state timeouts
        deadline_.expires_at(boost::posix_time::pos_infin);  // do nothing unit a read is invoked (deadline_ = +inf)
        eki_check_read_state_deadline();

        // Initialize joint_position_command_ from initial robot state (avoid bad (null) commands before controllers come up)
        if (!eki_read_state(joint_position_, joint_velocity_, joint_effort_, eki_cmd_buff_len_))
        {
            std::string msg = "Failed to read from robot EKI server within alloted time of "
                              + std::to_string(eki_read_state_timeout_) + " seconds.  Make sure eki_hw_interface is running "
                              "on the robot controller and all configurations are correct.";
            ROS_ERROR_STREAM(msg);
            throw std::runtime_error(msg);
        }
        joint_position_command_ = joint_position_;

        ROS_INFO_NAMED("kuka_eki_hw_interface", "... done. EKI hardware interface started!");
    }

} // namespace kuka_eki_hw_interface

@gavanderhoorn
Copy link
Member

Thanks for reporting back.

If you want to make it easier for others to check out your changes, please fork this repository, commit your changes to a new branch, then open a Pull Request.

Right now it's really difficult to figure out what you've changed. By opening a PR, we immediately get a nicely rendered diff.

@Levaru
Copy link
Author

Levaru commented Sep 28, 2021

I wanted to avoid this, since I changed the indentation in all files. But I changed it all back and made a pull-request with a readable diff. I hope that I did this correctly.

@Levaru
Copy link
Author

Levaru commented Sep 28, 2021

Some additional info about the issue with the EKX_Get***Element() FIFO/LIFO parameter. Here is an image showing the state of a joint angle during each eki_read_state and the current joint_state_command_ during that state. You can clearly see how the joint starts to move from 90deg to 0deg but about 87deg it moves back up again. This was with the parameter set to 0 which should be FIFO.
Stutter

@Levaru
Copy link
Author

Levaru commented Sep 28, 2021

And this is the same movement but with the parameter set to 1. (It's also a little bit faster but that's not relevant).
NoStutter

As you can see, there is no oscillation/reverse movement.

@ros-industrial ros-industrial locked and limited conversation to collaborators Apr 13, 2023
@gavanderhoorn gavanderhoorn converted this issue into discussion #246 Apr 13, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Development

No branches or pull requests

2 participants