Skip to content
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

Fixing joints moving to default position when their target is not set #773

Open
wants to merge 38 commits into
base: noetic-devel
Choose a base branch
from

Conversation

jeferal
Copy link
Contributor

@jeferal jeferal commented Oct 14, 2022

Proposed changes

DO NOT MERGE YET.

User story: https://shadowrobot.atlassian.net/browse/SP-376

PR to fix the issue desribed in the user story. The issue is that when planning in joint space through the robot commander, for the joints whose target is not specified, a plan would be created from the current state to the default target value, which is 0 or the value in the middle of the range of the joint if 0 is not contained.

This PR uses the set points of the controllers to set the start state of the plan as well as the target state. It needs more testing, unit tests for the hand. In the user story you can see the tests than have been done.

The set points are read through subscriptions to the trajectory controllers for all the joints except from J2 and J1 of the fingers. For these joints, the set point is read from the position controller of J0 and this value is splitted by calculating the underactuation ration from J1 and J2 position states.

If the hand is grasping an object, the joint states might be far from the set points since the object could be not allowing the joint to reach the goal position. In this case, if the difference between the start state of the trajectory (set points) and the state is higher than the allowed_start_tolerance parameter then moveit would throw an error. This is why a function has been added to check if the set point is far from the state and update it with the state if so.

Checklist

Before posting a PR ensure that from each of the below categories AT LEAST ONE BOX HAS BEEN CHECKED. If more than one category is applicable then more can be checked. Also ensure that the proposed changes have been filled out with relevant information for reviewers.

Tests

  • No tests required to be added. (For small changes that will be tested by CI/CD infrastructure).
  • Added/Modified automated and PhantomHand CI tests (if a new class is added (Python or C++), the interface of that class must be unit tested).
  • Manually tested in simulation (if simulation specific or no hardware required to test the functionality).
  • Manually tested on hardware (if hardware specific or related).

Documentation

  • No documentation required to be added.
  • Added documentation (For any new feature, explain what it does and how to use it. Write the documentation in a relevant space, e.g. Github, Confluence, etc).
  • Updated documentation (For changes to pre-existing features mentioned in the documentation).

@jeferal jeferal requested review from a team as code owners October 14, 2022 14:06
@jeferal jeferal changed the title B#sp 376 joints reseted when planning trajectory Fixing joints moving to default position when their target is not set Oct 14, 2022
@toliver toliver self-assigned this Oct 18, 2022
@@ -101,16 +105,63 @@ def __init__(self, name):

controller_list_param = rospy.get_param("/move_group/controller_list")

robot_description = rospy.get_param("/robot_description")
self._joint_limits = dict()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to have this self._joint_limits = {} to be compliant with the new linter (you have also a dictionary init below done with {} style :)

self.tf_buffer = tf2_ros.Buffer()
self.listener = tf2_ros.TransformListener(self.tf_buffer)

threading.Thread(None, rospy.spin)

self._wait_for_set_points()

def _read_joint_limits(self, robot_description):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is supposed to happen only once right? Maybe would be better to rename it to something like _initialize_joint_limits and have then a getter like get_joint_limits (returning self._joint_limits)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which seems you have done below :) skip the getter part of the comment :)

if child.nodeType is child.TEXT_NODE:
continue
if child.localName == 'joint':
jtype = child.getAttribute('type')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe joint_type instead jtype? same in 144

if jtype == 'continuous':
minval = -pi
maxval = pi
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the joint_type passed in this else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, thanks

can you then just move the name = child.getAttribute('name') before the first 'if' to slightly increase readability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the attribute name seems to be particular for nodes of type joint

Comment on lines 918 to 919
joint_member = joint_name[3:-2]
joint_number = joint_name[5:]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add as comment the structure of joint_name, or use any() in the if below if you can :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a regex with a substring match?

@@ -15,27 +15,31 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.

from __future__ import absolute_import
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not wrong, this repo still has the master branch of the linter , so if I remove this line, the tests might fail

Comment on lines 918 to 919
joint_member = joint_name[3:-2]
joint_number = joint_name[5:]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a regex with a substring match?

"""
joint_names_group = self._move_group_commander.get_active_joints()
with self._set_points_lock:
for i in range(len(msg.joint_names)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using enumerate()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I changed it

self._move_group_commander.set_joint_value_target(joint_states_cpy)
self._move_group_commander.go(wait=wait)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like go function uses the current state internally, I tried to find the line of code that does that, but I could not. By performing tests, I could not reproduce the problem on planning when an object is grasped and that makes me think that is using the current state instead of the state set when using .set_start_satate

rospy.wait_for_service('compute_ik')
self._compute_ik = rospy.ServiceProxy('compute_ik', GetPositionIK)
self._forward_k = rospy.ServiceProxy('compute_fk', GetPositionFK)

controller_list_param = rospy.get_param("/move_group/controller_list")
self._allowed_start_tolerance = rospy.get_param("/move_group/trajectory_execution/allowed_start_tolerance", 0.1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module is common for arm and hand (it's the parent of HandCommander and ArmCommander).
How do we distinguish between allowed_start_tolerance for arm (likely we want it quite low) and hand?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In there I am only reading that parameter. When launching roslaunch sr_robot_launch sr_ur_arm_hand.launch it seems (at least in simulation) that allowed_start_tolerance is set in here. And it looks like there is only that parameter for any group:
image
Correct me if I am wrong please.

"""
# Get joint names of the group
joint_names_group = self._move_group_commander.get_active_joints()
trajectory_controllers_subscribed = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call it trajectory_controllers_state_subscribed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 12e6217

# Get joint names of the group
joint_names_group = self._move_group_commander.get_active_joints()
trajectory_controllers_subscribed = []
j0_position_controllers_subscribed = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe j0_position_controllers_state_subscribed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 12e6217

set_point_j2 = raw_set_points[joint_2_name]
else:
# Split j0 given state of j1 and j2
set_point_j1 = state_j1 * set_point_j0 / (state_j1 + state_j2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that this is necessary.
If we plan to get a separate set point for J1 and J2, why don't we take the one form the trajectory controller state?

That is the actual one that get set when one uses trajectory messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should proably rethink if we need to listen to the J0 position controllers at all.

else:
if "J0" not in joint_name:
# Avoind adding set points of J0 to the output
set_points.update({joint_name: raw_set_points[joint_name]})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this method only needs to do this bit?


def fix_set_points_if_far_from_state(self, set_points, robot_state):
"""
Updates the set points with the current stateof the robot if the set point
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo stateof

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed in 543eb52


# Update set points
for joint_name in list(set_points.keys()):
if abs(set_points[joint_name] - current_state[joint_name]) > self._allowed_start_tolerance:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want the user to be able to specify the tolerance for every use case.
e.g. if you are grasping a big object you might want to allow J3 and J0 set points to be very far from the current positions (set point inside the object while the current pos is on the surface).
Otherwise this "fixing" could result in a dropped object.

self._are_set_points_ready = True
self._set_points_cv.notifyAll()

def _set_point_j0_cb(self, msg, joint_name):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned before, not really sure if we need to know the J0 goals at all.
Given that we know the J1 and J2 goals (because this whole RobotCommander relies on having a JointTrajectoryposition controller running), maybe we don't need this?

Copy link
Contributor Author

@jeferal jeferal Dec 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we took the decision of listening to J0's set points because the J1 and J2 set points because of the underactuation might be far from the real state and also outside of the joint limits. For instance, when the set point of FFJ2 is 90deg and the set point of FFJ1 is ~60deg I find this:
image
If I directly set these set points as the target state when planning, moveit will throw the error: target state is outside of the bounds. I think this also depends on a tolerance that is set through this method.
However, since we also encountered the "problem" of the set points being far from the state when grasping an object and added the "fix" of checking if the set point is far from the state and if it is, use the state then listening to j0's might not be useful.
I also agree that perhaps we should rethink of this whole architecture again.

Comment on lines 976 to 977
self._joints_effort = {n: v for n, v in
zip(joint_state.name, joint_state.effort)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

side note -> this can be just self._joints_effort = {zip(joint_state.name, joint_state.effort)}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants