diff --git a/serial_motor_demo/config/robot_params.yaml b/serial_motor_demo/config/robot_params.yaml
new file mode 100644
index 0000000..e048698
--- /dev/null
+++ b/serial_motor_demo/config/robot_params.yaml
@@ -0,0 +1,11 @@
+# robot_params.yaml
+wheel_odometry_node:
+ ros__parameters:
+ wheel_radius: 0.058 # Meters
+ wheel_base: 0.284 # Meters
+ # Add other parameters as needed
+
+motor_command_node:
+ ros__parameters:
+ wheel_radius: 0.058 # Meters
+ wheel_base: 0.284 # Meters
diff --git a/serial_motor_demo/launch/motor_driver.launch.py b/serial_motor_demo/launch/motor_driver.launch.py
new file mode 100644
index 0000000..77136ec
--- /dev/null
+++ b/serial_motor_demo/launch/motor_driver.launch.py
@@ -0,0 +1,65 @@
+# my_launch_file.launch.py
+
+from launch import LaunchDescription
+from launch.actions import DeclareLaunchArgument
+from launch.substitutions import LaunchConfiguration
+from launch_ros.actions import Node
+from ament_index_python.packages import get_package_share_directory
+import os
+
+def generate_launch_description():
+
+ config = os.path.join(
+ get_package_share_directory('serial_motor_demo'),
+ 'config',
+ 'robot_params.yaml'
+ )
+
+ return LaunchDescription([
+ DeclareLaunchArgument(
+ 'speed', default_value='0.3',
+ description='Speed for teleop_twist_keyboard'),
+ DeclareLaunchArgument(
+ 'turn', default_value='0.5',
+ description='Turn for teleop_twist_keyboard'),
+ DeclareLaunchArgument(
+ 'serial_port', default_value='/dev/ttyUSB0'),
+
+
+ Node(
+ package='serial_motor_demo',
+ executable='driver',
+ name='driver',
+ output='screen',
+ parameters=[
+ {'serial_port': LaunchConfiguration('serial_port')},
+ ],
+ ),
+ Node(
+ package='teleop_twist_keyboard',
+ executable='teleop_twist_keyboard',
+ name='teleop_twist_keyboard',
+ output='screen',
+ prefix='xterm -e',
+ parameters=[
+ {'speed': LaunchConfiguration('speed')},
+ {'turn': LaunchConfiguration('turn')},
+ ],
+ ),
+ Node(
+ package='serial_motor_demo',
+ executable='motor_command_node',
+ name='motor_command_node',
+ output='screen',
+ parameters=[config],
+
+ ),
+
+ Node(
+ package='serial_motor_demo',
+ executable='wheels_odometry',
+ name='wheel_odometry_node',
+ output='screen',
+ parameters=[config],
+ ),
+ ])
diff --git a/serial_motor_demo/motor_driver_launch.png b/serial_motor_demo/motor_driver_launch.png
new file mode 100644
index 0000000..3e552b0
Binary files /dev/null and b/serial_motor_demo/motor_driver_launch.png differ
diff --git a/serial_motor_demo/package.xml b/serial_motor_demo/package.xml
index 49b03ae..01fbb10 100644
--- a/serial_motor_demo/package.xml
+++ b/serial_motor_demo/package.xml
@@ -11,6 +11,9 @@
ament_flake8
ament_pep257
python3-pytest
+
+ geometry_msgs
+ tf2_ros
rclpy
serial_motor_demo_msgs
diff --git a/serial_motor_demo/serial_motor_demo/driver.py b/serial_motor_demo/serial_motor_demo/driver.py
index 6b2018a..dc2421c 100644
--- a/serial_motor_demo/serial_motor_demo/driver.py
+++ b/serial_motor_demo/serial_motor_demo/driver.py
@@ -18,17 +18,17 @@ def __init__(self):
# Setup parameters
- self.declare_parameter('encoder_cpr', value=0)
+ self.declare_parameter('encoder_cpr', value=17200)
if (self.get_parameter('encoder_cpr').value == 0):
print("WARNING! ENCODER CPR SET TO 0!!")
- self.declare_parameter('loop_rate', value=0)
+ self.declare_parameter('loop_rate', value=30)
if (self.get_parameter('loop_rate').value == 0):
print("WARNING! LOOP RATE SET TO 0!!")
- self.declare_parameter('serial_port', value="/dev/ttyUSB0")
+ self.declare_parameter('serial_port', value="/dev/ttyACM0")
self.serial_port = self.get_parameter('serial_port').value
diff --git a/serial_motor_demo/serial_motor_demo/motor_command_node.py b/serial_motor_demo/serial_motor_demo/motor_command_node.py
new file mode 100644
index 0000000..b0759fa
--- /dev/null
+++ b/serial_motor_demo/serial_motor_demo/motor_command_node.py
@@ -0,0 +1,69 @@
+import rclpy
+from rclpy.node import Node
+from geometry_msgs.msg import Twist
+from serial_motor_demo_msgs.msg import MotorCommand
+import math
+
+class MotorCommandNode(Node):
+ def __init__(self):
+ super().__init__('motor_command_node')
+
+ # Create a subscriber to listen to Twist messages
+ self.create_subscription(
+ Twist,
+ 'cmd_vel', # Topic name might be different based on your setup
+ self.twist_callback,
+ 10
+ )
+
+ # Create a publisher to send out MotorCommand messages
+ self.publisher = self.create_publisher(MotorCommand, 'motor_command', 10)
+
+ # Especificaciones del robot
+ self.declare_parameter('wheel_radius', 0.0553, ignore_override=True)
+ self.declare_parameter('wheel_base', 0.284, ignore_override=True)
+
+ # Retrieve parameter values
+ self.wheel_radius = self.get_parameter('wheel_radius').value
+ self.wheel_base = self.get_parameter('wheel_base').value
+
+
+ self.get_logger().info('Motor Command Node Started')
+
+ def twist_callback(self, msg):
+ try:
+ # Assuming linear.x controls the speed and angular.z controls the steering
+ speed = msg.linear.x
+ rotation = msg.angular.z
+
+
+
+ mot_1_speed = (speed + (rotation * self.wheel_base / 2)) / self.wheel_radius
+ mot_2_speed = (speed - (rotation * self.wheel_base / 2)) / self.wheel_radius
+
+ # Convert to rad/s
+ mot_1_speed = mot_1_speed / (2 * math.pi)
+ mot_2_speed = mot_2_speed / (2 * math.pi)
+
+ # Create and publish the motor command
+ motor_msg = MotorCommand()
+ motor_msg.is_pwm = False
+ motor_msg.mot_1_req_rad_sec = mot_1_speed
+ motor_msg.mot_2_req_rad_sec = mot_2_speed
+ self.publisher.publish(motor_msg)
+
+ self.get_logger().info(f'Motor Commands sent: mot_1 = {mot_1_speed:.2f} rad/s, mot_2 = {mot_2_speed:.2f} rad/s')
+
+ except Exception as e:
+ self.get_logger().error(f'Error in twist_callback: {str(e)}')
+
+
+
+def main(args=None):
+ rclpy.init(args=args)
+ node = MotorCommandNode()
+ rclpy.spin(node)
+ rclpy.shutdown()
+
+if __name__ == '__main__':
+ main()
diff --git a/serial_motor_demo/serial_motor_demo/wheels_odometry.py b/serial_motor_demo/serial_motor_demo/wheels_odometry.py
new file mode 100644
index 0000000..cf49c47
--- /dev/null
+++ b/serial_motor_demo/serial_motor_demo/wheels_odometry.py
@@ -0,0 +1,87 @@
+import rclpy
+from rclpy.node import Node
+from geometry_msgs.msg import Twist, Pose, Quaternion, Point, TransformStamped
+from nav_msgs.msg import Odometry
+from utils.transformations import euler2quaternion
+from math import sin, cos
+import tf2_ros
+
+from serial_motor_demo_msgs.msg import MotorVels
+
+
+class OdometryNode(Node):
+ def __init__(self):
+ super().__init__('wheel_odometry_node')
+ self.subscription = self.create_subscription(
+ MotorVels, 'motor_vels', self.listener_callback, 10)
+ self.publisher = self.create_publisher(Odometry, 'wheel_odom', 10)
+ self.odom_broadcaster = tf2_ros.TransformBroadcaster(self)
+ self.last_time = self.get_clock().now()
+
+ # Estado inicial del robot
+ self.x = 0.0
+ self.y = 0.0
+ self.theta = 0.0
+
+ # Especificaciones del robot
+ self.wheel_radius = self.declare_parameter('wheel_radius', 0.0553).value
+ self.wheel_base = self.declare_parameter('wheel_base', 0.284).value
+
+ def listener_callback(self, msg):
+ current_time = self.get_clock().now()
+ dt = (current_time - self.last_time).nanoseconds / 1e9
+ self.last_time = current_time
+
+ # Calcular velocidades lineales de las ruedas
+ v_left = msg.mot_1_rad_sec * self.wheel_radius
+ v_right = msg.mot_2_rad_sec * self.wheel_radius
+
+ # Calcular la velocidad lineal y angular del robot
+ V = (v_right + v_left) / 2
+ omega = (v_right - v_left) / self.wheel_base
+
+ # Actualizar la posición y orientación
+ self.x += V * cos(self.theta) * dt
+ self.y += V * sin(self.theta) * dt
+ self.theta += omega * dt
+
+ # Crear y llenar el mensaje de odometría
+ odom = Odometry()
+ odom.header.stamp = current_time.to_msg()
+ odom.header.frame_id = "odom"
+ odom.child_frame_id = "base_link"
+ orientation=euler2quaternion(0, 0, self.theta)
+ odom.pose.pose = Pose(
+ position=Point(x=self.x, y=self.y, z=0.0),
+ orientation=Quaternion(x=orientation[0], y=orientation[1], z=orientation[2], w=orientation[3])
+ )
+
+ # Aquí puedes agregar la velocidad (Twist) si es necesario
+
+ self.publisher.publish(odom)
+
+ # Enviar transformación
+ t = TransformStamped()
+ t.header.stamp = current_time.to_msg()
+ t.header.frame_id = 'odom'
+ t.child_frame_id = 'base_link'
+ t.transform.translation.x = self.x
+ t.transform.translation.y = self.y
+ quaternion = euler2quaternion(0, 0, self.theta)
+ # Use the quaternion for rotation
+ t.transform.rotation.x = quaternion[0]
+ t.transform.rotation.y = quaternion[1]
+ t.transform.rotation.z = quaternion[2]
+ t.transform.rotation.w = quaternion[3]
+
+ self.odom_broadcaster.sendTransform(t)
+
+def main(args=None):
+ rclpy.init(args=args)
+ odometry_node = OdometryNode()
+ rclpy.spin(odometry_node)
+ odometry_node.destroy_node()
+ rclpy.shutdown()
+
+if __name__ == '__main__':
+ main()
diff --git a/serial_motor_demo/setup.cfg b/serial_motor_demo/setup.cfg
index bd0b17c..eafb207 100644
--- a/serial_motor_demo/setup.cfg
+++ b/serial_motor_demo/setup.cfg
@@ -1,4 +1,4 @@
[develop]
-script-dir=$base/lib/serial_motor_demo
+script_dir=$base/lib/serial_motor_demo
[install]
-install-scripts=$base/lib/serial_motor_demo
+install_scripts=$base/lib/serial_motor_demo
diff --git a/serial_motor_demo/setup.py b/serial_motor_demo/setup.py
index d7e142e..5691f03 100644
--- a/serial_motor_demo/setup.py
+++ b/serial_motor_demo/setup.py
@@ -1,4 +1,6 @@
from setuptools import setup
+import os
+from glob import glob
package_name = 'serial_motor_demo'
@@ -6,10 +8,14 @@
name=package_name,
version='0.0.0',
packages=[package_name],
+ py_modules=['utils.transformations'],
data_files=[
('share/ament_index/resource_index/packages',
- ['resource/' + package_name]),
+ ['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
+ # Include launch files
+ (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
+ (os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
],
install_requires=['setuptools'],
zip_safe=True,
@@ -19,9 +25,12 @@
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
- 'console_scripts': [
- 'gui = serial_motor_demo.gui:main',
- 'driver = serial_motor_demo.driver:main'
- ],
- },
+ 'console_scripts': [
+ 'gui = serial_motor_demo.gui:main',
+ 'driver = serial_motor_demo.driver:main',
+ 'motor_command_node = serial_motor_demo.motor_command_node:main',
+ 'wheels_odometry = serial_motor_demo.wheels_odometry:main',
+ ],
+},
+
)
diff --git a/serial_motor_demo/utils/__init__.py b/serial_motor_demo/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/serial_motor_demo/utils/transformations.py b/serial_motor_demo/utils/transformations.py
new file mode 100644
index 0000000..542a289
--- /dev/null
+++ b/serial_motor_demo/utils/transformations.py
@@ -0,0 +1,179 @@
+"""
+$$$$$$$\ $$$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$$$\ $$$$$$$\ $$\
+$$ __$$\ $$ _____|$$ __$$\ $$ __$$\ $$ __$$\ $$ _____| $$ __$$\ $$$$ |
+$$ | $$ |$$ | $$ / \__|$$ / $$ |$$ | $$ |$$ | $$ | $$ |\_$$ |
+$$$$$$$ |$$$$$\ $$ | $$ | $$ |$$$$$$$ |$$$$$\ $$$$$$$ | $$ |
+$$ ____/ $$ __| $$ | $$ | $$ |$$ __$$< $$ __| $$ ____/ $$ |
+$$ | $$ | $$ | $$\ $$ | $$ |$$ | $$ |$$ | $$ | $$ |
+$$ | $$$$$$$$\ \$$$$$$ | $$$$$$ |$$ | $$ |$$$$$$$$\ $$ | $$$$$$\
+\__| \________| \______/ \______/ \__| \__|\________| \__| \______|
+"""
+# * PECORE - Master en Automática y Control en Robótica *
+# * Universitat Politècnica de Catalunya (UPC) *
+# * *
+# * Participantes: *
+# * - Victor Escribano Garcia *
+# * - Alejandro Acosta Montilla *
+# * *
+# * Año: 2023
+#
+# * FIND THE ORIGINAL CODE IN THE FOLLOWING GITHUB REPO: https://github.com/VictorEscribano/PECORE/tree/main
+"""
+Descripcion: Funciones para poder transformar entre matrices de transformacion y TransformStamped de ROS.
+"""
+
+from geometry_msgs.msg import TransformStamped, PoseStamped
+import numpy as np
+import math
+
+
+# Define a function to calculate the transformation matrix
+def Rt2homo_matrix(translation, rotation):
+ # Reconstruct the transformation matrix
+ # Create a 4x4 identity matrix
+ matrix = np.identity(4)
+
+ # Set the translation components in the matrix
+ matrix[0, 3] = translation.x
+ matrix[1, 3] = translation.y
+ matrix[2, 3] = translation.z
+
+ # Extract the rotation components
+ x = rotation.x
+ y = rotation.y
+ z = rotation.z
+ w = rotation.w
+
+ # Calculate the rotation matrix components
+ xx = x * x
+ xy = x * y
+ xz = x * z
+ yy = y * y
+ yz = y * z
+ zz = z * z
+ wx = w * x
+ wy = w * y
+ wz = w * z
+
+ # Set the rotation components in the matrix
+ matrix[0, 0] = 1 - 2 * (yy + zz)
+ matrix[0, 1] = 2 * (xy - wz)
+ matrix[0, 2] = 2 * (xz + wy)
+
+ matrix[1, 0] = 2 * (xy + wz)
+ matrix[1, 1] = 1 - 2 * (xx + zz)
+ matrix[1, 2] = 2 * (yz - wx)
+
+ matrix[2, 0] = 2 * (xz - wy)
+ matrix[2, 1] = 2 * (yz + wx)
+ matrix[2, 2] = 1 - 2 * (xx + yy)
+
+ return matrix
+
+
+def homo_matrix2tf(matrix, parent_frame, child_frame, clock_stamp):
+ transform_stamped = TransformStamped()
+ transform_stamped.header.stamp = clock_stamp
+ transform_stamped.header.frame_id = parent_frame
+ transform_stamped.child_frame_id = child_frame
+
+ # Extract translation from the matrix
+ translation = np.array([matrix[0, 3], matrix[1, 3], matrix[2, 3]])
+
+ # Extract rotation (quaternion) from the matrix
+ trace = matrix[0, 0] + matrix[1, 1] + matrix[2, 2]
+ if trace > 0:
+ s = 0.5 / np.sqrt(trace + 1.0)
+ w = 0.25 / s
+ x = (matrix[2, 1] - matrix[1, 2]) * s
+ y = (matrix[0, 2] - matrix[2, 0]) * s
+ z = (matrix[1, 0] - matrix[0, 1]) * s
+ elif matrix[0, 0] > matrix[1, 1] and matrix[0, 0] > matrix[2, 2]:
+ s = 2.0 * np.sqrt(1.0 + matrix[0, 0] - matrix[1, 1] - matrix[2, 2])
+ w = (matrix[2, 1] - matrix[1, 2]) / s
+ x = 0.25 * s
+ y = (matrix[0, 1] + matrix[1, 0]) / s
+ z = (matrix[0, 2] + matrix[2, 0]) / s
+ elif matrix[1, 1] > matrix[2, 2]:
+ s = 2.0 * np.sqrt(1.0 + matrix[1, 1] - matrix[0, 0] - matrix[2, 2])
+ w = (matrix[0, 2] - matrix[2, 0]) / s
+ x = (matrix[0, 1] + matrix[1, 0]) / s
+ y = 0.25 * s
+ z = (matrix[1, 2] + matrix[2, 1]) / s
+ else:
+ s = 2.0 * np.sqrt(1.0 + matrix[2, 2] - matrix[0, 0] - matrix[1, 1])
+ w = (matrix[1, 0] - matrix[0, 1]) / s
+ x = (matrix[0, 2] + matrix[2, 0]) / s
+ y = (matrix[1, 2] + matrix[2, 1]) / s
+ z = 0.25 * s
+
+ # Set the translation and rotation in the TransformStamped message
+ transform_stamped.transform.translation.x = translation[0]
+ transform_stamped.transform.translation.y = translation[1]
+ transform_stamped.transform.translation.z = translation[2]
+ transform_stamped.transform.rotation.x = x
+ transform_stamped.transform.rotation.y = y
+ transform_stamped.transform.rotation.z = z
+ transform_stamped.transform.rotation.w = w
+
+ return transform_stamped
+
+def transform2pose(transform):
+ pose = PoseStamped()
+ pose.header = transform.header
+ pose.pose.position.x = transform.transform.translation.x
+ pose.pose.position.y = transform.transform.translation.y
+ pose.pose.position.z = transform.transform.translation.z
+ pose.pose.orientation = transform.transform.rotation
+ return pose
+
+def euler2quaternion(ai, aj, ak):
+ ai /= 2.0
+ aj /= 2.0
+ ak /= 2.0
+ ci = math.cos(ai)
+ si = math.sin(ai)
+ cj = math.cos(aj)
+ sj = math.sin(aj)
+ ck = math.cos(ak)
+ sk = math.sin(ak)
+ cc = ci*ck
+ cs = ci*sk
+ sc = si*ck
+ ss = si*sk
+ q = np.empty((4, ))
+ q[0] = cj*sc - sj*cs
+ q[1] = cj*ss + sj*cc
+ q[2] = cj*cs - sj*sc
+ q[3] = cj*cc + sj*ss
+ return q
+
+
+def quaternion2euler(x, y, z, w):
+ """
+ Convert a quaternion into euler angles (roll, pitch, yaw)
+ roll is rotation around x in radians (counterclockwise)
+ pitch is rotation around y in radians (counterclockwise)
+ yaw is rotation around z in radians (counterclockwise)
+ """
+ t0 = +2.0 * (w * x + y * z)
+ t1 = +1.0 - 2.0 * (x * x + y * y)
+ roll_x = math.atan2(t0, t1)
+
+ t2 = +2.0 * (w * y - z * x)
+ t2 = +1.0 if t2 > +1.0 else t2
+ t2 = -1.0 if t2 < -1.0 else t2
+ pitch_y = math.asin(t2)
+
+ t3 = +2.0 * (w * z + x * y)
+ t4 = +1.0 - 2.0 * (y * y + z * z)
+ yaw_z = math.atan2(t3, t4)
+
+ return roll_x, pitch_y, yaw_z # in radians
+
+def normalize_angle(angle):
+ while angle > math.pi:
+ angle -= 2.0 * math.pi
+ while angle < -math.pi:
+ angle += 2.0 * math.pi
+ return angle
\ No newline at end of file