Skip to content

Commit

Permalink
Merge pull request #20 from small-thinking/send-real-command
Browse files Browse the repository at this point in the history
Use voice to control the arm in gazebo
  • Loading branch information
yxjiang authored Feb 20, 2024
2 parents e1f0480 + e95a5ac commit ffe1da9
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 34 deletions.
8 changes: 5 additions & 3 deletions mnlm/client/gpt_control/robot_arm.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,25 @@ def translate_prompt_to_sequence(self, prompt: str) -> str:
Please convert the following oral comamnd to machine readable operation json (list of json blobs)
according to the API document.
The expected output would be:
An example output would be:
{{
operations: [
{{
"operation": "move_single_servo",
"parameters": {{"id": 1, "angle": 60, "time": 500}}
"parameters": {{"id": "servo0", "angle": 60, "time": 500}}
}},
{{
"operation": "set_rgb_light",
"parameters": {{"R": 255, "G": 0, "B": 0}}
}},
{{
"operation": "move_single_servo",
"parameters": {{"id": 1, "angle": 90, "time": 500}}
"parameters": {{"id": "servo1", "angle": 90, "time": 500}}
}}
]
}}
Note: The list of operations can be 1 ore many. And the servos are with ids from servo0 to servo5.
Command:
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def generate_launch_description():
[
launch_ros.actions.Node(
package="robot_arm",
executable="joint_move_node",
executable="joint_move_script_node",
output="screen",
),
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ def generate_launch_description():
executable="command_dispatcher_node",
name="command_dispatcher_node",
)
joint_mover_node = Node(
package="robot_arm",
executable="joint_mover_node",
name="joint_mover_node",
)

# Include the project folder
pkg_robot_arm_gazebo = get_package_share_directory("robot_arm")
Expand Down Expand Up @@ -110,5 +115,6 @@ def generate_launch_description():
robot_controller_spawner_node,
command_receiver_node,
command_dispatcher_node,
joint_mover_node,
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,29 @@ def __init__(self):
self.subscription = self.create_subscription(
String, "json_command_topic", self._json_command_callback, 10
)
self.joint_command_pub = self.create_publisher(String, "/joint_commands", 10)
# Setup publish to servos
self.servo0_pub = self.create_publisher(Float64, "/servo0/control", 10)

def _json_command_callback(self, msg):
def _json_command_callback(self, msg: String):
"""This function is called every time a message is received on the json_command_topic.
An example input is:
{
"operations": [
{
"operation": "move_single_servo",
"parameters": {"id": 1, "angle": 60, "time": 500}
},
{
"operation": "set_rgb_light",
"parameters": {"R": 255, "G": 0, "B": 0}
},
{
"operation": "move_single_servo",
"parameters": {"id": 1, "angle": 90, "time": 500}
}
]
}
An example input is:
{
"operations": [
{
"operation": "move_single_servo",
"parameters": {"id": 1, "angle": 60, "time": 500}
},
{
"operation": "set_rgb_light",
"parameters": {"R": 255, "G": 0, "B": 0}
},
{
"operation": "move_single_servo",
"parameters": {"id": 1, "angle": 90, "time": 500}
}
]
}
"""
try:
self.get_logger().info(f"Dispatching command: {msg.data}")
Expand All @@ -43,7 +44,7 @@ def _json_command_callback(self, msg):
# Process the command_data to control the joint
self._process_command(command_data["operations"])
except json.JSONDecodeError:
self.get_logger().error("Invalid JSON received")
self.get_logger().error(f"Invalid JSON received from command: {msg.data}")

def _process_command(self, operations: List[Any]):
"""Parse the incoming command and take the appropriate action.
Expand All @@ -54,20 +55,26 @@ def _process_command(self, operations: List[Any]):
parameters: Dict[str, Any] = operation.get("parameters", {})

if func_name == "move_single_servo":
self._move_single_joint(parameters.get("id", 0), parameters.get("angle", 0), parameters.get("time", 0))
self._move_single_joint(
servo_id=parameters.get("id", "error_id"),
angle=parameters.get("angle", 0),
time=parameters.get("time", 0)
)
elif func_name == "set_rgb_light": # set the RGB light
pass
self.get_logger().warn(f"Setting RGB light to {parameters}")
else:
self.get_logger().warn(f"Unknown action: {operation}")
self.get_logger().error(f"Unknown action: {operation}")

def _move_single_joint(self, id: int, angle: float, time: int):
if 0 <= id <= self.num_joints:
control_msg = Float64()
# control_msg.data = value
# self.servo0_pub.publish(control_msg)
self.get_logger().error(f"{self.node_name} Moving {id} to {angle}")
def _move_single_joint(self, servo_id: str, angle: float, time: int):
# Joints can only be servo0-servo5 and left_gripper_joint, right_gripper_joint
if servo_id in ["servo0", "servo1", "servo2", "servo3", "servo4", "left_gripper_joint", "right_gripper_joint"]:
command = json.dumps({"id": servo_id, "angle": angle, "time": time})
msg = String()
msg.data = command
self.joint_command_pub.publish(msg)
self.get_logger().info(f"{msg.data} published to /joint_commands")
else:
self.get_logger().warn(f"{self.node_name} Unknown joint: {id}")
self.get_logger().error(f"{self.node_name} Unknown joint: {servo_id}")


def main(args=None):
Expand Down
140 changes: 140 additions & 0 deletions mnlm/robot/robot_arm_ws/src/robot_arm/robot_arm/joint_mover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
import json
import traceback
from std_msgs.msg import String
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from control_msgs.action import FollowJointTrajectory
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
from builtin_interfaces.msg import Duration
from typing import List
from sensor_msgs.msg import JointState


class JointMoverNode(Node):
def __init__(self):
super().__init__("joint_mover_node")
self.num_joints = 5
joint_names = []
for i in range(self.num_joints):
joint_names.append("servo" + str(i))
# Add left and right gripper joints
self.num_gripper_fingers = 2
joint_names.append("left_gripper_joint")
joint_names.append("right_gripper_joint")
self.declare_parameter("joint_names", joint_names) # Default joint names
self.joint_names = (
self.get_parameter("joint_names").get_parameter_value().string_array_value
)
# Used to receive joint states from the robot.
self.latest_joint_state = None
self.joint_state_subscription = self.create_subscription(
JointState, '/joint_states', self._joint_state_callback, 10)
# Used to receive commands from the CommandDispatcherNode.
self.subscription = self.create_subscription(
String,
"/joint_commands",
self._joint_command_callback,
10,
)
# Used to send commands to the robot.
self.action_client = ActionClient(
self,
FollowJointTrajectory,
"/joint_trajectory_controller/follow_joint_trajectory",
)

def _joint_state_callback(self, msg):
self.latest_joint_state = msg

def _get_current_joint_positions(self, joint_names: List[str]):
"""Return the positions of the specified joints using the latest joint state message."""
if self.latest_joint_state is None:
self.get_logger().error('No joint state message received yet.')
return None

positions = []
for name in joint_names:
try:
index = self.latest_joint_state.name.index(name)
positions.append(self.latest_joint_state.position[index])
except ValueError:
self.get_logger().error(f'Joint {name} not found in the latest joint state message.')
return None
return positions

def _joint_command_callback(self, msg: String):
self.get_logger().error(f"Received joint command: {msg.data}")
command_data = json.loads(msg.data)

joint_id = command_data["id"]
angle_in_degrees = command_data["angle"]
time_in_seconds = command_data["time"] / 1000.0 # Convert milliseconds to seconds

# Read the current position from /joint_states topic
positions = self._get_current_joint_positions(self.joint_names)
self.get_logger().info(f"Current joint positions: {positions}")
velocities = [0.0] * (self.num_joints + self.num_gripper_fingers)

# Update the angle for the specified joint, note the command is in degree and the position is in radians
if "servo" in joint_id:
target_servo_index = int(joint_id[-1])
target_position = angle_in_degrees * (3.14159 / 180.0) # Convert degrees to radians
positions[target_servo_index] = target_position

# Only set the velocity of the target joint as 0.5
velocities[target_servo_index] = 0.5
# Send the command to the action server
response = self.send_goal_and_wait(positions=positions, velocities=velocities, time_from_start_sec=3)
if response:
self.get_logger().info(f"Successfully moved joint {joint_id} to {angle_in_degrees} degrees.")
else:
self.get_logger().error(f"Invalid joint ID: {joint_id}")


def send_goal_and_wait(self, positions: List[float], velocities: List[float], time_from_start_sec: int) -> bool:
self.get_logger().info("Sending new goal to the action server...")
goal_msg = FollowJointTrajectory.Goal()
trajectory = JointTrajectory()

trajectory.joint_names = self.joint_names
# Construct the trajectory point to send to the action server.
point = JointTrajectoryPoint()
point.positions = positions
point.velocities = velocities
timeout_sec_full = int(time_from_start_sec)
point.time_from_start = Duration(sec=timeout_sec_full, nanosec=0)
trajectory.points.append(point)

goal_msg.trajectory = trajectory

if not self.action_client.wait_for_server(timeout_sec=3.0):
self.get_logger().error('Action server not available after waiting')
return False
else:
self.get_logger().info("Action server available.")
send_goal_future = self.action_client.send_goal_async(
goal_msg, feedback_callback=self._feedback_callback
)
# rclpy.spin_until_future_complete(self, send_goal_future, timeout_sec=3)
self.get_logger().info("Goal sent.")
return True

def _feedback_callback(self, feedback_msg):
actual_positions = feedback_msg.feedback.actual.positions
actual_velocities = feedback_msg.feedback.actual.velocities
self.get_logger().info(
f"Received feedback: positions: {actual_positions}, velocities: {actual_velocities}"
)


def main(args=None):
rclpy.init(args=args)
joint_mover_node = JointMoverNode()
rclpy.spin(joint_mover_node)
rclpy.shutdown()


if __name__ == "__main__":
main()
3 changes: 2 additions & 1 deletion mnlm/robot/robot_arm_ws/src/robot_arm/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"console_scripts": [
"command_receiver_node = robot_arm.command_receiver:main",
"command_dispatcher_node = robot_arm.command_dispatcher:main",
"joint_move_node = robot_arm.joint_move:main",
"joint_mover_node = robot_arm.joint_mover:main",
"joint_move_script_node = robot_arm.joint_move_script:main",
],
},
)

0 comments on commit ffe1da9

Please sign in to comment.