├── roboteq.repos ├── config └── roboteq.yaml ├── package.xml ├── LICENSE ├── launch └── roboteq_ros2_driver.launch.py ├── CMakeLists.txt ├── README.md ├── include └── roboteq_ros2_driver │ └── roboteq_ros2_driver.hpp └── src └── driver_dev.cpp /roboteq.repos: -------------------------------------------------------------------------------- 1 | repositories: 2 | ros2_serial: 3 | type: git 4 | url: https://github.com/CJdev99/serial.git 5 | version: ros2 6 | ros2_serial_utils: 7 | type: git 8 | url: https://github.com/wjwwood/serial_utils.git 9 | version: ros2 10 | -------------------------------------------------------------------------------- /config/roboteq.yaml: -------------------------------------------------------------------------------- 1 | roboteq_ros2_driver: 2 | ros__parameters: 3 | pub_odom_tf: false 4 | odom_frame: "odom" 5 | base_frame: "base_link" 6 | cmdvel_topic: "cmdvel" 7 | odom_topic: "odom" 8 | port: "/dev/ttyACM0" 9 | baud: 115200 10 | open_loop: true 11 | wheel_circumference: 0.3192 12 | track_width: 0.85 13 | encoder_ppr: 1024 14 | encoder_cpr: 4096 15 | max_amps: 5 16 | max_rpm: 100 17 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | roboteq_ros2_driver 5 | 0.0.0 6 | TODO: Package description 7 | chase 8 | TODO: License declaration 9 | 10 | ament_cmake 11 | 12 | io_context 13 | rclcpp 14 | std_msgs 15 | sensor_msgs 16 | nav_msgs 17 | geometry_msgs 18 | tf2_geometry_msgs 19 | tf2_ros 20 | tf2 21 | serial 22 | ament_lint_auto 23 | ament_lint_common 24 | 25 | ament_lint_auto 26 | ament_lint_common 27 | 28 | 29 | ament_cmake 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chase Devitt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /launch/roboteq_ros2_driver.launch.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) [] 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import os 24 | from ament_index_python.packages import get_package_share_directory 25 | from launch import LaunchDescription 26 | from launch_ros.actions import Node 27 | 28 | 29 | def generate_launch_description(): 30 | # ROS packages 31 | pkg_roboteq = get_package_share_directory('roboteq_ros2_driver') 32 | 33 | # Config 34 | roboteq_config = os.path.join(pkg_roboteq, 'config/roboteq', 35 | 'roboteq.yaml') 36 | 37 | # Nodes 38 | roboteq_ros2_driver = Node( 39 | package='roboteq_ros2_driver', 40 | executable='driver_dev', 41 | name='roboteq_ros2_driver', 42 | output='screen', 43 | parameters=[roboteq_config], 44 | ) 45 | 46 | return LaunchDescription([ 47 | # Nodes 48 | roboteq_ros2_driver, 49 | ]) 50 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(roboteq_ros2_driver) 3 | 4 | # Default to C99 5 | if(NOT CMAKE_C_STANDARD) 6 | set(CMAKE_C_STANDARD 99) 7 | endif() 8 | 9 | # Default to C++14 10 | if(NOT CMAKE_CXX_STANDARD) 11 | set(CMAKE_CXX_STANDARD 17) 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 dependencies 19 | find_package(ament_cmake REQUIRED) 20 | find_package(rclcpp REQUIRED) 21 | find_package(sensor_msgs REQUIRED) 22 | find_package(std_msgs REQUIRED) 23 | find_package(geometry_msgs REQUIRED) 24 | find_package(nav_msgs REQUIRED) 25 | find_package(tf2 REQUIRED) 26 | find_package(tf2_ros REQUIRED) 27 | find_package(tf2_geometry_msgs REQUIRED) 28 | find_package(serial REQUIRED) 29 | find_package(Boost 1.71 CONFIG REQUIRED COMPONENTS thread) 30 | # uncomment the following section in order to fill in 31 | # further dependencies manually. 32 | # find_package( REQUIRED) 33 | 34 | #add_executable(roboteq_diff_driver src/driver.cpp) 35 | add_executable(roboteq_ros2_driver src/driver_dev.cpp) 36 | 37 | ament_target_dependencies( 38 | roboteq_ros2_driver serial 39 | rclcpp 40 | sensor_msgs 41 | std_msgs 42 | geometry_msgs 43 | nav_msgs 44 | tf2 45 | tf2_ros 46 | tf2_geometry_msgs 47 | ) 48 | 49 | target_include_directories(roboteq_ros2_driver PUBLIC 50 | $ 51 | $) 52 | 53 | install( 54 | DIRECTORY launch config 55 | DESTINATION share/${PROJECT_NAME}/ 56 | ) 57 | 58 | install(TARGETS roboteq_ros2_driver 59 | DESTINATION lib/${PROJECT_NAME}) 60 | 61 | if(BUILD_TESTING) 62 | find_package(ament_lint_auto REQUIRED) 63 | # the following line skips the linter which checks for copyrights 64 | # uncomment the line when a copyright and license is not present in all source files 65 | #set(ament_cmake_copyright_FOUND TRUE) 66 | # the following line skips cpplint (only works in a git repo) 67 | # uncomment the line when this package is not in a git repo 68 | #set(ament_cmake_cpplint_FOUND TRUE) 69 | ament_lint_auto_find_test_dependencies() 70 | endif() 71 | 72 | ament_package() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roboteq_ros2_driver 2 | 3 | 4 | ROS2 driver for the Roboteq SDC21xx, HDC24xx family of motor controllers in a differential-drive configuration. 5 | Initially developed for SDC21xx and HDC24xx, but could work with other roboteq dual-channel motor drivers. 6 | 7 | Subscribes to cmd_vel, publishes to odom 8 | 9 | 10 | Does not require any MicroBasic script to operate. 11 | 12 | ## Usage 13 | 14 | Clone to src directory of ros2 workspace, then `colcon build` 15 | 16 | Requires serial package, which is not available as deb in ROS2. If not already installed, install ros2 branch of serial: 17 | 18 | Get the code: 19 | 20 | git clone -b ros2 https://github.com/SunnyApp-Robotics/serial.git 21 | 22 | Build: 23 | 24 | make 25 | 26 | Install: 27 | 28 | make install 29 | 30 | 31 | Sample launch files in roboteq_ros2_driver/launch, or run `ros2 run roboteq_ros2_driver roboteq_ros2_driver` 32 | 33 | ## Motor Power Connections 34 | 35 | This driver assumes right motor is connected to channel 1 (M1) of motor controller, and left motor is connected to channel 2 (M2). It also assumes a positive speed command will result in forward motion of each motor. Best to test motor directions using the roboteq utility software. 36 | 37 | 38 | ## TODO 39 | 40 | - [X] Initial ROS2 release with motor commands and odometry stream 41 | - [ ] Implement transform broadcasting with tf2 42 | - [ ] Add roboteq/voltage, roboteq/current, roboteq/energy, and roboteq/temperature publishers 43 | - [ ] Make topic names and frames configuration parameters configurable at runtime. 44 | - [ ] Make robot configuration parameters configurable at runtime. 45 | - [ ] Make motor controller device configuration parameters configurable at runtime. 46 | - [ ] Make miscellaneous motor controller configuration parameters configurable at runtime. 47 | - [ ] Implement dynamically enabled self-test mode to verify correct motor power and encoder connections and configuration. 48 | 49 | ### Note: I do not have access to Roboteq hardware anymore - feel free to contribute! 50 | [original work for ROS1](https://github.com/ecostech/roboteq_diff_driver) 51 | ## Authors 52 | 53 | * **Chad Attermann** - *Initial work* - [Ecos Technologies](https://github.com/ecostech) 54 | * **Chase Devitt** - *ROS2 Migration* 55 | -------------------------------------------------------------------------------- /include/roboteq_ros2_driver/roboteq_ros2_driver.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ROBOTEQ_ROS2_DRIVER__ROBOTEQ_ROS2_DRIVER_HPP_ 2 | #define ROBOTEQ_ROS2_DRIVER__ROBOTEQ_ROS2_DRIVER_HPP_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "geometry_msgs/msg/twist.hpp" 14 | #include "std_msgs/msg/header.hpp" 15 | #include "nav_msgs/msg/odometry.hpp" 16 | 17 | #include 18 | 19 | namespace Roboteq 20 | { 21 | class Roboteq : public rclcpp::Node 22 | { 23 | public: 24 | explicit Roboteq(); //(nodeOptions options?) 25 | ~Roboteq(); 26 | 27 | private: 28 | // class atributes 29 | //rclcpp::Node::SharedPtr nh{}; 30 | 31 | uint32_t starttime{}; 32 | uint32_t hstimer{}; 33 | uint32_t mstimer{}; 34 | uint32_t lstimer{}; 35 | rclcpp::TimerBase::SharedPtr timer_; 36 | 37 | // buffer for reading encoder counts 38 | unsigned int odom_idx{}; 39 | char odom_buf[24]{}; 40 | 41 | // toss out initial encoder readings 42 | char odom_encoder_toss{}; 43 | 44 | int32_t odom_encoder_left{}; 45 | int32_t odom_encoder_right{}; 46 | 47 | float odom_x{}; 48 | float odom_y{}; 49 | float odom_yaw{}; 50 | float odom_last_x{}; 51 | float odom_last_y{}; 52 | float odom_last_yaw{}; 53 | 54 | uint32_t odom_last_time{}; 55 | 56 | 57 | // settings 58 | bool pub_odom_tf{}; 59 | std::string odom_frame{}; 60 | std::string base_frame{}; 61 | std::string cmdvel_topic{}; 62 | std::string odom_topic{}; 63 | std::string port{}; 64 | int baud{}; 65 | bool open_loop{}; 66 | double wheel_circumference{}; 67 | double track_width{}; 68 | int encoder_ppr{}; 69 | int encoder_cpr{}; 70 | double max_amps{}; 71 | int max_rpm{}; 72 | // Test different odom msg memory 73 | //nav_msgs::msg::Odometry odom_msg{}; 74 | nav_msgs::msg::Odometry odom_msg{}; 75 | //geometry_msgs::msg::Twist twist_msg{}; 76 | 77 | 78 | 79 | // 80 | // cmd_vel subscriber 81 | // 82 | void cmdvel_callback(const geometry_msgs::msg::Twist::SharedPtr twist_msg); 83 | void cmdvel_setup(); 84 | void cmdvel_loop(); 85 | void cmdvel_run(); 86 | 87 | 88 | 89 | // 90 | // odom publisher 91 | // 92 | void odom_setup(); 93 | void odom_stream(); 94 | void odom_loop(); 95 | //void odom_hs_run(); 96 | void odom_ms_run(); 97 | void odom_ls_run(); 98 | void odom_publish(); 99 | void connect(); 100 | 101 | void update_parameters(); 102 | int run(); 103 | 104 | //subscriber 105 | rclcpp::Subscription::SharedPtr cmdvel_sub; 106 | 107 | //publisher 108 | rclcpp::Publisher::SharedPtr odom_pub; 109 | 110 | 111 | 112 | }; 113 | 114 | } 115 | 116 | 117 | 118 | 119 | 120 | #endif 121 | -------------------------------------------------------------------------------- /src/driver_dev.cpp: -------------------------------------------------------------------------------- 1 | #include "roboteq_ros2_driver/roboteq_ros2_driver.hpp" 2 | 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "rclcpp/rclcpp.hpp" 10 | #include "rclcpp/clock.hpp" 11 | #include 12 | 13 | #include "std_msgs/msg/string.hpp" 14 | 15 | // dependencies for ROS 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define DELTAT(_nowtime, _thentime) ((_thentime > _nowtime) ? ((0xffffffff - _thentime) + _nowtime) : (_nowtime - _thentime)) 22 | 23 | 24 | // Define following to enable cmdvel debug output 25 | #define _CMDVEL_DEBUG 26 | 27 | 28 | 29 | 30 | // Define following to enable odom debug output 31 | #define _ODOM_DEBUG 32 | 33 | // Define following to publish additional sensor information; comment out to not publish (TODO: write custom roboteq messages to support other reportable data from MC 34 | 35 | // #define _ODOM_SENSORS 36 | 37 | // Define following to enable service for returning covariance 38 | // #define _ODOM_COVAR_SERVER 39 | 40 | #define NORMALIZE(_z) atan2(sin(_z), cos(_z)) 41 | 42 | #include 43 | #include 44 | #include 45 | 46 | 47 | serial::Serial controller; 48 | uint32_t millis() 49 | { 50 | auto now = std::chrono::system_clock::now(); 51 | auto duration = now.time_since_epoch(); 52 | return std::chrono::duration_cast(duration).count(); 53 | } 54 | 55 | namespace Roboteq 56 | { 57 | Roboteq::Roboteq() : Node("roboteq_ros2_driver") 58 | // initialize parameters and variables 59 | { 60 | pub_odom_tf = this->declare_parameter("pub_odom_tf", false); 61 | odom_frame = this->declare_parameter("odom_frame", "odom"); 62 | base_frame = this->declare_parameter("base_frame", "base_link"); 63 | cmdvel_topic = this->declare_parameter("cmdvel_topic", "cmd_vel"); 64 | odom_topic = this->declare_parameter("odom_topic", "odom"); 65 | port = this->declare_parameter("port", "/dev/ttyACM0"); 66 | baud = this->declare_parameter("baud", 115200); 67 | open_loop = this->declare_parameter("open_loop", true); 68 | wheel_circumference = this->declare_parameter("wheel_circumference", 0.55); 69 | track_width = this->declare_parameter("track_width", 0.89); 70 | encoder_ppr = this->declare_parameter("encoder_ppr", 1024); 71 | encoder_cpr = this->declare_parameter("encoder_cpr", 4096); 72 | max_amps = this->declare_parameter("max_amps", 5.0); 73 | max_rpm = this->declare_parameter("max_rpm", 100); 74 | 75 | starttime = 0; 76 | hstimer = 0; 77 | mstimer = 0; 78 | odom_idx = 0; 79 | odom_encoder_toss = 5; 80 | odom_encoder_left = 0; 81 | odom_encoder_right = 0; 82 | odom_x = 0.0; 83 | odom_y = 0.0; 84 | odom_yaw = 0.0; 85 | odom_last_x = 0.0; 86 | odom_last_y = 0.0; 87 | odom_last_yaw = 0.0; 88 | odom_last_time = 0; 89 | 90 | //odom_msg = std::make_shared(); 91 | odom_msg = nav_msgs::msg::Odometry(); 92 | 93 | serial::Timeout timeout = serial::Timeout::simpleTimeout(1000); 94 | controller.setPort(port); 95 | controller.setBaudrate(baud); 96 | controller.setTimeout(timeout); 97 | // connect to serial port 98 | connect(); 99 | // configure motor controller 100 | cmdvel_setup(); 101 | odom_setup(); 102 | // 103 | // odom publisher 104 | // 105 | odom_pub = this->create_publisher(odom_topic, 1000); 106 | // 107 | // cmd_vel subscriber 108 | // 109 | 110 | cmdvel_sub = this->create_subscription( 111 | cmdvel_topic, // topic name 112 | 1000, // QoS history depth 113 | std::bind(&Roboteq::cmdvel_callback, this, std::placeholders::_1)); 114 | using namespace std::chrono_literals; 115 | // set odometry publishing loop timer at 10Hz 116 | timer_ = this->create_wall_timer(10ms,std::bind(&Roboteq::run, this)); 117 | // enable modifying params at run-time 118 | /* 119 | using namespace std::chrono_literals; 120 | 121 | param_update_timer = 122 | this->create_wall_timer(1000ms, std::bind(&Roboteq::update_params, this)); 123 | */ 124 | } 125 | 126 | void Roboteq::update_parameters() 127 | { 128 | RCLCPP_INFO(this->get_logger(), "Parameters updated ..."); 129 | this->get_parameter("pub_odom_tf", pub_odom_tf); 130 | this->get_parameter("odom_frame", odom_frame); 131 | this->get_parameter("base_frame", base_frame); 132 | this->get_parameter("cmdvel_topic", cmdvel_topic); 133 | this->get_parameter("odom_topic", odom_topic); 134 | this->get_parameter("port", port); 135 | this->get_parameter("baud", baud); 136 | this->get_parameter("open_loop", open_loop); 137 | this->get_parameter("wheel_circumference", wheel_circumference); 138 | this->get_parameter("track_width", track_width); 139 | this->get_parameter("encoder_ppr", encoder_ppr); 140 | this->get_parameter("encoder_cpr", encoder_cpr); 141 | this->get_parameter("max_amps", max_amps); 142 | this->get_parameter("max_rpm", max_rpm); 143 | } 144 | 145 | void Roboteq::connect(){ 146 | RCLCPP_INFO_STREAM(this->get_logger(),"Opening serial port on " << port << " at " << baud << "..." ); 147 | try 148 | { 149 | controller.open(); 150 | if (controller.isOpen()) 151 | { 152 | RCLCPP_INFO(this->get_logger(), "Successfully opened serial port"); 153 | return; 154 | 155 | } 156 | } 157 | catch (serial::IOException &e) 158 | { 159 | RCLCPP_WARN_STREAM(this->get_logger(), "serial::IOException: "); 160 | throw; 161 | } 162 | RCLCPP_WARN(this->get_logger(),"Failed to open serial port"); 163 | sleep(5); 164 | 165 | } 166 | 167 | 168 | void Roboteq::cmdvel_callback(const geometry_msgs::msg::Twist::SharedPtr twist_msg) // const??? 169 | { 170 | // wheel speed (m/s) 171 | float right_speed = twist_msg->linear.x + track_width * twist_msg->angular.z / 2.0; 172 | float left_speed = twist_msg->linear.x - track_width * twist_msg->angular.z / 2.0; 173 | 174 | std::stringstream right_cmd; 175 | std::stringstream left_cmd; 176 | 177 | 178 | if (open_loop) 179 | { 180 | // motor power (scale 0-1000) 181 | RCLCPP_INFO_STREAM(this->get_logger(),"open loop"); 182 | int32_t right_power = right_speed / wheel_circumference * 60.0 / max_rpm * 1000.0; 183 | int32_t left_power = left_speed / wheel_circumference * 60.0 / max_rpm * 1000.0; 184 | /* 185 | // set minimum to overcome friction if cmd_vel too low 186 | if (right_power < 150 && left_power > 0){ 187 | right_power = 150 188 | } 189 | if (left_power < 150 && left_power > 0){ 190 | left_power = 150 191 | } 192 | */ 193 | 194 | right_cmd << "!G 1 " << right_power << "\r"; 195 | left_cmd << "!G 2 " << left_power << "\r"; 196 | } 197 | else 198 | { 199 | // motor speed (rpm) 200 | int32_t right_rpm = right_speed / wheel_circumference * 60.0; 201 | int32_t left_rpm = left_speed / wheel_circumference * 60.0; 202 | 203 | right_cmd << "!S 1 " << right_rpm << "\r"; 204 | left_cmd << "!S 2 " << left_rpm << "\r"; 205 | } 206 | //write cmd to motor controller 207 | #ifndef _CMDVEL_FORCE_RUN 208 | controller.write(right_cmd.str()); 209 | controller.write(left_cmd.str()); 210 | controller.flush(); 211 | #endif 212 | } 213 | void Roboteq::cmdvel_setup() 214 | { 215 | RCLCPP_INFO(this->get_logger(), "configuring motor controller..."); 216 | 217 | // stop motors 218 | controller.write("!G 1 0\r"); 219 | controller.write("!G 2 0\r"); 220 | controller.write("!S 1 0\r"); 221 | controller.write("!S 2 0\r"); 222 | controller.flush(); 223 | 224 | // disable echo 225 | controller.write("^ECHOF 1\r"); 226 | controller.flush(); 227 | 228 | // enable watchdog timer (1000 ms) 229 | controller.write("^RWD 1000\r"); 230 | 231 | // set motor operating mode (1 for closed-loop speed) 232 | if (open_loop) 233 | { 234 | // open-loop speed mode 235 | controller.write("^MMOD 1 0\r"); 236 | controller.write("^MMOD 2 0\r"); 237 | } 238 | else 239 | { 240 | // closed-loop speed mode 241 | controller.write("^MMOD 1 1\r"); 242 | controller.write("^MMOD 2 1\r"); 243 | } 244 | 245 | // set motor amps limit (A * 10) 246 | std::stringstream right_ampcmd; 247 | std::stringstream left_ampcmd; 248 | right_ampcmd << "^ALIM 1 " << (int)(max_amps * 10) << "\r"; 249 | left_ampcmd << "^ALIM 2 " << (int)(max_amps * 10) << "\r"; 250 | controller.write(right_ampcmd.str()); 251 | controller.write(left_ampcmd.str()); 252 | 253 | // set max speed (rpm) for relative speed commands 254 | std::stringstream right_rpmcmd; 255 | std::stringstream left_rpmcmd; 256 | right_rpmcmd << "^MXRPM 1 " << max_rpm << "\r"; 257 | left_rpmcmd << "^MXRPM 2 " << max_rpm << "\r"; 258 | controller.write(right_rpmcmd.str()); 259 | controller.write(left_rpmcmd.str()); 260 | 261 | // set max acceleration rate (2000 rpm/s * 10) 262 | controller.write("^MAC 1 20000\r"); 263 | controller.write("^MAC 2 20000\r"); 264 | 265 | // set max deceleration rate (2000 rpm/s * 10) 266 | controller.write("^MDEC 1 20000\r"); 267 | controller.write("^MDEC 2 20000\r"); 268 | 269 | // set PID parameters (gain * 10) 270 | controller.write("^KP 1 10\r"); 271 | controller.write("^KP 2 10\r"); 272 | controller.write("^KI 1 80\r"); 273 | controller.write("^KI 2 80\r"); 274 | controller.write("^KD 1 0\r"); 275 | controller.write("^KD 2 0\r"); 276 | 277 | // set encoder mode (18 for feedback on motor1, 34 for feedback on motor2) 278 | controller.write("^EMOD 1 18\r"); 279 | controller.write("^EMOD 2 34\r"); 280 | 281 | // set encoder counts (ppr) 282 | std::stringstream right_enccmd; 283 | std::stringstream left_enccmd; 284 | right_enccmd << "^EPPR 1 " << encoder_ppr << "\r"; 285 | left_enccmd << "^EPPR 2 " << encoder_ppr << "\r"; 286 | controller.write(right_enccmd.str()); 287 | controller.write(left_enccmd.str()); 288 | 289 | controller.flush(); 290 | } 291 | 292 | void Roboteq::cmdvel_loop() 293 | { 294 | } 295 | 296 | void Roboteq::cmdvel_run() 297 | { 298 | #ifdef _CMDVEL_FORCE_RUN 299 | if (open_loop) 300 | { 301 | controller.write("!G 1 100\r"); 302 | controller.write("!G 2 100\r"); 303 | } 304 | else 305 | { 306 | std::stringstream right_cmd; 307 | std::stringstream left_cmd; 308 | right_cmd << "!S 1 " << (int)(max_rpm * 0.1) << "\r"; 309 | left_cmd << "!S 2 " << (int)(max_rpm * 0.1) << "\r"; 310 | controller.write(right_cmd.str()); 311 | controller.write(left_cmd.str()); 312 | } 313 | controller.flush(); 314 | #endif 315 | } 316 | 317 | 318 | void Roboteq::odom_setup() 319 | { 320 | RCLCPP_INFO(this->get_logger(),"setting up odom..."); 321 | if (pub_odom_tf) 322 | { 323 | //TODO: implement tf2 broadcaster 324 | 325 | } 326 | 327 | // maybe use this-> instead of 328 | 329 | 330 | odom_msg.header.stamp = this->get_clock()->now(); 331 | 332 | odom_msg.header.frame_id = odom_frame; 333 | odom_msg.child_frame_id = base_frame; 334 | 335 | // Set up the pose covariance 336 | for (size_t i = 0; i < 36; i++) 337 | { 338 | odom_msg.pose.covariance[i] = 0; 339 | odom_msg.twist.covariance[i] = 0; 340 | } 341 | 342 | odom_msg.pose.covariance[7] = 0.001; 343 | odom_msg.pose.covariance[14] = 1000000; 344 | odom_msg.pose.covariance[21] = 1000000; 345 | odom_msg.pose.covariance[28] = 1000000; 346 | odom_msg.pose.covariance[35] = 1000; 347 | 348 | // Set up the twist covariance 349 | odom_msg.twist.covariance[0] = 0.001; 350 | odom_msg.twist.covariance[7] = 0.001; 351 | odom_msg.twist.covariance[14] = 1000000; 352 | odom_msg.twist.covariance[21] = 1000000; 353 | odom_msg.twist.covariance[28] = 1000000; 354 | odom_msg.twist.covariance[35] = 1000; 355 | 356 | // Set up the transform message: move to odom_publish 357 | /* 358 | tf2::Quaternion q; 359 | q.setRPY(0, 0, odom_yaw); 360 | 361 | tf_msg.transform.translation.x = x; 362 | tf_msg.transform.translation.y = y; 363 | tf_msg.transform.translation.z = 0.0; 364 | tf_msg.transform.rotation.x = q.x(); 365 | tf_msg.transform.rotation.y = q.y(); 366 | tf_msg.transform.rotation.z = q.z(); 367 | tf_msg.transform.rotation.w = q.w(); 368 | */ 369 | 370 | // start encoder streaming 371 | RCLCPP_INFO_STREAM(this->get_logger(),"covariance set"); 372 | RCLCPP_INFO_STREAM(this->get_logger(),"odometry stream starting..."); 373 | odom_stream(); 374 | 375 | odom_last_time = millis(); 376 | #ifdef _ODOM_SENSORS 377 | current_last_time = millis(); 378 | #endif 379 | } 380 | 381 | // Odom msg streams 382 | 383 | void Roboteq::odom_stream() 384 | { 385 | 386 | #ifdef _ODOM_SENSORS 387 | // start encoder and current output (30 hz) 388 | // doubling frequency since one value is output at each cycle 389 | // controller.write("# C_?CR_?BA_# 17\r"); 390 | // start encoder, current and voltage output (30 hz) 391 | // tripling frequency since one value is output at each cycle 392 | controller.write("# C_?CR_?BA_?V_# 11\r"); 393 | #else 394 | // // start encoder output (10 hz) 395 | // controller.write("# C_?CR_# 100\r"); 396 | // start encoder output (30 hz) 397 | controller.write("# C_?CR_# 33\r"); 398 | 399 | #endif 400 | controller.flush(); 401 | } 402 | 403 | void Roboteq::odom_loop() 404 | { 405 | 406 | uint32_t nowtime = millis(); 407 | 408 | // if we haven't received encoder counts in some time then restart streaming 409 | if (DELTAT(nowtime, odom_last_time) >= 1000) 410 | { 411 | odom_stream(); 412 | odom_last_time = nowtime; 413 | } 414 | 415 | // read sensor data stream from motor controller 416 | // maybe use while loop to improve cpu usage? 417 | if (controller.available()) 418 | { 419 | char ch = 0; 420 | if (controller.read((uint8_t *)&ch, 1) == 0) 421 | return; 422 | if (ch == '\r') 423 | { 424 | odom_buf[odom_idx] = 0; 425 | // CR= is encoder counts 426 | if (odom_buf[0] == 'C' && odom_buf[1] == 'R' && odom_buf[2] == '=') 427 | { 428 | unsigned int delim; 429 | for (delim = 3; delim < odom_idx; delim++) 430 | { 431 | if (odom_encoder_toss > 0) 432 | { 433 | --odom_encoder_toss; 434 | break; 435 | } 436 | if (odom_buf[delim] == ':') 437 | { 438 | odom_buf[delim] = 0; 439 | odom_encoder_right = (int32_t)strtol(odom_buf + 3, NULL, 10); 440 | odom_encoder_left = (int32_t)strtol(odom_buf + delim + 1, NULL, 10); 441 | 442 | odom_publish(); 443 | break; 444 | } 445 | } 446 | } 447 | 448 | odom_idx = 0; 449 | } 450 | else if (odom_idx < (sizeof(odom_buf) - 1)) 451 | { 452 | odom_buf[odom_idx++] = ch; 453 | } 454 | } 455 | } 456 | void Roboteq::odom_publish() 457 | { 458 | 459 | // determine delta time in seconds 460 | uint32_t nowtime = millis(); 461 | float dt = (float)DELTAT(nowtime, odom_last_time) / 1000.0; 462 | odom_last_time = nowtime; 463 | 464 | // determine deltas of distance and angle 465 | float linear = ((float)odom_encoder_right / (float)encoder_cpr * wheel_circumference + (float)odom_encoder_left / (float)encoder_cpr * wheel_circumference) / 2.0; 466 | // float angular = ((float)odom_encoder_right / (float)encoder_cpr * wheel_circumference - (float)odom_encoder_left / (float)encoder_cpr * wheel_circumference) / track_width * -1.0; 467 | float angular = ((float)odom_encoder_right / (float)encoder_cpr * wheel_circumference - (float)odom_encoder_left / (float)encoder_cpr * wheel_circumference) / track_width; 468 | 469 | // Update odometry 470 | odom_x += linear * cos(odom_yaw); // m 471 | odom_y += linear * sin(odom_yaw); // m 472 | odom_yaw = NORMALIZE(odom_yaw + angular); // rad 473 | 474 | // Calculate velocities 475 | float vx = (odom_x - odom_last_x) / dt; 476 | float vy = (odom_y - odom_last_y) / dt; 477 | float vyaw = (odom_yaw - odom_last_yaw) / dt; 478 | 479 | odom_last_x = odom_x; 480 | odom_last_y = odom_y; 481 | odom_last_yaw = odom_yaw; 482 | // convert yaw to quat; 483 | tf2::Quaternion tf2_quat; 484 | tf2_quat.setRPY(0, 0, odom_yaw); 485 | // Convert tf2::Quaternion to geometry_msgs::msg::Quaternion 486 | geometry_msgs::msg::Quaternion quat = tf2::toMsg(tf2_quat); 487 | 488 | 489 | 490 | //tf2::Quaternion quat = tf2::createQuaternionMsgFromYaw(odom_yaw); 491 | // TODO: set up tf2_ros 492 | //update odom msg 493 | 494 | //odom_msg->header.seq++; //? not used in ros2 ? 495 | odom_msg.header.stamp = this->get_clock()->now(); 496 | odom_msg.pose.pose.position.x = odom_x; 497 | odom_msg.pose.pose.position.y = odom_y; 498 | odom_msg.pose.pose.position.z = 0.0; 499 | odom_msg.pose.pose.orientation = quat; 500 | odom_msg.twist.twist.linear.x = vx; 501 | odom_msg.twist.twist.linear.y = vy; 502 | odom_msg.twist.twist.linear.z = 0.0; 503 | odom_msg.twist.twist.angular.x = 0.0; 504 | odom_msg.twist.twist.angular.y = 0.0; 505 | odom_msg.twist.twist.angular.z = vyaw; 506 | odom_pub->publish(odom_msg); 507 | // odom_pub.publish(odom_msg); ROS1 508 | } 509 | 510 | int Roboteq::run() 511 | { 512 | 513 | starttime = millis(); 514 | hstimer = starttime; 515 | mstimer = starttime; 516 | lstimer = starttime; 517 | 518 | cmdvel_loop(); 519 | odom_loop(); 520 | cmdvel_run(); 521 | return 0; 522 | } 523 | 524 | Roboteq::~Roboteq() 525 | { 526 | 527 | if (controller.isOpen()){ 528 | controller.close(); 529 | } 530 | // rclcpp::shutdown(); // uncomment if node doesnt destroy properly 531 | 532 | } 533 | 534 | } // end of namespace 535 | 536 | int main(int argc, char* argv[]) 537 | { 538 | 539 | rclcpp::init(argc, argv); 540 | 541 | rclcpp::executors::SingleThreadedExecutor exec; 542 | rclcpp::NodeOptions options; 543 | auto node = std::make_shared(); 544 | exec.add_node(node); 545 | exec.spin(); 546 | rclcpp::shutdown(); 547 | return 0; 548 | 549 | 550 | } 551 | --------------------------------------------------------------------------------