├── src └── diff_drive │ ├── __init__.py │ ├── pose.py │ ├── mock_robot.py │ ├── controller.py │ ├── encoder.py │ ├── odometry.py │ └── goal_controller.py ├── action └── GoToPose.action ├── urdf └── diff_drive_description.xacro ├── setup.py ├── package.xml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── tests ├── test_mock_robot.py ├── test_encoder.py ├── test_controller.py ├── test_odometry.py └── test_goal_controller.py ├── nodes ├── diff_drive_mock_robot ├── diff_drive_controller ├── diff_drive_odometry └── diff_drive_go_to_goal ├── launch └── demo.launch ├── config └── view.rviz └── README.asciidoc /src/diff_drive/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /action/GoToPose.action: -------------------------------------------------------------------------------- 1 | # goal definition 2 | geometry_msgs/PoseStamped pose 3 | --- 4 | # result definition 5 | bool success 6 | --- 7 | # feedback 8 | bool processing 9 | 10 | -------------------------------------------------------------------------------- /urdf/diff_drive_description.xacro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ## ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD 2 | 3 | from distutils.core import setup 4 | from catkin_pkg.python_setup import generate_distutils_setup 5 | 6 | # fetch values from package.xml 7 | setup_args = generate_distutils_setup( 8 | packages=['diff_drive'], 9 | package_dir={'': 'src'}) 10 | 11 | setup(**setup_args) 12 | 13 | -------------------------------------------------------------------------------- /src/diff_drive/pose.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | class Pose: 4 | 5 | def __init__(self): 6 | self.x = 0 7 | self.y = 0 8 | self.theta = 0 9 | self.xVel = 0 10 | self.yVel = 0 11 | self.thetaVel = 0 12 | 13 | 14 | def __str__(self): 15 | return str({'x': self.x, 'y': self.y, 'theta': self.theta, 16 | 'xVel': self.xVel, 'yVel': self.yVel, 17 | 'thetaVel': self.thetaVel}) 18 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | diff_drive 4 | 1.0.0 5 | 6 | This package implements a control mechanism for a differential drive robot. 7 | 8 | Mark Rose 9 | Mark Rose 10 | https://github.com/merose/diff_drive 11 | https://github.com/merose/diff_drive.git 12 | https://github.com/merose/diff_drive/issues 13 | BSD 14 | catkin 15 | 16 | std_msgs 17 | geometry_msgs 18 | actionlib_msgs 19 | rostest 20 | message_generation 21 | 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(diff_drive) 3 | 4 | find_package(catkin REQUIRED COMPONENTS 5 | rospy 6 | std_msgs 7 | nav_msgs 8 | geometry_msgs 9 | tf 10 | tf2 11 | actionlib 12 | actionlib_msgs 13 | ) 14 | 15 | add_action_files( 16 | DIRECTORY action 17 | FILES GoToPose.action 18 | ) 19 | 20 | if (CATKIN_ENABLE_TESTING) 21 | catkin_add_nosetests(tests) 22 | endif() 23 | 24 | if (CATKIN_ENABLE_TESTING) 25 | find_package(rostest REQUIRED) 26 | # add_rostest(tests/your_first_rostest.test) 27 | # add_rostest(tests/your_second_rostest.test) 28 | endif() 29 | 30 | catkin_install_python( 31 | PROGRAMS 32 | nodes/diff_drive_odometry 33 | nodes/diff_drive_controller 34 | nodes/diff_drive_go_to_goal 35 | nodes/diff_drive_mock_robot 36 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}) 37 | 38 | catkin_python_setup() 39 | 40 | generate_messages ( 41 | DEPENDENCIES actionlib_msgs std_msgs geometry_msgs 42 | ) 43 | 44 | catkin_package( 45 | CATKIN_DEPENDS actionlib_msgs 46 | ) 47 | -------------------------------------------------------------------------------- /src/diff_drive/mock_robot.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | 4 | class MotorTicks: 5 | 6 | def __init__(self): 7 | self.left = 0 8 | self.right = 0 9 | 10 | class MockRobot: 11 | """Implements a mock robot that dutifully executes its wheel 12 | speed commands exactly. 13 | """ 14 | 15 | def __init__(self): 16 | self.leftSpeed = 0 17 | self.newLeftSpeed = 0 18 | self.rightSpeed = 0 19 | self.newRightSpeed = 0 20 | self.leftTicks = 0 21 | self.rightTicks = 0 22 | self.minTicks = -32768 23 | self.maxTicks = 32767 24 | 25 | def setSpeeds(self, left, right): 26 | self.newLeftSpeed = left 27 | self.newRightSpeed = right 28 | 29 | def updateRobot(self, dTime): 30 | self.leftTicks = self.addTicks(self.leftTicks, self.leftSpeed*dTime) 31 | self.rightTicks = self.addTicks(self.rightTicks, self.rightSpeed*dTime) 32 | self.leftSpeed = self.newLeftSpeed 33 | self.rightSpeed = self.newRightSpeed 34 | 35 | def getTicks(self): 36 | ticks = MotorTicks() 37 | ticks.left = self.leftTicks 38 | ticks.right = self.rightTicks 39 | return ticks 40 | 41 | def addTicks(self, ticks, deltaTicks): 42 | ticks += deltaTicks 43 | if ticks > self.maxTicks: 44 | return int(ticks - self.maxTicks + self.minTicks) 45 | elif ticks < self.minTicks: 46 | return int(ticks - self.minTicks + self.maxTicks) 47 | else: 48 | return int(ticks) 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Mark Rose 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of diff_drive nor the names of its contributors may 15 | be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/diff_drive/controller.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | class MotorCommand: 4 | """Holds motor control commands for a differential-drive robot. 5 | """ 6 | 7 | def __init__(self): 8 | self.left = 0 9 | self.right = 0 10 | 11 | 12 | class Controller: 13 | """Determines motor speeds to accomplish a desired motion. 14 | """ 15 | 16 | def __init__(self): 17 | # Set the max motor speed to a very large value so that it 18 | # is, essentially, unbound. 19 | self.maxMotorSpeed = 1E9 # ticks/s 20 | 21 | def getSpeeds(self, linearSpeed, angularSpeed): 22 | tickRate = linearSpeed*self.ticksPerMeter 23 | diffTicks = angularSpeed*self.wheelSeparation*self.ticksPerMeter 24 | 25 | speeds = MotorCommand() 26 | speeds.left = tickRate - diffTicks 27 | speeds.right = tickRate + diffTicks 28 | 29 | # Adjust speeds if they exceed the maximum. 30 | if max(abs(speeds.left), abs(speeds.right)) > self.maxMotorSpeed: 31 | factor = self.maxMotorSpeed / max(speeds.left, speeds.right) 32 | speeds.left *= factor 33 | speeds.right *= factor 34 | 35 | speeds.left = int(speeds.left) 36 | speeds.right = int(speeds.right) 37 | return speeds 38 | 39 | def setWheelSeparation(self, separation): 40 | self.wheelSeparation = separation 41 | 42 | def setMaxMotorSpeed(self, limit): 43 | self.maxMotorSpeed = limit 44 | 45 | def setTicksPerMeter(self, ticks): 46 | self.ticksPerMeter = ticks 47 | -------------------------------------------------------------------------------- /src/diff_drive/encoder.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | 4 | class Encoder: 5 | """Monitors a single wheel encoder and accumulates delta ticks 6 | since the last time they were requested. 7 | """ 8 | 9 | def __init__(self): 10 | self.setRange(-32768, 32767) 11 | self.initCount(0) 12 | self.isReversed = False 13 | 14 | def setRange(self, low, high): 15 | self.range = high - low + 1 16 | self.lowThresh = low + self.range*30//100 17 | self.highThresh = low + self.range*70//100 18 | 19 | def initCount(self, startCount): 20 | self.delta = 0 21 | self.last = startCount 22 | 23 | def update(self, newCount): 24 | if self.last > self.highThresh and newCount < self.lowThresh: 25 | # Wrapped around the upper limit 26 | increment = newCount + self.range - self.last 27 | elif self.last < self.lowThresh and newCount > self.highThresh: 28 | # Wrapped around the lower limit 29 | increment = newCount - self.range - self.last 30 | else: 31 | increment = newCount - self.last 32 | 33 | self.delta += increment 34 | self.last = newCount 35 | 36 | def setReversed(self, isReversed): 37 | self.isReversed = isReversed 38 | 39 | def getDelta(self): 40 | delta = self.delta 41 | self.delta = 0 42 | if self.isReversed: 43 | return -delta 44 | else: 45 | return delta 46 | 47 | def getLimits(self): 48 | return { 49 | 'range': self.range, 50 | 'lowThresh': self.lowThresh, 51 | 'highThresh': self.highThresh 52 | } 53 | -------------------------------------------------------------------------------- /tests/test_mock_robot.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | PKG='test_controller' 3 | 4 | import unittest 5 | from diff_drive.mock_robot import MockRobot 6 | 7 | 8 | class TestMockRobot(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.robot = MockRobot() 12 | 13 | def testNoMotion(self): 14 | self.robot.updateRobot(1) 15 | ticks = self.robot.getTicks() 16 | self.assertEquals(ticks.left, 0) 17 | self.assertEquals(ticks.right, 0) 18 | 19 | self.robot.updateRobot(1) 20 | ticks = self.robot.getTicks() 21 | self.assertEquals(ticks.left, 0) 22 | self.assertEquals(ticks.right, 0) 23 | 24 | def testStraightLine(self): 25 | self.robot.setSpeeds(100, 100) 26 | self.robot.updateRobot(1) 27 | ticks = self.robot.getTicks() 28 | self.assertEquals(ticks.left, 0) 29 | self.assertEquals(ticks.right, 0) 30 | 31 | self.robot.updateRobot(1) 32 | ticks = self.robot.getTicks() 33 | self.assertEquals(ticks.left, 100) 34 | self.assertEquals(ticks.right, 100) 35 | 36 | self.robot.updateRobot(0.1) 37 | ticks = self.robot.getTicks() 38 | self.assertEquals(ticks.left, 110) 39 | self.assertEquals(ticks.right, 110) 40 | 41 | def testRotateLeft(self): 42 | self.robot.setSpeeds(-100, 100) 43 | self.robot.updateRobot(0.1) 44 | 45 | self.robot.updateRobot(0.1) 46 | ticks = self.robot.getTicks() 47 | self.assertEquals(ticks.left, -10) 48 | self.assertEquals(ticks.right, 10) 49 | 50 | self.robot.updateRobot(0.1) 51 | ticks = self.robot.getTicks() 52 | self.assertEquals(ticks.left, -20) 53 | self.assertEquals(ticks.right, 20) 54 | 55 | 56 | if __name__ == '__main__': 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /tests/test_encoder.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import division 3 | PKG='test_encoder' 4 | 5 | import unittest 6 | from diff_drive.encoder import Encoder 7 | 8 | 9 | class TestEncoder(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.encoder = Encoder() 13 | 14 | def testInitialization(self): 15 | self.assertEquals(self.encoder.getDelta(), 0) 16 | 17 | def testClearedDelta(self): 18 | self.encoder.update(100) 19 | self.assertEquals(self.encoder.getDelta(), 100) 20 | self.assertEquals(self.encoder.getDelta(), 0) 21 | 22 | def testIncrement(self): 23 | self.encoder.update(100) 24 | self.assertEquals(self.encoder.getDelta(), 100) 25 | 26 | self.encoder.update(50) 27 | self.assertEquals(self.encoder.getDelta(), -50) 28 | 29 | def testWraparound(self): 30 | defaultRange = 32767 - (-32768) + 1 31 | self.encoder.update(20000) 32 | self.assertEquals(self.encoder.getDelta(), 20000) 33 | 34 | # Wrap around the high end. 35 | self.encoder.update(-20000) 36 | self.assertEquals(self.encoder.getDelta(), 37 | (-20000) + defaultRange - 20000) 38 | 39 | # Wrap around the low end. 40 | self.encoder.update(20000) 41 | self.assertEquals(self.encoder.getDelta(), 42 | 20000 - defaultRange - (-20000)) 43 | 44 | def testCustomRange(self): 45 | self.encoder.setRange(0, 999) 46 | self.encoder.update(500) 47 | self.assertEquals(self.encoder.getDelta(), 500) 48 | 49 | self.encoder.update(900) 50 | self.assertEquals(self.encoder.getDelta(), 400) 51 | 52 | # Wrap around the high end. 53 | self.encoder.update(100) 54 | self.assertEquals(self.encoder.getDelta(), 200) 55 | 56 | # Wrap around the low end. 57 | self.encoder.update(900) 58 | self.assertEquals(self.encoder.getDelta(), -200) 59 | 60 | 61 | if __name__ == '__main__': 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /nodes/diff_drive_mock_robot: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import division 3 | 4 | import rospy 5 | from math import pi, asin 6 | from geometry_msgs.msg import Twist, Pose 7 | from nav_msgs.msg import Odometry 8 | from std_msgs.msg import Int32 9 | 10 | from diff_drive import mock_robot 11 | 12 | class MockRobotNode: 13 | 14 | def __init__(self): 15 | self.robot = mock_robot.MockRobot() 16 | self.leftSpeed = 0 17 | self.rightSpeed = 0 18 | 19 | def main(self): 20 | rospy.init_node('diff_drive_mock_robot') 21 | self.leftPub = rospy.Publisher('~lwheel_ticks', 22 | Int32, queue_size=10) 23 | self.rightPub = rospy.Publisher('~rwheel_ticks', 24 | Int32, queue_size=10) 25 | 26 | self.nodeName = rospy.get_name() 27 | rospy.loginfo("{0} started".format(self.nodeName)) 28 | 29 | rospy.Subscriber('~lwheel_desired_rate', Int32, self.leftCallback) 30 | rospy.Subscriber('~rwheel_desired_rate', Int32, 31 | self.rightCallback) 32 | 33 | self.rate = rospy.get_param('~rate', 10.0) 34 | self.timeout = rospy.get_param('~timeout', 0.5) 35 | 36 | rate = rospy.Rate(self.rate) 37 | self.lastTime = rospy.get_time() 38 | while not rospy.is_shutdown(): 39 | self.publish() 40 | rate.sleep() 41 | 42 | def publish(self): 43 | newTime = rospy.get_time() 44 | diffTime = newTime - self.lastTime 45 | self.lastTime = newTime 46 | 47 | if diffTime > self.timeout: 48 | self.robot.setSpeeds(0, 0) 49 | 50 | try: 51 | self.robot.updateRobot(diffTime) 52 | except: 53 | rospy.logerror("Got exception updating robot") 54 | ticks = self.robot.getTicks() 55 | self.leftPub.publish(ticks.left) 56 | self.rightPub.publish(ticks.right) 57 | 58 | def leftCallback(self, leftSpeed): 59 | self.leftSpeed = leftSpeed.data 60 | self.robot.setSpeeds(self.leftSpeed, self.rightSpeed) 61 | 62 | def rightCallback(self, rightSpeed): 63 | self.rightSpeed = rightSpeed.data 64 | self.robot.setSpeeds(self.leftSpeed, self.rightSpeed) 65 | 66 | 67 | if __name__ == '__main__': 68 | try: 69 | node = MockRobotNode() 70 | node.main() 71 | except rospy.ROSInterruptException: 72 | pass 73 | -------------------------------------------------------------------------------- /nodes/diff_drive_controller: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import division 3 | 4 | import rospy 5 | from geometry_msgs.msg import Twist 6 | from std_msgs.msg import Int32 7 | 8 | from diff_drive import controller 9 | 10 | class ControllerNode: 11 | 12 | def __init__(self): 13 | self.controller = controller.Controller() 14 | self.linearVelocity = 0.0 15 | self.angularVelocity = 0.0 16 | 17 | def main(self): 18 | self.leftPub = rospy.Publisher('lwheel_desired_rate', 19 | Int32, queue_size=1) 20 | self.rightPub = rospy.Publisher('rwheel_desired_rate', 21 | Int32, queue_size=1) 22 | 23 | rospy.init_node('diff_drive_controller') 24 | self.nodeName = rospy.get_name() 25 | rospy.loginfo("{0} started".format(self.nodeName)) 26 | 27 | rospy.Subscriber("cmd_vel", Twist, self.twistCallback) 28 | 29 | self.ticksPerMeter = float(rospy.get_param('~ticks_per_meter')) 30 | self.wheelSeparation = float(rospy.get_param('~wheel_separation')) 31 | self.maxMotorSpeed = int(rospy.get_param('~max_motor_speed')) 32 | self.rate = float(rospy.get_param('~rate', 10.0)) 33 | self.timeout = float(rospy.get_param('~timeout', 0.2)) 34 | 35 | self.controller.setWheelSeparation(self.wheelSeparation) 36 | self.controller.setTicksPerMeter(self.ticksPerMeter) 37 | self.controller.setMaxMotorSpeed(self.maxMotorSpeed) 38 | 39 | rate = rospy.Rate(self.rate) 40 | self.lastTwistTime = rospy.get_time() 41 | while not rospy.is_shutdown(): 42 | self.publish() 43 | rate.sleep() 44 | 45 | def publish(self): 46 | if rospy.get_time() - self.lastTwistTime < self.timeout: 47 | speeds = self.controller.getSpeeds(self.linearVelocity, 48 | self.angularVelocity) 49 | self.leftPub.publish(int(speeds.left)) 50 | self.rightPub.publish(int(speeds.right)) 51 | else: 52 | self.leftPub.publish(0) 53 | self.rightPub.publish(0) 54 | 55 | def twistCallback(self, twist): 56 | self.linearVelocity = twist.linear.x 57 | self.angularVelocity = twist.angular.z 58 | self.lastTwistTime = rospy.get_time() 59 | 60 | if __name__ == '__main__': 61 | try: 62 | node = ControllerNode() 63 | node.main() 64 | except rospy.ROSInterruptException: 65 | pass 66 | -------------------------------------------------------------------------------- /launch/demo.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | ticks_per_meter: $(arg ticks_per_meter) 26 | wheel_separation: $(arg wheel_separation) 27 | max_motor_speed: 3000 28 | timeout: 1.0 29 | 30 | 31 | 32 | 33 | 35 | 36 | ticks_per_meter: $(arg ticks_per_meter) 37 | wheel_separation: $(arg wheel_separation) 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tests/test_controller.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | PKG='test_controller' 3 | 4 | import unittest 5 | from diff_drive.controller import Controller 6 | 7 | 8 | class TestController(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.ticksPerMeter = 10000 12 | self.wheelSeparation = 0.1 13 | self.controller = Controller() 14 | self.controller.setTicksPerMeter(self.ticksPerMeter) 15 | self.controller.setWheelSeparation(self.wheelSeparation) 16 | 17 | def testStraightForward(self): 18 | speeds = self.controller.getSpeeds(0.1, 0) 19 | self.assertAlmostEqual(speeds.left, self.ticksPerMeter*0.1) 20 | self.assertAlmostEqual(speeds.right, self.ticksPerMeter*0.1) 21 | 22 | def testStraightBackward(self): 23 | speeds = self.controller.getSpeeds(-0.1, 0) 24 | self.assertAlmostEqual(speeds.left, -self.ticksPerMeter*0.1) 25 | self.assertAlmostEqual(speeds.right, -self.ticksPerMeter*0.1) 26 | 27 | def testRotateLeft(self): 28 | speeds = self.controller.getSpeeds(0, 1) 29 | diffTicks = self.wheelSeparation * self.ticksPerMeter 30 | self.assertAlmostEqual(speeds.left, -diffTicks) 31 | self.assertAlmostEqual(speeds.right, diffTicks) 32 | 33 | def testRotateRight(self): 34 | speeds = self.controller.getSpeeds(0, -1) 35 | diffTicks = self.wheelSeparation * self.ticksPerMeter 36 | self.assertAlmostEqual(speeds.left, diffTicks) 37 | self.assertAlmostEqual(speeds.right, -diffTicks) 38 | 39 | def testCurveLeft(self): 40 | speeds = self.controller.getSpeeds(0.1, 1) 41 | aheadTicks = 0.1 * self.ticksPerMeter 42 | diffTicks = self.wheelSeparation * self.ticksPerMeter 43 | self.assertAlmostEqual(speeds.left, aheadTicks-diffTicks) 44 | self.assertAlmostEqual(speeds.right, aheadTicks+diffTicks) 45 | 46 | def testMotorLimitsStraight(self): 47 | maxTickSpeed = self.ticksPerMeter // 4 48 | self.controller.setMaxMotorSpeed(maxTickSpeed) 49 | speeds = self.controller.getSpeeds(1, 0) 50 | self.assertEqual(speeds.left, maxTickSpeed) 51 | self.assertEqual(speeds.right, maxTickSpeed) 52 | 53 | def testMotorLimitsCurved(self): 54 | maxTickSpeed = self.ticksPerMeter // 4 55 | self.controller.setMaxMotorSpeed(maxTickSpeed) 56 | speeds = self.controller.getSpeeds(1, 1) 57 | aheadTicks = self.ticksPerMeter 58 | diffTicks = self.wheelSeparation * self.ticksPerMeter 59 | factor = maxTickSpeed / (aheadTicks + diffTicks) 60 | self.assertEqual(speeds.left, 61 | int((aheadTicks-diffTicks) * factor)) 62 | self.assertEqual(speeds.right, maxTickSpeed) 63 | 64 | 65 | if __name__ == '__main__': 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /src/diff_drive/odometry.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from math import pi, sin, cos 3 | from diff_drive.encoder import Encoder 4 | from diff_drive.pose import Pose 5 | 6 | class Odometry: 7 | """Keeps track of the current position and velocity of a 8 | robot using differential drive. 9 | """ 10 | 11 | def __init__(self): 12 | self.leftEncoder = Encoder() 13 | self.rightEncoder = Encoder() 14 | self.pose = Pose() 15 | self.lastTime = 0 16 | 17 | def setWheelSeparation(self, separation): 18 | self.wheelSeparation = separation 19 | 20 | def setTicksPerMeter(self, ticks): 21 | self.ticksPerMeter = ticks 22 | 23 | def setEncoderRange(self, low, high): 24 | self.leftEncoder.setRange(low, high) 25 | self.rightEncoder.setRange(low, high) 26 | 27 | def setTime(self, newTime): 28 | self.lastTime = newTime 29 | 30 | def updateLeftWheel(self, newCount): 31 | self.leftEncoder.update(newCount) 32 | 33 | def updateRightWheel(self, newCount): 34 | self.rightEncoder.update(newCount) 35 | 36 | def updatePose(self, newTime): 37 | """Updates the pose based on the accumulated encoder ticks 38 | of the two wheels. See https://chess.eecs.berkeley.edu/eecs149/documentation/differentialDrive.pdf 39 | for details. 40 | """ 41 | leftTravel = self.leftEncoder.getDelta() / self.ticksPerMeter 42 | rightTravel = self.rightEncoder.getDelta() / self.ticksPerMeter 43 | deltaTime = newTime - self.lastTime 44 | 45 | deltaTravel = (rightTravel + leftTravel) / 2 46 | deltaTheta = (rightTravel - leftTravel) / self.wheelSeparation 47 | 48 | if rightTravel == leftTravel: 49 | deltaX = leftTravel*cos(self.pose.theta) 50 | deltaY = leftTravel*sin(self.pose.theta) 51 | else: 52 | radius = deltaTravel / deltaTheta 53 | 54 | # Find the instantaneous center of curvature (ICC). 55 | iccX = self.pose.x - radius*sin(self.pose.theta) 56 | iccY = self.pose.y + radius*cos(self.pose.theta) 57 | 58 | deltaX = cos(deltaTheta)*(self.pose.x - iccX) \ 59 | - sin(deltaTheta)*(self.pose.y - iccY) \ 60 | + iccX - self.pose.x 61 | 62 | deltaY = sin(deltaTheta)*(self.pose.x - iccX) \ 63 | + cos(deltaTheta)*(self.pose.y - iccY) \ 64 | + iccY - self.pose.y 65 | 66 | self.pose.x += deltaX 67 | self.pose.y += deltaY 68 | self.pose.theta = (self.pose.theta + deltaTheta) % (2*pi) 69 | self.pose.xVel = deltaTravel / deltaTime if deltaTime > 0 else 0. 70 | self.pose.yVel = 0 71 | self.pose.thetaVel = deltaTheta / deltaTime if deltaTime > 0 else 0. 72 | 73 | self.lastTime = newTime 74 | 75 | def getPose(self): 76 | return self.pose; 77 | 78 | def setPose(self, newPose): 79 | self.pose = newPose 80 | -------------------------------------------------------------------------------- /nodes/diff_drive_odometry: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import division 3 | 4 | import rospy 5 | from geometry_msgs.msg import Quaternion 6 | from geometry_msgs.msg import Twist 7 | from nav_msgs.msg import Odometry 8 | from std_msgs.msg import Int32 9 | from geometry_msgs.msg import PoseWithCovarianceStamped 10 | from tf.broadcaster import TransformBroadcaster 11 | from tf.transformations import quaternion_from_euler, euler_from_quaternion 12 | from math import sin, cos 13 | from diff_drive.pose import Pose 14 | from diff_drive import odometry 15 | 16 | class OdometryNode: 17 | 18 | def __init__(self): 19 | self.odometry = odometry.Odometry() 20 | 21 | def main(self): 22 | self.odomPub = rospy.Publisher('odom', Odometry, queue_size=10) 23 | self.tfPub = TransformBroadcaster() 24 | 25 | rospy.init_node('diff_drive_odometry') 26 | self.nodeName = rospy.get_name() 27 | rospy.loginfo("{0} started".format(self.nodeName)) 28 | 29 | rospy.Subscriber("lwheel_ticks", Int32, self.leftCallback) 30 | rospy.Subscriber("rwheel_ticks", Int32, self.rightCallback) 31 | rospy.Subscriber("initialpose", PoseWithCovarianceStamped, 32 | self.on_initial_pose) 33 | 34 | self.ticksPerMeter = int(rospy.get_param('~ticks_per_meter')) 35 | self.wheelSeparation = float(rospy.get_param('~wheel_separation')) 36 | self.rate = float(rospy.get_param('~rate', 10.0)) 37 | self.baseFrameID = rospy.get_param('~base_frame_id', 'base_link') 38 | self.odomFrameID = rospy.get_param('~odom_frame_id', 'odom') 39 | self.encoderMin = int(rospy.get_param('~encoder_min', -32768)) 40 | self.encoderMax = int(rospy.get_param('~encoder_max', 32767)) 41 | 42 | self.odometry.setWheelSeparation(self.wheelSeparation) 43 | self.odometry.setTicksPerMeter(self.ticksPerMeter) 44 | self.odometry.setEncoderRange(self.encoderMin, self.encoderMax) 45 | self.odometry.setTime(rospy.get_time()) 46 | 47 | rate = rospy.Rate(self.rate) 48 | while not rospy.is_shutdown(): 49 | self.publish() 50 | rate.sleep() 51 | 52 | def publish(self): 53 | self.odometry.updatePose(rospy.get_time()) 54 | now = rospy.get_rostime() 55 | pose = self.odometry.getPose() 56 | 57 | q = quaternion_from_euler(0, 0, pose.theta) 58 | self.tfPub.sendTransform( 59 | (pose.x, pose.y, 0), 60 | (q[0], q[1], q[2], q[3]), 61 | now, 62 | self.baseFrameID, 63 | self.odomFrameID 64 | ) 65 | 66 | odom = Odometry() 67 | odom.header.stamp = now 68 | odom.header.frame_id = self.odomFrameID 69 | odom.child_frame_id = self.baseFrameID 70 | odom.pose.pose.position.x = pose.x 71 | odom.pose.pose.position.y = pose.y 72 | odom.pose.pose.orientation.x = q[0] 73 | odom.pose.pose.orientation.y = q[1] 74 | odom.pose.pose.orientation.z = q[2] 75 | odom.pose.pose.orientation.w = q[3] 76 | odom.twist.twist.linear.x = pose.xVel 77 | odom.twist.twist.angular.z = pose.thetaVel 78 | self.odomPub.publish(odom) 79 | 80 | def on_initial_pose(self, msg): 81 | q = [msg.pose.pose.orientation.x, 82 | msg.pose.pose.orientation.x, 83 | msg.pose.pose.orientation.x, 84 | msg.pose.pose.orientation.w] 85 | roll, pitch, yaw = euler_from_quaternion(q) 86 | 87 | pose = Pose() 88 | pose.x = msg.pose.pose.position.x 89 | pose.y = msg.pose.pose.position.y 90 | pose.theta = yaw 91 | 92 | rospy.loginfo('Setting initial pose to %s', pose) 93 | self.odometry.setPose(pose) 94 | 95 | def leftCallback(self, msg): 96 | self.odometry.updateLeftWheel(msg.data) 97 | 98 | def rightCallback(self, msg): 99 | self.odometry.updateRightWheel(msg.data) 100 | 101 | 102 | if __name__ == '__main__': 103 | try: 104 | node = OdometryNode() 105 | node.main() 106 | except rospy.ROSInterruptException: 107 | pass 108 | -------------------------------------------------------------------------------- /tests/test_odometry.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import division 3 | PKG='test_odometry' 4 | 5 | from math import pi, sin, cos 6 | import unittest 7 | from diff_drive.odometry import Odometry 8 | 9 | 10 | class TestOdometry(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.wheelSeparation = 0.10 14 | self.ticksPerMeter = 10000 15 | self.odom = Odometry() 16 | self.odom.setWheelSeparation(self.wheelSeparation) 17 | self.odom.setTicksPerMeter(self.ticksPerMeter) 18 | 19 | def testInitialization(self): 20 | pose = self.odom.getPose() 21 | self.assertEquals(pose.x, 0) 22 | self.assertEquals(pose.y, 0) 23 | self.assertEquals(pose.theta, 0) 24 | self.assertEquals(pose.xVel, 0) 25 | self.assertEquals(pose.yVel, 0) 26 | self.assertEquals(pose.thetaVel, 0) 27 | 28 | def testTravelForward(self): 29 | self.checkUpdate(10000, 10000, 2, {'x': 1, 'xVel': 1/2}) 30 | 31 | def testSpinLeft(self): 32 | angle = (200/self.ticksPerMeter / self.wheelSeparation) % (2*pi) 33 | self.checkUpdate(-100, 100, 2, 34 | {'theta': angle, 'thetaVel': angle/2}) 35 | 36 | def testSpinRight(self): 37 | angle = (200/self.ticksPerMeter / self.wheelSeparation) % (2*pi) 38 | self.checkUpdate(100, -100, 2, 39 | {'theta': (-angle) % (2*pi), 40 | 'thetaVel': -angle/2}) 41 | 42 | def testCurveLeft(self): 43 | radius = self.wheelSeparation / 2 44 | angle = pi 45 | s = angle * self.wheelSeparation 46 | ticks = int(s * self.ticksPerMeter) 47 | self.checkUpdate(0, ticks, 2, 48 | {'x': 0, 49 | 'y': 2*radius, 50 | 'theta': angle, 51 | 'xVel': s/4, 52 | 'yVel': 0, 53 | 'thetaVel': angle/2}) 54 | 55 | def testCurveRight(self): 56 | radius = self.wheelSeparation / 2 57 | angle = pi 58 | s = angle * self.wheelSeparation 59 | ticks = int(s * self.ticksPerMeter) 60 | self.checkUpdate(ticks, 0, 2, 61 | {'x': 0, 62 | 'y': -2*radius, 63 | 'theta': -angle, 64 | 'xVel': s/4, 65 | 'yVel': 0, 66 | 'thetaVel': -angle/2}) 67 | 68 | def testCircle(self): 69 | self.checkCircleRight(8, 9) 70 | self.checkCircleRight(0, 100) 71 | self.checkCircleRight(100, 0) 72 | 73 | def checkCircle(self, vr, vl): 74 | radius = abs(self.wheelSeparation/2 * (vr+vl)/(vr-vl)) 75 | circumference = 2*pi*radius 76 | deltaTravel = (vr+vl)/2 * self.ticksPerMeter; 77 | for i in range(int(circumference/deltaTravel)): 78 | self.odom.updateLeftWheel(vl) 79 | self.odom.updateRightWheel(vr) 80 | self.odom.updatePose(i+1) 81 | self.checkPose(self.odom.getPose(), {'x': 0, 'y': 0}) 82 | 83 | def checkUpdate(self, leftTicks, rightTicks, deltaTime, attrs): 84 | self.odom.updateLeftWheel(leftTicks) 85 | self.odom.updateRightWheel(rightTicks) 86 | self.odom.updatePose(deltaTime) 87 | self.checkPose(self.odom.getPose(), attrs) 88 | 89 | def checkPose(self, pose, attrs): 90 | for key in ['x', 'y', 'theta', 'xVel', 'yVel', 'thetaVel']: 91 | if key in attrs: 92 | self.assertClose( 93 | getattr(pose, key), attrs[key], 94 | msg="{0}: {1}!={2}".format(key, 95 | getattr(pose, key), 96 | attrs[key])) 97 | else: 98 | self.assertEquals(getattr(pose, key), 0, key) 99 | 100 | def assertClose(self, x, y, msg): 101 | if y == 0: 102 | self.assertLess(abs(x), 0.0001, msg) 103 | else: 104 | self.assertLess(abs(x-y)/y, 0.001, msg) 105 | 106 | if __name__ == '__main__': 107 | unittest.main() 108 | -------------------------------------------------------------------------------- /config/view.rviz: -------------------------------------------------------------------------------- 1 | Panels: 2 | - Class: rviz/Displays 3 | Help Height: 144 4 | Name: Displays 5 | Property Tree Widget: 6 | Expanded: 7 | - /Global Options1 8 | - /Status1 9 | - /Grid1 10 | - /Axes1 11 | Splitter Ratio: 0.5 12 | Tree Height: 1176 13 | - Class: rviz/Selection 14 | Name: Selection 15 | - Class: rviz/Tool Properties 16 | Expanded: 17 | - /2D Pose Estimate1 18 | - /2D Nav Goal1 19 | - /Publish Point1 20 | Name: Tool Properties 21 | Splitter Ratio: 0.588679016 22 | - Class: rviz/Views 23 | Expanded: 24 | - /Current View1 25 | Name: Views 26 | Splitter Ratio: 0.5 27 | - Class: rviz/Time 28 | Experimental: false 29 | Name: Time 30 | SyncMode: 0 31 | SyncSource: "" 32 | - Class: rviz_plugin_tutorials/Teleop 33 | Name: Teleop 34 | Topic: /cmd_vel 35 | Visualization Manager: 36 | Class: "" 37 | Displays: 38 | - Alpha: 0.5 39 | Cell Size: 0.25 40 | Class: rviz/Grid 41 | Color: 160; 160; 164 42 | Enabled: true 43 | Line Style: 44 | Line Width: 0.0299999993 45 | Value: Lines 46 | Name: Grid 47 | Normal Cell Count: 0 48 | Offset: 49 | X: 0 50 | Y: 0 51 | Z: 0 52 | Plane: XY 53 | Plane Cell Count: 40 54 | Reference Frame: 55 | Value: true 56 | - Class: rviz/MarkerArray 57 | Enabled: true 58 | Marker Topic: visualization_marker_array 59 | Name: MarkerArray 60 | Namespaces: 61 | {} 62 | Queue Size: 100 63 | Value: true 64 | - Class: rviz/Axes 65 | Enabled: true 66 | Length: 0.200000003 67 | Name: Axes 68 | Radius: 0.0299999993 69 | Reference Frame: base_link 70 | Value: true 71 | Enabled: true 72 | Global Options: 73 | Background Color: 48; 48; 48 74 | Default Light: true 75 | Fixed Frame: odom 76 | Frame Rate: 30 77 | Name: root 78 | Tools: 79 | - Class: rviz/Interact 80 | Hide Inactive Objects: true 81 | - Class: rviz/MoveCamera 82 | - Class: rviz/Select 83 | - Class: rviz/FocusCamera 84 | - Class: rviz/Measure 85 | - Class: rviz/SetInitialPose 86 | Topic: /initialpose 87 | - Class: rviz/SetGoal 88 | Topic: /move_base_simple/goal 89 | - Class: rviz/PublishPoint 90 | Single click: true 91 | Topic: /clicked_point 92 | Value: true 93 | Views: 94 | Current: 95 | Class: rviz/Orbit 96 | Distance: 3.00951147 97 | Enable Stereo Rendering: 98 | Stereo Eye Separation: 0.0599999987 99 | Stereo Focal Distance: 1 100 | Swap Stereo Eyes: false 101 | Value: false 102 | Focal Point: 103 | X: 0 104 | Y: 0 105 | Z: 0 106 | Focal Shape Fixed Size: true 107 | Focal Shape Size: 0.0500000007 108 | Invert Z Axis: false 109 | Name: Current View 110 | Near Clip Distance: 0.00999999978 111 | Pitch: 1.56979632 112 | Target Frame: 113 | Value: Orbit (rviz) 114 | Yaw: 4.71318531 115 | Saved: ~ 116 | Window Geometry: 117 | Displays: 118 | collapsed: false 119 | Height: 1561 120 | Hide Left Dock: false 121 | Hide Right Dock: false 122 | QMainWindow State: 000000ff00000000fd00000004000000000000019300000576fc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b0000006600fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003100000576000000e400fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000a0049006d0061006700650100000373000002340000000000000000fb0000000c00540065006c0065006f007000000004da000000cd0000005c00ffffff000000010000012200000576fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003100000576000000c200fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e1000001970000000300000a1000000046fc0100000002fb0000000800540069006d0065010000000000000a10000003de00fffffffb0000000800540069006d006501000000000000045000000000000000000000074b0000057600000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 123 | Selection: 124 | collapsed: false 125 | Teleop: 126 | collapsed: false 127 | Time: 128 | collapsed: false 129 | Tool Properties: 130 | collapsed: false 131 | Views: 132 | collapsed: false 133 | Width: 2576 134 | X: 89 135 | Y: 60 136 | -------------------------------------------------------------------------------- /src/diff_drive/goal_controller.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | from math import pi, sqrt, sin, cos, atan2 3 | from diff_drive.pose import Pose 4 | #import rospy 5 | 6 | class GoalController: 7 | """Finds linear and angular velocities necessary to drive toward 8 | a goal pose. 9 | """ 10 | 11 | def __init__(self): 12 | self.kP = 3 13 | self.kA = 8 14 | self.kB = -1.5 15 | self.max_linear_speed = 1E9 16 | self.min_linear_speed = 0 17 | self.max_angular_speed = 1E9 18 | self.min_angular_speed = 0 19 | self.max_linear_acceleration = 1E9 20 | self.max_angular_acceleration = 1E9 21 | self.linear_tolerance = 0.025 # 2.5cm 22 | self.angular_tolerance = 3/180*pi # 3 degrees 23 | self.forward_movement_only = False 24 | 25 | def set_constants(self, kP, kA, kB): 26 | self.kP = kP 27 | self.kA = kA 28 | self.kB = kB 29 | 30 | def set_max_linear_speed(self, speed): 31 | self.max_linear_speed = speed 32 | 33 | def set_min_linear_speed(self, speed): 34 | self.min_linear_speed = speed 35 | 36 | def set_max_angular_speed(self, speed): 37 | self.max_angular_speed = speed 38 | 39 | def set_min_angular_speed(self, speed): 40 | self.min_angular_speed = speed 41 | 42 | def set_max_linear_acceleration(self, accel): 43 | self.max_linear_acceleration = accel 44 | 45 | def set_max_angular_acceleration(self, accel): 46 | self.max_angular_acceleration = accel 47 | 48 | def set_linear_tolerance(self, tolerance): 49 | self.linear_tolerance = tolerance 50 | 51 | def set_angular_tolerance(self, tolerance): 52 | self.angular_tolerance = tolerance 53 | 54 | def set_forward_movement_only(self, forward_only): 55 | self.forward_movement_only = forward_only 56 | 57 | def get_goal_distance(self, cur, goal): 58 | if goal is None: 59 | return 0 60 | diffX = cur.x - goal.x 61 | diffY = cur.y - goal.y 62 | return sqrt(diffX*diffX + diffY*diffY) 63 | 64 | def at_goal(self, cur, goal): 65 | if goal is None: 66 | return True 67 | d = self.get_goal_distance(cur, goal) 68 | dTh = abs(self.normalize_pi(cur.theta - goal.theta)) 69 | return d < self.linear_tolerance and dTh < self.angular_tolerance 70 | 71 | def get_velocity(self, cur, goal, dT): 72 | desired = Pose() 73 | 74 | goal_heading = atan2(goal.y - cur.y, goal.x - cur.x) 75 | a = -cur.theta + goal_heading 76 | 77 | # In Automomous Mobile Robots, they assume theta_G=0. So for 78 | # the error in heading, we have to adjust theta based on the 79 | # (possibly non-zero) goal theta. 80 | theta = self.normalize_pi(cur.theta - goal.theta) 81 | b = -theta - a 82 | 83 | # rospy.loginfo('cur=%f goal=%f a=%f b=%f', cur.theta, goal_heading, 84 | # a, b) 85 | 86 | d = self.get_goal_distance(cur, goal) 87 | if self.forward_movement_only: 88 | direction = 1 89 | a = self.normalize_pi(a) 90 | b = self.normalize_pi(b) 91 | else: 92 | direction = self.sign(cos(a)) 93 | a = self.normalize_half_pi(a) 94 | b = self.normalize_half_pi(b) 95 | 96 | # rospy.loginfo('After normalization, a=%f b=%f', a, b) 97 | 98 | if abs(d) < self.linear_tolerance: 99 | desired.xVel = 0 100 | desired.thetaVel = self.kB * theta 101 | else: 102 | desired.xVel = self.kP * d * direction 103 | desired.thetaVel = self.kA*a + self.kB*b 104 | 105 | # Adjust velocities if X velocity is too high. 106 | if abs(desired.xVel) > self.max_linear_speed: 107 | ratio = self.max_linear_speed / abs(desired.xVel) 108 | desired.xVel *= ratio 109 | desired.thetaVel *= ratio 110 | 111 | # Adjust velocities if turning velocity too high. 112 | if abs(desired.thetaVel) > self.max_angular_speed: 113 | ratio = self.max_angular_speed / abs(desired.thetaVel) 114 | desired.xVel *= ratio 115 | desired.thetaVel *= ratio 116 | 117 | # TBD: Adjust velocities if linear or angular acceleration 118 | # too high. 119 | 120 | # Adjust velocities if too low, so robot does not stall. 121 | if abs(desired.xVel) > 0 and abs(desired.xVel) < self.min_linear_speed: 122 | ratio = self.min_linear_speed / abs(desired.xVel) 123 | desired.xVel *= ratio 124 | desired.thetaVel *= ratio 125 | elif desired.xVel==0 and abs(desired.thetaVel) < self.min_angular_speed: 126 | ratio = self.min_angular_speed / abs(desired.thetaVel) 127 | desired.xVel *= ratio 128 | desired.thetaVel *= ratio 129 | 130 | return desired 131 | 132 | def normalize_half_pi(self, alpha): 133 | alpha = self.normalize_pi(alpha) 134 | if alpha > pi/2: 135 | return alpha - pi 136 | elif alpha < -pi/2: 137 | return alpha + pi 138 | else: 139 | return alpha 140 | 141 | def normalize_pi(self, alpha): 142 | while alpha > pi: 143 | alpha -= 2*pi 144 | while alpha < -pi: 145 | alpha += 2*pi 146 | return alpha 147 | 148 | def sign(self, x): 149 | if x >= 0: 150 | return 1 151 | else: 152 | return -1 153 | -------------------------------------------------------------------------------- /nodes/diff_drive_go_to_goal: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import division 3 | 4 | import rospy 5 | from math import pi, asin, acos 6 | from tf.transformations import euler_from_quaternion 7 | from geometry_msgs.msg import Twist, PoseStamped 8 | from nav_msgs.msg import Odometry 9 | from std_msgs.msg import Float32, Bool 10 | import actionlib 11 | 12 | from diff_drive import goal_controller 13 | from diff_drive import pose 14 | from diff_drive.msg import GoToPoseAction, GoToPoseGoal, GoToPoseResult 15 | 16 | class GoToGoalNode: 17 | 18 | def __init__(self): 19 | self.controller = goal_controller.GoalController() 20 | 21 | def main(self): 22 | rospy.init_node('diff_drive_go_to_goal') 23 | 24 | self.action_name = 'diff_drive_go_to_goal' 25 | self.action_server \ 26 | = actionlib.SimpleActionServer(self.action_name, GoToPoseAction, 27 | execute_cb=self.on_execute, 28 | auto_start=False) 29 | 30 | self.action_client = actionlib.SimpleActionClient( 31 | 'diff_drive_go_to_goal', GoToPoseAction) 32 | 33 | self.dist_pub = rospy.Publisher('~distance_to_goal', 34 | Float32, queue_size=10) 35 | self.twist_pub = rospy.Publisher('cmd_vel', 36 | Twist, queue_size=10) 37 | 38 | self.node_name = rospy.get_name() 39 | rospy.loginfo("{0} started".format(self.node_name)) 40 | 41 | rospy.Subscriber('odom', Odometry, self.on_odometry) 42 | rospy.Subscriber('move_base_simple/goal', PoseStamped, self.on_goal) 43 | 44 | self.goal_achieved_pub = rospy.Publisher('goal_achieved', Bool, 45 | queue_size=1) 46 | 47 | rate = rospy.get_param('~rate', 10.0) 48 | self.rate = rospy.Rate(rate) 49 | self.dT = 1 / rate 50 | 51 | self.kP = rospy.get_param('~kP', 3.0) 52 | self.kA = rospy.get_param('~kA', 8.0) 53 | self.kB = rospy.get_param('~kB', -1.5) 54 | self.controller.set_constants(self.kP, self.kA, self.kB) 55 | 56 | self.controller.set_linear_tolerance( 57 | rospy.get_param('~linear_tolerance', 0.05)) 58 | self.controller.set_angular_tolerance( 59 | rospy.get_param('~angular_tolerance', 3/180*pi)) 60 | 61 | self.controller.set_max_linear_speed( 62 | rospy.get_param('~max_linear_speed', 0.2)) 63 | self.controller.set_min_linear_speed( 64 | rospy.get_param('~min_linear_speed', 0)) 65 | self.controller.set_max_angular_speed( 66 | rospy.get_param('~max_angular_speed', 1.0)) 67 | self.controller.set_min_angular_speed( 68 | rospy.get_param('~min_angular_speed', 0)) 69 | self.controller.set_max_linear_acceleration( 70 | rospy.get_param('~max_linear_acceleration', 0.1)) 71 | self.controller.set_max_angular_acceleration( 72 | rospy.get_param('~max_angular_acceleration', 0.3)) 73 | 74 | # Set whether to allow movement backward. Backward movement is 75 | # safe if the robot can avoid obstacles while traveling in 76 | # reverse. We default to forward movement only since many 77 | # sensors are front-facing. 78 | self.controller.set_forward_movement_only( 79 | rospy.get_param('~forwardMovementOnly', True)) 80 | 81 | self.init_pose() 82 | self.goal = None 83 | 84 | self.action_server.start() 85 | rospy.spin() 86 | 87 | def on_execute(self, goal): 88 | self.goal = self.get_angle_pose(goal.pose.pose) 89 | rospy.loginfo('Goal: (%f,%f,%f)', self.goal.x, self.goal.y, 90 | self.goal.theta) 91 | 92 | success = True 93 | while not rospy.is_shutdown() and self.goal is not None: 94 | # Allow client to preempt the goal. 95 | if self.action_server.is_preempt_requested(): 96 | rospy.loginfo('Goal preempted') 97 | self.send_velocity(0, 0) 98 | self.action_server.set_preempted() 99 | success = False 100 | break 101 | self.publish() 102 | self.rate.sleep() 103 | if self.goal is None: # Goal reached and cleared 104 | result = GoToPoseResult() 105 | result.success = success 106 | self.action_server.set_succeeded(result) 107 | 108 | def init_pose(self): 109 | self.pose = pose.Pose() 110 | self.pose.x = 0 111 | self.pose.y = 0 112 | self.pose.theta = 0 113 | 114 | def publish(self): 115 | if self.controller.at_goal(self.pose, self.goal): 116 | desired = pose.Pose() 117 | else: 118 | desired = self.controller.get_velocity(self.pose, self.goal, 119 | self.dT) 120 | 121 | # if self.goal is not None \ 122 | # and (desired.xVel!=0.0 or desired.thetaVel!=0.0): 123 | # rospy.loginfo( 124 | # 'current=(%f,%f,%f) goal=(%f,%f,%f) xVel=%f thetaVel=%f', 125 | # self.pose.x, self.pose.y, self.pose.theta, 126 | # self.goal.x, self.goal.y, self.goal.theta, 127 | # desired.xVel, desired.thetaVel) 128 | 129 | d = self.controller.get_goal_distance(self.pose, self.goal) 130 | self.dist_pub.publish(d) 131 | 132 | self.send_velocity(desired.xVel, desired.thetaVel) 133 | 134 | # Forget the goal if achieved. 135 | if self.controller.at_goal(self.pose, self.goal): 136 | rospy.loginfo('Goal achieved') 137 | self.goal = None 138 | msg = Bool() 139 | msg.data = True 140 | self.goal_achieved_pub.publish(msg) 141 | 142 | def send_velocity(self, xVel, thetaVel): 143 | twist = Twist() 144 | twist.linear.x = xVel 145 | twist.angular.z = thetaVel 146 | self.twist_pub.publish(twist) 147 | 148 | def on_odometry(self, newPose): 149 | self.pose = self.get_angle_pose(newPose.pose.pose) 150 | 151 | def on_goal(self, goal): 152 | self.action_client.wait_for_server() 153 | action_goal = GoToPoseGoal() 154 | action_goal.pose.pose = goal.pose 155 | self.action_client.send_goal(action_goal) 156 | 157 | def get_angle_pose(self, quaternion_pose): 158 | q = [quaternion_pose.orientation.x, 159 | quaternion_pose.orientation.y, 160 | quaternion_pose.orientation.z, 161 | quaternion_pose.orientation.w] 162 | roll, pitch, yaw = euler_from_quaternion(q) 163 | 164 | angle_pose = pose.Pose() 165 | angle_pose.x = quaternion_pose.position.x 166 | angle_pose.y = quaternion_pose.position.y 167 | angle_pose.theta = yaw 168 | return angle_pose 169 | 170 | if __name__ == '__main__': 171 | try: 172 | node = GoToGoalNode() 173 | node.main() 174 | except rospy.ROSInterruptException: 175 | pass 176 | -------------------------------------------------------------------------------- /tests/test_goal_controller.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | PKG='test_goal_controller' 3 | 4 | import unittest 5 | from math import pi, sin, cos 6 | from diff_drive.goal_controller import GoalController 7 | from diff_drive.pose import Pose 8 | 9 | 10 | class TestGoalController(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.controller = GoalController() 14 | 15 | # Test that the robot does not move when already at the goal 16 | # at the right heading. 17 | def testAtGoal(self): 18 | cur = Pose() 19 | desired = self.controller.getVelocity(cur, cur, 0.1) 20 | self.assertEquals(desired.xVel, 0) 21 | self.assertEquals(desired.thetaVel, 0) 22 | 23 | # Test that a goal pose ahead of the current position at the same 24 | # heading causes a straight-ahead move. 25 | def testStraightAhead(self): 26 | cur = Pose() 27 | goal = Pose() 28 | goal.x = 1 29 | desired = self.controller.getVelocity(cur, goal, 0.1) 30 | self.assertGreater(desired.xVel, 0) 31 | self.assertEquals(desired.thetaVel, 0) 32 | 33 | # Test that a goal pose behind the current position at the same 34 | # heading causes a straight-back move. 35 | def testStraightBack(self): 36 | cur = Pose() 37 | goal = Pose() 38 | goal.x = -1 39 | desired = self.controller.getVelocity(cur, goal, 0.1) 40 | self.assertLess(desired.xVel, 0) 41 | self.assertEquals(desired.thetaVel, 0) 42 | 43 | # Test that a goal at the current position with a leftward goal 44 | # heading causes a leftward rotation. 45 | def testRotateLeft(self): 46 | cur = Pose() 47 | goal = Pose() 48 | goal.theta = pi/2 49 | desired = self.controller.getVelocity(cur, goal, 0.1) 50 | self.assertEquals(desired.xVel, 0) 51 | self.assertGreater(desired.thetaVel, 0) 52 | 53 | # Test that a goal at the current position with a rightward goal 54 | # heading causes a rightward rotation. 55 | def testRotateRight(self): 56 | cur = Pose() 57 | goal = Pose() 58 | goal.theta = -pi/2 59 | desired = self.controller.getVelocity(cur, goal, 0.1) 60 | self.assertEquals(desired.xVel, 0) 61 | self.assertLess(desired.thetaVel, 0) 62 | 63 | # Test that a goal pose that is reachable with a forward, leftward 64 | # arc causes a forward movement with some leftward rotation. 65 | def testCurveLeft(self): 66 | cur = Pose() 67 | goal = Pose() 68 | goal.x = 1 69 | goal.y = 1 70 | goal.theta = pi/2 71 | desired = self.controller.getVelocity(cur, goal, 0.1) 72 | self.assertGreater(desired.xVel, 0) 73 | self.assertGreater(desired.thetaVel, 0) 74 | 75 | # Test that a goal pose that is reachable with a forward, rightward 76 | # arc causes a forward movement with some rightward rotation. 77 | def testCurveRight(self): 78 | cur = Pose() 79 | cur.theta = pi/2 80 | goal = Pose() 81 | goal.x = 1 82 | goal.y = 1 83 | desired = self.controller.getVelocity(cur, goal, 0.1) 84 | self.assertGreater(desired.xVel, 0) 85 | self.assertLess(desired.thetaVel, 0) 86 | 87 | # Test that a goal pose behind the robot that is reachable with a 88 | # leftward arc causes a backward movement with some rightward 89 | # rotation. 90 | def testCurveBackLeft(self): 91 | cur = Pose() 92 | goal = Pose() 93 | cur.x = 1 94 | cur.y = 1 95 | goal.theta = pi/2 96 | desired = self.controller.getVelocity(cur, goal, 0.1) 97 | self.assertLess(desired.xVel, 0) 98 | self.assertGreater(desired.thetaVel, 0) 99 | 100 | # Test that a goal pose behind the robot that is reachable with a 101 | # rightward arc causes a backward movement with some leftward 102 | # rotation. 103 | def testCurveBackRigth(self): 104 | cur = Pose() 105 | goal = Pose() 106 | cur.x = 1 107 | cur.y = 1 108 | cur.theta = pi/2 109 | desired = self.controller.getVelocity(cur, goal, 0.1) 110 | self.assertLess(desired.xVel, 0) 111 | self.assertLess(desired.thetaVel, 0) 112 | 113 | def testButtonHookLeft(self): 114 | cur = Pose() 115 | goal = Pose() 116 | goal.x = 1 117 | goal.y = 1 118 | goal.theta = pi 119 | desired = self.controller.getVelocity(cur, goal, 0.1) 120 | self.assertGreater(desired.xVel, 0) 121 | self.assertGreater(desired.thetaVel, 0) 122 | 123 | def testButtonHookRight(self): 124 | cur = Pose() 125 | goal = Pose() 126 | goal.x = 1 127 | goal.y = -1 128 | goal.theta = -pi 129 | desired = self.controller.getVelocity(cur, goal, 0.1) 130 | self.assertGreater(desired.xVel, 0) 131 | self.assertLess(desired.thetaVel, 0) 132 | 133 | def testGoToGoal(self): 134 | self.checkGoToGoal(0, 0, 0, 1, 0, 0) # Straight ahead 135 | self.checkGoToGoal(0, 0, 0, 1, 1, pi/2) # Arc left 136 | self.checkGoToGoal(0, 0, 0, 1, 1, -pi/2) # Left, then turn right 137 | self.checkGoToGoal(0, 0, 0, 0, 1, pi) # Go left 138 | self.checkGoToGoal(0, 0, 0, 1, 0, pi) # Go ahead and u-turn 139 | self.checkGoToGoal(0, 0, 0, -1, 0, 0) # Straight back 140 | self.checkGoToGoal(0, 0, 0, -1, -1, 0) # Back up to right 141 | self.checkGoToGoal(0, 0, 0, -1, -1, pi) # Back up and turn left 142 | 143 | def testGoToGoalForwardOnly(self): 144 | self.controller.setForwardMovementOnly(True) 145 | self.checkGoToGoal(0, 0, 0, 1, 0, 0) # Straight ahead 146 | self.checkGoToGoal(0, 0, 0, 1, 1, pi/2) # Arc left 147 | self.checkGoToGoal(0, 0, 0, 1, 1, -pi/2) # Left, then turn right 148 | self.checkGoToGoal(0, 0, 0, 0, 1, pi) # Go left 149 | self.checkGoToGoal(0, 0, 0, 1, 0, pi) # Go ahead and u-turn 150 | self.checkGoToGoal(0, 0, 0, -1, 0, 0) # Straight back 151 | self.checkGoToGoal(0, 0, 0, -1, -1, 0) # Back up to right 152 | self.checkGoToGoal(0, 0, 0, -1, -1, pi) # Back up and turn left 153 | 154 | def checkGoToGoal(self, x0, y0, th0, x1, y1, th1): 155 | dTol = 0.05 # 5cm 156 | thTol = 0.04 # Approx 2.5 degrees 157 | 158 | self.controller.setLinearTolerance(dTol) 159 | self.controller.setAngularTolerance(thTol) 160 | 161 | cur = Pose() 162 | cur.x = x0 163 | cur.y = y0 164 | cur.theta = th0 165 | 166 | goal = Pose() 167 | goal.x = x1 168 | goal.y = y1 169 | goal.theta = th1 170 | 171 | lastDistance = self.controller.getGoalDistance(cur, goal) 172 | dT = 0.05 173 | for i in range(1000): 174 | if self.controller.atGoal(cur, goal): 175 | return 176 | 177 | desired = self.controller.getVelocity(cur, goal, dT) 178 | newTheta = cur.theta + dT*desired.thetaVel 179 | midTheta = (cur.theta + newTheta) / 2.0 180 | cur.x += dT * desired.xVel * cos(midTheta) 181 | cur.y += dT * desired.xVel * sin(midTheta) 182 | cur.theta = newTheta 183 | 184 | # If we get here, we didn't reach the goal. 185 | self.assertFalse('Did not reach the goal: p0=' 186 | + str((x0,y0,th0)) 187 | + ' p1=' + str((x1,y1,th1))) 188 | 189 | if __name__ == '__main__': 190 | unittest.main() 191 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | = diff_drive -- Differential-Drive Controller 2 | :imagesdir: ./images 3 | 4 | This package implements ROS nodes to control and monitor a differential-drive robot. 5 | 6 | The package 7 | is intended as a lighter-weight solution than the ROS controller framework, albeit with lower 8 | performance since it is written in Python. If you need tight, real-time control, you may want 9 | to look at link:http://wiki.ros.org/ros_controllers[ros_controllers], 10 | a C++ package which includes a differential-drive controller that is integrated with 11 | link:http://wiki.ros.org/ros_control[ros_control] and 12 | link:http://wiki.ros.org/controller_manager[controller_manager]. Those controllers are designed 13 | to integrate with hardware in the same process, rather than using topics. Instead, this package 14 | expects to publish the desired motor speeds using standard ROS messages. 15 | 16 | == Supplied Nodes 17 | 18 | * `diff_drive_controller` -- Converts from twist to wheel velocities for motors. 19 | * `diff_drive_odometry` -- Publishes odometry from wheel encoder data. 20 | * `diff_drive_go_to_goal` -- Moves the robot to a goal position. 21 | * `diff_drive_mock_robot` -- Implements a mock differential drive robot, for testing. 22 | 23 | The nodes in this package are designed with these considerations: 24 | 25 | * The node and hardware implementing differential drive should deal only in encoder ticks. 26 | * Conversions to and from physical coordinates should happen within the nodes in this package. 27 | * This package should integrate cleanly with the navigation stack, perhaps with remappings. 28 | * Nodes should use standard topic and parameter names used by the navigation stack, but should allow remapping. 29 | 30 | == Demo 31 | 32 | To see all the nodes in this package in action, you can launch a demo from ROS. There are no 33 | dependencies other than the standard ROS packages. 34 | 35 | roslaunch diff_drive demo.launch 36 | 37 | This launches _rviz_ as part of the demo, and shows the robot position as a small coordinate system on 38 | 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. 39 | Then click and drag within the grid to set the robot goal position and heading. The mock robot will move to 40 | that new pose, which you can see by the movement of the robot axes. 41 | 42 | In the demo, both forward and backward movement is allowed, so if the goal position is behind the robot, 43 | it will move backward. You can force the robot to move foward only by setting the parameter `~forwardMovementOnly` 44 | to `true`. 45 | 46 | == ROS API 47 | 48 | === 1. diff_drive_controller 49 | 50 | Listens for desired linear and angular velocity, and publishes corresponding wheel velocities, in encoder ticks per second, required to achieve those velocities. 51 | 52 | ==== Published Topics 53 | 54 | `~lwheel_desired_rate` (std_msgs/Int32):: 55 | Desired left wheel rotation rate, in encoder ticks per second. 56 | 57 | `~rwheel_desired_rate` (std_msgs/Int32):: 58 | Desired right wheel rotation rate, in encoder ticks per second. 59 | 60 | ==== Subscribed Topics 61 | 62 | `~cmd_vel` (geometry_msgs/Twist):: 63 | Desired linear and angular velocity. 64 | 65 | ==== Parameters 66 | 67 | `~ticks_per_meter` (double):: 68 | Number of encoder ticks per meter of travel. 69 | 70 | `~wheel_separation` (double):: 71 | Distance between the two wheels (meters). 72 | 73 | `~rate` (int, default: 50):: 74 | The rate that the output velocity target messages will be published (Hz). 75 | 76 | `~timeout` (int, default: 0.2):: 77 | The maximum number of seconds expected between `cmd_vel` messages. If `cmd_vel` 78 | is not received before this limit, the controller will assume the commanding 79 | node has died and will set the desired wheel rates to zero, to stop the robot. 80 | 81 | === 2. diff_drive_odometry 82 | 83 | Listens for wheel movement and rates and publishes the transform between the odom frame and the robot frame. 84 | 85 | ==== Published Topics 86 | 87 | `~odom` -- (nav_msgs/Odometry):: 88 | The robot odometry -- the current robot pose. 89 | 90 | `~tf`:: 91 | The transform between the odometry frame and the robot frame. 92 | 93 | ==== Subscribed Topics 94 | 95 | `~lwheel_ticks` (std_msgs/Int32):: 96 | Cumulative encoder ticks of the left wheel. 97 | 98 | `~rwheel_ticks` (std_msgs/Int32):: 99 | Cumulative encoder ticks of the right wheel. 100 | 101 | `~lwheel_rate` (std_msgs/Float32):: 102 | Left wheel rotation rate, in encoder ticks per second. 103 | 104 | `~rwheel_rate` (std_msgs/Float32):: 105 | Right wheel rotation rate, in encoder ticks per second. 106 | 107 | ==== Parameters 108 | 109 | `~ticks_per_meter` (double):: 110 | Number of encoder ticks per meter of travel. 111 | 112 | `~wheel_separation` (double):: 113 | Distance between the two wheels (m). 114 | 115 | `~rate` (double, default 10.0):: 116 | The rate at which the `tf` and `odom` topics are published (Hz). 117 | 118 | `~timeout` (double, default 0.2):: 119 | The amount of time to continue publishing desired wheel rates after receiving a twist message (seconds). 120 | If set to zero, wheel velocities will be sent only when a new twist message is received. 121 | 122 | `~base_frame_id` (string, default: "base_link"):: 123 | The name of the base frame of the robot. 124 | 125 | `~odom_frame_id` (string, default: "odom"):: 126 | The name of the odometry reference frame. 127 | 128 | `~encoder_min` (int, default: -32768):: 129 | 130 | `~encoder_max` (int, default: 32768):: 131 | The min and max value the encoder should output. Used to calculate odometry when the values wrap around. 132 | 133 | `~wheel_low_wrap` (int, default: 0.3 * (encoder_max - encoder_min + 1) + encoder_min):: 134 | 135 | `~wheel_high_wrap` (int, default: 0.7 * (encoder_max - encoder_min + 1) + encoder_min):: 136 | 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. 137 | 138 | === 3. diff_drive_go_to_goal 139 | 140 | Listens for new goal poses and computes velocities needed to achieve the goal. 141 | 142 | ==== Published Topics 143 | 144 | `~distance_to_goal` (std_msgs/Float32):: 145 | Distance to the goal position (meters). 146 | 147 | `~cmd_vel` (geometry_msgs/Twist):: 148 | Desired linear and angular velocity to move toward the goal pose. 149 | 150 | ==== Subscribed Topics 151 | 152 | `~goal` (geometry_msgs/Pose):: 153 | Desired goal pose. 154 | 155 | ==== Parameters 156 | 157 | `~rate` (float, default: 10):: 158 | Rate at which to publish desired velocities (Hz). 159 | 160 | `~goal_linear_tolerance` (float, default: 0.1):: 161 | The distance from the goal at which the robot is assumed to have accomplished the goal position (meters). 162 | 163 | `~goal_angular_tolerance` (float, default: 0.087):: 164 | The difference between robot angle and goal pose angle at which the robot is assumed to have 165 | accomplished the goal attitude (radians). Default value is approximately 5 degrees. 166 | 167 | `~max_linear_velocity` (float, default: 0.2):: 168 | The maximum linear velocity toward the goal (meters/second). 169 | 170 | `~max_angular_velocity` (float, default: 1.5):: 171 | The maximum angular velocity (radians/second). 172 | 173 | `~max_linear_acceleration` (float, default: 4.0):: 174 | The maximum linear acceleration (meters/second^2). 175 | 176 | `~forwardMovementOnly` (boolean, default: true):: 177 | If true, only forward movement is allowed to achieve the goal position. 178 | If false, the robot will move backward to the goal if that is the most 179 | direct path. 180 | 181 | `~Kp` (float, default: 3.0):: 182 | Linear distance proportionality constant. Higher values make the robot accelerate more quickly toward the goal and decelerate less quickly. 183 | 184 | `~Ka` (float: default: 8.0):: 185 | Proportionality constant for angle to goal position. Higher values make the robot turn more quickly toward the goal. 186 | 187 | `~Kb` (float: default: -1.5):: 188 | 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_. 189 | 190 | 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: 191 | 192 | v = Kp * d 193 | w = Ka*a + Kb*b 194 | 195 | See _Autonomous Mobile Robots, Second Edition_ by Siegwart et. al., section 3.6.2.4. In this code, when the robot 196 | is near enough to the goal, _v_ is set to zero, and _w_ is simply _Kb*b_. 197 | 198 | To ensure convergence toward the goal, _K~p~_ and _K~a~_ must be positive, _K~b~_ must be negative, and _K~a~_ 199 | must be greater than _K~p~_. To ensure robust convergence, so that the robot never changes direction, 200 | _K~a~_ - 5/3*_K~b~_ - 2/pi*_K~p~_ must be greater than zero. 201 | 202 | === 4. diff_drive_mock_robot 203 | 204 | Implements a simulation of perfect differential drive robot hardware. It immediately follows any speed 205 | commands received with infinite acceleration, and publishes the wheel encoder values and encoder 206 | rates. 207 | 208 | ==== Published Topics 209 | 210 | `~lwheel_ticks` (std_msgs/Int32):: 211 | Cumulative encoder ticks of the left wheel. 212 | 213 | `~rwheel_ticks` (std_msgs/Int32):: 214 | Cumulative encoder ticks of the right wheel. 215 | 216 | `~lwheel_rate` (std_msgs/Float32):: 217 | Left wheel rotation rate, in encoder ticks per second. 218 | 219 | `~rwheel_rate` (std_msgs/Float32):: 220 | Right wheel rotation rate, in encoder ticks per second. 221 | 222 | ==== Subscribed Topics 223 | 224 | `~lwheel_desired_rate` (std_msgs/Int32):: 225 | Desired left wheel rotation rate, in encoder ticks per second. 226 | 227 | `~rwheel_desired_rate` (std_msgs/Int32):: 228 | Desired right wheel rotation rate, in encoder ticks per second. 229 | 230 | ==== Parameters 231 | 232 | `~cmd_timeout` (float, default: 0.2):: 233 | The amount of time after the last wheel rate message when the robot should stop automatically (seconds). 234 | 235 | `~rate` (float, default 10.0):: 236 | The rate at which the simulated wheel encoder values and rates should be published (Hz). 237 | --------------------------------------------------------------------------------