Skip to content

Commit a6afd97

Browse files
authored
Check payload state in gpio_controller (backport #1770) (#1775)
Before, we were returning success, as soon as the hardware interface attempted to setup the payload. This change reads the payload information from the robot and only returns success if the read payload matches with what the user requested. This is active by default, but will be inactive when using mock hardware. Add tests verifying setPayload works with URSim and mock hardware
1 parent bb199fd commit a6afd97

10 files changed

Lines changed: 129 additions & 4 deletions

File tree

ur_controllers/include/ur_controllers/gpio_controller.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ enum StateInterfaces
102102
SAFETY_STATUS_BITS = 58,
103103
INITIALIZED_FLAG = 69,
104104
PROGRAM_RUNNING = 70,
105+
PAYLOAD_STATE_MASS = 71,
106+
PAYLOAD_STATE_COG_X = 72,
107+
PAYLOAD_STATE_COG_Y = 73,
108+
PAYLOAD_STATE_COG_Z = 74,
105109
};
106110

107111
class GPIOController : public controller_interface::ControllerInterface
@@ -196,6 +200,8 @@ class GPIOController : public controller_interface::ControllerInterface
196200
* have been reached
197201
*/
198202
bool waitForAsyncCommand(std::function<double(void)> get_value);
203+
204+
bool waitForPayloadRtdeMatch(double mass, double cx, double cy, double cz);
199205
};
200206
} // namespace ur_controllers
201207

ur_controllers/src/gpio_controller.cpp

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
#include "ur_controllers/gpio_controller.hpp"
3939

40+
#include <cmath>
4041
#include <string>
4142

4243
namespace ur_controllers
@@ -160,6 +161,11 @@ controller_interface::InterfaceConfiguration ur_controllers::GPIOController::sta
160161
// program running
161162
config.names.emplace_back(tf_prefix + "gpio/program_running");
162163

164+
config.names.emplace_back(tf_prefix + "payload/mass");
165+
config.names.emplace_back(tf_prefix + "payload/cog.x");
166+
config.names.emplace_back(tf_prefix + "payload/cog.y");
167+
config.names.emplace_back(tf_prefix + "payload/cog.z");
168+
163169
return config;
164170
}
165171

@@ -530,13 +536,25 @@ bool GPIOController::setPayload(const ur_msgs::srv::SetPayload::Request::SharedP
530536

531537
resp->success = static_cast<bool>(command_interfaces_[CommandInterfaces::PAYLOAD_ASYNC_SUCCESS].get_value());
532538

533-
if (resp->success) {
534-
RCLCPP_INFO(get_node()->get_logger(), "Payload has been set successfully");
535-
} else {
539+
if (!resp->success) {
536540
RCLCPP_ERROR(get_node()->get_logger(), "Could not set the payload");
537541
return false;
538542
}
539543

544+
if (params_.verify_payload_on_set) {
545+
if (!waitForPayloadRtdeMatch(static_cast<double>(req->mass), req->center_of_gravity.x, req->center_of_gravity.y,
546+
req->center_of_gravity.z)) {
547+
RCLCPP_WARN(get_node()->get_logger(), "setPayload reported success but RTDE payload / payload_cog do not match "
548+
"the "
549+
"request yet. (This might "
550+
"happen when using the mocked interface.)");
551+
resp->success = false;
552+
RCLCPP_ERROR(get_node()->get_logger(), "Payload RTDE verification failed");
553+
return false;
554+
}
555+
556+
RCLCPP_INFO(get_node()->get_logger(), "Payload has been set and verified against RTDE feedback");
557+
}
540558
return true;
541559
}
542560

@@ -588,6 +606,26 @@ bool GPIOController::waitForAsyncCommand(std::function<double(void)> get_value)
588606
return true;
589607
}
590608

609+
bool GPIOController::waitForPayloadRtdeMatch(double mass, double cx, double cy, double cz)
610+
{
611+
constexpr double tol_mass = 1e-3;
612+
constexpr double tol_cog = 1e-4;
613+
const auto maximum_retries = params_.check_io_successfull_retries;
614+
615+
for (int retries = 0; retries <= maximum_retries; ++retries) {
616+
const auto m = state_interfaces_[StateInterfaces::PAYLOAD_STATE_MASS].get_value();
617+
const auto sx = state_interfaces_[StateInterfaces::PAYLOAD_STATE_COG_X].get_value();
618+
const auto sy = state_interfaces_[StateInterfaces::PAYLOAD_STATE_COG_Y].get_value();
619+
const auto sz = state_interfaces_[StateInterfaces::PAYLOAD_STATE_COG_Z].get_value();
620+
if (std::abs(m - mass) <= tol_mass && std::abs(sx - cx) <= tol_cog && std::abs(sy - cy) <= tol_cog &&
621+
std::abs(sz - cz) <= tol_cog) {
622+
return true;
623+
}
624+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
625+
}
626+
return false;
627+
}
628+
591629
} // namespace ur_controllers
592630

593631
#include "pluginlib/class_list_macros.hpp"

ur_controllers/src/gpio_controller_parameters.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ gpio_controller:
99
default_value: 10,
1010
description: "Amount of retries for checking if the selected gpio was set successfully"
1111
}
12+
verify_payload_on_set: {
13+
type: bool,
14+
default_value: true,
15+
description: "Whether to check if the payload of the set command is correct. If false, the controller will only check if the command was successful, but not if the correct value was set. Setting this to false can be useful if the controller is used on a robot with mock hardware or GZ simulation."
16+
}

ur_robot_driver/include/ur_robot_driver/hardware_interface.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ class URPositionHardwareInterface : public hardware_interface::SystemInterface
264264
urcl::vector3d_t payload_center_of_gravity_;
265265
double payload_mass_;
266266
double payload_async_success_;
267+
double rtde_payload_mass_ = 0.0;
268+
urcl::vector3d_t rtde_payload_cog_{ 0.0, 0.0, 0.0 };
267269

268270
// Friction model parameters
269271
urcl::vector6d_t friction_model_viscous_;

ur_robot_driver/launch/ur_control.launch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ def launch_setup(context, *args, **kwargs):
235235
robot_description,
236236
update_rate_config_file,
237237
ParameterFile(initial_joint_controllers, allow_substs=True),
238+
{"verify_payload_on_set": NotSubstitution(use_fake_hardware)},
238239
],
239240
output="screen",
240241
condition=IfCondition(use_fake_hardware),
@@ -247,6 +248,7 @@ def launch_setup(context, *args, **kwargs):
247248
robot_description,
248249
update_rate_config_file,
249250
ParameterFile(initial_joint_controllers, allow_substs=True),
251+
{"verify_payload_on_set": NotSubstitution(use_fake_hardware)},
250252
],
251253
output="screen",
252254
condition=UnlessCondition(use_fake_hardware),

ur_robot_driver/resources/rtde_output_recipe.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ robot_status_bits
2727
safety_status_bits
2828
actual_current
2929
tcp_offset
30+
payload
31+
payload_cog

ur_robot_driver/src/hardware_interface.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ std::vector<hardware_interface::StateInterface> URPositionHardwareInterface::exp
341341
state_interfaces.emplace_back(
342342
hardware_interface::StateInterface(tf_prefix + TOOL_CONTACT_GPIO, "tool_contact_state", &tool_contact_state_));
343343

344+
state_interfaces.emplace_back(hardware_interface::StateInterface(tf_prefix + "payload", "mass", &rtde_payload_mass_));
345+
state_interfaces.emplace_back(
346+
hardware_interface::StateInterface(tf_prefix + "payload", "cog.x", &rtde_payload_cog_[0]));
347+
state_interfaces.emplace_back(
348+
hardware_interface::StateInterface(tf_prefix + "payload", "cog.y", &rtde_payload_cog_[1]));
349+
state_interfaces.emplace_back(
350+
hardware_interface::StateInterface(tf_prefix + "payload", "cog.z", &rtde_payload_cog_[2]));
351+
344352
return state_interfaces;
345353
}
346354

@@ -813,6 +821,8 @@ hardware_interface::return_type URPositionHardwareInterface::read(const rclcpp::
813821
readBitsetData<uint32_t>(data_package_buffer_, "analog_io_types", analog_io_types_);
814822
readBitsetData<uint32_t>(data_package_buffer_, "tool_analog_input_types", tool_analog_input_types_);
815823
readData(data_package_buffer_, "tcp_offset", tcp_offset_);
824+
readData(data_package_buffer_, "payload", rtde_payload_mass_);
825+
readData(data_package_buffer_, "payload_cog", rtde_payload_cog_);
816826

817827
// required transforms
818828
extractToolPose();

ur_robot_driver/test/integration_test_io_controller.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import launch_testing
3737
import pytest
3838
import rclpy
39+
from geometry_msgs.msg import Vector3
3940
from rclpy.node import Node
4041
from ur_msgs.msg import IOStates
4142

@@ -130,3 +131,31 @@ def io_msg_cb(msg):
130131

131132
# Clean up io subscription
132133
self.node.destroy_subscription(io_states_sub)
134+
135+
def test_set_payload(self):
136+
"""
137+
Test that setting a payload succeeds and the value is verified against RTDE feedback.
138+
139+
With ``verify_payload_on_set`` enabled (default for the real driver), the
140+
controller only reports success after the requested payload mass and
141+
center-of-gravity have been confirmed via the RTDE state interfaces.
142+
"""
143+
# Set a non-default payload so we can detect the change reliably
144+
mass = 1.5
145+
cog = Vector3(x=0.01, y=0.02, z=0.03)
146+
147+
logging.info("Setting payload to mass=%f, cog=(%f, %f, %f)", mass, cog.x, cog.y, cog.z)
148+
result = self._io_status_controller_interface.set_payload(mass=mass, center_of_gravity=cog)
149+
self.assertTrue(
150+
result.success,
151+
"set_payload returned success=False. With verify_payload_on_set=true the "
152+
"controller only returns success once the RTDE feedback matches the request.",
153+
)
154+
155+
# Reset the payload to zero and verify again. This makes sure the verification
156+
# logic also detects subsequent changes and is not just matching the initial state.
157+
logging.info("Resetting payload to zero")
158+
result = self._io_status_controller_interface.set_payload(
159+
mass=0.0, center_of_gravity=Vector3(x=0.0, y=0.0, z=0.0)
160+
)
161+
self.assertTrue(result.success, "Resetting payload via set_payload failed")

ur_robot_driver/test/test_common.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@
7373
SetOperationalMode,
7474
SetUserRole,
7575
)
76-
from ur_msgs.srv import SetIO, GetRobotSoftwareVersion, SetForceMode, SetFrictionModelParameters
76+
from ur_msgs.srv import (
77+
SetIO,
78+
SetPayload,
79+
GetRobotSoftwareVersion,
80+
SetForceMode,
81+
SetFrictionModelParameters,
82+
)
7783
from builtin_interfaces.msg import Duration
7884
from control_msgs.action import FollowJointTrajectory
7985
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
@@ -326,6 +332,7 @@ class IoStatusInterface(
326332
initial_services={"set_io": SetIO},
327333
services={
328334
"resend_robot_program": Trigger,
335+
"set_payload": SetPayload,
329336
},
330337
):
331338
pass

ur_robot_driver/test/test_mock_hardware.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import launch_testing
3636
import pytest
3737
import rclpy
38+
from geometry_msgs.msg import Vector3
3839
from rclpy.node import Node
3940
from control_msgs.action import FollowJointTrajectory
4041
from controller_manager_msgs.srv import SwitchController
@@ -120,3 +121,26 @@ def test_trajectory(self, tf_prefix):
120121

121122
def test_illegal_trajectory(self, tf_prefix):
122123
sjtc_illegal_trajectory_test(self, tf_prefix)
124+
125+
def test_set_payload(self):
126+
"""
127+
Test that ``set_payload`` succeeds with mock hardware.
128+
129+
Mock hardware does not feed back the payload via RTDE, so the controller
130+
is launched with ``verify_payload_on_set:=false`` (set automatically by
131+
``ur_control.launch.py`` when ``use_mock_hardware:=true``). The service
132+
should therefore return success without performing the RTDE verification.
133+
"""
134+
result = self._io_status_controller_interface.set_payload(
135+
mass=1.5, center_of_gravity=Vector3(x=0.01, y=0.02, z=0.03)
136+
)
137+
self.assertTrue(
138+
result.success,
139+
"set_payload returned success=False on mock hardware. The controller "
140+
"should be launched with verify_payload_on_set=false in this case.",
141+
)
142+
143+
result = self._io_status_controller_interface.set_payload(
144+
mass=0.0, center_of_gravity=Vector3(x=0.0, y=0.0, z=0.0)
145+
)
146+
self.assertTrue(result.success, "Resetting payload via set_payload failed on mock hardware")

0 commit comments

Comments
 (0)