From d289bc0ebd169bbd0dc963280be76964ba837ac0 Mon Sep 17 00:00:00 2001
From: daviddudas <david.dudas@outlook.com>
Date: Wed, 15 Jan 2020 16:58:50 +0100
Subject: [PATCH] Initial commit

---
 CMakeLists.txt                   |  44 ++++++
 LICENSE                          |  27 ++++
 README.asciidoc                  | 234 +++++++++++++++++++++++++++++++
 action/GoToPose.action           |   9 ++
 launch/demo.launch               |  68 +++++++++
 launch/revvy-config.launch       |  42 ++++++
 nodes/mecanum_drive_controller   |  68 +++++++++
 nodes/mecanum_drive_odometry     | 123 ++++++++++++++++
 package.xml                      |  22 +++
 setup.py                         |  12 ++
 src/mecanum_drive/__init__.py    |   0
 src/mecanum_drive/controller.py  |  62 ++++++++
 src/mecanum_drive/controller.pyc | Bin 0 -> 2858 bytes
 src/mecanum_drive/encoder.py     |  52 +++++++
 src/mecanum_drive/encoder.pyc    | Bin 0 -> 2416 bytes
 src/mecanum_drive/odometry.py    |  79 +++++++++++
 src/mecanum_drive/odometry.pyc   | Bin 0 -> 3929 bytes
 src/mecanum_drive/pose.py        |  17 +++
 src/mecanum_drive/pose.pyc       | Bin 0 -> 1015 bytes
 tests/test_controller.py         |  66 +++++++++
 tests/test_encoder.py            |  62 ++++++++
 tests/test_odometry.py           | 107 ++++++++++++++
 22 files changed, 1094 insertions(+)
 create mode 100755 CMakeLists.txt
 create mode 100755 LICENSE
 create mode 100755 README.asciidoc
 create mode 100755 action/GoToPose.action
 create mode 100755 launch/demo.launch
 create mode 100755 launch/revvy-config.launch
 create mode 100755 nodes/mecanum_drive_controller
 create mode 100755 nodes/mecanum_drive_odometry
 create mode 100755 package.xml
 create mode 100755 setup.py
 create mode 100755 src/mecanum_drive/__init__.py
 create mode 100755 src/mecanum_drive/controller.py
 create mode 100644 src/mecanum_drive/controller.pyc
 create mode 100755 src/mecanum_drive/encoder.py
 create mode 100644 src/mecanum_drive/encoder.pyc
 create mode 100755 src/mecanum_drive/odometry.py
 create mode 100644 src/mecanum_drive/odometry.pyc
 create mode 100755 src/mecanum_drive/pose.py
 create mode 100644 src/mecanum_drive/pose.pyc
 create mode 100755 tests/test_controller.py
 create mode 100755 tests/test_encoder.py
 create mode 100755 tests/test_odometry.py

diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100755
index 0000000..8bafa33
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 2.8.3)
+project(mecanum_drive)
+
+find_package(catkin REQUIRED COMPONENTS
+  rospy
+  std_msgs
+  nav_msgs
+  geometry_msgs
+  tf
+  tf2
+  actionlib
+  actionlib_msgs
+)
+
+add_action_files(
+  DIRECTORY action
+  FILES GoToPose.action
+)
+
+if (CATKIN_ENABLE_TESTING)
+  catkin_add_nosetests(tests)
+endif()
+
+if (CATKIN_ENABLE_TESTING)
+  find_package(rostest REQUIRED)
+#  add_rostest(tests/your_first_rostest.test)
+#  add_rostest(tests/your_second_rostest.test)
+endif()
+
+catkin_install_python(
+    PROGRAMS
+        nodes/mecanum_drive_odometry
+        nodes/mecanum_drive_controller
+    DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
+
+catkin_python_setup()
+
+generate_messages (
+  DEPENDENCIES actionlib_msgs std_msgs geometry_msgs
+)
+
+catkin_package(
+  CATKIN_DEPENDS actionlib_msgs
+)
diff --git a/LICENSE b/LICENSE
new file mode 100755
index 0000000..8626745
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2016, Mark Rose
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of diff_drive nor the names of its contributors may
+  be used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.asciidoc b/README.asciidoc
new file mode 100755
index 0000000..54127dc
--- /dev/null
+++ b/README.asciidoc
@@ -0,0 +1,234 @@
+= diff_drive -- Differential-Drive Controller
+:imagesdir: ./images
+
+This package implements ROS nodes to control and monitor a differential-drive robot.
+
+The package
+is intended as a lighter-weight solution than the ROS controller framework, albeit with lower
+performance since it is written in Python. If you need tight, real-time control, you may want
+to look at link:http://wiki.ros.org/ros_controllers[ros_controllers],
+a C++ package which includes a differential-drive controller that is integrated with
+link:http://wiki.ros.org/ros_control[ros_control] and
+link:http://wiki.ros.org/controller_manager[controller_manager]. Those controllers are designed
+to integrate with hardware in the same process, rather than using topics. Instead, this package
+expects to publish the desired motor speeds using standard ROS messages.
+
+== Supplied Nodes
+
+* `diff_drive_controller` -- Converts from twist to wheel velocities for motors.
+* `diff_drive_odometry` -- Publishes odometry from wheel encoder data.
+* `diff_drive_go_to_goal` -- Moves the robot to a goal position.
+* `diff_drive_mock_robot` -- Implements a mock differential drive robot, for testing.
+
+The nodes in this package are designed with these considerations:
+
+* The node and hardware implementing differential drive should deal only in encoder ticks.
+* Conversions to and from physical coordinates should happen within the nodes in this package.
+* This package should integrate cleanly with the navigation stack, perhaps with remappings.
+* Nodes should use standard topic and parameter names used by the navigation stack, but should allow remapping.
+
+== Demo
+
+To see all the nodes in this package in action, you can launch a demo from ROS. There are no
+dependencies other than the standard ROS packages.
+
+     roslaunch diff_drive demo.launch
+     
+This launches _rviz_ as part of the demo, and shows the robot position as a small coordinate system on
+a 0.25m grid. In rviz you can move the robot by clicking the _2D Nav Goal_ button in the tools panel at the top.
+Then click and drag within the grid to set the robot goal position and heading. The mock robot will move to
+that new pose, which you can see by the movement of the robot axes.
+
+In the demo, both forward and backward movement is allowed, so if the goal position is behind the robot,
+it will move backward. You can force the robot to move foward only by setting the parameter `~forwardMovementOnly`
+to `true`.
+
+== ROS API
+
+=== 1. diff_drive_controller
+
+Listens for desired linear and angular velocity, and publishes corresponding wheel velocities, in encoder ticks per second, required to achieve those velocities.
+
+==== Published Topics
+
+`~lwheel_desired_rate` (std_msgs/Int32)::
+Desired left wheel rotation rate, in encoder ticks per second.
+
+`~rwheel_desired_rate` (std_msgs/Int32)::
+Desired right wheel rotation rate, in encoder ticks per second.
+
+==== Subscribed Topics
+
+`~cmd_vel` (geometry_msgs/Twist)::
+Desired linear and angular velocity.
+
+==== Parameters
+
+`~ticks_per_meter` (double)::
+Number of encoder ticks per meter of travel.
+
+`~wheel_separation` (double)::
+Distance between the two wheels (meters).
+
+`~rate` (int, default: 50)::
+The rate that the output velocity target messages will be published (Hz).
+
+`~timeout_ticks` (int, default: 2)::
+The number of velocity target messages that will be published after the last twist message is received.
+
+=== 2. diff_drive_odometry
+
+Listens for wheel movement and rates and publishes the transform between the odom frame and the robot frame.
+
+==== Published Topics
+
+`~odom` -- (nav_msgs/Odometry)::
+The robot odometry -- the current robot pose.
+
+`~tf`::
+The transform between the odometry frame and the robot frame.
+
+==== Subscribed Topics
+
+`~lwheel_ticks` (std_msgs/Int32)::
+Cumulative encoder ticks of the left wheel.
+
+`~rwheel_ticks` (std_msgs/Int32)::
+Cumulative encoder ticks of the right wheel.
+
+`~lwheel_rate` (std_msgs/Float32)::
+Left wheel rotation rate, in encoder ticks per second.
+
+`~rwheel_rate` (std_msgs/Float32)::
+Right wheel rotation rate, in encoder ticks per second.
+
+==== Parameters
+
+`~ticks_per_meter` (double)::
+Number of encoder ticks per meter of travel.
+
+`~wheel_separation` (double)::
+Distance between the two wheels (m).
+
+`~rate` (double, default 10.0)::
+The rate at which the `tf` and `odom` topics are published (Hz).
+
+`~timeout` (double, default 0.2)::
+The amount of time to continue publishing desired wheel rates after receiving a twist message (seconds).
+If set to zero, wheel velocities will be sent only when a new twist message is received.
+
+`~base_frame_id` (string, default: "base_link")::
+The name of the base frame of the robot. 
+
+`~odom_frame_id` (string, default: "odom")::
+The name of the odometry reference frame. 
+
+`~encoder_min` (int, default: -32768)::
+
+`~encoder_max` (int, default: 32768)::
+The min and max value the encoder should output. Used to calculate odometry when the values wrap around. 
+
+`~wheel_low_wrap` (int, default: 0.3 * (encoder_max - encoder_min + 1) + encoder_min)::
+
+`~wheel_high_wrap` (int, default: 0.7 * (encoder_max - encoder_min + 1) + encoder_min)::
+If a reading is greater than wheel_high_wrap and the next reading is less than wheel_low_wrap, then the reading has wrapped around in the positive direction, and the odometry will be calculated appropriately. The same concept applies for the negative direction.
+
+=== 3. diff_drive_go_to_goal
+
+Listens for new goal poses and computes velocities needed to achieve the goal.
+
+==== Published Topics
+
+`~distance_to_goal` (std_msgs/Float32)::
+Distance to the goal position (meters).
+
+`~cmd_vel` (geometry_msgs/Twist)::
+Desired linear and angular velocity to move toward the goal pose.
+
+==== Subscribed Topics
+
+`~goal` (geometry_msgs/Pose)::
+Desired goal pose.
+
+==== Parameters
+
+`~rate` (float, default: 10)::
+Rate at which to publish desired velocities (Hz).
+
+`~goal_linear_tolerance` (float, default: 0.1)::
+The distance from the goal at which the robot is assumed to have accomplished the goal position (meters).
+
+`~goal_angular_tolerance` (float, default: 0.087)::
+The difference between robot angle and goal pose angle at which the robot is assumed to have
+accomplished the goal attitude (radians). Default value is approximately 5 degrees.
+
+`~max_linear_velocity` (float, default: 0.2)::
+The maximum linear velocity toward the goal (meters/second).
+
+`~max_angular_velocity` (float, default: 1.5)::
+The maximum angular velocity (radians/second).
+
+`~max_linear_acceleration` (float, default: 4.0)::
+The maximum linear acceleration (meters/second^2).
+
+`~forwardMovementOnly` (boolean, default: true)::
+If true, only forward movement is allowed to achieve the goal position.
+If false, the robot will move backward to the goal if that is the most
+direct path.
+
+`~Kp` (float, default: 3.0)::
+Linear distance proportionality constant. Higher values make the robot accelerate more quickly toward the goal and decelerate less quickly.
+
+`~Ka` (float: default: 8.0)::
+Proportionality constant for angle to goal position. Higher values make the robot turn more quickly toward the goal.
+
+`~Kb` (float: default: -1.5)::
+Proportionality constant for angle to goal pose direction. Higher values make the robot turn more quickly toward the goal pose direction. This value should be negative, per _Autonomous Mobile Robots_.
+
+The control law for determining the linear and angular velocity to move toward the goal works as follows. Let _d_ be the distance to the goal. Let _a_ be the angle between the robot heading and the goal position, where left is positive. Let _b_ be the angle between the goal direction and the final pose angle, where left is positive. Then the robot linear and angular velocities are calculated like this:
+
+    v = Kp * d
+    w = Ka*a + Kb*b
+
+See _Autonomous Mobile Robots, Second Edition_ by Siegwart et. al., section 3.6.2.4. In this code, when the robot
+is near enough to the goal, _v_ is set to zero, and _w_ is simply _Kb*b_.
+
+To ensure convergence toward the goal, _K~p~_ and _K~a~_ must be positive, _K~b~_ must be negative, and _K~a~_
+must be greater than _K~p~_. To ensure robust convergence, so that the robot never changes direction,
+_K~a~_ - 5/3*_K~b~_ - 2/pi*_K~p~_ must be greater than zero.
+
+=== 4. diff_drive_mock_robot
+
+Implements a simulation of perfect differential drive robot hardware. It immediately follows any speed
+commands received with infinite acceleration, and publishes the wheel encoder values and encoder
+rates.
+
+==== Published Topics
+
+`~lwheel_ticks` (std_msgs/Int32)::
+Cumulative encoder ticks of the left wheel.
+
+`~rwheel_ticks` (std_msgs/Int32)::
+Cumulative encoder ticks of the right wheel.
+
+`~lwheel_rate` (std_msgs/Float32)::
+Left wheel rotation rate, in encoder ticks per second.
+
+`~rwheel_rate` (std_msgs/Float32)::
+Right wheel rotation rate, in encoder ticks per second.
+
+==== Subscribed Topics
+
+`~lwheel_desired_rate` (std_msgs/Int32)::
+Desired left wheel rotation rate, in encoder ticks per second.
+
+`~rwheel_desired_rate` (std_msgs/Int32)::
+Desired right wheel rotation rate, in encoder ticks per second.
+
+==== Parameters
+
+`~cmd_timeout` (float, default: 0.2)::
+The amount of time after the last wheel rate message when the robot should stop automatically (seconds).
+
+`~rate` (float, default 10.0)::
+The rate at which the simulated wheel encoder values and rates should be published (Hz).
diff --git a/action/GoToPose.action b/action/GoToPose.action
new file mode 100755
index 0000000..1cf9b79
--- /dev/null
+++ b/action/GoToPose.action
@@ -0,0 +1,9 @@
+# goal definition
+geometry_msgs/PoseStamped pose
+---
+# result definition
+bool success
+---
+# feedback
+bool processing
+
diff --git a/launch/demo.launch b/launch/demo.launch
new file mode 100755
index 0000000..60711d5
--- /dev/null
+++ b/launch/demo.launch
@@ -0,0 +1,68 @@
+<launch>
+
+  <arg name="ticks_per_meter" value="10000" />
+  <arg name="wheel_separation" value="0.2" />
+
+  <arg name="urdf_file" default="$(find diff_drive)/urdf/diff_drive_description.xacro" />
+  <param name="robot_description" command="$(find xacro)/xacro --inorder $(arg urdf_file)" />
+
+  <!-- Publish the robot state -->
+  <node name="robot_state_publisher" pkg="robot_state_publisher"
+        type="robot_state_publisher">
+    <param name="publish_frequency" value="10.0"/>
+  </node>
+
+  <!-- Provide simulated control of the robot joint angles -->
+  <node name="joint_state_publisher" pkg="joint_state_publisher"
+        type="joint_state_publisher">
+    <param name="use_gui" value="False" />
+    <param name="rate" value="10.0"/>
+  </node>
+
+  <node name="controller" pkg="diff_drive" type="diff_drive_controller"
+        output="screen">
+    <rosparam subst_value="true">
+      ticks_per_meter: $(arg ticks_per_meter)
+      wheel_separation: $(arg wheel_separation)
+      max_motor_speed: 3000
+      timeout: 1.0
+    </rosparam>
+    <remap from="cmd_vel" to="/robot/cmd_vel" />
+  </node>
+  
+  <node name="odom_publisher" pkg="diff_drive" type="diff_drive_odometry"
+        output="screen">
+    <rosparam subst_value="true">
+      ticks_per_meter: $(arg ticks_per_meter)
+      wheel_separation: $(arg wheel_separation)
+    </rosparam>
+  </node>
+
+  <node name="robot" pkg="diff_drive" type="diff_drive_mock_robot"
+        output="screen">
+    <remap from="~lwheel_desired_rate" to="lwheel_desired_rate" />
+    <remap from="~rwheel_desired_rate" to="rwheel_desired_rate" />
+    <remap from="~lwheel_ticks" to="lwheel_ticks" />
+    <remap from="~rwheel_ticks" to="rwheel_ticks" />
+  </node>
+
+  <node name="diff_drive_go_to_goal" pkg="diff_drive"
+        type="diff_drive_go_to_goal" output="screen">
+    <param name="~rate" value="20" />
+    <param name="~kP" value="0.5" />
+    <param name="~kA" value="1.0" />
+    <param name="~kB" value="-0.8" />
+    <param name="~max_linear_speed" value="0.2" />
+    <param name="~min_linear_speed" value="0.05" />
+    <param name="~max_angular_speed" value="0.7" />
+    <param name="~min_angular_speed" value="0.1" />
+    <param name="~linear_tolerance" value="0.01" />
+    <param name="~angular_tolerance" value="0.04" />
+    <param name="~forwardMovementOnly" value="false" />
+    <remap from="cmd_vel" to="/robot/cmd_vel" />
+  </node>
+
+  <node name="rviz" pkg="rviz" type="rviz"
+        args="-d $(find diff_drive)/config/view.rviz" />
+
+</launch>
diff --git a/launch/revvy-config.launch b/launch/revvy-config.launch
new file mode 100755
index 0000000..39ab4be
--- /dev/null
+++ b/launch/revvy-config.launch
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<launch>
+
+  <arg name="ticks_per_meter" value="1800" />
+  <arg name="wheel_separation" value="0.1" />
+
+  <arg name="urdf_file" default="$(find diff_drive)/urdf/diff_drive_description.xacro" />
+  <param name="robot_description" command="$(find xacro)/xacro --inorder $(arg urdf_file)" />
+
+  <!-- Publish the robot state -->
+  <node name="robot_state_publisher" pkg="robot_state_publisher"
+        type="robot_state_publisher">
+    <param name="publish_frequency" value="10.0"/>
+  </node>
+
+  <!-- Provide simulated control of the robot joint angles -->
+  <node name="joint_state_publisher" pkg="joint_state_publisher"
+        type="joint_state_publisher">
+    <param name="use_gui" value="False" />
+    <param name="rate" value="10.0"/>
+  </node>
+
+  <node name="controller" pkg="diff_drive" type="diff_drive_controller"
+        output="screen">
+    <rosparam subst_value="true">
+      ticks_per_meter: $(arg ticks_per_meter)
+      wheel_separation: $(arg wheel_separation)
+      max_motor_speed: 3000
+      timeout: 1.0
+    </rosparam>
+  </node>
+  
+  <node name="odom_publisher" pkg="diff_drive" type="diff_drive_odometry"
+        output="screen">
+    <rosparam subst_value="true">
+      ticks_per_meter: $(arg ticks_per_meter)
+      wheel_separation: $(arg wheel_separation)
+    </rosparam>
+  </node>
+
+
+</launch>
diff --git a/nodes/mecanum_drive_controller b/nodes/mecanum_drive_controller
new file mode 100755
index 0000000..d788a3a
--- /dev/null
+++ b/nodes/mecanum_drive_controller
@@ -0,0 +1,68 @@
+#! /usr/bin/env python
+from __future__ import division
+
+import rospy
+from geometry_msgs.msg import Twist
+from std_msgs.msg import Int16, Int16MultiArray
+
+from mecanum_drive import controller
+
+class ControllerNode:
+
+    def __init__(self):
+        self.controller = controller.Controller()
+        self.linearXVelocity = 0.0
+        self.linearYVelocity = 0.0
+        self.angularVelocity = 0.0
+        self.wheels_to_send = Int16MultiArray()
+
+    def main(self):                                        
+        self.wheelPub = rospy.Publisher('wheels_desired_rate',
+                                        Int16MultiArray, queue_size=1)
+                                        
+        rospy.init_node('mecanum_drive_controller')
+        self.nodeName = rospy.get_name()
+        rospy.loginfo("{0} started".format(self.nodeName))
+
+        rospy.Subscriber("cmd_vel", Twist, self.twistCallback)
+
+        self.ticksPerMeter = float(rospy.get_param('~ticks_per_meter'))
+        self.wheelSeparation = float(rospy.get_param('~wheel_separation'))
+        self.maxMotorSpeed = int(rospy.get_param('~max_motor_speed'))
+        self.rate = float(rospy.get_param('~rate', 10.0))
+        self.timeout = float(rospy.get_param('~timeout', 0.2))
+
+        self.controller.setWheelSeparation(self.wheelSeparation)
+        self.controller.setTicksPerMeter(self.ticksPerMeter)
+        self.controller.setMaxMotorSpeed(self.maxMotorSpeed)
+
+        rate = rospy.Rate(self.rate)
+        self.lastTwistTime = rospy.get_time()
+        while not rospy.is_shutdown():
+            self.publish()
+            rate.sleep()
+
+    def publish(self):
+        if rospy.get_time() - self.lastTwistTime < self.timeout:
+            speeds = self.controller.getSpeeds(self.linearXVelocity,self.linearYVelocity,
+                                               self.angularVelocity)
+
+            self.wheels_to_send.data = [int(speeds.frontLeft), int(speeds.frontRight), int(speeds.rearLeft), int(speeds.rearRight)]
+            self.wheelPub.publish(self.wheels_to_send)
+        else:
+            self.wheels_to_send.data = [0, 0, 0, 0]
+            self.wheelPub.publish(self.wheels_to_send)
+
+    def twistCallback(self, twist):
+        self.linearXVelocity = twist.linear.x
+        self.linearYVelocity = twist.linear.y
+        self.angularVelocity = twist.angular.z
+        # print("%d,%d" % (self.linearVelocity, self.angularVelocity))
+        self.lastTwistTime = rospy.get_time()
+
+if __name__ == '__main__':
+    try:
+        node = ControllerNode()
+        node.main()
+    except rospy.ROSInterruptException:
+        pass
diff --git a/nodes/mecanum_drive_odometry b/nodes/mecanum_drive_odometry
new file mode 100755
index 0000000..74c05ac
--- /dev/null
+++ b/nodes/mecanum_drive_odometry
@@ -0,0 +1,123 @@
+#! /usr/bin/env python
+from __future__ import division
+
+import rospy
+from geometry_msgs.msg import Quaternion
+from geometry_msgs.msg import Twist
+from nav_msgs.msg import Odometry
+from std_msgs.msg import Int32
+from geometry_msgs.msg import PoseWithCovarianceStamped
+from tf.broadcaster import TransformBroadcaster
+from tf.transformations import quaternion_from_euler, euler_from_quaternion
+from math import sin, cos
+from mecanum_drive.pose import Pose
+from mecanum_drive import odometry
+
+ODOM_POSE_COVARIANCE = [1e-3, 0, 0, 0, 0, 0, 
+                        0, 1e-3, 0, 0, 0, 0,
+                        0, 0, 1e6, 0, 0, 0,
+                        0, 0, 0, 1e6, 0, 0,
+                        0, 0, 0, 0, 1e6, 0,
+                        0, 0, 0, 0, 0, 1e3]
+
+ODOM_TWIST_COVARIANCE = [1e-3, 0, 0, 0, 0, 0, 
+                         0, 1e-3, 0, 0, 0, 0,
+                         0, 0, 1e6, 0, 0, 0,
+                         0, 0, 0, 1e6, 0, 0,
+                         0, 0, 0, 0, 1e6, 0,
+                         0, 0, 0, 0, 0, 1e3]
+
+class OdometryNode:
+
+    def __init__(self):
+        self.odometry = odometry.Odometry()
+
+    def main(self):
+        self.odomPub = rospy.Publisher('odom', Odometry, queue_size=10)
+        self.tfPub = TransformBroadcaster()
+
+        rospy.init_node('mecanum_drive_odometry')
+        self.nodeName = rospy.get_name()
+        rospy.loginfo("{0} started".format(self.nodeName))
+
+        rospy.Subscriber("lwheel_ticks", Int32, self.leftCallback)
+        rospy.Subscriber("rwheel_ticks", Int32, self.rightCallback)
+        rospy.Subscriber("initialpose", PoseWithCovarianceStamped,
+                         self.on_initial_pose)
+
+        self.ticksPerMeter = int(rospy.get_param('~ticks_per_meter'))
+        self.wheelSeparation = float(rospy.get_param('~wheel_separation'))
+        self.rate = float(rospy.get_param('~rate', 20.0))
+        self.baseFrameID = rospy.get_param('~base_frame_id', 'base_link')
+        self.odomFrameID = rospy.get_param('~odom_frame_id', 'odom')
+        self.encoderMin = int(rospy.get_param('~encoder_min', -32768))
+        self.encoderMax = int(rospy.get_param('~encoder_max', 32767))
+
+        self.odometry.setWheelSeparation(self.wheelSeparation)
+        self.odometry.setTicksPerMeter(self.ticksPerMeter)
+        self.odometry.setEncoderRange(self.encoderMin, self.encoderMax)
+        self.odometry.setTime(rospy.get_time())
+
+        rate = rospy.Rate(self.rate)
+        while not rospy.is_shutdown():
+            self.publish()
+            rate.sleep()
+
+    def publish(self):
+        self.odometry.updatePose(rospy.get_time())
+        now = rospy.get_rostime()
+        pose = self.odometry.getPose()
+
+        q = quaternion_from_euler(0, 0, pose.theta)
+        self.tfPub.sendTransform(
+            (pose.x, pose.y, 0),
+            (q[0], q[1], q[2], q[3]),
+            now,
+            self.baseFrameID,
+            self.odomFrameID
+        )
+
+        odom = Odometry()
+        odom.header.stamp = now
+        odom.header.frame_id = self.odomFrameID
+        odom.child_frame_id = self.baseFrameID
+        odom.pose.pose.position.x = pose.x
+        odom.pose.pose.position.y = pose.y
+        odom.pose.pose.orientation.x = q[0]
+        odom.pose.pose.orientation.y = q[1]
+        odom.pose.pose.orientation.z = q[2]
+        odom.pose.pose.orientation.w = q[3]
+        odom.twist.twist.linear.x = pose.xVel
+        odom.twist.twist.angular.z = pose.thetaVel
+        odom.pose.covariance = ODOM_POSE_COVARIANCE
+        odom.twist.covariance = ODOM_TWIST_COVARIANCE
+        self.odomPub.publish(odom)
+
+    def on_initial_pose(self, msg):
+        q = [msg.pose.pose.orientation.x,
+             msg.pose.pose.orientation.x,
+             msg.pose.pose.orientation.x,
+             msg.pose.pose.orientation.w]
+        roll, pitch, yaw = euler_from_quaternion(q)
+
+        pose = Pose()
+        pose.x = msg.pose.pose.position.x
+        pose.y = msg.pose.pose.position.y
+        pose.theta = yaw
+
+        rospy.loginfo('Setting initial pose to %s', pose)
+        self.odometry.setPose(pose)
+
+    def leftCallback(self, msg):
+        self.odometry.updateLeftWheel(msg.data)
+
+    def rightCallback(self, msg):
+        self.odometry.updateRightWheel(msg.data)
+
+
+if __name__ == '__main__':
+    try:
+        node = OdometryNode()
+        node.main()
+    except rospy.ROSInterruptException:
+        pass
diff --git a/package.xml b/package.xml
new file mode 100755
index 0000000..eb225cf
--- /dev/null
+++ b/package.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<package format="2">
+  <name>mecanum_drive</name>
+  <version>1.0.0</version>
+  <description>
+    This package implements a control mechanism for a mecanum drive robot.
+  </description>
+  <author>David Dudas</author>
+  <maintainer email="david.dudas@outlook.com">David Dudas</maintainer>
+  <url type="website">https://github.com/dudasdavid/mecanum_drive</url>
+  <url type="repository">https://github.com/dudasdavid/mecanum_drive.git</url>
+  <url type="bugtracker">https://github.com/dudasdavid/mecanum_drive/issues</url>
+  <license>BSD</license>
+  <buildtool_depend>catkin</buildtool_depend>
+
+  <depend>std_msgs</depend>
+  <depend>geometry_msgs</depend>
+  <depend>actionlib_msgs</depend>
+  <build_depend>rostest</build_depend>
+  <depend>message_generation</depend>
+  
+</package>
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..dec9110
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,12 @@
+## ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD
+
+from distutils.core import setup
+from catkin_pkg.python_setup import generate_distutils_setup
+
+# fetch values from package.xml
+setup_args = generate_distutils_setup(
+    packages=['mecanum_drive'],
+    package_dir={'': 'src'})
+
+setup(**setup_args)
+
diff --git a/src/mecanum_drive/__init__.py b/src/mecanum_drive/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/src/mecanum_drive/controller.py b/src/mecanum_drive/controller.py
new file mode 100755
index 0000000..c7f8be4
--- /dev/null
+++ b/src/mecanum_drive/controller.py
@@ -0,0 +1,62 @@
+from __future__ import division
+
+class MotorCommand:
+    """Holds motor control commands for a differential-drive robot.
+    """
+
+    def __init__(self):
+        self.frontLeft = 0
+        self.frontRight = 0
+        self.rearLeft = 0
+        self.rearRight = 0
+        
+
+class Controller:
+    """Determines motor speeds to accomplish a desired motion.
+    """
+
+    def __init__(self):
+        # Set the max motor speed to a very large value so that it
+        # is, essentially, unbound.
+        self.maxMotorSpeed = 10000 # ticks/s
+
+    def getSpeeds(self, linearXSpeed, linearYSpeed, angularSpeed):
+    
+    
+        # print(linearXSpeed,linearYSpeed,angularSpeed)
+    
+        WHEEL_SEPARATION_WIDTH = 0.1
+        WHEEL_SEPARATION_LENGTH = 0.13
+        
+        speeds = MotorCommand()
+        
+        speeds.frontLeft = self.ticksPerMeter * (linearXSpeed - linearYSpeed - (WHEEL_SEPARATION_WIDTH + WHEEL_SEPARATION_LENGTH)*angularSpeed)
+        speeds.frontRight = self.ticksPerMeter * (linearXSpeed + linearYSpeed + (WHEEL_SEPARATION_WIDTH + WHEEL_SEPARATION_LENGTH)*angularSpeed)
+        speeds.rearLeft = self.ticksPerMeter * (linearXSpeed + linearYSpeed - (WHEEL_SEPARATION_WIDTH + WHEEL_SEPARATION_LENGTH)*angularSpeed)
+        speeds.rearRight = self.ticksPerMeter * (linearXSpeed - linearYSpeed + (WHEEL_SEPARATION_WIDTH + WHEEL_SEPARATION_LENGTH)*angularSpeed)
+        
+        # print(speeds.frontLeft, speeds.frontRight, speeds.rearLeft, speeds.rearRight)
+        
+
+        # Adjust speeds if they exceed the maximum.
+        if max(speeds.frontLeft, speeds.frontRight, speeds.rearLeft, speeds.rearRight) > self.maxMotorSpeed:
+            factor = self.maxMotorSpeed / max(speeds.frontLeft, speeds.frontRight, speeds.rearLeft, speeds.rearRight)
+            speeds.frontLeft *= factor
+            speeds.frontRight *= factor
+            speeds.rearLeft *= factor
+            speeds.rearRight *= factor
+
+        speeds.frontLeft = int(speeds.frontLeft)
+        speeds.frontRight = int(speeds.frontRight)
+        speeds.rearLeft = int(speeds.rearLeft)
+        speeds.rearRight = int(speeds.rearRight)
+        return speeds
+
+    def setWheelSeparation(self, separation):
+        self.wheelSeparation = separation
+
+    def setMaxMotorSpeed(self, limit):
+        self.maxMotorSpeed = limit
+
+    def setTicksPerMeter(self, ticks):
+        self.ticksPerMeter = ticks
diff --git a/src/mecanum_drive/controller.pyc b/src/mecanum_drive/controller.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bf6c72d36464af76c9709cda0465ab1b6a10f0cd
GIT binary patch
literal 2858
zcmZSn%*(a)nQUA#0~9bbFfceMFfbHbF)%QsFfgPrGUPBY<T5fuF)}cwFfpXCGo&yx
zv@kF<GcZI(F)^gDFr;uWq_Dz;m>E*o7=kr885kHc|NsC0UxSf>fuV#0WLjofW^rbI
zUI`Ba1A}jVNq&)Yer|4JUJBfDkWwd*<>Cws45<ttJ5v}KK(;V~%#UJb2nyC<Vqjn>
zc4S~+@W{_eDOSh@8KjV$pI1_ppQDfrF|k-7Ex$-1Q6VKWEiJVuHLoNyF-JG0D6=e8
zp(sBozeJBqK|w(w8Dt*_gN$?r*{cC^CL==)14BI6>mUXrC=5UhCK!Vm6fh8n_+^5^
zO@jsGM@|L?hP0ymyb_<(w2~4o7%wO@J);C107a>ZMPNCQN)QJu1qmRK3B{>7X&@Ic
zfQe#v1_lQGjQreG{glMA%oP3P#FFgHy!i5B{o<lz{oK^##Jtknc(AXqh(JRmC$&hg
zpb~6-e0*kJW=VWJJ177_5yHU835F2ogGA!v^AdAY<Kw}ukB`sIPbtj-aY0(*<5TjJ
z<Ku%sY_J0n1T~!+1d0$)Dl7&C2^$j|6WB4%kO<C6Ey7GK*0@qj3KKZlut1Y+6e}pX
zwlFY6u|ZR86gwzIv4H}_hJk^>CAB2AC^s`N6_%`u3sO^4iWN%o6%vz^^K%PwGK(`n
z$ttxtvnVwMl=d?7^Kd05UX;YdzyM1z0_qG5kVprq$W5#O=Y?R9Rbb&Dkb7tpxS+@r
z0(lpd<QbTgL1E0zz`y{q5}X4rGlCO-Eek^m6GJU4LkT#i*Rp}~L>41M4I@Jh6N5wz
z3quVngLn-ygNTGk4I4u|6N<bzlDs%ro*Ab+L_Z6PJhJ%^{j3Z%Yz$dUV3HY3vVcie
zhBOuicMp)W7#Ok`8ANK?8EPPwu`!6$K*cy<-r!`YVP~k}V34Tc1iKl`5eIRY8A{l}
zCStRr8R9$+hHMswqIO1*u^a@{OV@INjAH@0nTx@a2^2zX3@lm93^iab*b^*SEKn}k
zH!NAKP%bM&9s@%OC&+{>CWdB45F?%mCc+F8VTOsYz(iPJBCIeGaL5H~fO3>y`mC8C
zu+u($Nr5fb^~DG5HMkfU7{FPs1e6#{GLy5515%58K^Y+kltzL;8iPQ|B#0GM$ni5U
zFqCJcrsf2v79<uWmVnB0kdoZQiXa}40uU!NuSA0zWH2aMfs6Z`%)HdZq6lyzM-qrc
z2qfmEm*ylEK?KAY7#PAmTwQ(QgIxn0gB(LV{r%#@JzYXPN<dWuhLn%1pL>W$3CKD~
zmIW7hAm0QfmZX+&f^uSJT3QImdnKSe08wQG){&N&T#{czn+ylGIz6=n?5$#u+lviA
zH3lmiBO4PZlL8|cGRZRYGKw&AGV(I=vS~4DF!F<;5R(ui1SW%`0*1l)6ck&KR1d1D
zK!ppa-1LJKAbboA4B&baT$F&z&tiCV(a59VQYEz{95rn?FfcF_gB;GlNTMG=i3!OM
zpv0GxnVVTcBj*c%9GY6<i(DEzgT%;kJScY191lv2#k6%iIC;ZzuN%Vg8lb8@2uy%-
zLJ&C5gK|NTG$@nGf(UsKp$JMhpyWv#cLssV1W?*621PEYYUgC-)Z}F5WcEW?2x*Lf
woERUUR$5Y8lo}r&1acF&lmZ)pq!=VY&DICF+^~VPd+b0t3fu}2U=rpA0Gk<du>b%7

literal 0
HcmV?d00001

diff --git a/src/mecanum_drive/encoder.py b/src/mecanum_drive/encoder.py
new file mode 100755
index 0000000..d99e597
--- /dev/null
+++ b/src/mecanum_drive/encoder.py
@@ -0,0 +1,52 @@
+from __future__ import division
+
+
+class Encoder:
+    """Monitors a single wheel encoder and accumulates delta ticks
+    since the last time they were requested.
+    """
+
+    def __init__(self):
+        self.setRange(-32768, 32767)
+        self.initCount(0)
+        self.isReversed = False
+
+    def setRange(self, low, high):
+        self.range = high - low + 1
+        self.lowThresh = low + self.range*30//100
+        self.highThresh = low + self.range*70//100
+
+    def initCount(self, startCount):
+        self.delta = 0
+        self.last = startCount
+
+    def update(self, newCount):
+        if self.last > self.highThresh and newCount < self.lowThresh:
+            # Wrapped around the upper limit
+            increment = newCount + self.range - self.last
+        elif self.last < self.lowThresh and newCount > self.highThresh:
+            # Wrapped around the lower limit
+            increment = newCount - self.range - self.last
+        else:
+            increment = newCount - self.last
+
+        self.delta += increment
+        self.last = newCount
+        
+    def setReversed(self, isReversed):
+        self.isReversed = isReversed
+
+    def getDelta(self):
+        delta = self.delta
+        self.delta = 0
+        if self.isReversed:
+            return -delta
+        else:
+            return delta
+
+    def getLimits(self):
+        return {
+            'range': self.range,
+            'lowThresh': self.lowThresh,
+            'highThresh': self.highThresh
+        }
diff --git a/src/mecanum_drive/encoder.pyc b/src/mecanum_drive/encoder.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5b0ef9b08957448661a87ccd075d53fc9a4f8427
GIT binary patch
literal 2416
zcmZSn%*$nAC?1#000qnp3=9qm3=GBU3=9k@3=AoZ3^@!8xr_`^j0}t^ObjWk3@OYE
zEes6J3=EM`ObjV348a;~3=9mJ|NsC0uffQ`z)->g(wbS8S)7@lSHjM~z~GvfoS%|f
z1h)jF$_ZqNF9QQZDg(&u6b1&6X^bGFqnH^|n7|AcsAW;CAd6ZU7^2ufE&wsu8B*B5
zYB(5D*ue}=hM-^#kln?(3=9mu`FWWo`9;MFi3-J;dFeT+3gsE8sW}R%5Vt5K=A|ek
zCMTEXmgXduq!ufrq~??)DwJd<XBTrRC@6q5C8sKsWTYzOBo>z_lw{_Dd6f#~sYR&@
zMX80Qsl_F!DSBYV$siwrFevz(K|a%BU|^_WV8{Z8U<wmMGZO<Nh|dV}Q8Ob0V+j*T
zKAssA$iW&c3=9l@nG6m8|7ZTMXJE(##Rgaq9GJzaB|(XK>8T~0pn%THEOE{+%_}Kk
zWnf@%OUx-wE#U%*7YC)5r4|*Zra)o=WMpw_PMQYTED%xb!oa|wpOK%Ns-KcrmYJfT
zoLG{bnHOJPtY2J|te=~joS0Xd8=q2?S(b`L1R8RB1(je&#m8snWtPOpb22b66oXO#
z1ET;VFJm$&2w)f-6t1A4U}C6YWROT<WDu`mV2Eb`@j$`B0E%gm6lMk~us9=<I12+p
zoCy+2AP@Uxg2Fjd4&>t$5aGtaz@P!L5F8#w@Nmk>FAvEmN-fR+hfYRjdIp3GRvQFz
za0y6PPJVd_D5yXRXb@CEObiSRe25?tWRze`28qEiIEchRIffCOAYefg&j|4|NSz-f
zJ%E^CPlD4b*wvu4sR0s)cnFjZic1oUAPIyP-T?)fFvvTgFk@ikgm?xd3c_H|%ml?Z
zIO&68n2{lyg`vm-7O_kW*$hZrW^j7QVqlP{Wn##41%*HsB;!C77QpmDjDd5R86-f4
zL-p~(R6+PP3=Aw;EDSYF4CYMnEKs?4aB&t4&KrIjpp+B@ik2WykOgrvFfasxijE*&
zSZaa<C`c?XwH%V)K&mqHl8aJvQ}aq_5V$3vL|Iypl30=|4hmwB`xqD%nZ!YuiI<5V
z64;>V24QeugWOxg2nsh;4}vmD5XhH^)B)jw3Ied(saG0sgB+V$0xN!0KwOXm85qIk
zB8UmXVE2NohQwSgBdBtLmIe$AS&R%t3ZS^+4z6Kj2u@*O2nIVHDeZ$J2jUBmdEgiV
zi-8Pf029>n6e#A?Q%hVx)dHw4ECwYMP=e=V<YZ)J0{aXU$na#Z1S*V}K`{X-^vyut
zWCWFOU=|ao;5UPo^pLd12}*S!V<ANn$VNy?1dD+T2YV6hF)%?r-+`=7Pc8Au%*`w*
z)(1rx$aV%sW=1YXE=CQI1E7U`USe))d_1@ejgQaGPbtj-aY40ue0)lNa(sLchz&00
zf<R#rBn0vmsE7y>0kK3u848~0sOya&P#A#<qGACC1_m}}HYQG1PF7A9PF_xCP7Xh?
uW#F)dIzO$nq_ikCK0XNK9<XMJ5)c>6qFw-lg3|^PTy`K=7K2i(05bqjnET)W

literal 0
HcmV?d00001

diff --git a/src/mecanum_drive/odometry.py b/src/mecanum_drive/odometry.py
new file mode 100755
index 0000000..5e36026
--- /dev/null
+++ b/src/mecanum_drive/odometry.py
@@ -0,0 +1,79 @@
+from __future__ import division
+from math import pi, sin, cos
+from mecanum_drive.encoder import Encoder
+from mecanum_drive.pose import Pose
+
+class Odometry:
+    """Keeps track of the current position and velocity of a
+    robot using differential drive.
+    """
+
+    def __init__(self):
+        self.leftEncoder = Encoder()
+        self.rightEncoder = Encoder()
+        self.pose = Pose()
+        self.lastTime = 0
+
+    def setWheelSeparation(self, separation):
+        self.wheelSeparation = separation
+
+    def setTicksPerMeter(self, ticks):
+        self.ticksPerMeter = ticks
+        
+    def setEncoderRange(self, low, high):
+        self.leftEncoder.setRange(low, high)
+        self.rightEncoder.setRange(low, high)
+
+    def setTime(self, newTime):
+        self.lastTime = newTime
+        
+    def updateLeftWheel(self, newCount):
+        self.leftEncoder.update(newCount)
+
+    def updateRightWheel(self, newCount):
+        self.rightEncoder.update(newCount)
+
+    def updatePose(self, newTime):
+        """Updates the pose based on the accumulated encoder ticks
+        of the two wheels. See https://chess.eecs.berkeley.edu/eecs149/documentation/differentialDrive.pdf
+        for details.
+        """
+        leftTravel = self.leftEncoder.getDelta() / self.ticksPerMeter
+        rightTravel = self.rightEncoder.getDelta() / self.ticksPerMeter
+        deltaTime = newTime - self.lastTime
+
+        deltaTravel = (rightTravel + leftTravel) / 2
+        deltaTheta = (rightTravel - leftTravel) / self.wheelSeparation
+
+        if rightTravel == leftTravel:
+            deltaX = leftTravel*cos(self.pose.theta)
+            deltaY = leftTravel*sin(self.pose.theta)
+        else:
+            radius = deltaTravel / deltaTheta
+
+            # Find the instantaneous center of curvature (ICC).
+            iccX = self.pose.x - radius*sin(self.pose.theta)
+            iccY = self.pose.y + radius*cos(self.pose.theta)
+
+            deltaX = cos(deltaTheta)*(self.pose.x - iccX) \
+                - sin(deltaTheta)*(self.pose.y - iccY) \
+                + iccX - self.pose.x
+
+            deltaY = sin(deltaTheta)*(self.pose.x - iccX) \
+                + cos(deltaTheta)*(self.pose.y - iccY) \
+                + iccY - self.pose.y
+
+        self.pose.x += deltaX
+        self.pose.y += deltaY
+        self.pose.theta = (self.pose.theta + deltaTheta) % (2*pi)
+        self.pose.xVel = deltaTravel / deltaTime if deltaTime > 0 else 0.
+        self.pose.yVel = 0
+        self.pose.thetaVel = deltaTheta / deltaTime if deltaTime > 0 else 0.
+
+        self.lastTime = newTime
+
+    def getPose(self):
+        return self.pose;
+
+    def setPose(self, newPose):
+        self.pose = newPose
diff --git a/src/mecanum_drive/odometry.pyc b/src/mecanum_drive/odometry.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..905fe8fea72ed0ae9e457a22a2305313fa948a06
GIT binary patch
literal 3929
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
T*ntu}xYr@TD8negD8K{&^4(ZH

literal 0
HcmV?d00001

diff --git a/src/mecanum_drive/pose.py b/src/mecanum_drive/pose.py
new file mode 100755
index 0000000..a9b7f36
--- /dev/null
+++ b/src/mecanum_drive/pose.py
@@ -0,0 +1,17 @@
+from __future__ import division
+
+class Pose:
+
+    def __init__(self):
+        self.x = 0
+        self.y = 0
+        self.theta = 0
+        self.xVel = 0
+        self.yVel = 0
+        self.thetaVel = 0
+
+
+    def __str__(self):
+        return str({'x': self.x, 'y': self.y, 'theta': self.theta,
+                    'xVel': self.xVel, 'yVel': self.yVel,
+                    'thetaVel': self.thetaVel})
diff --git a/src/mecanum_drive/pose.pyc b/src/mecanum_drive/pose.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..01e65ddc0f3632bb9b99143f2296020fce3c8e1f
GIT binary patch
literal 1015
zcmZSn%*$nAC?1#000qnp3=9qm3=GBU3=9k@3=AoZ3^@!8xr_`^j0}t^ObjWk3@OYE
zEes6J3=EM`ObjV348a;~3=9mJ|NsC0uffQ`z)->g(wbS8S)7@lSHi-;zz~pMoC>!A
zq{InigA@Y;Ln;Hv;1mXsi6GM$!3<`GpkNIq1_p*?kYW%9ahyS#tUzWnGSo0I#Dg6N
zVlaYS31Tq87|bvR3yi@E@(jdKzf6!{!HzBgnOy-UD@#~GZqG<9Nd&vOA}lor%%}u0
zKwc^V$%UonKs*WZS8-}ing-Y~5K-*Nz`&rNk)NBYpORRXnWCSZSdyKY7hhhiUtE-|
zpPQPTm{*z`pHh@rmWoBBAip?Oub>hfdhzj@d6^~g@gOG^^MDLz<b*?bpn#kJ4iq<#
z%NZCl*+Aiv1r83g8U}_eCWaIy1~7{m6l-7>3n<FKELKotf>~?~DQpa8&5R7e8te=V
z41PhN;0^*Mg&>d}K_F)Yfg(8wBn?(m0+KB*DI%a06uv>Az@vIFfm{+FUtCfYAJ50Y
zz)%cw9s{EaqX?r0$j#7T%uCEojgK$kVqjp1kI&6dDa}cZj}PKxU|<O1W?*1IxC+do
zP7DTtJPi&WkVDv*I63{mrfY!2piW6EEh#MmIRzBAU=0u@ATF3i9jAap!3N@QJCL)A
JK`|@91OQV*%BKJT

literal 0
HcmV?d00001

diff --git a/tests/test_controller.py b/tests/test_controller.py
new file mode 100755
index 0000000..29c67f7
--- /dev/null
+++ b/tests/test_controller.py
@@ -0,0 +1,66 @@
+from __future__ import division
+PKG='test_controller'
+
+import unittest
+from diff_drive.controller import Controller
+
+
+class TestController(unittest.TestCase):
+
+    def setUp(self):
+        self.ticksPerMeter = 10000
+        self.wheelSeparation = 0.1
+        self.controller = Controller()
+        self.controller.setTicksPerMeter(self.ticksPerMeter)
+        self.controller.setWheelSeparation(self.wheelSeparation)
+
+    def testStraightForward(self):
+        speeds = self.controller.getSpeeds(0.1, 0)
+        self.assertAlmostEqual(speeds.left, self.ticksPerMeter*0.1)
+        self.assertAlmostEqual(speeds.right, self.ticksPerMeter*0.1)
+
+    def testStraightBackward(self):
+        speeds = self.controller.getSpeeds(-0.1, 0)
+        self.assertAlmostEqual(speeds.left, -self.ticksPerMeter*0.1)
+        self.assertAlmostEqual(speeds.right, -self.ticksPerMeter*0.1)
+
+    def testRotateLeft(self):
+        speeds = self.controller.getSpeeds(0, 1)
+        diffTicks = self.wheelSeparation * self.ticksPerMeter
+        self.assertAlmostEqual(speeds.left, -diffTicks)
+        self.assertAlmostEqual(speeds.right, diffTicks)
+
+    def testRotateRight(self):
+        speeds = self.controller.getSpeeds(0, -1)
+        diffTicks = self.wheelSeparation * self.ticksPerMeter
+        self.assertAlmostEqual(speeds.left, diffTicks)
+        self.assertAlmostEqual(speeds.right, -diffTicks)
+
+    def testCurveLeft(self):
+        speeds = self.controller.getSpeeds(0.1, 1)
+        aheadTicks = 0.1 * self.ticksPerMeter
+        diffTicks = self.wheelSeparation * self.ticksPerMeter
+        self.assertAlmostEqual(speeds.left, aheadTicks-diffTicks)
+        self.assertAlmostEqual(speeds.right, aheadTicks+diffTicks)
+
+    def testMotorLimitsStraight(self):
+        maxTickSpeed = self.ticksPerMeter // 4
+        self.controller.setMaxMotorSpeed(maxTickSpeed)
+        speeds = self.controller.getSpeeds(1, 0)
+        self.assertEqual(speeds.left, maxTickSpeed)
+        self.assertEqual(speeds.right, maxTickSpeed)
+
+    def testMotorLimitsCurved(self):
+        maxTickSpeed = self.ticksPerMeter // 4
+        self.controller.setMaxMotorSpeed(maxTickSpeed)
+        speeds = self.controller.getSpeeds(1, 1)
+        aheadTicks = self.ticksPerMeter
+        diffTicks = self.wheelSeparation * self.ticksPerMeter
+        factor = maxTickSpeed / (aheadTicks + diffTicks)
+        self.assertEqual(speeds.left,
+                         int((aheadTicks-diffTicks) * factor))
+        self.assertEqual(speeds.right, maxTickSpeed)
+
+        
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_encoder.py b/tests/test_encoder.py
new file mode 100755
index 0000000..5923d17
--- /dev/null
+++ b/tests/test_encoder.py
@@ -0,0 +1,62 @@
+#! /usr/bin/env python
+from __future__ import division
+PKG='test_encoder'
+
+import unittest
+from diff_drive.encoder import Encoder
+
+
+class TestEncoder(unittest.TestCase):
+
+    def setUp(self):
+        self.encoder = Encoder()
+        
+    def testInitialization(self):
+        self.assertEquals(self.encoder.getDelta(), 0)
+
+    def testClearedDelta(self):
+        self.encoder.update(100)
+        self.assertEquals(self.encoder.getDelta(), 100)
+        self.assertEquals(self.encoder.getDelta(), 0)
+
+    def testIncrement(self):
+        self.encoder.update(100)
+        self.assertEquals(self.encoder.getDelta(), 100)
+
+        self.encoder.update(50)
+        self.assertEquals(self.encoder.getDelta(), -50)
+
+    def testWraparound(self):
+        defaultRange = 32767 - (-32768) + 1
+        self.encoder.update(20000)
+        self.assertEquals(self.encoder.getDelta(), 20000)
+
+        # Wrap around the high end.
+        self.encoder.update(-20000)
+        self.assertEquals(self.encoder.getDelta(),
+                          (-20000) + defaultRange - 20000)
+
+        # Wrap around the low end.
+        self.encoder.update(20000)
+        self.assertEquals(self.encoder.getDelta(),
+                          20000 - defaultRange - (-20000))
+
+    def testCustomRange(self):
+        self.encoder.setRange(0, 999)
+        self.encoder.update(500)
+        self.assertEquals(self.encoder.getDelta(), 500)
+
+        self.encoder.update(900)
+        self.assertEquals(self.encoder.getDelta(), 400)
+
+        # Wrap around the high end.
+        self.encoder.update(100)
+        self.assertEquals(self.encoder.getDelta(), 200)
+
+        # Wrap around the low end.
+        self.encoder.update(900)
+        self.assertEquals(self.encoder.getDelta(), -200)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_odometry.py b/tests/test_odometry.py
new file mode 100755
index 0000000..7f33b99
--- /dev/null
+++ b/tests/test_odometry.py
@@ -0,0 +1,107 @@
+#! /usr/bin/env python
+from __future__ import division
+PKG='test_odometry'
+
+from math import pi, sin, cos
+import unittest
+from diff_drive.odometry import Odometry
+
+
+class TestOdometry(unittest.TestCase):
+
+    def setUp(self):
+        self.wheelSeparation = 0.10
+        self.ticksPerMeter = 10000
+        self.odom = Odometry()
+        self.odom.setWheelSeparation(self.wheelSeparation)
+        self.odom.setTicksPerMeter(self.ticksPerMeter)
+
+    def testInitialization(self):
+        pose = self.odom.getPose()
+        self.assertEquals(pose.x, 0)
+        self.assertEquals(pose.y, 0)
+        self.assertEquals(pose.theta, 0)
+        self.assertEquals(pose.xVel, 0)
+        self.assertEquals(pose.yVel, 0)
+        self.assertEquals(pose.thetaVel, 0)
+
+    def testTravelForward(self):
+        self.checkUpdate(10000, 10000, 2, {'x': 1, 'xVel': 1/2})
+
+    def testSpinLeft(self):
+        angle = (200/self.ticksPerMeter / self.wheelSeparation) % (2*pi)
+        self.checkUpdate(-100, 100, 2,
+                         {'theta': angle, 'thetaVel': angle/2})
+
+    def testSpinRight(self):
+        angle = (200/self.ticksPerMeter / self.wheelSeparation) % (2*pi)
+        self.checkUpdate(100, -100, 2,
+                         {'theta': (-angle) % (2*pi),
+                          'thetaVel': -angle/2})
+
+    def testCurveLeft(self):
+        radius = self.wheelSeparation / 2
+        angle = pi
+        s = angle * self.wheelSeparation
+        ticks = int(s * self.ticksPerMeter)
+        self.checkUpdate(0, ticks, 2,
+                         {'x': 0,
+                          'y': 2*radius,
+                          'theta': angle,
+                          'xVel': s/4,
+                          'yVel': 0,
+                          'thetaVel': angle/2})
+                          
+    def testCurveRight(self):
+        radius = self.wheelSeparation / 2
+        angle = pi
+        s = angle * self.wheelSeparation
+        ticks = int(s * self.ticksPerMeter)
+        self.checkUpdate(ticks, 0, 2,
+                         {'x': 0,
+                          'y': -2*radius,
+                          'theta': -angle,
+                          'xVel': s/4,
+                          'yVel': 0,
+                          'thetaVel': -angle/2})
+                          
+    def testCircle(self):
+        self.checkCircleRight(8, 9)
+        self.checkCircleRight(0, 100)
+        self.checkCircleRight(100, 0)
+
+    def checkCircle(self, vr, vl):
+        radius = abs(self.wheelSeparation/2 * (vr+vl)/(vr-vl))
+        circumference = 2*pi*radius
+        deltaTravel = (vr+vl)/2 * self.ticksPerMeter;
+        for i in range(int(circumference/deltaTravel)):
+            self.odom.updateLeftWheel(vl)
+            self.odom.updateRightWheel(vr)
+            self.odom.updatePose(i+1)
+        self.checkPose(self.odom.getPose(), {'x': 0, 'y': 0})
+
+    def checkUpdate(self, leftTicks, rightTicks, deltaTime, attrs):
+        self.odom.updateLeftWheel(leftTicks)
+        self.odom.updateRightWheel(rightTicks)
+        self.odom.updatePose(deltaTime)
+        self.checkPose(self.odom.getPose(), attrs)
+        
+    def checkPose(self, pose, attrs):
+        for key in ['x', 'y', 'theta', 'xVel', 'yVel', 'thetaVel']:
+            if key in attrs:
+                self.assertClose(
+                    getattr(pose, key), attrs[key],
+                            msg="{0}: {1}!={2}".format(key,
+                                                    getattr(pose, key),
+                                                    attrs[key]))
+            else:
+                self.assertEquals(getattr(pose, key), 0, key)
+
+    def assertClose(self, x, y, msg):
+        if y == 0:
+            self.assertLess(abs(x), 0.0001, msg)
+        else:
+            self.assertLess(abs(x-y)/y, 0.001, msg)
+        
+if __name__ == '__main__':
+    unittest.main()
-- 
GitLab