Articles Download Safety & Security Forum myUR Go to Main Site

Universal Robots A/S
Energivej 51
DK-5260 Odense S

T: +45 8993 8989
sales@universal-robots.com

 
  • Articles
  • Download
  • Safety & Security
  • Forum
  • myUR
  • Go to Main Site
  • Support
  • Articles
  • Trajectory improvements for servoj()

Trajectory improvements for servoj()

The function servoj() can be used for online realtime control of joint positions. In this guide you will find an extensive explaination of how to improve trajectories.

Last modified on May 09, 2022

Introduction

A number of users stream joint positions dynamically to the robot and execute them using servoj() due to the simple PID-type control provided by servoj(). One of the common approaches is to stream the joint-space waypoints via RTDE registers and calling servoj() repeatedly at the same rate as the waypoint time period.

UR Speed Scaling and servoj()

The UR controller may perform speed scaling in its trajectory planner to maintain the motion within the hardware or safety constraints. For example, when the motion to the next target joint positions with given acceleration or velocity is expected to exceed the hardware torque limits, the UR controller scales the time (speed factor) to keep the motion within the torque limits while still moving towards the given setpoint. Hence, the robot would move slower, and the motion commands would take longer (in terms of physical time) to complete when speed scaling is triggered. For example, if servoj() is executed with the duration (t) as 2ms, and speed scaling is at 50%, servoj() would complete in 4ms instead.

Handling servoj() duration changes due to speed scaling

Such changes in the actual duration of servoj() may lead to unexpected side effects in waypoint streaming use-cases. If the waypoints are streamed to the robot via an unbuffered medium such as RTDE registers, it may lead to skipping certain waypoints depending on the rate at which waypoints are published and consumed. Publishing rates faster than servoj() duration would likely cause at least one waypoint to be skipped, causing undesirable changes in the robot trajectory. Slower rates would cause a slower and jerky motion due to the robot slowing down close to the target positions before next waypoint.

Hence, it is important to ensure both – calling servoj() with every streamed waypoint without skipping any important ones, as well as always keeping the next waypoint available by the time servoj() completes with the last one. This can be done by maintaining the waypoints in form of a buffer and feeding servoj() from the buffer at the UR Controller (using URScript). The buffering may be done either by using a TCP socket for streaming the waypoints (like ROS driver) or by maintaining a buffer in URScript (an implementation demonstrated in example script below).

One issue with buffering the waypoint positions is that the buffer size may grow if the waypoints are received faster than the robot motion reaches them. Speed scaling may cause the robot to move slower than the waypoint streamer’s expectations, leading to an increase in buffer size over time. While this may not pose a problem with finite (bounded) trajectories with a limited number of waypoints, it is a significant issue for long trajectories with large number of waypoints or unbounded trajectories where the streamer keeps generating the next waypoints continuously. Hence, a closed loop may be needed between the UR controller and the waypoint generator (planner) to keep the buffer size (and robot lag time behind the streamed joint positions) within bounds.

For loop closure between the UR Controller and the external planner/waypoint-generator, at least two forms of feedback can be used. The UR controller publishes an output register named speed_scaling to provide feedback on the current speed scaling value, which can indicate the current slowdown to the planner or the waypoint generator. Another form of the feedback can be in form of the buffer size (if all waypoints have the same time duration) or expected total duration for all waypoints in the buffer.

Handling insufficient duration for servoj()

Due to hardware or safety limitations or due to parameter differences like low gain, servoj may not be able to complete the motion to the desired waypoint in given duration. Such limitations can also cause the trajectory to change undesirably. Hence, we should add some logic to ensure a maximum error threshold from the desired target joint positions for each waypoint. In our sample script below, we do the same by re-running the servoj() command with the same parameters if it doesn't reach the thresholds of the target waypoint. This script may be further improved to vary the duration parameter based on the previous and the remaining motion.

Example script and results

We present a sample script that demonstrates an implementation and use of a circular buffer in URScript for feeding servoj(). This script receives the joint position waypoints over RTDE and sends a lag time back to the external planner to help control the lag duration. The external controller used in this example follows the value of “speed_scaling” to ramp its own speed scaling.

Sample script:

def ec_external_control():
  textmsg("external_control init starting")

  #--------------------------------------------------------------------
  # CONSTANTS
  #--------------------------------------------------------------------
  # input registers
  #    int
  ec_DESIRED_MODE_INPUT_INT_REG = 24
  ec_SEQUENCE_ID_INPUT_INT_REG = 25
  ec_EXTERNAL_ERROR_INPUT_INT_REG = 26
  ec_EXTERNAL_SYNC_STATE_INPUT_INT_REG = 27

  #    float/double
  ec_DESIRED_JOINT_POSITIONS_BASE_INPUT_FLOAT_REG = 24 #24 - 29
  ec_MAX_JOINT_DIVERGENCE_THRESHOLDS_BASE_INPUT_FLOAT_REG = 30 #30 - 35
  ec_STATE_TIME_INPUT_FLOAT_REG = 46

  # output registers
  #    int
  ec_ACTUAL_MODE_OUTPUT_INT_REG = 24
  ec_LAST_SEQUENCE_ID_OUTPUT_INT_REG = 25

  #    double
  ec_LAG_TIME_OUTPUT_FLOAT_REG = 24

  # modes
  ec_MODE_IDLE = 0
  ec_MODE_TEACH = 1
  ec_MODE_SERVO = 2

  # External synchronization state
  ec_SYNC_STATE_WAITING_TO_CONNECT = 0
  ec_SYNC_STATE_NONE = 1
  ec_SYNC_STATE_EXTERNAL_TO_ROBOT = 2
  ec_SYNC_STATE_ROBOT_TO_EXTERNAL = 3

  # stale sequence ID counter limit
  ec_STALE_SEQUENCE_ID_COUNTER_LIMIT = 2000 # 4000ms (1 count = 2 milliseconds)

  # external errors
  ec_ERROR_NONE = 0
  ec_ERROR_HALT = 1

  # servoj parameters (determined experimentally, UR defaults shown)
  # servoj(q, a=NOT_USED, v=NOT_USED, t=0.002, lookahead_time=0.1, gain=300)
  ec_SERVOJ_T = 0.002
  ec_SERVOJ_LOOKAHEAD_TIME = 0.06
  ec_SERVOJ_GAIN = 1000

  #--------------------------------------------------------------------
  # globals
  #--------------------------------------------------------------------
  ec_actual_mode = ec_MODE_IDLE
  ec_last_mode = ec_actual_mode
  ec_last_mode_comm = ec_actual_mode
  ec_desired_mode = ec_actual_mode
  ec_sync_state = ec_SYNC_STATE_WAITING_TO_CONNECT
  ec_sequence_id = 0
  ec_last_sequence_id = 0
  ec_stale_sequence_id_counter = 0

  ec_external_error = ec_ERROR_NONE

  global ec_desired_joint_positions = get_actual_joint_positions()
  global ec_desired_movement_time = get_steptime()

  global ec_joint_convergence_thresholds = [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]
  global ec_max_joint_divergence_thresholds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
  ec_last_max_joint_divergence_thresholds = ec_max_joint_divergence_thresholds

  ##################################
  # Waypoint Buffer Management
  ##################################

  # Prepare buffer matrix
  # Create time buffer of 1000 elements
  ec_buf_t = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
  ec_qbuf = transpose([ec_buf_t]) * [ec_desired_joint_positions]
  ec_buf_t = ec_buf_t * 0.0
  global ec_buf_front = 0
  global ec_buf_back = 0
  global ec_buf_size = 0
  ec_buf_cap = size(ec_buf_t)
  global ec_buf_can_pop = False
  global ec_lag_time = 0.0
  ec_last_state_time = 0.0

  def updateSize():
    diff = ec_buf_back - ec_buf_front
    if (0 > diff):
      diff = diff + ec_buf_cap
    end
    if (ec_buf_can_pop == True):
      ec_buf_size = diff + 1
    else:
      ec_buf_size = 0
    end
  end

  def next_index():
    nextIndex = (ec_buf_back + 1) % ec_buf_cap
    if (nextIndex == ec_buf_front):
      return -1
    end
    return nextIndex
  end

  def push():
    nextIndex = (ec_buf_back + 1) % ec_buf_cap
    if (nextIndex == ec_buf_front):
      return False
    end
    ec_buf_back = nextIndex
    if (ec_buf_can_pop == False):
      ec_buf_front = ec_buf_back
      ec_buf_can_pop = True
    end
    updateSize()
    return True
  end

  def pop():
    if (ec_buf_can_pop == False):
      return False
    end
    if (ec_buf_front == ec_buf_back):
      ec_buf_can_pop = False
    else:
      ec_buf_front = (ec_buf_front + 1) % ec_buf_cap
    end
    updateSize()
    return True
  end

  def reset_buf():
    ec_buf_back = ec_buf_front
    ec_buf_size = 0
    ec_buf_can_pop = False
    updateSize()

    ec_lag_time = 0.0
    ec_last_state_time = read_input_float_register(ec_STATE_TIME_INPUT_FLOAT_REG) - get_steptime()
  end

  global num_push = 0
  global num_pop = 0

  def push_waypoint():
    state_time = read_input_float_register(ec_STATE_TIME_INPUT_FLOAT_REG)
    waypoint_time = state_time - ec_last_state_time
    if (waypoint_time > 50 * get_steptime() or waypoint_time < 0.0):
      waypoint_time = get_steptime()
    elif (waypoint_time == 0.0):
      return None
    end
    ec_last_state_time = state_time

    nextIndex = next_index()
    if (nextIndex < 0):
      textmsg("Buffer overflow")
      nextIndex = ec_buf_back
    end

    ec_buf_t[nextIndex] = waypoint_time

    ec_qbuf[nextIndex, 0] = read_input_float_register(ec_DESIRED_JOINT_POSITIONS_BASE_INPUT_FLOAT_REG + 0)
    ec_qbuf[nextIndex, 1] = read_input_float_register(ec_DESIRED_JOINT_POSITIONS_BASE_INPUT_FLOAT_REG + 1)
    ec_qbuf[nextIndex, 2] = read_input_float_register(ec_DESIRED_JOINT_POSITIONS_BASE_INPUT_FLOAT_REG + 2)
    ec_qbuf[nextIndex, 3] = read_input_float_register(ec_DESIRED_JOINT_POSITIONS_BASE_INPUT_FLOAT_REG + 3)
    ec_qbuf[nextIndex, 4] = read_input_float_register(ec_DESIRED_JOINT_POSITIONS_BASE_INPUT_FLOAT_REG + 4)
    ec_qbuf[nextIndex, 5] = read_input_float_register(ec_DESIRED_JOINT_POSITIONS_BASE_INPUT_FLOAT_REG + 5)

    push()
    ec_lag_time = ec_lag_time + waypoint_time
    num_push = num_push + 1
  end

  def pop_waypoint():
    ec_desired_joint_positions[0] = ec_qbuf[ec_buf_front, 0]
    ec_desired_joint_positions[1] = ec_qbuf[ec_buf_front, 1]
    ec_desired_joint_positions[2] = ec_qbuf[ec_buf_front, 2]
    ec_desired_joint_positions[3] = ec_qbuf[ec_buf_front, 3]
    ec_desired_joint_positions[4] = ec_qbuf[ec_buf_front, 4]
    ec_desired_joint_positions[5] = ec_qbuf[ec_buf_front, 5]

    ec_desired_movement_time = ec_buf_t[ec_buf_front]

    if (pop() == True):
      ec_lag_time = ec_lag_time - ec_desired_movement_time
      num_pop = num_pop + 1
      return True
    else:
      popup("Buffer underflow")
      textmsg("Buffer underflow")
      return False
    end
  end

  textmsg("external_control init done")

  ##################################
  # Communication Thread
  ##################################
  thread external_comm_thread():
    while (True):
      # read input registers (from external controller)
      # int registers
      ec_desired_mode = read_input_integer_register(ec_DESIRED_MODE_INPUT_INT_REG)
      ec_sequence_id = read_input_integer_register(ec_SEQUENCE_ID_INPUT_INT_REG)
      ec_external_error = read_input_integer_register(ec_EXTERNAL_ERROR_INPUT_INT_REG)
      ec_sync_state = read_input_integer_register(ec_EXTERNAL_SYNC_STATE_INPUT_INT_REG)

      # double registers
      ec_max_joint_divergence_thresholds[0] = read_input_float_register(ec_MAX_JOINT_DIVERGENCE_THRESHOLDS_BASE_INPUT_FLOAT_REG + 0)
      ec_max_joint_divergence_thresholds[1] = read_input_float_register(ec_MAX_JOINT_DIVERGENCE_THRESHOLDS_BASE_INPUT_FLOAT_REG + 1)
      ec_max_joint_divergence_thresholds[2] = read_input_float_register(ec_MAX_JOINT_DIVERGENCE_THRESHOLDS_BASE_INPUT_FLOAT_REG + 2)
      ec_max_joint_divergence_thresholds[3] = read_input_float_register(ec_MAX_JOINT_DIVERGENCE_THRESHOLDS_BASE_INPUT_FLOAT_REG + 3)
      ec_max_joint_divergence_thresholds[4] = read_input_float_register(ec_MAX_JOINT_DIVERGENCE_THRESHOLDS_BASE_INPUT_FLOAT_REG + 4)
      ec_max_joint_divergence_thresholds[5] = read_input_float_register(ec_MAX_JOINT_DIVERGENCE_THRESHOLDS_BASE_INPUT_FLOAT_REG + 5)

      # halt for any external_error
      if (ec_external_error != ec_ERROR_NONE):
        if (ec_external_error == ec_ERROR_HALT):
          textmsg("stopping external control")
        else:
          textmsg("unknown external error received: external_error = ", ec_external_error)
        end
        halt
      end

      # check if the divergence thresholds have changed
      if (ec_max_joint_divergence_thresholds != ec_last_max_joint_divergence_thresholds):
        textmsg("max_joint_divergence_thresholds = ", ec_max_joint_divergence_thresholds)
        ec_last_max_joint_divergence_thresholds = ec_max_joint_divergence_thresholds
      end

      # check if the transition to the desired mode is allowed (currently all mode transitions are allowed)
      if (ec_desired_mode != ec_actual_mode):
        if (ec_actual_mode == ec_MODE_IDLE):
          ec_actual_mode = ec_desired_mode
        elif (ec_actual_mode == ec_MODE_TEACH):
          ec_actual_mode = ec_desired_mode
        elif (ec_actual_mode == ec_MODE_SERVO):
          ec_actual_mode = ec_desired_mode
        else:
          textmsg("unknown mode received: mode = ", ec_desired_mode)
          halt
        end
      end

      # entry/exit invokes
      if (ec_actual_mode != ec_last_mode_comm):
        # entry invokes
        if (ec_actual_mode == ec_MODE_IDLE):
          textmsg("running entry invokes for IDLE")
        elif (ec_actual_mode == ec_MODE_TEACH):
          textmsg("running entry invokes for TEACH")
        elif (ec_actual_mode == ec_MODE_SERVO):
          textmsg("running entry invokes for SERVO")
        end
        ec_last_mode_comm = ec_actual_mode
        reset_buf()
      end

      if (ec_sync_state == ec_SYNC_STATE_EXTERNAL_TO_ROBOT and ec_actual_mode == ec_MODE_SERVO):
        push_waypoint()
      end

      # check for sequence_id timeout
      if (ec_sequence_id > ec_last_sequence_id):
        ec_last_sequence_id = ec_sequence_id
        ec_stale_sequence_id_counter = 0
      else:
        ec_stale_sequence_id_counter = ec_stale_sequence_id_counter + 1
        if (ec_stale_sequence_id_counter > ec_STALE_SEQUENCE_ID_COUNTER_LIMIT):
          textmsg("stale sequence ID counter exceeded limit")
          textmsg("sequence_id = ", ec_sequence_id)
          textmsg("sequence_id last = ", ec_last_sequence_id)
          textmsg("sequence_id counter = ", ec_stale_sequence_id_counter)
          textmsg("sequence_id limit = ", ec_STALE_SEQUENCE_ID_COUNTER_LIMIT)
          halt
        end
      end

      # write output registers (to external controller)
      write_output_integer_register(ec_ACTUAL_MODE_OUTPUT_INT_REG, ec_actual_mode)
      write_output_integer_register(ec_LAST_SEQUENCE_ID_OUTPUT_INT_REG, ec_last_sequence_id)
      write_output_float_register(ec_LAG_TIME_OUTPUT_FLOAT_REG, ec_lag_time)

      # update invokes (NOTE: must consume remaining period)
      sync()
    end
  end
  ec_rcv_thread = run external_comm_thread()

  ##################################
  # Divergence threshold check
  ##################################
  def is_diverged(desired_positions, max_divergence):
    ec_actual_joint_values = get_actual_joint_positions()
    ec_joint0_error = norm(desired_positions[0] - ec_actual_joint_values[0])
    ec_joint1_error = norm(desired_positions[1] - ec_actual_joint_values[1])
    ec_joint2_error = norm(desired_positions[2] - ec_actual_joint_values[2])
    ec_joint3_error = norm(desired_positions[3] - ec_actual_joint_values[3])
    ec_joint4_error = norm(desired_positions[4] - ec_actual_joint_values[4])
    ec_joint5_error = norm(desired_positions[5] - ec_actual_joint_values[5])

    return (ec_joint0_error > max_divergence[0]) or (ec_joint1_error > max_divergence[1]) or (ec_joint2_error > max_divergence[2]) or (ec_joint3_error > max_divergence[3]) or (ec_joint4_error > max_divergence[4]) or (ec_joint5_error > max_divergence[5])
  end

  ##################################
  # Control Thread
  ##################################
  thread external_control_thread():
    while (True):
      # entry/exit invokes
      if (ec_actual_mode != ec_last_mode):
        textmsg("changing mode")
        textmsg("from mode = ", ec_last_mode)
        textmsg("to mode = ", ec_actual_mode)

        # exit invokes
        if (ec_last_mode == ec_MODE_IDLE):
          textmsg("running exit invokes for IDLE")
        elif (ec_last_mode == ec_MODE_TEACH):
          textmsg("running exit invokes for TEACH")
          end_teach_mode()
        elif (ec_last_mode == ec_MODE_SERVO):
          textmsg("running exit invokes for SERVO: Stopping...")
          while (not is_steady()):
            stopj(50)
          end
          textmsg("finished exit invokes for SERVO")
        end

        # entry invokes
        if (ec_actual_mode == ec_MODE_IDLE):
          textmsg("running entry invokes for IDLE")
        elif (ec_actual_mode == ec_MODE_TEACH):
          textmsg("running entry invokes for TEACH")
          teach_mode()
        elif (ec_actual_mode == ec_MODE_SERVO):
          textmsg("running entry invokes for SERVO")
        end
        ec_last_mode = ec_actual_mode
      end

      # update invokes (NOTE: must consume remaining period)
      if (ec_sync_state == ec_SYNC_STATE_EXTERNAL_TO_ROBOT and ec_actual_mode == ec_MODE_SERVO):
        if (ec_buf_can_pop == True):
          pop_waypoint()
        end

        if (is_diverged(ec_desired_joint_positions, ec_max_joint_divergence_thresholds)):
          textmsg("Max divergence threshold crossed.")
          halt
        end

        # passed all the divergence checks, move the robot
        servoj(ec_desired_joint_positions, t = ec_desired_movement_time, lookahead_time = ec_SERVOJ_LOOKAHEAD_TIME, gain = ec_SERVOJ_GAIN)
        # Optional: Call servoj again until converged to safe threshold. This would kick-in when speed scaling occurs.
        # We're using the same time 't' as previous servoj() call for now since we don't have access to the effect of time scaling changes in previous servoj
        while (is_diverged(ec_desired_joint_positions, ec_joint_convergence_thresholds)):
          servoj(ec_desired_joint_positions, t = ec_desired_movement_time, lookahead_time = ec_SERVOJ_LOOKAHEAD_TIME, gain = ec_SERVOJ_GAIN)
        end
      else:
        sync()
      end
    end
  end

  ec_ctrl_thread = run external_control_thread()

  ##################################
  # Main loop for interpreter mode
  ##################################

  # Use main thread for interpreter mode until halt or stop
  while (True):
    interpreter_mode(False, False)
  end
end

The plots below show the desired (streamed) joint positions (magenta) and the target/actual joint positions (blue) observed over RTDE for 3 robot joints (Shoulder, Elbow and Wrist-1) when moving the tool up and down repeatedly in a linear motion along the vertical axis. The first set of plots displays the behavior without buffering and the second set displays the behavior using the sample script with buffering.

For this set of plots above (servoj() without buffering), the streamed waypoints were read directly from RTDE right before executing servoj().

The sample URScript (servoj() with buffer and repeat waypoint to threshold) provided here was used to control the motion for this set of plots above.

Attached files


external_control_ur_script_example.script

Attached files

external_control_ur_script_example.script

Related articles

ServoJ command
logo

Product

  • UR3e Robot
  • UR5e Robot
  • UR10e Robot
  • UR16e Robot
  • UR20 Robot
  • UR+ Products

Company

  • About us
  • Contact us
  • Careers We're hiring
  • UR merchandise

Training / Resources

  • Academy
  • Technical Resources
  • Articles
  • FAQ

Insights

  • Blog
  • Case stories
  • Content library
  • News centre
  • Podcast
  • Webinars & Events

Get in touch

  • Ask an Expert
  • Schedule a no-cost assessment
  • Find a distributor
  • Customer support

Connect with us

  • LinkedIn
  • Facebook
  • Twitter
  • YouTube
  • Instagram
  • Universal Robots A/S
  • Energivej 51
  • DK-5260 Odense S
  • T: +45 89 93 89 89
  • sales@universal-robots.com
  • US Corporate Office
  • 27-43 Wormwood St.
  • 02210 Boston, MA.
  • +1-844-GO-COBOT
  • ur.na@universal-robots.com
  • Copyright @ Universal Robots 2025
  • Cookie policy
  • Privacy policy
  • Universal Robots A/S
  • Energivej 51
  • DK-5260 Odense S
  • T: +45 89 93 89 89
  • sales@universal-robots.com
  • US Corporate Office
  • 27-43 Wormwood St.
  • 02210 Boston, MA.
  • +1-844-GO-COBOT
  • ur.na@universal-robots.com

Copyright © Universal Robots 2025

Cookie policy
Privacy policy