├── .github └── workflows │ ├── ci.yml │ └── lint.yml ├── README.md ├── codecov.yml ├── joy_teleop ├── CHANGELOG.rst ├── README.md ├── config │ └── joy_teleop_example.yaml ├── joy_teleop │ ├── __init__.py │ ├── incrementer_server.py │ └── joy_teleop.py ├── launch │ └── example.launch.py ├── package.xml ├── resource │ └── joy_teleop ├── setup.cfg ├── setup.py └── test │ ├── joy_teleop_testing_common.py │ ├── test_action_fibonacci.py │ ├── test_array_indexing.py │ ├── test_copyright.py │ ├── test_debounce.py │ ├── test_flake8.py │ ├── test_get_interface_type.py │ ├── test_multi_button_command.py │ ├── test_parameter_failures.py │ ├── test_pep257.py │ ├── test_service_add_two_ints.py │ ├── test_service_not_ready.py │ ├── test_service_trigger.py │ ├── test_topic_axis_mappings.py │ ├── test_topic_message_value.py │ └── test_xmllint.py ├── key_teleop ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── config │ └── key_teleop.yaml ├── key_teleop │ ├── __init__.py │ └── key_teleop.py ├── package.xml ├── resource │ └── key_teleop ├── setup.cfg ├── setup.py └── test │ ├── test_copyright.py │ ├── test_flake8.py │ └── test_pop257.py ├── mouse_teleop ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── config │ └── mouse_teleop.yaml ├── launch │ └── mouse_teleop.launch.py ├── mouse_teleop │ ├── __init__.py │ └── mouse_teleop.py ├── package.xml ├── resource │ └── mouse_teleop ├── setup.cfg ├── setup.py └── test │ ├── test_copyright.py │ ├── test_flake8.py │ ├── test_pop257.py │ └── test_xmllint.py ├── pytest.ini ├── teleop_tools ├── CHANGELOG.rst ├── CMakeLists.txt └── package.xml └── teleop_tools_msgs ├── CHANGELOG.rst ├── CMakeLists.txt ├── action └── Increment.action └── package.xml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test teleop_tools 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | schedule: 8 | # Run every morning to detect flakiness and broken dependencies 9 | - cron: '17 8 * * *' 10 | 11 | jobs: 12 | ci: 13 | name: Rolling source job 14 | runs-on: ubuntu-24.04 15 | strategy: 16 | fail-fast: false 17 | steps: 18 | - uses: ros-tooling/setup-ros@v0.7 19 | with: 20 | required-ros-distributions: rolling 21 | - uses: ros-tooling/action-ros-ci@v0.4 22 | with: 23 | target-ros2-distro: rolling 24 | # build all packages listed in the meta package 25 | package-name: 26 | joy_teleop 27 | key_teleop 28 | mouse_teleop 29 | teleop_tools 30 | teleop_tools_msgs 31 | - uses: codecov/codecov-action@v3 32 | with: 33 | file: ros_ws/lcov/total_coverage.info 34 | flags: unittests 35 | name: codecov-umbrella 36 | - uses: actions/upload-artifact@v4 37 | with: 38 | name: colcon-logs-${{ matrix.os }} 39 | path: ros_ws/log 40 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint teleop_tools 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | ament_lint: 7 | name: ament_${{ matrix.linter }} 8 | runs-on: ubuntu-22.04 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | linter: [copyright, flake8, pep257, xmllint] 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: ros-tooling/setup-ros@v0.6 16 | - uses: ros-tooling/action-ros-lint@v0.1 17 | with: 18 | distribution: rolling 19 | linter: ${{ matrix.linter }} 20 | package-name: 21 | joy_teleop 22 | key_teleop 23 | mouse_teleop 24 | teleop_tools 25 | teleop_tools_msgs 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | teleop_tools 2 | ============ 3 | 4 | A set of generic teleoperation tools for any robot. 5 | 6 | This contains the following teleoperation tools: 7 | 8 | * `joy_teleop`, a generic joystick interface for topics and actions 9 | * `key_teleop`, a lightweight console keyboard teleoperation utility 10 | * `mouse_teleop`, a pointing device (e.g. mouse, touchpad) teleoperation utility 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "35...100" 5 | status: 6 | project: 7 | default: 8 | informational: true 9 | flags: 10 | - unittests 11 | patch: off 12 | fixes: 13 | - "ros_ws/src/teleop_tools/::" 14 | comment: 15 | layout: "diff, flags, files" 16 | behavior: default 17 | flags: 18 | unittests: 19 | paths: 20 | - joy_teleop 21 | - key_teleop 22 | - mouse_teleop 23 | - teleop_tools 24 | - teleop_tools_msgs 25 | -------------------------------------------------------------------------------- /joy_teleop/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package joy_teleop 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.0.0 (2025-04-23) 6 | ------------------ 7 | * Use Fibonacci from example_interfaces (`#97 `_) 8 | * Add missing test dependency action_tutorials_interfaces (`#95 `_) 9 | * Contributors: Noel Jiménez García 10 | 11 | 1.8.0 (2025-04-16) 12 | ------------------ 13 | 14 | 1.7.0 (2024-11-06) 15 | ------------------ 16 | * Fix excessive cpu consumption (`#92 `_) 17 | * Fix excessive cpu consumption 18 | * Reorder imports 19 | --------- 20 | Co-authored-by: Isaac Acevedo 21 | * Contributors: Isaac Acevedo 22 | 23 | 1.6.0 (2024-10-01) 24 | ------------------ 25 | * Extend mapping to index of an array (`#76 `_) 26 | * Contributors: El Jawad Alaa 27 | 28 | 1.5.1 (2024-09-02) 29 | ------------------ 30 | * Removed action tutorials interfaces dependency (`#88 `_) 31 | * Contributors: Alejandro Hernández Cordero 32 | 33 | 1.5.0 (2023-11-01) 34 | ------------------ 35 | * Fix deprecated topics on JTC (`#86 `_) 36 | * Fix multi-button commands (`#84 `_) 37 | * replace deprecated dash by underscore (`#85 `_) 38 | * Contributors: Noel Jiménez García 39 | 40 | 1.4.0 (2023-03-28) 41 | ------------------ 42 | * fix incrementer_server 43 | * Contributors: Borong Yuan 44 | 45 | 1.3.0 (2022-11-23) 46 | ------------------ 47 | * launch: fix deprecated attributes 48 | * Fix some warnings from tests. 49 | In here are some flake8 fixes and fixes to the joy_teleop tests 50 | now that some of the error messages have changed. 51 | * Allow a `value` type within an axis mapping. Useful for frame data. 52 | * Add offsets to example yaml 53 | * add ci & lint 54 | * joy_teleop: convert current time to message type for timestamping 55 | * Contributors: AndyZe, Chris Lalancette, Kazunari Tanaka, Marcel Zeilinger, Russ Webber 56 | 57 | 1.2.1 (2020-10-29) 58 | ------------------ 59 | 60 | 1.2.0 (2020-10-16) 61 | ------------------ 62 | * Change the file mode of the python files in joy_teleop. 63 | They don't need to be executable. 64 | * Add in Python typing to joy_teleop. 65 | This also showed a few bugs. 66 | * Add a test for service becoming ready. 67 | * Add in tests for parameter failures. 68 | * Add a test for debouncing. 69 | The test works by having a subscription to the output topic. 70 | Every time the subscription is received, we increment a counter. 71 | There is also a timer callback that executes every 0.1 seconds, 72 | publishing the 'joy' message. Like other tests, this publication 73 | toggles the button on and off to avoid debouncing. Unlike other 74 | tests, once we have seen one message come in, we stop toggling 75 | and just set it to one all of the time to ensure debouncing 76 | works properly. 77 | * Add in a test for actions. 78 | * Add in tests for services. 79 | * Add a test for topic axis mappings. 80 | * Add in a test for a simple message. 81 | * Add in joy_teleop common testing tools. 82 | These will be used by the rest of the tests. 83 | * Add in unit tests for get_interface_type. 84 | * Rename test_pop257 -> test_pep257. 85 | * Rewrite to use classes instead of maps. 86 | This has a number of benefits: 87 | 1. I think it is much easier to read; the classes only implement 88 | the pieces they are concerned with. 89 | 2. Actions and services now automatically reconnect as action 90 | servers or service servers come and go. 91 | 3. It is easier to write tests for individual functionality. 92 | The API to this node stays the same; the parameters and topics 93 | that were used before are still honored. 94 | * Raise errors when parsing configuration fails. 95 | This lets the user know that their configuration is wrong 96 | much earlier. 97 | * Get rid of self.config. 98 | We never use it outside of the constructor, so just make it 99 | a local variable. 100 | * Rename al_clients -> action_clients. 101 | * Contributors: Chris Lalancette 102 | 103 | 1.1.0 (2020-04-21) 104 | ------------------ 105 | * Add the ability to have deadman axes. (`#46 `_) 106 | * Add the ability to have deadman axes. 107 | Some controllers don't have a convenient shoulder trigger 108 | button, but do have shoulder "axes". Allow the axes to 109 | be used for a deadman trigger, assuming they are pressed 110 | all the way. Note that I used a dict for the list of 111 | axes, as this provides the most convenient way to deal 112 | with controllers that use 1.0, -1.0, or 0.0 as the "far" 113 | end of the axis. 114 | * Make sure to ignore buttons and axes that don't exist. 115 | * Contributors: Chris Lalancette 116 | 117 | 1.0.2 (2020-02-10) 118 | ------------------ 119 | * Avoid halting on action server status checks. (`#48 `_) 120 | * Depend action_tutorials_interfaces (`#44 `_) 121 | * log JoyTeleopException (`#41 `_) 122 | * Contributors: Michel Hidalgo, Yutaka Kondo 123 | 124 | 1.0.1 (2019-09-18) 125 | ------------------ 126 | * Fix install rules and dashing changes (`#38 `_) 127 | * fix ament indexing 128 | * fix package resource files 129 | * add tk depenndency 130 | * add check for param index-ability 131 | * data files are now package agnostic 132 | Signed-off-by: Ted Kern 133 | * Contributors: Ted Kern 134 | 135 | 1.0.0 (2019-09-10) 136 | ------------------ 137 | * ROS2 port (`#35 `_) 138 | * key_teleop pkg format 3 139 | * port teleop_tools_msgs 140 | * key_teleop catch KeyboardInterrupt 141 | * port mouse_teleop 142 | * add key_teleop.yaml 143 | * add xmllint test 144 | * fix xmllint tests 145 | * remove useless class KeyTeleop 146 | * Fixes for dynamic topic joy publishers 147 | - match_command() now compares button array length to the max 148 | deadman button index (apples to apples) 149 | - match_command function now checks if any of the deadman buttons 150 | are depressed before returning a match 151 | - properly handle a std_msgs/msg/Empty 'message_value' by not 152 | attempting to access its value 153 | - utilizes iter-items to correctly index into the config dict 154 | for 'axis_mappings''s 'axis' and 'button' values 155 | - set_member() now splits according to a dash (-) rather than a 156 | periond (.) to be consistent with ros2 param parsing & example yaml 157 | - adds the correct name to setup.py for test_key_teleop.py test 158 | * reduce copy/pasta 159 | * Contributors: Jeremie Deray 160 | 161 | 0.3.0 (2019-01-03) 162 | ------------------ 163 | * Fill in the timestamp of outgoing messages, if applicable. 164 | * add service example 165 | * Add option for persistent service, defaulted false 166 | * Contributors: AndyZe, Jeremie Deray, Bence Magyar 167 | 168 | 0.2.6 (2018-04-06) 169 | ------------------ 170 | * Support using buttons and axis in the same message 171 | * Contributors: Tim Clephas 172 | 173 | 0.2.5 (2017-04-21) 174 | ------------------ 175 | * Remove duplicate examples, add list ones 176 | * Contributors: Bence Magyar 177 | 178 | 0.2.4 (2016-11-30) 179 | ------------------ 180 | * Replace joy_teleop.fill_msg with genpy.message.fill_message_args 181 | * Contributors: Stephen Street 182 | 183 | 0.2.3 (2016-07-18) 184 | ------------------ 185 | * Add hello publish to example 186 | * Rename to fix example launch file 187 | * Added example of feature to config file 188 | * Added message_value parameter to specify message content on topics 189 | * PEP8 style stuff 190 | * Fixes bug when keep asking for increments 191 | would make the goal position grow infinitely instead of be of maximum 'current joint position' + 'increment quantity' 192 | * Contributors: Bence Magyar, Sam Pfeiffer, SomeshDaga 193 | 194 | 0.2.2 (2016-03-24) 195 | ------------------ 196 | * Add install rules for example files 197 | * gracefully handle missing joy axes 198 | * Contributors: Bence Magyar, Kopias Peter 199 | 200 | 0.2.1 (2016-01-29) 201 | ------------------ 202 | * Add support for services 203 | it is now possible to asynchronously send service requests on button presses 204 | * Adds queue_size keyword 205 | * Contributors: Bence Magyar, Nils Berg, Enrique Fernandez 206 | 207 | 0.2.0 (2015-08-03) 208 | ------------------ 209 | * Add example for incrementer 210 | * Update package.xmls 211 | * Add incrementer_server 212 | * Contributors: Bence Magyar 213 | 214 | 0.1.2 (2015-02-15) 215 | ------------------ 216 | * joy_teleop: fix minor typo 217 | * Contributors: G.A. vd. Hoorn 218 | 219 | 0.1.1 (2014-11-17) 220 | ------------------ 221 | * Change maintainer 222 | * checks for index out of bounds in buttons list 223 | `buttons` is a list, not a dict 224 | Filter out buttons not available 225 | * Check for b in buttons 226 | * Check for IndexError 227 | * joy_teleop: add action server auto-refresh 228 | * Move everything to joy_teleop subfolder 229 | * Contributors: Bence Magyar, Enrique Fernández Perdomo, Paul Mathieu 230 | 231 | 0.1.0 (2013-11-28) 232 | ------------------ 233 | * joy_teleop: nice, generic joystick control for ROS 234 | -------------------------------------------------------------------------------- /joy_teleop/README.md: -------------------------------------------------------------------------------- 1 | joy_teleop 2 | ========== 3 | 4 | A configurable node to map joystick controls to robot teleoperation commands 5 | -------------------------------------------------------------------------------- /joy_teleop/config/joy_teleop_example.yaml: -------------------------------------------------------------------------------- 1 | joy_teleop: 2 | ros__parameters: 3 | 4 | walk: 5 | type: topic 6 | interface_type: geometry_msgs/msg/TwistStamped 7 | topic_name: cmd_vel 8 | deadman_buttons: [4] 9 | axis_mappings: 10 | linear-x: 11 | axis: 1 12 | scale: 0.5 13 | offset: -0.03 14 | angular-z: 15 | axis: 0 16 | scale: 0.5 17 | offset: 0 18 | linear-y: 19 | axis: 2 20 | scale: 0.3 21 | offset: 0 22 | linear-z: 23 | button: 2 24 | scale: 3.0 25 | header-frame_id: 26 | value: 'my_tf_frame' 27 | 28 | force_push: 29 | type: topic 30 | interface_type: geometry_msgs/msg/Wrench 31 | topic_name: base_link_wrench 32 | deadman_buttons: [5, 7] 33 | axis_mappings: 34 | force-x: 35 | axis: 3 36 | scale: 40 37 | offset: 0 38 | force-y: 39 | axis: 2 40 | scale: 40 41 | offset: 0 42 | 43 | stop: 44 | type: topic 45 | interface_type: geometry_msgs/msg/Twist 46 | topic_name: cmd_vel 47 | deadman_buttons: [0, 2] 48 | message_value: 49 | linear-x: 50 | value: 0.0 51 | angular-z: 52 | value: 0.0 53 | linear-y: 54 | value: 0.0 55 | 56 | hello: 57 | type: topic 58 | interface_type: std_msgs/msg/String 59 | topic_name: chatter 60 | deadman_buttons: [2] 61 | message_value: 62 | data: 63 | value: 'Hello' 64 | 65 | array: 66 | type: topic 67 | interface_type: std_msgs/msg/UInt8MultiArray 68 | topic_name: bytes 69 | deadman_buttons: [5] 70 | message_value: 71 | data: 72 | value: [1,3,3,7] 73 | 74 | array2: 75 | type: topic 76 | interface_type: std_msgs/msg/UInt8MultiArray 77 | topic_name: bytes 78 | deadman_buttons: [3] 79 | message_value: 80 | data: 81 | value: 82 | - 4 83 | - 2 84 | 85 | array3: 86 | type: topic 87 | interface_type: std_msgs/msg/UInt8MultiArray 88 | topic_name: array3 89 | deadman_buttons: [0] 90 | axis_mappings: 91 | data: 92 | index: 0 93 | axis: 0 94 | scale: 1 95 | offset: 0 96 | # leading dash are going to be striped away, but this allows us to have the same 97 | # field name (yaml doesn't allow duplicate keys) 98 | data-: 99 | index: 1 100 | axis: 1 101 | scale: 1 102 | offset: 0 103 | data--: 104 | index: 4 105 | axis: 2 106 | scale: 1 107 | offset: 0 108 | 109 | torso_up: 110 | type: action 111 | interface_type: teleop_tools_msgs/action/Increment 112 | action_name: torso_controller/increment 113 | action_goal: 114 | increment_by: [0.05] 115 | buttons: [4] 116 | 117 | torso_down: 118 | type: action 119 | interface_type: teleop_tools_msgs/action/Increment 120 | action_name: torso_controller/increment 121 | action_goal: 122 | increment_by: [-0.05] 123 | buttons: [6] 124 | 125 | fibonacci: 126 | type: action 127 | interface_type: example_interfaces/action/Fibonacci 128 | action_name: fibonacci 129 | action_goal: 130 | order: 5 131 | buttons: [4, 5, 6, 7] 132 | 133 | add_two_ints: 134 | type: service 135 | interface_type: example_interfaces/srv/AddTwoInts 136 | service_name: add_two_ints 137 | service_request: 138 | a: 11 139 | b: 31 140 | buttons: [10] 141 | -------------------------------------------------------------------------------- /joy_teleop/joy_teleop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-teleop/teleop_tools/d162759de377d0afd2e62420fa89c2d1127f2fee/joy_teleop/joy_teleop/__init__.py -------------------------------------------------------------------------------- /joy_teleop/joy_teleop/incrementer_server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2019 PAL Robotics SL. 5 | # All rights reserved. 6 | # 7 | # Software License Agreement (BSD License 2.0) 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions 11 | # are met: 12 | # 13 | # * Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # * Redistributions in binary form must reproduce the above 16 | # copyright notice, this list of conditions and the following 17 | # disclaimer in the documentation and/or other materials provided 18 | # with the distribution. 19 | # * Neither the name of PAL Robotics SL. nor the names of its 20 | # contributors may be used to endorse or promote products derived 21 | # from this software without specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 29 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 33 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | # POSSIBILITY OF SUCH DAMAGE. 35 | # 36 | # 37 | # Authors: 38 | # * Bence Magyar 39 | # * Sam Pfeiffer 40 | # * Jeremie Deray (artivis) 41 | # * Borong Yuan 42 | 43 | 44 | from control_msgs.msg import JointTrajectoryControllerState as JTCS 45 | import rclpy 46 | from rclpy.action import ActionServer 47 | from rclpy.duration import Duration 48 | from rclpy.node import Node 49 | from rclpy.wait_for_message import wait_for_message 50 | from teleop_tools_msgs.action import Increment as TTIA 51 | from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint 52 | 53 | 54 | class IncrementerServer(Node): 55 | 56 | def __init__(self): 57 | super().__init__('incrementer_server', namespace='joint_trajectory_controller') 58 | 59 | self._as = ActionServer(self, TTIA, 'increment', 60 | self._as_cb) 61 | 62 | self._command_pub = self.create_publisher( 63 | JointTrajectory, 'joint_trajectory', 1) 64 | 65 | self._goal = JointTrajectory() 66 | self.get_logger().info(f'Connected to {self.get_namespace()}') 67 | 68 | def _as_cb(self, goal): 69 | self.increment_by(goal.request.increment_by) 70 | goal.succeed() 71 | return TTIA.Result() 72 | 73 | def _wait_for_state_message(self): 74 | msg_ok = False 75 | while not msg_ok: 76 | msg_ok, state = wait_for_message(JTCS, self, 'controller_state') 77 | return state 78 | 79 | def increment_by(self, increment): 80 | state = self._wait_for_state_message() 81 | self._goal.joint_names = state.joint_names 82 | self._value = state.feedback.positions 83 | self._value = [x + y for x, y in zip(self._value, increment)] 84 | self.get_logger().info('Sent goal of {}'.format(self._value)) 85 | point = JointTrajectoryPoint() 86 | point.positions = self._value 87 | point.time_from_start = Duration(seconds=0.1).to_msg() 88 | self._goal.points = [point] 89 | self._command_pub.publish(self._goal) 90 | 91 | 92 | def main(): 93 | rclpy.init() 94 | 95 | node = IncrementerServer() 96 | 97 | try: 98 | rclpy.spin(node) 99 | except KeyboardInterrupt: 100 | pass 101 | 102 | node.destroy_node() 103 | rclpy.shutdown() 104 | 105 | 106 | if __name__ == '__main__': 107 | main() 108 | -------------------------------------------------------------------------------- /joy_teleop/joy_teleop/joy_teleop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2019 PAL Robotics SL. 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of PAL Robotics SL. nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | import array 36 | import importlib 37 | import typing 38 | 39 | import rclpy 40 | from rclpy.action import ActionClient 41 | from rclpy.node import Node 42 | from rclpy.parameter import PARAMETER_SEPARATOR_STRING 43 | from rosidl_runtime_py import set_message_fields 44 | import sensor_msgs.msg 45 | 46 | 47 | class JoyTeleopException(Exception): 48 | pass 49 | 50 | 51 | def get_interface_type(type_name: str, interface_type: str) -> typing.Any: 52 | split = type_name.split('/') 53 | if len(split) != 3: 54 | raise JoyTeleopException("Invalid type_name '{}'".format(type_name)) 55 | package = split[0] 56 | interface = split[1] 57 | message = split[2] 58 | if interface != interface_type: 59 | raise JoyTeleopException("Cannot use interface of type '{}' for an '{}'" 60 | .format(interface, interface_type)) 61 | 62 | mod = importlib.import_module(package + '.' + interface_type) 63 | return getattr(mod, message) 64 | 65 | 66 | def get_parent_member(msg: typing.Any, member: str) -> typing.Tuple[typing.Any, str]: 67 | ml = member.strip('-').split('-') 68 | if len(ml) < 1: 69 | return 70 | target = msg 71 | for i in ml[:-1]: 72 | target = getattr(target, i) 73 | 74 | return target, ml[-1] 75 | 76 | 77 | class JoyTeleopCommand: 78 | 79 | def __init__(self, name: str, config: typing.Dict[str, typing.Any], 80 | button_name: str, axes_name: str) -> None: 81 | self.buttons: typing.List[str] = [] 82 | if button_name in config: 83 | self.buttons = config[button_name] 84 | self.axes: typing.List[str] = [] 85 | if axes_name in config: 86 | self.axes = config[axes_name] 87 | 88 | if len(self.buttons) == 0 and len(self.axes) == 0: 89 | raise JoyTeleopException("No buttons or axes configured for command '{}'".format(name)) 90 | 91 | # Used to short-circuit the run command if there aren't enough buttons in the message. 92 | self.min_button = 0 93 | if len(self.buttons) > 0: 94 | self.min_button = int(min(self.buttons)) 95 | self.min_axis = 0 96 | if len(self.axes) > 0: 97 | self.min_axis = int(min(self.axes)) 98 | 99 | # This can be used to "debounce" the message; if there are multiple presses of the buttons 100 | # or axes, the command may only activate on the first one until it toggles again. But this 101 | # is a command-specific behavior, the base class only provides the mechanism. 102 | self.active = False 103 | 104 | def update_active_from_buttons_and_axes(self, joy_state: sensor_msgs.msg.Joy) -> None: 105 | self.active = True 106 | 107 | if (self.min_button is not None and len(joy_state.buttons) <= self.min_button) and \ 108 | (self.min_axis is not None and len(joy_state.axes) <= self.min_axis): 109 | # Not enough buttons or axes, so it can't possibly be a message for this command. 110 | return 111 | 112 | for button in self.buttons: 113 | try: 114 | self.active &= joy_state.buttons[button] == 1 115 | except IndexError: 116 | # An index error can occur if this command is configured for multiple buttons 117 | # like (0, 10), but the length of the joystick buttons is only 1. Ignore these. 118 | pass 119 | 120 | for axis in self.axes: 121 | try: 122 | self.active &= joy_state.axes[axis] == 1.0 123 | except IndexError: 124 | # An index error can occur if this command is configured for multiple buttons 125 | # like (0, 10), but the length of the joystick buttons is only 1. Ignore these. 126 | pass 127 | 128 | 129 | class JoyTeleopTopicCommand(JoyTeleopCommand): 130 | 131 | def __init__(self, name: str, config: typing.Dict[str, typing.Any], node: Node) -> None: 132 | super().__init__(name, config, 'deadman_buttons', 'deadman_axes') 133 | 134 | self.name = name 135 | 136 | self.topic_type = get_interface_type(config['interface_type'], 'msg') 137 | 138 | # A 'message_value' is a fixed message that is sent in response to an activation. It is 139 | # mutually exclusive with an 'axis_mapping'. 140 | self.msg_value = None 141 | if 'message_value' in config: 142 | msg_config = config['message_value'] 143 | 144 | # Construct the fixed message and try to fill it in. This message will be reused 145 | # during runtime, and has the side benefit of giving the user early feedback if the 146 | # config can't work. 147 | self.msg_value = self.topic_type() 148 | for target, param in msg_config.items(): 149 | res = get_parent_member(self.msg_value, target) 150 | if res: 151 | parent, attr_name = res 152 | setattr(parent, attr_name, param['value']) 153 | 154 | # An 'axis_mapping' takes data from one part of the message and scales and offsets it to 155 | # publish if an activation happens. This is typically used to take joystick analog data 156 | # and republish it as a cmd_vel. It is mutually exclusive with a 'message_value'. 157 | self.axis_mappings = {} 158 | if 'axis_mappings' in config: 159 | self.axis_mappings = config['axis_mappings'] 160 | # Now check that the mappings have all of the required configuration. 161 | for mapping, values in self.axis_mappings.items(): 162 | if 'axis' not in values and 'button' not in values and 'value' not in values: 163 | raise JoyTeleopException("Axis mapping for '{}' must have an axis, button, " 164 | 'or value'.format(name)) 165 | 166 | if 'axis' in values: 167 | if 'offset' not in values: 168 | raise JoyTeleopException("Axis mapping for '{}' must have an offset" 169 | .format(name)) 170 | 171 | if 'scale' not in values: 172 | raise JoyTeleopException("Axis mapping for '{}' must have a scale" 173 | .format(name)) 174 | 175 | if self.msg_value is None and not self.axis_mappings: 176 | raise JoyTeleopException("No 'message_value' or 'axis_mappings' " 177 | "configured for command '{}'".format(name)) 178 | if self.msg_value is not None and self.axis_mappings: 179 | raise JoyTeleopException("Only one of 'message_value' or 'axis_mappings' " 180 | "can be configured for command '{}'".format(name)) 181 | 182 | qos = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 183 | depth=1, 184 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE, 185 | durability=rclpy.qos.QoSDurabilityPolicy.VOLATILE) 186 | 187 | self.pub = node.create_publisher(self.topic_type, config['topic_name'], qos) 188 | 189 | def run(self, node: Node, joy_state: sensor_msgs.msg.Joy) -> None: 190 | # The logic for responding to this joystick press is: 191 | # 1. Save off the current state of active. 192 | # 2. Update the current state of active based on buttons and axes. 193 | # 3. If this command is currently not active, return without publishing. 194 | # 4. If this is a msg_value, and the value of the previous active is the same as now, 195 | # debounce and return without publishing. 196 | # 5. In all other cases, publish. This means that this is a msg_value and the button 197 | # transitioned from 0 -> 1, or it means that this is an axis mapping and data should 198 | # continue to be published without debouncing. 199 | 200 | last_active = self.active 201 | self.update_active_from_buttons_and_axes(joy_state) 202 | if not self.active: 203 | return 204 | if self.msg_value is not None and last_active == self.active: 205 | return 206 | 207 | if self.msg_value is not None: 208 | # This is the case for a static message. 209 | msg = self.msg_value 210 | else: 211 | # This is the case to forward along mappings. 212 | msg = self.topic_type() 213 | 214 | for mapping, values in self.axis_mappings.items(): 215 | if 'axis' in values: 216 | if len(joy_state.axes) > values['axis']: 217 | val = joy_state.axes[values['axis']] * values.get('scale', 1.0) + \ 218 | values.get('offset', 0.0) 219 | else: 220 | node.get_logger().error('Joystick has only {} axes (indexed from 0),' 221 | 'but #{} was referenced in config.'.format( 222 | len(joy_state.axes), values['axis'])) 223 | val = 0.0 224 | elif 'button' in values: 225 | if len(joy_state.buttons) > values['button']: 226 | val = joy_state.buttons[values['button']] * values.get('scale', 1.0) + \ 227 | values.get('offset', 0.0) 228 | else: 229 | node.get_logger().error('Joystick has only {} buttons (indexed from 0),' 230 | 'but #{} was referenced in config.'.format( 231 | len(joy_state.buttons), values['button'])) 232 | val = 0.0 233 | elif 'value' in values: 234 | # Pass on the value as its Python-implicit type 235 | val = values.get('value') 236 | else: 237 | node.get_logger().error( 238 | 'No Supported axis_mappings type found in: {}'.format(mapping)) 239 | val = 0.0 240 | 241 | res = get_parent_member(msg, mapping) 242 | if res: 243 | parent, sub_field_name = res 244 | if isinstance(getattr(parent, sub_field_name), (list, array.array)): 245 | index_el = values.get('index', 0) 246 | field_list = getattr(parent, sub_field_name) 247 | while len(field_list) <= index_el: 248 | # complete 249 | field_list.append(0) 250 | if isinstance(field_list, list): 251 | field_list[index_el] = val 252 | else: 253 | # array.array: use first element which has correct type to cast 254 | field_list[index_el] = type(field_list[0])(val) 255 | else: 256 | setattr(parent, sub_field_name, val) 257 | 258 | # If there is a stamp field, fill it with now(). 259 | if hasattr(msg, 'header'): 260 | msg.header.stamp = node.get_clock().now().to_msg() 261 | 262 | self.pub.publish(msg) 263 | 264 | 265 | class JoyTeleopServiceCommand(JoyTeleopCommand): 266 | 267 | def __init__(self, name: str, config: typing.Dict[str, typing.Any], node: Node) -> None: 268 | super().__init__(name, config, 'buttons', 'axes') 269 | 270 | self.name = name 271 | 272 | service_name = config['service_name'] 273 | 274 | service_type = get_interface_type(config['interface_type'], 'srv') 275 | 276 | self.request = service_type.Request() 277 | 278 | if 'service_request' in config: 279 | # Set the message fields in the request in the constructor. This request will be used 280 | # during runtime, and has the side benefit of giving the user early feedback if the 281 | # config can't work. 282 | set_message_fields(self.request, config['service_request']) 283 | 284 | self.service_client = node.create_client(service_type, service_name) 285 | self.client_ready = False 286 | 287 | def run(self, node: Node, joy_state: sensor_msgs.msg.Joy) -> None: 288 | # The logic for responding to this joystick press is: 289 | # 1. Save off the current state of active. 290 | # 2. Update the current state of active. 291 | # 3. If this command is currently not active, return without calling the service. 292 | # 4. Save off the current state of whether the service was ready. 293 | # 5. Update whether the service is ready. 294 | # 6. If the service is not currently ready, return without calling the service. 295 | # 7. If the service was already ready, and the state of the button is the same as before, 296 | # debounce and return without calling the service. 297 | # 8. In all other cases, call the service. This means that either this is a button 298 | # transition 0 -> 1, or that the service became ready since the last call. 299 | 300 | last_active = self.active 301 | self.update_active_from_buttons_and_axes(joy_state) 302 | if not self.active: 303 | return 304 | last_ready = self.client_ready 305 | self.client_ready = self.service_client.service_is_ready() 306 | if not self.client_ready: 307 | return 308 | if last_ready == self.client_ready and last_active == self.active: 309 | return 310 | 311 | self.service_client.call_async(self.request) 312 | 313 | 314 | class JoyTeleopActionCommand(JoyTeleopCommand): 315 | 316 | def __init__(self, name: str, config: typing.Dict[str, typing.Any], node: Node) -> None: 317 | super().__init__(name, config, 'buttons', 'axes') 318 | 319 | self.name = name 320 | 321 | action_type = get_interface_type(config['interface_type'], 'action') 322 | 323 | self.goal = action_type.Goal() 324 | 325 | if 'action_goal' in config: 326 | # Set the message fields for the goal in the constructor. This goal will be used 327 | # during runtime, and has hte side benefit of giving the user early feedback if the 328 | # config can't work. 329 | set_message_fields(self.goal, config['action_goal']) 330 | 331 | action_name = config['action_name'] 332 | 333 | self.action_client = ActionClient(node, action_type, action_name) 334 | self.client_ready = False 335 | 336 | def run(self, node: Node, joy_state: sensor_msgs.msg.Joy) -> None: 337 | # The logic for responding to this joystick press is: 338 | # 1. Save off the current state of active. 339 | # 2. Update the current state of active. 340 | # 3. If this command is currently not active, return without calling the action. 341 | # 4. Save off the current state of whether the action was ready. 342 | # 5. Update whether the action is ready. 343 | # 6. If the action is not currently ready, return without calling the action. 344 | # 7. If the action was already ready, and the state of the button is the same as before, 345 | # debounce and return without calling the action. 346 | # 8. In all other cases, call the action. This means that either this is a button 347 | # transition 0 -> 1, or that the action became ready since the last call. 348 | 349 | last_active = self.active 350 | self.update_active_from_buttons_and_axes(joy_state) 351 | if not self.active: 352 | return 353 | last_ready = self.client_ready 354 | self.client_ready = self.action_client.server_is_ready() 355 | if not self.client_ready: 356 | return 357 | if last_ready == self.client_ready and last_active == self.active: 358 | return 359 | 360 | self.action_client.send_goal_async(self.goal) 361 | 362 | 363 | class JoyTeleop(Node): 364 | """ 365 | Generic joystick teleoperation node. 366 | 367 | Will not start without configuration, has to be stored in 'teleop' parameter. 368 | See config/joy_teleop.yaml for an example. 369 | """ 370 | 371 | def __init__(self): 372 | super().__init__('joy_teleop', allow_undeclared_parameters=True, 373 | automatically_declare_parameters_from_overrides=True) 374 | 375 | self.commands = [] 376 | 377 | names = [] 378 | 379 | for name, config in self.retrieve_config().items(): 380 | if name in names: 381 | raise JoyTeleopException("command '{}' was duplicated".format(name)) 382 | 383 | try: 384 | interface_group = config['type'] 385 | 386 | if interface_group == 'topic': 387 | self.commands.append(JoyTeleopTopicCommand(name, config, self)) 388 | elif interface_group == 'service': 389 | self.commands.append(JoyTeleopServiceCommand(name, config, self)) 390 | elif interface_group == 'action': 391 | self.commands.append(JoyTeleopActionCommand(name, config, self)) 392 | else: 393 | raise JoyTeleopException("unknown type '{interface_group}' " 394 | "for command '{name}'".format_map(locals())) 395 | except TypeError: 396 | # This can happen on parameters we don't control, like 'use_sim_time'. 397 | self.get_logger().debug('parameter {} is not a dict'.format(name)) 398 | 399 | names.append(name) 400 | 401 | # Don't subscribe until everything has been initialized. 402 | qos = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 403 | depth=1, 404 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE, 405 | durability=rclpy.qos.QoSDurabilityPolicy.VOLATILE) 406 | self._subscription = self.create_subscription( 407 | sensor_msgs.msg.Joy, 'joy', self.joy_callback, qos) 408 | 409 | def retrieve_config(self): 410 | config = {} 411 | for param_name in sorted(self._parameters.keys()): 412 | pval = self.get_parameter(param_name).value 413 | self.insert_dict(config, param_name, pval) 414 | return config 415 | 416 | def insert_dict(self, dictionary: typing.Dict[str, typing.Any], key: str, value: str) -> None: 417 | split = key.partition(PARAMETER_SEPARATOR_STRING) 418 | if split[0] == key and split[1] == '' and split[2] == '': 419 | dictionary[key] = value 420 | else: 421 | if not split[0] in dictionary: 422 | dictionary[split[0]] = {} 423 | self.insert_dict(dictionary[split[0]], split[2], value) 424 | 425 | def joy_callback(self, msg: sensor_msgs.msg.Joy) -> None: 426 | for command in self.commands: 427 | command.run(self, msg) 428 | 429 | 430 | def main(args=None): 431 | rclpy.init(args=args) 432 | node = JoyTeleop() 433 | 434 | try: 435 | rclpy.spin(node) 436 | except JoyTeleopException as e: 437 | node.get_logger().error(e.message) 438 | except KeyboardInterrupt: 439 | pass 440 | 441 | node.destroy_node() 442 | rclpy.shutdown() 443 | -------------------------------------------------------------------------------- /joy_teleop/launch/example.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Canonical, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | from ament_index_python.packages import get_package_share_directory 18 | from launch import LaunchDescription 19 | import launch.actions 20 | import launch_ros.actions 21 | 22 | 23 | # Please note that this is only an example! 24 | # It is not guaranteed to work with your setup but can be used as a starting point. 25 | 26 | 27 | def generate_launch_description(): 28 | 29 | parameters_file = os.path.join( 30 | get_package_share_directory('joy_teleop'), 31 | 'config', 'joy_teleop_example.yaml' 32 | ) 33 | 34 | ld = LaunchDescription([ 35 | launch.actions.DeclareLaunchArgument('cmd_vel', default_value='input_joy/cmd_vel'), 36 | launch.actions.DeclareLaunchArgument('teleop_config', default_value=parameters_file), 37 | ]) 38 | 39 | ld.add_action(launch_ros.actions.Node( 40 | package='joy_teleop', executable='joy_teleop', 41 | parameters=[launch.substitutions.LaunchConfiguration('teleop_config')])) 42 | 43 | ld.add_action(launch_ros.actions.Node( 44 | package='joy_teleop', executable='incrementer_server', 45 | name='incrementer', namespace='torso_controller')) 46 | 47 | return ld 48 | -------------------------------------------------------------------------------- /joy_teleop/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | joy_teleop 5 | 2.0.0 6 | A (to be) generic joystick interface to control a robot 7 | 8 | Bence Magyar 9 | BSD 10 | 11 | Paul Mathieu 12 | 13 | control_msgs 14 | rclpy 15 | sensor_msgs 16 | teleop_tools_msgs 17 | trajectory_msgs 18 | rosidl_runtime_py 19 | 20 | ament_copyright 21 | ament_flake8 22 | ament_pep257 23 | ament_xmllint 24 | example_interfaces 25 | geometry_msgs 26 | std_msgs 27 | std_srvs 28 | test_msgs 29 | launch_ros 30 | launch_testing 31 | 32 | 33 | ament_python 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /joy_teleop/resource/joy_teleop: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-teleop/teleop_tools/d162759de377d0afd2e62420fa89c2d1127f2fee/joy_teleop/resource/joy_teleop -------------------------------------------------------------------------------- /joy_teleop/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/joy_teleop 3 | [install] 4 | install_scripts=$base/lib/joy_teleop 5 | -------------------------------------------------------------------------------- /joy_teleop/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | 7 | package_name = 'joy_teleop' 8 | share_path = 'share/' + package_name 9 | 10 | 11 | setup( 12 | name=package_name, 13 | version='1.8.0', 14 | packages=find_packages(exclude=['test']), 15 | data_files=[ 16 | (share_path, ['package.xml']), 17 | (os.path.join(share_path, 'config'), [os.path.join('config', 'joy_teleop_example.yaml')]), 18 | (os.path.join(share_path, 'launch'), [os.path.join('launch', 'example.launch.py')]), 19 | (os.path.join('share', 'ament_index', 'resource_index', 'packages'), 20 | [os.path.join('resource', package_name)]), 21 | ], 22 | install_requires=['setuptools'], 23 | zip_safe=True, 24 | author='Paul Mathieu', 25 | author_email='paul.mathieu@pal-robotics.com', 26 | maintainer='Bence Magyar', 27 | maintainer_email='bence.magyar.robotics@gmail.com', 28 | url='https://github.com/ros-teleop/teleop_tools', 29 | keywords=['ROS'], 30 | classifiers=[ 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD', 33 | 'Programming Language :: Python', 34 | 'Topic :: Software Development', 35 | ], 36 | description='A (to be) generic joystick interface to control a robot.', 37 | long_description="""\ 38 | joy_teleop interfaces a joystick to control/actions sent to a robot. \ 39 | Its flexibility allows to map any joystick button to any message/service/action.""", 40 | license='BSD', 41 | tests_require=['pytest'], 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'joy_teleop = joy_teleop.joy_teleop:main', 45 | 'incrementer_server = joy_teleop.incrementer_server:main', 46 | ], 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /joy_teleop/test/joy_teleop_testing_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | import re 36 | import unittest 37 | 38 | import launch 39 | import launch_ros 40 | import launch_testing 41 | import rclpy 42 | import sensor_msgs.msg 43 | 44 | 45 | def generate_joy_test_description(parameters): 46 | joy_teleop_node = launch_ros.actions.Node( 47 | package='joy_teleop', 48 | executable='joy_teleop', 49 | output='both', 50 | parameters=[parameters]) 51 | 52 | return ( 53 | launch.LaunchDescription([ 54 | joy_teleop_node, 55 | launch_testing.actions.ReadyToTest(), 56 | ]), {} 57 | ) 58 | 59 | 60 | class TestJoyTeleop(unittest.TestCase): 61 | 62 | def publish_message(self): 63 | pass 64 | 65 | def setUp(self): 66 | self.context = rclpy.context.Context() 67 | rclpy.init(context=self.context) 68 | node_name = re.sub('(.)([A-Z]{1})', r'\1_\2', self.__class__.__name__).lower() 69 | self.node = rclpy.create_node(node_name, context=self.context) 70 | self.executor = rclpy.executors.SingleThreadedExecutor(context=self.context) 71 | self.executor.add_node(self.node) 72 | qos = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 73 | depth=1, 74 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE, 75 | durability=rclpy.qos.QoSDurabilityPolicy.VOLATILE) 76 | self.joy_publisher = self.node.create_publisher( 77 | msg_type=sensor_msgs.msg.Joy, 78 | topic='joy', 79 | qos_profile=qos, 80 | ) 81 | 82 | self.joy_msg = sensor_msgs.msg.Joy() 83 | self.joy_msg.header.stamp.sec = 0 84 | self.joy_msg.header.stamp.nanosec = 0 85 | self.joy_msg.header.frame_id = '' 86 | self.joy_msg.axes = [] 87 | self.joy_msg.buttons = [] 88 | 89 | self.publish_timer = self.node.create_timer(0.1, self.publish_message) 90 | 91 | def tearDown(self): 92 | self.node.destroy_timer(self.publish_timer) 93 | self.joy_publisher.destroy() 94 | self.node.destroy_node() 95 | rclpy.shutdown(context=self.context) 96 | -------------------------------------------------------------------------------- /joy_teleop/test/test_action_fibonacci.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | import example_interfaces.action 36 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 37 | import pytest 38 | import rclpy 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['fibonacci.type'] = 'action' 45 | parameters['fibonacci.interface_type'] = 'example_interfaces/action/Fibonacci' 46 | parameters['fibonacci.action_name'] = '/fibonacci' 47 | parameters['fibonacci.buttons'] = [2] 48 | parameters['fibonacci.action_goal'] = {'order': 5} 49 | 50 | return generate_joy_test_description(parameters) 51 | 52 | 53 | class TestJoyTeleopActionFibonacci(TestJoyTeleop): 54 | 55 | def publish_message(self): 56 | self.joy_publisher.publish(self.joy_msg) 57 | self.joy_msg.buttons[2] = int(not self.joy_msg.buttons[2]) 58 | 59 | def test_simple_message(self): 60 | sequence = [] 61 | future = rclpy.task.Future() 62 | 63 | def fibonacci_callback(goal_handle): 64 | nonlocal future 65 | 66 | sequence.append(0) 67 | sequence.append(1) 68 | for i in range(1, goal_handle.request.order): 69 | sequence.append(sequence[i] + sequence[i-1]) 70 | 71 | goal_handle.succeed() 72 | result = example_interfaces.action.Fibonacci.Result() 73 | result.sequence = sequence 74 | future.set_result(True) 75 | return result 76 | 77 | action_server = rclpy.action.ActionServer( 78 | self.node, 79 | example_interfaces.action.Fibonacci, 80 | 'fibonacci', 81 | fibonacci_callback) 82 | 83 | try: 84 | # Above we set the button to be used as '2', so here we set the '2' button active. 85 | self.joy_msg.buttons = [0, 0, 1] 86 | 87 | self.executor.spin_until_future_complete(future, timeout_sec=10) 88 | 89 | # Check 90 | self.assertTrue(future.done() and future.result(), 91 | 'Timed out waiting for action to complete') 92 | self.assertEqual(sequence, [0, 1, 1, 2, 3, 5]) 93 | finally: 94 | # Cleanup 95 | action_server.destroy() 96 | -------------------------------------------------------------------------------- /joy_teleop/test/test_array_indexing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 36 | import pytest 37 | import rclpy 38 | from std_msgs.msg import UInt8MultiArray 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['array3.type'] = 'topic' 45 | parameters['array3.interface_type'] = 'std_msgs/msg/UInt8MultiArray' 46 | parameters['array3.topic_name'] = '/array3' 47 | parameters['array3.deadman_buttons'] = [0] 48 | 49 | parameters['array3.axis_mappings.data.index'] = 0 50 | parameters['array3.axis_mappings.data.axis'] = 0 51 | parameters['array3.axis_mappings.data.scale'] = 1 52 | parameters['array3.axis_mappings.data.offset'] = 0 53 | 54 | parameters['array3.axis_mappings.data-.index'] = 1 55 | parameters['array3.axis_mappings.data-.axis'] = 1 56 | parameters['array3.axis_mappings.data-.scale'] = 1 57 | parameters['array3.axis_mappings.data-.offset'] = 0 58 | 59 | parameters['array3.axis_mappings.data--.index'] = 4 60 | parameters['array3.axis_mappings.data--.axis'] = 2 61 | parameters['array3.axis_mappings.data--.scale'] = 1 62 | parameters['array3.axis_mappings.data--.offset'] = 0 63 | 64 | return generate_joy_test_description(parameters) 65 | 66 | 67 | class ArrayIndexingMappingTestSuite(TestJoyTeleop): 68 | 69 | def publish_message(self): 70 | self.joy_publisher.publish(self.joy_msg) 71 | 72 | def test_array_mapping(self): 73 | array: UInt8MultiArray = None 74 | future = rclpy.task.Future() 75 | 76 | def receive_array(msg): 77 | nonlocal array 78 | nonlocal future 79 | 80 | array = msg 81 | future.set_result(True) 82 | 83 | qos = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 84 | depth=1, 85 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE, 86 | durability=rclpy.qos.QoSDurabilityPolicy.VOLATILE) 87 | 88 | array_subscriber = self.node.create_subscription( 89 | UInt8MultiArray, 90 | '/array3', 91 | receive_array, 92 | qos, 93 | ) 94 | 95 | try: 96 | self.joy_msg.buttons = [1] # deadman button pressed 97 | self.joy_msg.axes = [1.0, 1.0, 1.0] 98 | 99 | self.executor.spin_until_future_complete(future, timeout_sec=10) 100 | 101 | # Check 102 | self.assertTrue(future.done() and future.result(), 103 | 'Timed out waiting for array topic to complete') 104 | self.assertSequenceEqual(array.data, [1, 1, 0, 0, 1]) 105 | 106 | finally: 107 | # Cleanup 108 | self.node.destroy_subscription(array_subscriber) 109 | 110 | 111 | if __name__ == '__main__': 112 | pytest.main() 113 | -------------------------------------------------------------------------------- /joy_teleop/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /joy_teleop/test/test_debounce.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 36 | import pytest 37 | import rclpy 38 | import std_msgs.msg 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['simple_message.type'] = 'topic' 45 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 46 | parameters['simple_message.topic_name'] = '/simple_message_type' 47 | parameters['simple_message.deadman_buttons'] = [2] 48 | parameters['simple_message.message_value.data.value'] = 'button2' 49 | 50 | return generate_joy_test_description(parameters) 51 | 52 | 53 | class TestJoyTeleopDebounce(TestJoyTeleop): 54 | 55 | def publish_message(self): 56 | self.joy_publisher.publish(self.joy_msg) 57 | 58 | def test_debounce(self): 59 | # This test works by having a subscription to the output topic. Every 60 | # time the subscription is received, we increment a counter. There is 61 | # also a timer callback that executes every 0.1 seconds, publishing the 62 | # the 'joy' message with button 2 on. We then let the system run for 63 | # 5 seconds. Due to debouncing, we should only ever receive one 64 | # subscription callback in that time. 65 | 66 | num_received_messages = 0 67 | 68 | def receive_simple_message(msg): 69 | nonlocal num_received_messages 70 | num_received_messages += 1 71 | 72 | simple_message_subscriber = self.node.create_subscription( 73 | std_msgs.msg.String, 74 | '/simple_message_type', 75 | receive_simple_message, 76 | 1, 77 | ) 78 | 79 | try: 80 | self.joy_msg.buttons = [0, 0, 1] 81 | 82 | # This spin will *always* wait the full timeout. That's because we want 83 | # to ensure that no matter how many times the button is pressed, the 84 | # debouncing only lets a single message through. 85 | self.executor.spin_until_future_complete(rclpy.task.Future(), timeout_sec=5) 86 | 87 | # Check 88 | self.assertEqual(num_received_messages, 1) 89 | finally: 90 | # Cleanup 91 | self.node.destroy_subscription(simple_message_subscriber) 92 | -------------------------------------------------------------------------------- /joy_teleop/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /joy_teleop/test/test_get_interface_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | from example_interfaces.action import Fibonacci 36 | from joy_teleop.joy_teleop import get_interface_type 37 | from joy_teleop.joy_teleop import JoyTeleopException 38 | 39 | import pytest 40 | 41 | import sensor_msgs.msg 42 | import std_srvs.srv 43 | 44 | 45 | def test_message(): 46 | interface_type = get_interface_type('sensor_msgs/msg/Joy', 'msg') 47 | msg = interface_type() 48 | assert isinstance(msg, sensor_msgs.msg.Joy) 49 | 50 | 51 | def test_service(): 52 | interface_type = get_interface_type('std_srvs/srv/Trigger', 'srv') 53 | srv = interface_type.Request() 54 | assert isinstance(srv, std_srvs.srv.Trigger.Request) 55 | 56 | 57 | def test_action(): 58 | interface_type = get_interface_type('example_interfaces/action/Fibonacci', 'action') 59 | action = interface_type.Goal() 60 | assert isinstance(action, Fibonacci.Goal) 61 | 62 | 63 | def test_bad_message(): 64 | with pytest.raises(JoyTeleopException): 65 | get_interface_type('sensor_msgs/msg', 'msg') 66 | 67 | 68 | def test_invalid_type(): 69 | with pytest.raises(JoyTeleopException): 70 | get_interface_type('sensor_msgs/msg/Joy', 'ms') 71 | 72 | 73 | def test_bad_import(): 74 | with pytest.raises(ModuleNotFoundError): 75 | get_interface_type('foo_msgs/msg/Bar', 'msg') 76 | -------------------------------------------------------------------------------- /joy_teleop/test/test_multi_button_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2023 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | import example_interfaces.srv 36 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 37 | import pytest 38 | import rclpy 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['addtwoints.type'] = 'service' 45 | parameters['addtwoints.interface_type'] = 'example_interfaces/srv/AddTwoInts' 46 | parameters['addtwoints.service_name'] = '/addtwoints' 47 | parameters['addtwoints.buttons'] = [0, 4] 48 | parameters['addtwoints.service_request.a'] = 6 49 | parameters['addtwoints.service_request.b'] = 1 50 | 51 | return generate_joy_test_description(parameters) 52 | 53 | 54 | class TestJoyTeleopServiceAddTwoInts(TestJoyTeleop): 55 | 56 | def publish_message(self): 57 | self.joy_publisher.publish(self.joy_msg) 58 | 59 | def test_addtwoints_service(self): 60 | service_result = None 61 | future = rclpy.task.Future() 62 | 63 | def addtwoints(request, response): 64 | nonlocal service_result 65 | nonlocal future 66 | 67 | service_result = request.a + request.b 68 | response.sum = service_result 69 | future.set_result(True) 70 | 71 | return response 72 | 73 | srv = self.node.create_service(example_interfaces.srv.AddTwoInts, '/addtwoints', 74 | addtwoints) 75 | 76 | try: 77 | # Above we set the buttons to be used as '0' and '4', 78 | # so here we set ONLY the '4' button active. 79 | self.joy_msg.buttons = [0, 0, 0, 0, 1] 80 | 81 | self.executor.spin_until_future_complete(future, timeout_sec=10) 82 | 83 | # Check 84 | self.assertFalse(future.done() and future.result(), 85 | 'Completed addtwoints service call but timeout expected') 86 | 87 | # Above we set the buttons to be used as '0' and '4', 88 | # so here we set ONLY the '0' button active. 89 | self.joy_msg.buttons = [1, 0, 0, 0, 0] 90 | 91 | self.executor.spin_until_future_complete(future, timeout_sec=10) 92 | 93 | # Check 94 | self.assertFalse(future.done() and future.result(), 95 | 'Completed addtwoints service call but timeout expected') 96 | 97 | # Above we set the buttons to be used as '0' and '4', 98 | # so here we set BOTH buttons active. 99 | self.joy_msg.buttons = [1, 0, 0, 0, 1] 100 | 101 | self.executor.spin_until_future_complete(future, timeout_sec=10) 102 | 103 | # Check 104 | self.assertTrue(future.done() and future.result(), 105 | 'Timed out waiting for addtwoints service to complete') 106 | self.assertEqual(service_result, 7) 107 | finally: 108 | # Cleanup 109 | self.node.destroy_service(srv) 110 | -------------------------------------------------------------------------------- /joy_teleop/test/test_parameter_failures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | import contextlib 36 | import unittest 37 | 38 | import launch 39 | import launch_ros 40 | import launch_testing 41 | import launch_testing.markers 42 | import pytest 43 | 44 | 45 | @pytest.mark.rostest 46 | @launch_testing.markers.keep_alive 47 | def generate_test_description(): 48 | return launch.LaunchDescription([launch_testing.actions.ReadyToTest()]) 49 | 50 | 51 | class TestJoyTeleopParameterFailures(unittest.TestCase): 52 | 53 | @classmethod 54 | def setUpClass(cls, launch_service, proc_info, proc_output): 55 | @contextlib.contextmanager 56 | def launch_joy_teleop(self, parameters): 57 | joy_teleop_node = launch_ros.actions.Node( 58 | package='joy_teleop', 59 | executable='joy_teleop', 60 | output='both', 61 | parameters=[parameters]) 62 | 63 | with launch_testing.tools.launch_process( 64 | launch_service, joy_teleop_node, proc_info, proc_output) as joy_teleop: 65 | yield joy_teleop 66 | 67 | cls.launch_joy_teleop = launch_joy_teleop 68 | 69 | def test_missing_type(self): 70 | parameters = {} 71 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 72 | parameters['simple_message.topic_name'] = '/simple_message_type' 73 | parameters['simple_message.message_value.data.value'] = 'button2' 74 | 75 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 76 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 77 | 78 | self.assertEqual(joy_teleop_process.exit_code, 1) 79 | self.assertTrue("KeyError: 'type'" in joy_teleop_process.output) 80 | 81 | def test_invalid_type(self): 82 | parameters = {} 83 | parameters['simple_message.type'] = 'foo' 84 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 85 | parameters['simple_message.topic_name'] = '/simple_message_type' 86 | parameters['simple_message.message_value.data.value'] = 'button2' 87 | 88 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 89 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 90 | 91 | self.assertEqual(joy_teleop_process.exit_code, 1) 92 | self.assertTrue('JoyTeleopException: unknown type' in joy_teleop_process.output) 93 | 94 | def test_no_buttons_or_axes(self): 95 | parameters = {} 96 | parameters['simple_message.type'] = 'topic' 97 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 98 | parameters['simple_message.topic_name'] = '/simple_message_type' 99 | parameters['simple_message.message_value.data.value'] = 'button2' 100 | 101 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 102 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 103 | 104 | self.assertEqual(joy_teleop_process.exit_code, 1) 105 | self.assertTrue('JoyTeleopException: No buttons or axes configured for command' 106 | in joy_teleop_process.output) 107 | 108 | def test_teleop_no_message_value_or_axis_mappings(self): 109 | parameters = {} 110 | parameters['simple_message.type'] = 'topic' 111 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 112 | parameters['simple_message.topic_name'] = '/simple_message_type' 113 | parameters['simple_message.deadman_buttons'] = [2] 114 | 115 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 116 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 117 | 118 | self.assertEqual(joy_teleop_process.exit_code, 1) 119 | self.assertTrue("JoyTeleopException: No 'message_value' or 'axis_mappings' configured " 120 | 'for command' in joy_teleop_process.output) 121 | 122 | def test_teleop_both_message_value_and_axis_mappings(self): 123 | parameters = {} 124 | parameters['simple_message.type'] = 'topic' 125 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 126 | parameters['simple_message.topic_name'] = '/simple_message_type' 127 | parameters['simple_message.deadman_buttons'] = [2] 128 | parameters['simple_message.message_value.data.value'] = 'button2' 129 | parameters['simple_message.axis_mappings.linear-x.axis'] = 1 130 | parameters['simple_message.axis_mappings.linear-x.scale'] = 0.8 131 | parameters['simple_message.axis_mappings.linear-x.offset'] = 0.0 132 | 133 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 134 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 135 | 136 | self.assertEqual(joy_teleop_process.exit_code, 1) 137 | self.assertTrue("JoyTeleopException: Only one of 'message_value' or 'axis_mappings' " 138 | 'can be configured for command' in joy_teleop_process.output) 139 | 140 | def test_teleop_axis_mappings_missing_axis_or_button(self): 141 | parameters = {} 142 | parameters['simple_message.type'] = 'topic' 143 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 144 | parameters['simple_message.topic_name'] = '/simple_message_type' 145 | parameters['simple_message.deadman_buttons'] = [2] 146 | parameters['simple_message.axis_mappings.linear-x.scale'] = 0.8 147 | parameters['simple_message.axis_mappings.linear-x.offset'] = 0.0 148 | 149 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 150 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 151 | 152 | self.assertEqual(joy_teleop_process.exit_code, 1) 153 | self.assertTrue('must have an axis, button, or value' in joy_teleop_process.output) 154 | 155 | def test_teleop_axis_mappings_missing_offset(self): 156 | parameters = {} 157 | parameters['simple_message.type'] = 'topic' 158 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 159 | parameters['simple_message.topic_name'] = '/simple_message_type' 160 | parameters['simple_message.deadman_buttons'] = [2] 161 | parameters['simple_message.axis_mappings.linear-x.axis'] = 1 162 | parameters['simple_message.axis_mappings.linear-x.scale'] = 0.8 163 | 164 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 165 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 166 | 167 | self.assertEqual(joy_teleop_process.exit_code, 1) 168 | self.assertTrue('must have an offset' in joy_teleop_process.output) 169 | 170 | def test_teleop_axis_mappings_missing_scale(self): 171 | parameters = {} 172 | parameters['simple_message.type'] = 'topic' 173 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 174 | parameters['simple_message.topic_name'] = '/simple_message_type' 175 | parameters['simple_message.deadman_buttons'] = [2] 176 | parameters['simple_message.axis_mappings.linear-x.axis'] = 1 177 | parameters['simple_message.axis_mappings.linear-x.offset'] = 0.0 178 | 179 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 180 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 181 | 182 | self.assertEqual(joy_teleop_process.exit_code, 1) 183 | self.assertTrue('must have a scale' in joy_teleop_process.output) 184 | 185 | def test_teleop_invalid_message_fields(self): 186 | parameters = {} 187 | parameters['simple_message.type'] = 'topic' 188 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 189 | parameters['simple_message.topic_name'] = '/simple_message_type' 190 | parameters['simple_message.deadman_buttons'] = [2] 191 | parameters['simple_message.message_value.foo.value'] = 'button2' 192 | 193 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 194 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 195 | 196 | self.assertEqual(joy_teleop_process.exit_code, 1) 197 | self.assertTrue("AttributeError: 'String' object has no attribute 'foo'" 198 | in joy_teleop_process.output) 199 | 200 | def test_service_invalid_message_fields(self): 201 | parameters = {} 202 | parameters['trigger.type'] = 'service' 203 | parameters['trigger.interface_type'] = 'std_srvs/srv/Trigger' 204 | parameters['trigger.service_name'] = '/trigger' 205 | parameters['trigger.buttons'] = [4] 206 | parameters['trigger.service_request.foo'] = 5 207 | 208 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 209 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 210 | 211 | self.assertEqual(joy_teleop_process.exit_code, 1) 212 | self.assertTrue("AttributeError: 'Trigger_Request' object has no attribute 'foo'" 213 | in joy_teleop_process.output) 214 | 215 | def test_action_invalid_message_fields(self): 216 | parameters = {} 217 | parameters['fibonacci.type'] = 'action' 218 | parameters['fibonacci.interface_type'] = 'example_interfaces/action/Fibonacci' 219 | parameters['fibonacci.action_name'] = '/fibonacci' 220 | parameters['fibonacci.buttons'] = [2] 221 | parameters['fibonacci.action_goal'] = {'foo': 5} 222 | 223 | with self.launch_joy_teleop(parameters) as joy_teleop_process: 224 | self.assertTrue(joy_teleop_process.wait_for_shutdown(timeout=10)) 225 | 226 | self.assertEqual(joy_teleop_process.exit_code, 1) 227 | self.assertTrue("AttributeError: 'Fibonacci_Goal' object has no attribute 'foo'" 228 | in joy_teleop_process.output) 229 | -------------------------------------------------------------------------------- /joy_teleop/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=['.']) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /joy_teleop/test/test_service_add_two_ints.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | import example_interfaces.srv 36 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 37 | import pytest 38 | import rclpy 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['addtwoints.type'] = 'service' 45 | parameters['addtwoints.interface_type'] = 'example_interfaces/srv/AddTwoInts' 46 | parameters['addtwoints.service_name'] = '/addtwoints' 47 | parameters['addtwoints.buttons'] = [4] 48 | parameters['addtwoints.service_request.a'] = 5 49 | parameters['addtwoints.service_request.b'] = 4 50 | 51 | return generate_joy_test_description(parameters) 52 | 53 | 54 | class TestJoyTeleopServiceAddTwoInts(TestJoyTeleop): 55 | 56 | def publish_message(self): 57 | self.joy_publisher.publish(self.joy_msg) 58 | self.joy_msg.buttons[4] = int(not self.joy_msg.buttons[4]) 59 | 60 | def test_addtwoints_service(self): 61 | service_result = None 62 | future = rclpy.task.Future() 63 | 64 | def addtwoints(request, response): 65 | nonlocal service_result 66 | nonlocal future 67 | 68 | service_result = request.a + request.b 69 | response.sum = service_result 70 | future.set_result(True) 71 | 72 | return response 73 | 74 | srv = self.node.create_service(example_interfaces.srv.AddTwoInts, '/addtwoints', 75 | addtwoints) 76 | 77 | try: 78 | # Above we set the button to be used as '4', so here we set the '4' button active. 79 | self.joy_msg.buttons = [0, 0, 0, 0, 1] 80 | 81 | self.executor.spin_until_future_complete(future, timeout_sec=10) 82 | 83 | # Check 84 | self.assertTrue(future.done() and future.result(), 85 | 'Timed out waiting for addtwoints service to complete') 86 | self.assertEqual(service_result, 9) 87 | finally: 88 | # Cleanup 89 | self.node.destroy_service(srv) 90 | -------------------------------------------------------------------------------- /joy_teleop/test/test_service_not_ready.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 36 | import pytest 37 | import rclpy 38 | import std_msgs.msg 39 | import std_srvs.srv 40 | 41 | 42 | @pytest.mark.rostest 43 | def generate_test_description(): 44 | parameters = {} 45 | parameters['trigger.type'] = 'service' 46 | parameters['trigger.interface_type'] = 'std_srvs/srv/Trigger' 47 | parameters['trigger.service_name'] = '/trigger' 48 | parameters['trigger.buttons'] = [4] 49 | parameters['simple_message.type'] = 'topic' 50 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 51 | parameters['simple_message.topic_name'] = '/simple_message_type' 52 | parameters['simple_message.deadman_buttons'] = [2] 53 | parameters['simple_message.message_value.data.value'] = 'button2' 54 | 55 | return generate_joy_test_description(parameters) 56 | 57 | 58 | class TestJoyTeleopServiceNotReady(TestJoyTeleop): 59 | 60 | def setUp(self): 61 | self.toggle = True 62 | super().setUp() 63 | 64 | def publish_message(self): 65 | self.joy_publisher.publish(self.joy_msg) 66 | if self.toggle: 67 | self.joy_msg.buttons[2] = int(not self.joy_msg.buttons[2]) 68 | 69 | def test_not_ready_service(self): 70 | # The point of this test is to ensure that joy_teleop can attach to 71 | # services that come up *after* it has come up. The way it works is 72 | # that we start off only adding our subscriber to the topic. We then 73 | # start publishing joystick data. Once the simple topic fires once 74 | # (signalling that joy_teleop is up and ready to work), we then create 75 | # the service, and try a trigger. 76 | 77 | simple_message = None 78 | future = rclpy.task.Future() 79 | 80 | def receive_simple_message(msg): 81 | nonlocal simple_message 82 | nonlocal future 83 | simple_message = msg.data 84 | future.set_result(True) 85 | 86 | qos = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 87 | depth=1, 88 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE, 89 | durability=rclpy.qos.QoSDurabilityPolicy.VOLATILE) 90 | 91 | simple_message_subscriber = self.node.create_subscription( 92 | std_msgs.msg.String, 93 | '/simple_message_type', 94 | receive_simple_message, 95 | qos, 96 | ) 97 | 98 | try: 99 | # Above we set the button to be used as '2', so here we set the '2' button active. 100 | # We also set the '4' button for the service trigger; this shouldn't do anything 101 | # right now, but ensures that we test the not active -> active logic in joy_teleop. 102 | self.joy_msg.buttons = [0, 0, 1, 0, 1] 103 | 104 | self.executor.spin_until_future_complete(future, timeout_sec=10) 105 | 106 | # Check 107 | assert future.done() and future.result(), \ 108 | 'Timed out waiting for simple_message topic to complete' 109 | self.assertTrue(simple_message == 'button2') 110 | 111 | # If we've made it to this point with no assertions, then we are ready 112 | # to setup the service and trigger. 113 | 114 | saw_trigger = False 115 | future = rclpy.task.Future() 116 | 117 | def trigger(request, response): 118 | nonlocal saw_trigger 119 | nonlocal future 120 | response.success = True 121 | response.message = 'service_response' 122 | saw_trigger = True 123 | future.set_result(True) 124 | 125 | return response 126 | 127 | self.toggle = False 128 | 129 | srv = self.node.create_service(std_srvs.srv.Trigger, '/trigger', trigger) 130 | 131 | try: 132 | self.executor.spin_until_future_complete(future, timeout_sec=10) 133 | 134 | # Check 135 | self.assertTrue(future.done() and future.result(), 136 | 'Timed out waiting for trigger service to complete') 137 | self.assertTrue(saw_trigger) 138 | finally: 139 | # Cleanup 140 | self.node.destroy_service(srv) 141 | finally: 142 | self.node.destroy_subscription(simple_message_subscriber) 143 | -------------------------------------------------------------------------------- /joy_teleop/test/test_service_trigger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 36 | import pytest 37 | import rclpy 38 | import std_srvs.srv 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['trigger.type'] = 'service' 45 | parameters['trigger.interface_type'] = 'std_srvs/srv/Trigger' 46 | parameters['trigger.service_name'] = '/trigger' 47 | parameters['trigger.buttons'] = [4] 48 | 49 | return generate_joy_test_description(parameters) 50 | 51 | 52 | class TestJoyTeleopServiceTrigger(TestJoyTeleop): 53 | 54 | def publish_message(self): 55 | self.joy_publisher.publish(self.joy_msg) 56 | self.joy_msg.buttons[4] = int(not self.joy_msg.buttons[4]) 57 | 58 | def test_trigger_service(self): 59 | saw_trigger = False 60 | future = rclpy.task.Future() 61 | 62 | def trigger(request, response): 63 | nonlocal saw_trigger 64 | nonlocal future 65 | response.success = True 66 | response.message = 'service_response' 67 | saw_trigger = True 68 | future.set_result(True) 69 | 70 | return response 71 | 72 | srv = self.node.create_service(std_srvs.srv.Trigger, '/trigger', trigger) 73 | 74 | try: 75 | # Above we set the button to be used as '4', so here we set the '4' button active. 76 | self.joy_msg.buttons = [0, 0, 0, 0, 1] 77 | 78 | self.executor.spin_until_future_complete(future, timeout_sec=10) 79 | 80 | # Check 81 | self.assertTrue(future.done() and future.result(), 82 | 'Timed out waiting for trigger service to complete') 83 | self.assertTrue(saw_trigger) 84 | finally: 85 | # Cleanup 86 | self.node.destroy_service(srv) 87 | -------------------------------------------------------------------------------- /joy_teleop/test/test_topic_axis_mappings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | import geometry_msgs.msg 36 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 37 | import pytest 38 | import rclpy 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['twist.type'] = 'topic' 45 | parameters['twist.interface_type'] = 'geometry_msgs/msg/Twist' 46 | parameters['twist.topic_name'] = '/cmd_vel' 47 | parameters['twist.deadman_buttons'] = [2] 48 | parameters['twist.axis_mappings.linear-x.axis'] = 1 49 | parameters['twist.axis_mappings.linear-x.scale'] = 0.8 50 | parameters['twist.axis_mappings.linear-x.offset'] = 0.0 51 | parameters['twist.axis_mappings.angular-z.axis'] = 3 52 | parameters['twist.axis_mappings.angular-z.scale'] = 0.5 53 | parameters['twist.axis_mappings.angular-z.offset'] = 0.0 54 | 55 | return generate_joy_test_description(parameters) 56 | 57 | 58 | class TestJoyTeleopTopicAxisMappings(TestJoyTeleop): 59 | 60 | def setUp(self): 61 | self.publish_from_timer = True 62 | super().setUp() 63 | 64 | def publish_message(self): 65 | if self.publish_from_timer: 66 | self.joy_publisher.publish(self.joy_msg) 67 | self.joy_msg.buttons[2] = int(not self.joy_msg.buttons[2]) 68 | 69 | def test_twist(self): 70 | twist = None 71 | num_received_twists = 0 72 | future = rclpy.task.Future() 73 | 74 | def receive_twist(msg): 75 | nonlocal twist 76 | nonlocal num_received_twists 77 | nonlocal future 78 | 79 | twist = msg 80 | num_received_twists += 1 81 | future.set_result(True) 82 | 83 | qos = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 84 | depth=1, 85 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE, 86 | durability=rclpy.qos.QoSDurabilityPolicy.VOLATILE) 87 | 88 | twist_subscriber = self.node.create_subscription( 89 | geometry_msgs.msg.Twist, 90 | '/cmd_vel', 91 | receive_twist, 92 | qos, 93 | ) 94 | 95 | try: 96 | self.joy_msg.axes = [0.0, 1.0, 0.0, 1.0] 97 | # Above we set the button to be used as '2', so here we set the '2' button active. 98 | self.joy_msg.buttons = [0, 0, 1] 99 | 100 | self.executor.spin_until_future_complete(future, timeout_sec=10) 101 | 102 | # Check 103 | self.assertTrue(future.done() and future.result(), 104 | 'Timed out waiting for twist topic to complete') 105 | self.assertEqual(twist.linear.x, 0.8) 106 | self.assertEqual(twist.angular.z, 0.5) 107 | 108 | # We got one output above. Now publish a known number of messages, spinning in between 109 | # to let the executor run the subscription callbacks. Note that there is no toggling 110 | # here, so we are testing that the axis_mapping does *not* debounce. 111 | self.publish_from_timer = False 112 | self.joy_msg.buttons[2] = 1 113 | 114 | for i in range(0, 10): 115 | self.joy_publisher.publish(self.joy_msg) 116 | self.executor.spin_once(0.1) 117 | 118 | self.assertEqual(num_received_twists, 11) 119 | 120 | finally: 121 | # Cleanup 122 | self.node.destroy_subscription(twist_subscriber) 123 | -------------------------------------------------------------------------------- /joy_teleop/test/test_topic_message_value.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020 Open Source Robotics Foundation 4 | # All rights reserved. 5 | # 6 | # Software License Agreement (BSD License 2.0) 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | from joy_teleop_testing_common import generate_joy_test_description, TestJoyTeleop 36 | import pytest 37 | import rclpy 38 | import std_msgs.msg 39 | 40 | 41 | @pytest.mark.rostest 42 | def generate_test_description(): 43 | parameters = {} 44 | parameters['simple_message.type'] = 'topic' 45 | parameters['simple_message.interface_type'] = 'std_msgs/msg/String' 46 | parameters['simple_message.topic_name'] = '/simple_message_type' 47 | parameters['simple_message.deadman_buttons'] = [2] 48 | parameters['simple_message.message_value.data.value'] = 'button2' 49 | 50 | return generate_joy_test_description(parameters) 51 | 52 | 53 | class TestJoyTeleopTopicMessageValue(TestJoyTeleop): 54 | 55 | def publish_message(self): 56 | self.joy_publisher.publish(self.joy_msg) 57 | self.joy_msg.buttons[2] = int(not self.joy_msg.buttons[2]) 58 | 59 | def test_simple_message(self): 60 | simple_message = None 61 | future = rclpy.task.Future() 62 | 63 | def receive_simple_message(msg): 64 | nonlocal simple_message 65 | nonlocal future 66 | simple_message = msg.data 67 | future.set_result(True) 68 | 69 | qos = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 70 | depth=1, 71 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE, 72 | durability=rclpy.qos.QoSDurabilityPolicy.VOLATILE) 73 | 74 | simple_message_subscriber = self.node.create_subscription( 75 | std_msgs.msg.String, 76 | '/simple_message_type', 77 | receive_simple_message, 78 | qos, 79 | ) 80 | 81 | try: 82 | # Above we set the button to be used as '2', so here we set the '2' button active. 83 | self.joy_msg.buttons = [0, 0, 1] 84 | 85 | self.executor.spin_until_future_complete(future, timeout_sec=10) 86 | 87 | # Check 88 | self.assertTrue(future.done() and future.result(), 89 | 'Timed out waiting for simple_message topic to complete') 90 | self.assertEqual(simple_message, 'button2') 91 | finally: 92 | # Cleanup 93 | self.node.destroy_subscription(simple_message_subscriber) 94 | -------------------------------------------------------------------------------- /joy_teleop/test/test_xmllint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_xmllint.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.xmllint 20 | @pytest.mark.linter 21 | def test_xmllint(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /key_teleop/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package key_teleop 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.0.0 (2025-04-23) 6 | ------------------ 7 | 8 | 1.8.0 (2025-04-16) 9 | ------------------ 10 | * Default to TwistStamped (`#90 `_) 11 | * Contributors: Bence Magyar 12 | 13 | 1.7.0 (2024-11-06) 14 | ------------------ 15 | 16 | 1.6.0 (2024-10-01) 17 | ------------------ 18 | 19 | 1.5.1 (2024-09-02) 20 | ------------------ 21 | 22 | 1.5.0 (2023-11-01) 23 | ------------------ 24 | * replace deprecated dash by underscore (`#85 `_) 25 | * Contributors: Noel Jiménez García 26 | 27 | 1.4.0 (2023-03-28) 28 | ------------------ 29 | 30 | 1.3.0 (2022-11-23) 31 | ------------------ 32 | * Fix some warnings from tests. 33 | In here are some flake8 fixes and fixes to the joy_teleop tests 34 | now that some of the error messages have changed. 35 | * added ability to use twiststamped 36 | * add ci & lint 37 | * Add QoS profile to key_teleop publisher 38 | * Contributors: Andreas Klintberg, Chris Lalancette, Kazunari Tanaka, nfry321 39 | 40 | 1.2.1 (2020-10-29) 41 | ------------------ 42 | 43 | 1.2.0 (2020-10-16) 44 | ------------------ 45 | 46 | 1.1.0 (2020-04-21) 47 | ------------------ 48 | 49 | 1.0.2 (2020-02-10) 50 | ------------------ 51 | 52 | 1.0.1 (2019-09-18) 53 | ------------------ 54 | * Fix install rules and dashing changes (`#38 `_) 55 | * fix ament indexing 56 | * fix package resource files 57 | * add tk depenndency 58 | * add check for param index-ability 59 | * data files are now package agnostic 60 | Signed-off-by: Ted Kern 61 | * Contributors: Ted Kern 62 | 63 | 1.0.0 (2019-09-10) 64 | ------------------ 65 | * ROS2 port (`#35 `_) 66 | * ROS2 port 67 | * key_teleop pkg format 3 68 | * port teleop_tools_msgs 69 | * key_teleop catch KeyboardInterrupt 70 | * port mouse_teleop 71 | * add key_teleop.yaml 72 | * prepare tests 73 | * add xmllint test 74 | * fix xmllint tests 75 | * simplify joy_teleop retrieve_config 76 | * remove useless class KeyTeleop 77 | * Fixes for dynamic topic joy publishers 78 | - match_command() now compares button array length to the max 79 | deadman button index (apples to apples) 80 | - match_command function now checks if any of the deadman buttons 81 | are depressed before returning a match 82 | - properly handle a std_msgs/msg/Empty 'message_value' by not 83 | attempting to access its value 84 | - utilizes iter-items to correctly index into the config dict 85 | for 'axis_mappings''s 'axis' and 'button' values 86 | - set_member() now splits according to a dash (-) rather than a 87 | periond (.) to be consistent with ros2 param parsing & example yaml 88 | - adds the correct name to setup.py for test_key_teleop.py test 89 | * reduce copy/pasta 90 | * Contributors: Jeremie Deray 91 | 92 | 0.3.0 (2019-01-03) 93 | ------------------ 94 | 95 | 0.2.6 (2018-04-06) 96 | ------------------ 97 | 98 | 0.2.5 (2017-04-21) 99 | ------------------ 100 | 101 | 0.2.4 (2016-11-30) 102 | ------------------ 103 | 104 | 0.2.3 (2016-07-18) 105 | ------------------ 106 | 107 | 0.2.2 (2016-03-24) 108 | ------------------ 109 | 110 | 0.2.1 (2016-01-29) 111 | ------------------ 112 | 113 | 0.2.0 (2015-08-03) 114 | ------------------ 115 | * Update package.xmls 116 | * Contributors: Bence Magyar 117 | 118 | 0.1.2 (2015-02-15) 119 | ------------------ 120 | 121 | 0.1.1 (2014-11-17) 122 | ------------------ 123 | * Change maintainer 124 | * Remove rosbuild legacy 125 | * Merge key_teleop into teleop_tools 126 | * Contributors: Bence Magyar, Paul Mathieu 127 | -------------------------------------------------------------------------------- /key_teleop/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Siegfried-Angel Gevatter Pujals 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | 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, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /key_teleop/README.md: -------------------------------------------------------------------------------- 1 | key_teleop 2 | ========== 3 | 4 | A text-based interface to send a ROS-powered robot movement commands 5 | -------------------------------------------------------------------------------- /key_teleop/config/key_teleop.yaml: -------------------------------------------------------------------------------- 1 | key_teleop: 2 | ros__parameters: 3 | forward_rate : 0.8 4 | backward_rate: 0.5 5 | rotation_rate: 1.0 6 | -------------------------------------------------------------------------------- /key_teleop/key_teleop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-teleop/teleop_tools/d162759de377d0afd2e62420fa89c2d1127f2fee/key_teleop/key_teleop/__init__.py -------------------------------------------------------------------------------- /key_teleop/key_teleop/key_teleop.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2013 PAL Robotics SL. 5 | # All rights reserved. 6 | # 7 | # Software License Agreement (BSD License 2.0) 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions 11 | # are met: 12 | # 13 | # * Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # * Redistributions in binary form must reproduce the above 16 | # copyright notice, this list of conditions and the following 17 | # disclaimer in the documentation and/or other materials provided 18 | # with the distribution. 19 | # * Neither the name of PAL Robotics SL. nor the names of its 20 | # contributors may be used to endorse or promote products derived 21 | # from this software without specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 29 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 33 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | # POSSIBILITY OF SUCH DAMAGE. 35 | # 36 | # 37 | # Authors: 38 | # * Siegfried-A. Gevatter 39 | # * Jeremie Deray (artivis) 40 | 41 | import curses 42 | 43 | # For 'q' keystroke exit 44 | import os 45 | import signal 46 | import time 47 | 48 | from geometry_msgs.msg import Twist, TwistStamped 49 | import rclpy 50 | from rclpy.duration import Duration 51 | from rclpy.node import Node 52 | from rclpy.qos import qos_profile_system_default 53 | from std_msgs.msg import Header 54 | 55 | 56 | class Velocity(object): 57 | 58 | def __init__(self, min_velocity, max_velocity, num_steps): 59 | assert min_velocity > 0 and max_velocity > 0 and num_steps > 0 60 | self._min = min_velocity 61 | self._max = max_velocity 62 | self._num_steps = num_steps 63 | if self._num_steps > 1: 64 | self._step_incr = (max_velocity - min_velocity) / (self._num_steps - 1) 65 | else: 66 | # If num_steps is one, we always use the minimum velocity. 67 | self._step_incr = 0 68 | 69 | def __call__(self, value, step): 70 | """ 71 | Form a velocity. 72 | 73 | Take a value in the range [0, 1] and the step and returns the 74 | velocity (usually m/s or rad/s). 75 | """ 76 | if step == 0: 77 | return 0 78 | 79 | assert step > 0 and step <= self._num_steps 80 | max_value = self._min + self._step_incr * (step - 1) 81 | return value * max_value 82 | 83 | 84 | class TextWindow(): 85 | 86 | _screen = None 87 | _window = None 88 | _num_lines = None 89 | 90 | def __init__(self, stdscr, lines=10): 91 | self._screen = stdscr 92 | self._screen.nodelay(True) 93 | curses.curs_set(0) 94 | 95 | self._num_lines = lines 96 | 97 | def read_key(self): 98 | keycode = self._screen.getch() 99 | return keycode if keycode != -1 else None 100 | 101 | def clear(self): 102 | self._screen.clear() 103 | 104 | def write_line(self, lineno, message): 105 | if lineno < 0 or lineno >= self._num_lines: 106 | raise ValueError('lineno out of bounds') 107 | height, width = self._screen.getmaxyx() 108 | y = (height / self._num_lines) * lineno 109 | x = 10 110 | for text in message.split('\n'): 111 | text = text.ljust(width) 112 | # TODO(artivis) Why are those floats ?? 113 | self._screen.addstr(int(y), int(x), text) 114 | y += 1 115 | 116 | def refresh(self): 117 | self._screen.refresh() 118 | 119 | def beep(self): 120 | curses.flash() 121 | 122 | 123 | class SimpleKeyTeleop(Node): 124 | 125 | def __init__(self, interface): 126 | super().__init__('key_teleop') 127 | 128 | self._interface = interface 129 | 130 | self._publish_stamped_twist = self.declare_parameter('twist_stamped_enabled', True).value 131 | 132 | if self._publish_stamped_twist: 133 | self._pub_cmd = self.create_publisher(TwistStamped, 'key_vel', 134 | qos_profile_system_default) 135 | else: 136 | self._pub_cmd = self.create_publisher(Twist, 'key_vel', qos_profile_system_default) 137 | 138 | self._hz = self.declare_parameter('hz', 10).value 139 | 140 | self._forward_rate = self.declare_parameter('forward_rate', 0.8).value 141 | self._backward_rate = self.declare_parameter('backward_rate', 0.5).value 142 | self._rotation_rate = self.declare_parameter('rotation_rate', 1.0).value 143 | self._last_pressed = {} 144 | self._angular = 0 145 | self._linear = 0 146 | 147 | movement_bindings = { 148 | curses.KEY_UP: (1, 0), 149 | curses.KEY_DOWN: (-1, 0), 150 | curses.KEY_LEFT: (0, 1), 151 | curses.KEY_RIGHT: (0, -1), 152 | } 153 | 154 | def run(self): 155 | self._running = True 156 | while self._running: 157 | while True: 158 | keycode = self._interface.read_key() 159 | if keycode is None: 160 | break 161 | self._key_pressed(keycode) 162 | self._set_velocity() 163 | self._publish() 164 | # TODO(artivis) use Rate once available 165 | time.sleep(1.0/self._hz) 166 | 167 | def _make_twist(self, linear, angular): 168 | twist = Twist() 169 | twist.linear.x = linear 170 | twist.angular.z = angular 171 | return twist 172 | 173 | def _make_twist_stamped(self, linear, angular): 174 | twist_stamped = TwistStamped() 175 | header = Header() 176 | header.stamp = rclpy.clock.Clock().now().to_msg() 177 | header.frame_id = 'key_teleop' 178 | 179 | twist_stamped.header = header 180 | twist_stamped.twist.linear.x = linear 181 | twist_stamped.twist.angular.z = angular 182 | return twist_stamped 183 | 184 | def _set_velocity(self): 185 | now = self.get_clock().now() 186 | keys = [] 187 | for a in self._last_pressed: 188 | if now - self._last_pressed[a] < Duration(seconds=0.4): 189 | keys.append(a) 190 | linear = 0.0 191 | angular = 0.0 192 | for k in keys: 193 | l, a = self.movement_bindings[k] 194 | linear += l 195 | angular += a 196 | if linear > 0: 197 | linear = linear * self._forward_rate 198 | else: 199 | linear = linear * self._backward_rate 200 | angular = angular * self._rotation_rate 201 | self._angular = angular 202 | self._linear = linear 203 | 204 | def _key_pressed(self, keycode): 205 | if keycode == ord('q'): 206 | self._running = False 207 | # TODO(artivis) no rclpy.signal_shutdown ? 208 | os.kill(os.getpid(), signal.SIGINT) 209 | elif keycode in self.movement_bindings: 210 | self._last_pressed[keycode] = self.get_clock().now() 211 | 212 | def _publish(self): 213 | self._interface.clear() 214 | self._interface.write_line(2, 'Linear: %f, Angular: %f' % (self._linear, self._angular)) 215 | self._interface.write_line(5, 'Use arrow keys to move, q to exit.') 216 | self._interface.refresh() 217 | 218 | if self._publish_stamped_twist: 219 | twist = self._make_twist_stamped(self._linear, self._angular) 220 | else: 221 | twist = self._make_twist(self._linear, self._angular) 222 | 223 | self._pub_cmd.publish(twist) 224 | 225 | 226 | def execute(stdscr): 227 | rclpy.init() 228 | 229 | app = SimpleKeyTeleop(TextWindow(stdscr)) 230 | app.run() 231 | 232 | app.destroy_node() 233 | rclpy.shutdown() 234 | 235 | 236 | def main(): 237 | try: 238 | curses.wrapper(execute) 239 | except KeyboardInterrupt: 240 | pass 241 | 242 | 243 | if __name__ == '__main__': 244 | main() 245 | -------------------------------------------------------------------------------- /key_teleop/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | key_teleop 5 | 2.0.0 6 | A text-based interface to send a robot movement commands. 7 | 8 | Bence Magyar 9 | BSD 10 | 11 | Siegfried-A. Gevatter Pujals 12 | 13 | geometry_msgs 14 | rclpy 15 | 16 | ament_copyright 17 | ament_flake8 18 | ament_pep257 19 | python3-pytest 20 | 21 | 22 | ament_python 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /key_teleop/resource/key_teleop: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-teleop/teleop_tools/d162759de377d0afd2e62420fa89c2d1127f2fee/key_teleop/resource/key_teleop -------------------------------------------------------------------------------- /key_teleop/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/key_teleop 3 | [install] 4 | install_scripts=$base/lib/key_teleop 5 | -------------------------------------------------------------------------------- /key_teleop/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | 7 | package_name = 'key_teleop' 8 | share_path = os.path.join('share', package_name) 9 | 10 | 11 | setup( 12 | name=package_name, 13 | version='1.8.0', 14 | packages=find_packages(exclude=['test']), 15 | data_files=[ 16 | (share_path, ['package.xml']), 17 | (os.path.join(share_path, 'config'), [os.path.join('config', f'{package_name}.yaml')]), 18 | (os.path.join('share', 'ament_index', 'resource_index', 'packages'), 19 | [os.path.join('resource', package_name)]), 20 | ], 21 | install_requires=['setuptools'], 22 | zip_safe=True, 23 | author='Siegfried-A. Gevatter Pujals', 24 | author_email='siegfried.gevatter@pal-robotics.com', 25 | maintainer='Bence Magyar', 26 | maintainer_email='bence.magyar.robotics@gmail.com', 27 | url='https://github.com/ros-teleop/teleop_tools', 28 | keywords=['ROS'], 29 | classifiers=[ 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: BSD', 32 | 'Programming Language :: Python', 33 | 'Topic :: Software Development', 34 | ], 35 | description='A text-based interface to send a robot movement commands.', 36 | long_description="""\ 37 | key_teleop provides command-line interface to send Twist commands \ 38 | to drive a robot around.""", 39 | license='BSD', 40 | tests_require=['pytest'], 41 | entry_points={ 42 | 'console_scripts': [ 43 | 'key_teleop = key_teleop.key_teleop:main', 44 | ], 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /key_teleop/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /key_teleop/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /key_teleop/test/test_pop257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=['.']) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /mouse_teleop/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package mouse_teleop 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.0.0 (2025-04-23) 6 | ------------------ 7 | 8 | 1.8.0 (2025-04-16) 9 | ------------------ 10 | * Default to TwistStamped (`#90 `_) 11 | * Contributors: Bence Magyar 12 | 13 | 1.7.0 (2024-11-06) 14 | ------------------ 15 | 16 | 1.6.0 (2024-10-01) 17 | ------------------ 18 | * Do periodic publishing on tkinter loop instead of ROS timer. (`#72 `_) 19 | * Contributors: Anthony Deschamps 20 | 21 | 1.5.1 (2024-09-02) 22 | ------------------ 23 | 24 | 1.5.0 (2023-11-01) 25 | ------------------ 26 | * replace deprecated dash by underscore (`#85 `_) 27 | * Contributors: Noel Jiménez García 28 | 29 | 1.4.0 (2023-03-28) 30 | ------------------ 31 | 32 | 1.3.0 (2022-11-23) 33 | ------------------ 34 | * launch: fix deprecated attributes 35 | * add ci & lint 36 | * Contributors: Kazunari Tanaka, Russ Webber 37 | 38 | 1.2.1 (2020-10-29) 39 | ------------------ 40 | * Use the python3-* rosdep keys. 41 | * Contributors: Chris Lalancette 42 | 43 | 1.2.0 (2020-10-16) 44 | ------------------ 45 | 46 | 1.1.0 (2020-04-21) 47 | ------------------ 48 | 49 | 1.0.2 (2020-02-10) 50 | ------------------ 51 | 52 | 1.0.1 (2019-09-18) 53 | ------------------ 54 | * Fix install rules and dashing changes (`#38 `_) 55 | * fix ament indexing 56 | * fix package resource files 57 | * add tk depenndency 58 | * add check for param index-ability 59 | * data files are now package agnostic 60 | Signed-off-by: Ted Kern 61 | * Contributors: Ted Kern 62 | 63 | 1.0.0 (2019-09-10) 64 | ------------------ 65 | * ROS2 port (`#35 `_) 66 | * ROS2 port 67 | * key_teleop pkg format 3 68 | * port teleop_tools_msgs 69 | * key_teleop catch KeyboardInterrupt 70 | * port mouse_teleop 71 | * add key_teleop.yaml 72 | * prepare tests 73 | * add xmllint test 74 | * fix xmllint tests 75 | * simplify joy_teleop retrieve_config 76 | * remove useless class KeyTeleop 77 | * Fixes for dynamic topic joy publishers 78 | - match_command() now compares button array length to the max 79 | deadman button index (apples to apples) 80 | - match_command function now checks if any of the deadman buttons 81 | are depressed before returning a match 82 | - properly handle a std_msgs/msg/Empty 'message_value' by not 83 | attempting to access its value 84 | - utilizes iter-items to correctly index into the config dict 85 | for 'axis_mappings''s 'axis' and 'button' values 86 | - set_member() now splits according to a dash (-) rather than a 87 | periond (.) to be consistent with ros2 param parsing & example yaml 88 | - adds the correct name to setup.py for test_key_teleop.py test 89 | * reduce copy/pasta 90 | * Contributors: Jeremie Deray 91 | 92 | 0.3.0 (2019-01-03) 93 | ------------------ 94 | 95 | 0.2.6 (2018-04-06) 96 | ------------------ 97 | 98 | 0.2.5 (2017-04-21) 99 | ------------------ 100 | 101 | 0.2.4 (2016-11-30) 102 | ------------------ 103 | 104 | 0.2.3 (2016-07-18) 105 | ------------------ 106 | 107 | 0.2.2 (2016-03-24) 108 | ------------------ 109 | 110 | 0.2.1 (2016-01-29) 111 | ------------------ 112 | * Add mouse_teleop 113 | * Contributors: Enrique Fernandez 114 | 115 | 0.2.0 (2015-08-03) 116 | ------------------ 117 | 118 | 0.1.2 (2015-02-15) 119 | ------------------ 120 | 121 | 0.1.1 (2014-11-17) 122 | ------------------ 123 | 124 | 0.1.0 (2013-11-28) 125 | ------------------ 126 | -------------------------------------------------------------------------------- /mouse_teleop/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Enrique Fernandez 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | 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, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /mouse_teleop/README.md: -------------------------------------------------------------------------------- 1 | mouse_teleop 2 | ============ 3 | 4 | A pointing device (e.g. mouse, touchpad) teleop tool for mobile robots, 5 | supporting holonomic and differential drive platforms. 6 | -------------------------------------------------------------------------------- /mouse_teleop/config/mouse_teleop.yaml: -------------------------------------------------------------------------------- 1 | mouse_teleop: 2 | ros__parameters: 3 | frequency: 10.0 4 | scale : 1.0 5 | holonomic: False 6 | -------------------------------------------------------------------------------- /mouse_teleop/launch/mouse_teleop.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Canonical, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | from ament_index_python.packages import get_package_share_directory 18 | from launch import LaunchDescription 19 | import launch_ros.actions 20 | 21 | 22 | def generate_launch_description(): 23 | 24 | parameters_file = os.path.join( 25 | get_package_share_directory('mouse_teleop'), 26 | 'config', 'mouse_teleop.yaml' 27 | ) 28 | 29 | mouse_teleop = launch_ros.actions.Node( 30 | package='mouse_teleop', executable='mouse_teleop', 31 | parameters=[parameters_file]) 32 | 33 | return LaunchDescription([mouse_teleop]) 34 | -------------------------------------------------------------------------------- /mouse_teleop/mouse_teleop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-teleop/teleop_tools/d162759de377d0afd2e62420fa89c2d1127f2fee/mouse_teleop/mouse_teleop/__init__.py -------------------------------------------------------------------------------- /mouse_teleop/mouse_teleop/mouse_teleop.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2015 Enrique Fernandez 5 | # All rights reserved. 6 | # 7 | # Software License Agreement (BSD License 2.0) 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions 11 | # are met: 12 | # 13 | # * Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # * Redistributions in binary form must reproduce the above 16 | # copyright notice, this list of conditions and the following 17 | # disclaimer in the documentation and/or other materials provided 18 | # with the distribution. 19 | # * Neither the name of Enrique Fernandez nor the names of its 20 | # contributors may be used to endorse or promote products derived 21 | # from this software without specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 29 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 33 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | # POSSIBILITY OF SUCH DAMAGE. 35 | # 36 | # 37 | # Authors: 38 | # * Enrique Fernandez 39 | # * Jeremie Deray (artivis) 40 | 41 | import signal 42 | import tkinter 43 | 44 | from geometry_msgs.msg import TwistStamped, Vector3 45 | import numpy 46 | import rclpy 47 | from rclpy.node import Node 48 | 49 | 50 | class MouseTeleop(Node): 51 | 52 | def __init__(self): 53 | super().__init__('mouse_teleop') 54 | 55 | # Retrieve params: 56 | self._frequency = self.declare_parameter('frequency', 0.0).value 57 | self._scale = self.declare_parameter('scale', 1.0).value 58 | self._holonomic = self.declare_parameter('holonomic', False).value 59 | 60 | # Create twist publisher: 61 | self._pub_cmd = self.create_publisher(TwistStamped, 'mouse_vel', 10) 62 | 63 | # Initialize twist components to zero: 64 | self._v_x = 0.0 65 | self._v_y = 0.0 66 | self._w = 0.0 67 | 68 | # Initialize mouse position (x, y) to None (unknown); it's initialized 69 | # when the mouse button is pressed on the _start callback that handles 70 | # that event: 71 | self._x = None 72 | self._y = None 73 | 74 | # Create window: 75 | self._root = tkinter.Tk() 76 | self._root.title('Mouse Teleop') 77 | 78 | # Make window non-resizable: 79 | self._root.resizable(0, 0) 80 | 81 | # Create canvas: 82 | self._canvas = tkinter.Canvas(self._root, bg='white') 83 | 84 | # Create canvas objects: 85 | self._canvas.create_arc(0, 0, 0, 0, fill='red', outline='red', 86 | width=1, style=tkinter.PIESLICE, start=90.0, tag='w') 87 | self._canvas.create_line(0, 0, 0, 0, fill='blue', width=4, tag='v_x') 88 | 89 | if self._holonomic: 90 | self._canvas.create_line(0, 0, 0, 0, fill='blue', width=4, tag='v_y') 91 | 92 | # Create canvas text objects: 93 | self._text_v_x = tkinter.StringVar() 94 | if self._holonomic: 95 | self._text_v_y = tkinter.StringVar() 96 | self._text_w = tkinter.StringVar() 97 | 98 | self._label_v_x = tkinter.Label(self._root, anchor=tkinter.W, textvariable=self._text_v_x) 99 | if self._holonomic: 100 | self._label_v_y = tkinter.Label( 101 | self._root, anchor=tkinter.W, textvariable=self._text_v_y) 102 | self._label_w = tkinter.Label(self._root, anchor=tkinter.W, textvariable=self._text_w) 103 | 104 | if self._holonomic: 105 | self._text_v_x.set('v_x = %0.2f m/s' % self._v_x) 106 | self._text_v_y.set('v_y = %0.2f m/s' % self._v_y) 107 | self._text_w.set('w = %0.2f deg/s' % self._w) 108 | else: 109 | self._text_v_x.set('v = %0.2f m/s' % self._v_x) 110 | self._text_w.set('w = %0.2f deg/s' % self._w) 111 | 112 | self._label_v_x.pack() 113 | if self._holonomic: 114 | self._label_v_y.pack() 115 | self._label_w.pack() 116 | 117 | # Bind event handlers: 118 | self._canvas.bind('', self._start) 119 | self._canvas.bind('', self._release) 120 | 121 | self._canvas.bind('', self._configure) 122 | 123 | if self._holonomic: 124 | self._canvas.bind('', self._mouse_motion_linear) 125 | self._canvas.bind('', self._mouse_motion_angular) 126 | 127 | self._root.bind('', self._change_to_motion_angular) 128 | self._root.bind('', self._change_to_motion_linear) 129 | else: 130 | self._canvas.bind('', self._mouse_motion_angular) 131 | 132 | self._canvas.pack() 133 | 134 | # If frequency is positive, initialize the `_period` 135 | # field. The Tkinter event loop will be used to periodically 136 | # send messages while the mouse button is down. 137 | self._publish_handle = None 138 | if self._frequency > 0.0: 139 | self._period = int(1000 / self._frequency) 140 | else: 141 | self._period = None 142 | 143 | # Handle ctrl+c on the window 144 | self._root.bind('', self._quit) 145 | 146 | # Nasty polling-trick to handle ctrl+c in terminal 147 | self._root.after(50, self._check) 148 | signal.signal(2, self._handle_signal) 149 | 150 | # Start window event manager main loop: 151 | self._root.mainloop() 152 | 153 | def _quit(self, ev): 154 | self._root.quit() 155 | 156 | def __del__(self): 157 | self._root.quit() 158 | 159 | def _check(self): 160 | self._root.after(50, self._check) 161 | 162 | def _handle_signal(self, signum, frame): 163 | self._quit(None) 164 | 165 | def _start(self, event): 166 | self._x, self._y = event.y, event.x 167 | 168 | self._y_linear = self._y_angular = 0 169 | 170 | self._v_x = self._v_y = self._w = 0.0 171 | 172 | if self._period is not None: 173 | self._publish_handle = self._root.after(self._period, self._publish_twist) 174 | 175 | def _release(self, event): 176 | self._v_x = self._v_y = self._w = 0.0 177 | 178 | self._send_motion() 179 | 180 | if self._publish_handle is not None: 181 | self._root.after_cancel(self._publish_handle) 182 | self._publish_handle = None 183 | 184 | def _configure(self, event): 185 | self._width, self._height = event.height, event.width 186 | 187 | self._c_x = self._height / 2.0 188 | self._c_y = self._width / 2.0 189 | 190 | self._r = min(self._height, self._width) * 0.25 191 | 192 | def _mouse_motion_linear(self, event): 193 | self._v_x, self._v_y = self._relative_motion(event.y, event.x) 194 | 195 | self._send_motion() 196 | 197 | def _mouse_motion_angular(self, event): 198 | self._v_x, self._w = self._relative_motion(event.y, event.x) 199 | 200 | self._send_motion() 201 | 202 | def _update_coords(self, tag, x0, y0, x1, y1): 203 | x0 += self._c_x 204 | y0 += self._c_y 205 | 206 | x1 += self._c_x 207 | y1 += self._c_y 208 | 209 | self._canvas.coords(tag, (x0, y0, x1, y1)) 210 | 211 | def _draw_v_x(self, v): 212 | x = -v * float(self._width) 213 | 214 | self._update_coords('v_x', 0, 0, 0, x) 215 | 216 | def _draw_v_y(self, v): 217 | y = -v * float(self._height) 218 | 219 | self._update_coords('v_y', 0, 0, y, 0) 220 | 221 | def _draw_w(self, w): 222 | x0 = y0 = -self._r 223 | x1 = y1 = self._r 224 | 225 | self._update_coords('w', x0, y0, x1, y1) 226 | 227 | yaw = w * numpy.rad2deg(self._scale) 228 | 229 | self._canvas.itemconfig('w', extent=yaw) 230 | 231 | def _send_motion(self): 232 | 233 | self._draw_v_x(self._v_x) 234 | if self._holonomic: 235 | self._draw_v_y(self._v_y) 236 | self._draw_w(self._w) 237 | 238 | if self._holonomic: 239 | self._text_v_x.set('v_x = %0.2f m/s' % self._v_x) 240 | self._text_v_y.set('v_y = %0.2f m/s' % self._v_y) 241 | self._text_w.set('w = %0.2f deg/s' % numpy.rad2deg(self._w)) 242 | else: 243 | self._text_v_x.set('v = %0.2f m/s' % self._v_x) 244 | self._text_w.set('w = %0.2f deg/s' % numpy.rad2deg(self._w)) 245 | 246 | v_x = self._v_x * self._scale 247 | v_y = self._v_y * self._scale 248 | w = self._w * self._scale 249 | 250 | lin = Vector3(x=v_x, y=v_y, z=0.0) 251 | ang = Vector3(x=0.0, y=0.0, z=w) 252 | 253 | twist_stamped = TwistStamped() 254 | twist_stamped.header.stamp = rclpy.clock.Clock().now().to_msg() 255 | twist_stamped.header.frame_id = 'mouse_teleop' 256 | twist_stamped.twist.linear = lin 257 | twist_stamped.twist.angular = ang 258 | 259 | self._pub_cmd.publish(twist_stamped) 260 | 261 | def _publish_twist(self): 262 | self._send_motion() 263 | self._publish_handle = self._root.after(self._period, self._publish_twist) 264 | 265 | def _relative_motion(self, x, y): 266 | dx = self._x - x 267 | dy = self._y - y 268 | 269 | dx /= float(self._width) 270 | dy /= float(self._height) 271 | 272 | dx = max(-1.0, min(dx, 1.0)) 273 | dy = max(-1.0, min(dy, 1.0)) 274 | 275 | return dx, dy 276 | 277 | def _change_to_motion_linear(self, event): 278 | if self._y is not None: 279 | y = event.x 280 | 281 | self._y_angular = self._y - y 282 | self._y = self._y_linear + y 283 | 284 | def _change_to_motion_angular(self, event): 285 | if self._y is not None: 286 | y = event.x 287 | 288 | self._y_linear = self._y - y 289 | self._y = self._y_angular + y 290 | 291 | 292 | def main(): 293 | try: 294 | rclpy.init() 295 | 296 | node = MouseTeleop() 297 | 298 | node.destroy_node() 299 | rclpy.shutdown() 300 | except KeyboardInterrupt: 301 | pass 302 | 303 | 304 | if __name__ == '__main__': 305 | main() 306 | -------------------------------------------------------------------------------- /mouse_teleop/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mouse_teleop 5 | 2.0.0 6 | A mouse teleop tool for holonomic mobile robots. 7 | 8 | Enrique Fernandez 9 | BSD 10 | 11 | Enrique Fernandez 12 | 13 | geometry_msgs 14 | rclpy 15 | python3-numpy 16 | python3-tk 17 | 18 | ament_copyright 19 | ament_flake8 20 | ament_pep257 21 | ament_xmllint 22 | 23 | 24 | ament_python 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /mouse_teleop/resource/mouse_teleop: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-teleop/teleop_tools/d162759de377d0afd2e62420fa89c2d1127f2fee/mouse_teleop/resource/mouse_teleop -------------------------------------------------------------------------------- /mouse_teleop/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/mouse_teleop 3 | [install] 4 | install_scripts=$base/lib/mouse_teleop 5 | -------------------------------------------------------------------------------- /mouse_teleop/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | 7 | package_name = 'mouse_teleop' 8 | share_path = os.path.join('share', package_name) 9 | 10 | 11 | setup( 12 | name=package_name, 13 | version='1.8.0', 14 | packages=find_packages(exclude=['test']), 15 | data_files=[ 16 | (share_path, ['package.xml']), 17 | (os.path.join(share_path, 'config'), [os.path.join('config', f'{package_name}.yaml')]), 18 | (os.path.join(share_path, 'launch'), 19 | [os.path.join('launch', f'{package_name}.launch.py')]), 20 | (os.path.join('share', 'ament_index', 'resource_index', 'packages'), 21 | [os.path.join('resource', package_name)]), 22 | ], 23 | install_requires=['setuptools'], 24 | zip_safe=True, 25 | author='Enrique Fernandez', 26 | author_email='enrique.fernandez.perdomo@gmail.com', 27 | maintainer='Enrique Fernandez', 28 | maintainer_email='enrique.fernandez.perdomo@gmail.com', 29 | url='https://github.com/ros-teleop/teleop_tools', 30 | keywords=['ROS'], 31 | classifiers=[ 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: BSD', 34 | 'Programming Language :: Python', 35 | 'Topic :: Software Development', 36 | ], 37 | description='A text-based interface to send a robot movement commands.', 38 | long_description="""\ 39 | key_teleop provides command-line interface to send Twist commands \ 40 | to drive a robot around.""", 41 | license='BSD', 42 | tests_require=['pytest'], 43 | entry_points={ 44 | 'console_scripts': [ 45 | 'mouse_teleop = mouse_teleop.mouse_teleop:main', 46 | ], 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /mouse_teleop/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /mouse_teleop/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /mouse_teleop/test/test_pop257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=['.']) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /mouse_teleop/test/test_xmllint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_xmllint.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.xmllint 20 | @pytest.mark.linter 21 | def test_xmllint(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | junit_family=xunit2 3 | -------------------------------------------------------------------------------- /teleop_tools/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package teleop_tools 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.0.0 (2025-04-23) 6 | ------------------ 7 | 8 | 1.8.0 (2025-04-16) 9 | ------------------ 10 | 11 | 1.7.0 (2024-11-06) 12 | ------------------ 13 | 14 | 1.6.0 (2024-10-01) 15 | ------------------ 16 | 17 | 1.5.1 (2024-09-02) 18 | ------------------ 19 | 20 | 1.5.0 (2023-11-01) 21 | ------------------ 22 | 23 | 1.4.0 (2023-03-28) 24 | ------------------ 25 | 26 | 1.3.0 (2022-11-23) 27 | ------------------ 28 | 29 | 1.2.1 (2020-10-29) 30 | ------------------ 31 | 32 | 1.2.0 (2020-10-16) 33 | ------------------ 34 | 35 | 1.1.0 (2020-04-21) 36 | ------------------ 37 | 38 | 1.0.2 (2020-02-10) 39 | ------------------ 40 | 41 | 1.0.1 (2019-09-18) 42 | ------------------ 43 | 44 | 1.0.0 (2019-09-10) 45 | ------------------ 46 | * ROS2 port (`#35 `_) 47 | * ROS2 port 48 | * key_teleop pkg format 3 49 | * port teleop_tools_msgs 50 | * key_teleop catch KeyboardInterrupt 51 | * port mouse_teleop 52 | * add key_teleop.yaml 53 | * prepare tests 54 | * add xmllint test 55 | * fix xmllint tests 56 | * simplify joy_teleop retrieve_config 57 | * remove useless class KeyTeleop 58 | * Fixes for dynamic topic joy publishers 59 | - match_command() now compares button array length to the max 60 | deadman button index (apples to apples) 61 | - match_command function now checks if any of the deadman buttons 62 | are depressed before returning a match 63 | - properly handle a std_msgs/msg/Empty 'message_value' by not 64 | attempting to access its value 65 | - utilizes iter-items to correctly index into the config dict 66 | for 'axis_mappings''s 'axis' and 'button' values 67 | - set_member() now splits according to a dash (-) rather than a 68 | periond (.) to be consistent with ros2 param parsing & example yaml 69 | - adds the correct name to setup.py for test_key_teleop.py test 70 | * reduce copy/pasta 71 | * Contributors: Jeremie Deray 72 | 73 | 0.3.0 (2019-01-03) 74 | ------------------ 75 | 76 | 0.2.6 (2018-04-06) 77 | ------------------ 78 | 79 | 0.2.5 (2017-04-21) 80 | ------------------ 81 | 82 | 0.2.4 (2016-11-30) 83 | ------------------ 84 | 85 | 0.2.3 (2016-07-18) 86 | ------------------ 87 | 88 | 0.2.2 (2016-03-24) 89 | ------------------ 90 | 91 | 0.2.1 (2016-01-29) 92 | ------------------ 93 | 94 | 0.2.0 (2015-08-03) 95 | ------------------ 96 | * Update package.xmls 97 | * Contributors: Bence Magyar 98 | 99 | 0.1.2 (2015-02-15) 100 | ------------------ 101 | 102 | 0.1.1 (2014-11-17) 103 | ------------------ 104 | * Change maintainer 105 | * Add key_teleop to metapackage 106 | * Add teleop_tools metapackage 107 | * Contributors: Bence Magyar, Paul Mathieu 108 | -------------------------------------------------------------------------------- /teleop_tools/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(teleop_tools NONE) 4 | 5 | find_package(ament_cmake REQUIRED) 6 | 7 | ament_package() 8 | -------------------------------------------------------------------------------- /teleop_tools/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | teleop_tools 4 | 2.0.0 5 | A set of generic teleoperation tools for any robot. 6 | Bence Magyar 7 | BSD 8 | 9 | ament_cmake 10 | 11 | joy_teleop 12 | key_teleop 13 | teleop_tools_msgs 14 | 15 | 16 | ament_cmake 17 | 18 | 19 | -------------------------------------------------------------------------------- /teleop_tools_msgs/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package teleop_tools_msgs 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.0.0 (2025-04-23) 6 | ------------------ 7 | 8 | 1.8.0 (2025-04-16) 9 | ------------------ 10 | 11 | 1.7.0 (2024-11-06) 12 | ------------------ 13 | 14 | 1.6.0 (2024-10-01) 15 | ------------------ 16 | 17 | 1.5.1 (2024-09-02) 18 | ------------------ 19 | 20 | 1.5.0 (2023-11-01) 21 | ------------------ 22 | 23 | 1.4.0 (2023-03-28) 24 | ------------------ 25 | 26 | 1.3.0 (2022-11-23) 27 | ------------------ 28 | 29 | 1.2.1 (2020-10-29) 30 | ------------------ 31 | 32 | 1.2.0 (2020-10-16) 33 | ------------------ 34 | 35 | 1.1.0 (2020-04-21) 36 | ------------------ 37 | 38 | 1.0.2 (2020-02-10) 39 | ------------------ 40 | 41 | 1.0.1 (2019-09-18) 42 | ------------------ 43 | 44 | 1.0.0 (2019-09-10) 45 | ------------------ 46 | * ROS2 port (`#35 `_) 47 | * Contributors: Jeremie Deray 48 | 49 | 0.3.0 (2019-01-03) 50 | ------------------ 51 | 52 | 0.2.6 (2018-04-06) 53 | ------------------ 54 | 55 | 0.2.5 (2017-04-21) 56 | ------------------ 57 | 58 | 0.2.4 (2016-11-30) 59 | ------------------ 60 | 61 | 0.2.3 (2016-07-18) 62 | ------------------ 63 | 64 | 0.2.2 (2016-03-24) 65 | ------------------ 66 | 67 | 0.2.1 (2016-01-29) 68 | ------------------ 69 | * Remove rospy dependency 70 | * Contributors: Bence Magyar 71 | 72 | 0.2.0 (2015-08-03) 73 | ------------------ 74 | * Add teleop_tools_msgs 75 | * Contributors: Bence Magyar 76 | 77 | 0.1.2 (2015-02-15) 78 | ------------------ 79 | 80 | 0.1.1 (2014-11-17) 81 | ------------------ 82 | 83 | 0.1.0 (2013-11-28) 84 | ------------------ 85 | -------------------------------------------------------------------------------- /teleop_tools_msgs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(teleop_tools_msgs) 3 | 4 | # Default to C11 5 | if(NOT CMAKE_C_STANDARD) 6 | set(CMAKE_C_STANDARD 11) 7 | endif() 8 | 9 | # Default to C++14 10 | if(NOT CMAKE_CXX_STANDARD) 11 | set(CMAKE_CXX_STANDARD 14) 12 | endif() 13 | 14 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 15 | add_compile_options(-Wall -Wextra -Wpedantic) 16 | endif() 17 | 18 | find_package(ament_cmake REQUIRED) 19 | find_package(rosidl_default_generators REQUIRED) 20 | 21 | if(BUILD_TESTING) 22 | find_package(ament_lint_auto REQUIRED) 23 | ament_lint_auto_find_test_dependencies() 24 | endif() 25 | 26 | rosidl_generate_interfaces(${PROJECT_NAME} 27 | "action/Increment.action" 28 | ADD_LINTER_TESTS 29 | ) 30 | ament_export_dependencies(rosidl_default_runtime) 31 | ament_package() 32 | -------------------------------------------------------------------------------- /teleop_tools_msgs/action/Increment.action: -------------------------------------------------------------------------------- 1 | float32[] increment_by 2 | --- 3 | --- 4 | -------------------------------------------------------------------------------- /teleop_tools_msgs/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | teleop_tools_msgs 4 | 2.0.0 5 | The teleop_tools_msgs package 6 | Bence Magyar 7 | BSD 8 | 9 | ament_cmake 10 | rosidl_default_generators 11 | 12 | action_msgs 13 | 14 | rosidl_default_runtime 15 | 16 | ament_lint_auto 17 | ament_lint_common 18 | 19 | rosidl_interface_packages 20 | 21 | 22 | ament_cmake 23 | 24 | 25 | --------------------------------------------------------------------------------