Skip to content

Commit 4f6c4f3

Browse files
Merge pull request #20 from NASA-JSC-Robotics/ndunkelb/use-services-for-mockup-states
Use services for mockup states
2 parents f898f85 + 3c4f409 commit 4f6c4f3

6 files changed

Lines changed: 133 additions & 66 deletions

File tree

‎mockup_msgs/CMakeLists.txt‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
project(mockup_msgs)
3+
4+
# Default to C++14
5+
if(NOT CMAKE_CXX_STANDARD)
6+
set(CMAKE_CXX_STANDARD 14)
7+
endif()
8+
9+
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
10+
add_compile_options(-Wall -Wextra -Wpedantic)
11+
endif()
12+
13+
# find dependencies
14+
find_package(ament_cmake REQUIRED)
15+
find_package(rosidl_default_generators REQUIRED)
16+
find_package(sensor_msgs REQUIRED)
17+
18+
set(service_files
19+
"srv/SetJointState.srv"
20+
)
21+
22+
rosidl_generate_interfaces(${PROJECT_NAME}
23+
${service_files}
24+
DEPENDENCIES
25+
sensor_msgs
26+
)
27+
28+
ament_export_dependencies(rosidl_default_runtime)
29+
30+
if(BUILD_TESTING)
31+
endif()
32+
33+
ament_package()

‎mockup_msgs/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Mockup messages
2+
3+
Services to help with handling mockup states.
4+
For now, this just includes SetJointState, which can be used with a mockup_state_manager to set the state of joints to be published

‎mockup_msgs/package.xml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>mockup_msgs</name>
5+
<version>1.0.0</version>
6+
<description>ROS2 interface declaration for managing mockups</description>
7+
<maintainer email="nathan.b.dunkelberger@nasa.gov">ndunkelb</maintainer>
8+
<license>Apache-2.0</license>
9+
10+
<buildtool_depend>ament_cmake</buildtool_depend>
11+
12+
<buildtool_depend>rosidl_default_generators</buildtool_depend>
13+
<member_of_group>rosidl_interface_packages</member_of_group>
14+
15+
<build_depend>sensor_msgs</build_depend>
16+
17+
<export>
18+
<build_type>ament_cmake</build_type>
19+
</export>
20+
</package>

‎mockup_msgs/srv/SetJointState.srv‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# This service takes in a joint state, and uses that to update an internal representation of joint state,
2+
# returning success if the set was successful, and optionally returning a message
3+
4+
sensor_msgs/JointState joint_state
5+
---
6+
bool success
7+
string message

‎mockups_launch_common/package.xml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<exec_depend>launch</exec_depend>
1818
<exec_depend>launch_ros</exec_depend>
1919
<exec_depend>tf2_ros</exec_depend>
20+
<exec_depend>mockup_msgs</exec_depend>
2021

2122
<test_depend>ament_cmake_pytest</test_depend>
2223
<test_depend>python3-pytest</test_depend>

‎mockups_launch_common/scripts/mockup_state_manager.py‎

Lines changed: 68 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
#!/usr/bin/env python3
22

33
import rclpy
4+
from rclpy.executors import ExternalShutdownException
45
from rclpy.node import Node
5-
from std_msgs.msg import Float64
66
from sensor_msgs.msg import JointState
7-
import copy
8-
import threading
7+
from mockup_msgs.srv import SetJointState
98

109

1110
# Class that stores the information for each mockup config
1211
class MockupConfig:
13-
def __init__(self, topic_name, min_position, max_position, initial_position, joint_name):
14-
self.topic_name = topic_name
12+
def __init__(self, min_position, max_position, initial_position, joint_name):
1513
self.joint_name = joint_name
1614
self.min_position = min_position
1715
self.max_position = max_position
1816
self.position = initial_position
17+
self.velocity = 0.0
18+
self.effort = 0.0
19+
20+
def set_position(self, desired_position):
21+
self.position = max(self.min_position, min(desired_position, self.max_position))
1922

2023

2124
class MockupStateManager(Node):
@@ -34,99 +37,98 @@ def __init__(self):
3437
# create the joint state publisher
3538
self.publisher_ = self.create_publisher(JointState, "joint_states", 10)
3639

37-
subscriptions = []
38-
for index, mockup_config in enumerate(self.mockup_configs):
39-
# this was a bit funky for copying in index
40-
# see https://github.com/ros2/rclpy/issues/629#issuecomment-1542151499 for reference
41-
subscriptions.append(
42-
self.create_subscription(
43-
Float64,
44-
self.prefix + mockup_config.topic_name,
45-
lambda msg, idx=index: self.position_cb(msg, idx),
46-
10,
47-
)
48-
)
40+
self.set_joint_state_service = self.create_service(SetJointState, "~/set_joint_state", self.set_joint_state_cb)
4941

5042
# create the timer for joint state publisher callback
5143
timer_period_sec = 0.5 # unit: seconds
5244
self.timer = self.create_timer(timer_period_sec, self.joint_state_cb)
5345

54-
self.lock = threading.Lock()
55-
5646
def load_mockup_configs(self):
5747
"""loads the parameters provided with each of the relevaant joints and populates self.mockup_configs"""
5848
# get the list of topic names first
5949

60-
topic_params = self.get_parameters_by_prefix("topics")
50+
joints_params = self.get_parameters_by_prefix("joints")
6151

62-
topic_names = {key.split(".")[0] for key in topic_params.keys()}
52+
joints = {key.split(".")[0] for key in joints_params.keys()}
6353

6454
# puopulate self.mockup_configs based on loaded parameters
65-
self.mockup_configs = []
66-
for topic in topic_names:
67-
self.get_logger().info(f"Loading: {self.prefix + topic}")
55+
self.mockup_configs = dict()
56+
for joint in joints:
57+
self.get_logger().info(f"Loading: {self.prefix + joint}")
6858

69-
joint_name = topic_params[f"{topic}.joint_name"].value
70-
min_position = topic_params[f"{topic}.min_position"].value
71-
max_position = topic_params[f"{topic}.max_position"].value
72-
initial_position = topic_params[f"{topic}.initial_position"].value
59+
joint_name = joint
60+
min_position = joints_params[f"{joint}.min_position"].value
61+
max_position = joints_params[f"{joint}.max_position"].value
62+
initial_position = joints_params[f"{joint}.initial_position"].value
7363

7464
# add mockups to the member variable
75-
self.mockup_configs.append(MockupConfig(topic, min_position, max_position, initial_position, joint_name))
65+
self.mockup_configs[joint_name] = MockupConfig(min_position, max_position, initial_position, joint_name)
7666

7767
def joint_state_cb(self):
7868
"""Publisher for the manager which publishes the joint state info."""
7969
msg = JointState()
8070
msg.header.stamp = self.get_clock().now().to_msg()
81-
# with mutex locking, add all of the joint states
82-
with self.lock:
83-
for mockup_config in self.mockup_configs:
84-
msg.name.append(self.prefix + mockup_config.joint_name)
85-
msg.position.append(mockup_config.position)
86-
msg.velocity.append(0.0)
87-
msg.effort.append(0.0)
71+
# add all of the joint states
72+
for mockup_config in self.mockup_configs.values():
73+
msg.name.append(self.prefix + mockup_config.joint_name)
74+
msg.position.append(mockup_config.position)
75+
msg.velocity.append(mockup_config.position)
76+
msg.effort.append(mockup_config.effort)
8877

8978
self.publisher_.publish(msg)
9079

91-
self.get_logger().debug(
92-
f"This is the hatch joint state message: {msg}"
93-
) # this will fill in the string with formatted msg data
80+
def set_joint_state_cb(self, req: SetJointState.Request, res: SetJointState.Response):
81+
"""
82+
Callback for the setting internal joint states which will then be published.
83+
"""
9484

95-
def position_cb(self, msg: Float64, index):
96-
"""callback for the position setting
85+
# Check first to make sure that data is the right size.
86+
# Any non-empty lists must be of the same size of names
87+
error_msg = ""
88+
msg_size = len(req.joint_state.name)
89+
valid = True
90+
91+
for field in ("position", "velocity", "effort"):
92+
values = getattr(req.joint_state, field)
93+
if values and (len(values) != msg_size):
94+
error_msg += (
95+
f"The size of `{field}` ({len(values)}) does not match the size of"
96+
"`name` ({msg_size}) in SetJointState. "
97+
)
98+
valid = False
9799

98-
Args:
99-
msg (Float64): This message transmits the joint position (in radians or meters)
100-
index (int): the index that we are accessing (to ineracte with self.mockup_configs
101-
"""
102-
self.get_logger().debug(
103-
f"This is the new position of {self.prefix + self.mockup_configs[index].joint_name}: {msg}"
104-
)
105-
if self.mockup_configs[index].min_position <= msg.data and msg.data <= self.mockup_configs[index].max_position:
106-
with self.lock:
107-
self.mockup_configs[index].position = msg.data
108-
elif self.mockup_configs[index].min_position >= msg.data:
109-
with self.lock:
110-
self.mockup_configs[index].position = copy.deepcopy(self.mockup_configs[index].min_position)
111-
elif self.mockup_configs[index].max_position <= msg.data:
112-
with self.lock:
113-
self.mockup_configs[index].position = copy.deepcopy(self.mockup_configs[index].max_position)
114-
else: # I don't think this will ever happen 0.o
115-
raise Exception(self.mockup_configs[index].joint_name + " joint angle out of range")
100+
# return early if not valid
101+
if not valid:
102+
res.message = error_msg
103+
res.success = False
104+
return res
105+
106+
for i, name in enumerate(req.joint_state.name):
107+
mockup_config = self.mockup_configs[name]
108+
# position is special because we have to clamp it, so it uses a member function
109+
if req.joint_state.position:
110+
mockup_config.set_position(req.joint_state.position[i])
111+
if req.joint_state.velocity:
112+
mockup_config.velocity = req.joint_state.velocity[i]
113+
if req.joint_state.effort:
114+
mockup_config.effort = req.joint_state.effort[i]
115+
116+
res.success = True
117+
return res
116118

117119

118120
def main(args=None):
119121
rclpy.init(args=args)
120122

121123
mockup_state_manager = MockupStateManager()
122124

123-
rclpy.spin(mockup_state_manager)
124-
125-
# Destroy the node explicitly
126-
# (optional - otherwise it will be done automatically
127-
# when the garbage collector destroys the node object)
128-
mockup_state_manager.destroy_node()
129-
rclpy.shutdown()
125+
try:
126+
rclpy.spin(mockup_state_manager)
127+
except (KeyboardInterrupt, ExternalShutdownException):
128+
pass
129+
finally:
130+
mockup_state_manager.destroy_node()
131+
rclpy.try_shutdown()
130132

131133

132134
if __name__ == "__main__":

0 commit comments

Comments
 (0)