上传ROS驱动

main
詹力 2024-07-03 17:19:25 +08:00
commit 94b2281895
37 changed files with 2796 additions and 0 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
## 自主机器人笨笨
采用无刷电机的大型机器人框架目前ROS驱动暂时没有验证。

View File

@ -0,0 +1,42 @@
cmake_minimum_required(VERSION 2.8.3)
project(hoverboard_driver)
add_compile_options(-std=c++11)
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
hardware_interface
controller_manager
dynamic_reconfigure
control_toolbox
rosparam_shortcuts
)
## System dependencies are found with CMake's conventions
find_package(Boost REQUIRED COMPONENTS system)
generate_dynamic_reconfigure_options(
config/Hoverboard.cfg
)
catkin_package(
INCLUDE_DIRS include
)
include_directories(
include
include/hoverboard_driver
${catkin_INCLUDE_DIRS}
${Boost_INCLUDE_DIRS}
)
add_executable(${PROJECT_NAME}
src/hoverboard.cpp
src/pid.cpp
src/main.cpp
)
target_link_libraries(${PROJECT_NAME} ${catkin_LIBRARIES} ${Boost_LIBRARIES})
add_dependencies(hoverboard_driver ${PROJECT_NAME}_gencfg)

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Alex Makarov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,26 @@
## Usage
1. Hoverboard port can be set as a "port" parameter in the node namespace. Check `hoverboard.launch` and `4x4.launch` for examples of 2WD and 4WD configurations. 4WD config is purely for illustration on how to use several ports, but was **not** tested with two hoverboards.
2. `roslaunch hoverboard_driver hoverboard.launch`
3. Use any tool (keyboard_teleop, rqt) to send speed commands to `hoverboard_velocity_controller/cmd_vel`.
In 4x4 configuration, if you want to control both axis with the same `cmd_vel`, you can apply remapping as following:
```xml
<launch>
<group ns="front">
<param name="port" type="str" value="/dev/ttyTHS1"/>
<remap from="/front/hoverboard_velocity_controller/cmd_vel" to="/cmd_vel"/>
<include file="$(find hoverboard_driver)/launch/hoverboard.launch" />
</group>
<group ns="rear">
<param name="port" type="str" value="/dev/ttyTHS2"/>
<remap from="/rear/hoverboard_velocity_controller/cmd_vel" to="/cmd_vel"/>
<include file="$(find hoverboard_driver)/launch/hoverboard.launch" />
</group>
</launch>
```
## DISCLAIMER
I bear **no responsibility** for any damage, direct or indirect, caused by using this project. Hoverboards are powerful and can be dangerous! Make sure you take all safety precautions!

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python
# Desc: Allows PID parameters and feed forward gain, etc to be tuned in realtime using dynamic reconfigure
PACKAGE = "hoverboard_driver"
from dynamic_reconfigure.parameter_generator_catkin import *
gen = ParameterGenerator()
# Name Type Level Description Default Min Max
gen.add( "f" , double_t, 1, "Feed forward gain.", 10.0, -100, 100)
gen.add( "p" , double_t, 1, "Proportional gain.", 10.0, -100, 100)
gen.add( "i" , double_t, 1, "Integral gain.", 0.1, -100, 100)
gen.add( "d" , double_t, 1, "Derivative gain.", 1.0, -100, 100)
gen.add( "i_clamp_min" , double_t, 1, "Min bounds for the integral windup", -10.0, -100, 0)
gen.add( "i_clamp_max" , double_t, 1, "Max bounds for the integral windup", 10.0, 0, 100)
gen.add( "antiwindup" , bool_t, 1, "Antiwindup.", False)
# PkgName #NodeName #Prefix for generated .h include file, e.g. ParametersConfig.py
exit(gen.generate(PACKAGE, "hoverboard_driver", "Hoverboard"))

View File

@ -0,0 +1,51 @@
hoverboard_joint_publisher:
type: "joint_state_controller/JointStateController"
publish_rate: 50
left_wheel : 'left_wheel'
right_wheel : 'right_wheel'
hoverboard_velocity_controller:
type : "diff_drive_controller/DiffDriveController"
left_wheel : 'left_wheel'
right_wheel : 'right_wheel'
publish_rate: 50.0 # default: 50
pose_covariance_diagonal : [0.001, 0.001, 1000000.0, 1000000.0, 1000000.0, 1000.0]
twist_covariance_diagonal: [0.001, 0.001, 1000000.0, 1000000.0, 1000000.0, 1000.0]
# Wheel separation and diameter. These are both optional.
# diff_drive_controller will attempt to read either one or both from the
# URDF if not specified as a parameter
wheel_separation : 0.32
wheel_radius : 0.0825
# Wheel separation and radius multipliers
wheel_separation_multiplier: 1.0 # default: 1.0
wheel_radius_multiplier : 1.0 # default: 1.0
# Velocity commands timeout [s], default 0.5
cmd_vel_timeout: 0.5
# Base frame_id
base_frame_id: base_footprint #default: base_link
allow_multiple_cmd_vel_publishers: true
odom_frame_id: '/raw_odom'
enable_odom_tf: false
# Velocity and acceleration limits
# Whenever a min_* is unspecified, default to -max_*
linear:
x:
has_velocity_limits : true
max_velocity : 1.0 # m/s
has_acceleration_limits: true
max_acceleration : 0.8 # m/s^2
has_jerk_limits : false
max_jerk : 0.0 # m/s^3
angular:
z:
has_velocity_limits : true # was true
max_velocity : 6.28 # rad/s
has_acceleration_limits: true # was true
max_acceleration : 3.14 # rad/s^2
has_jerk_limits : true
max_jerk : 3.14 # rad/s^3

View File

@ -0,0 +1,7 @@
robaka:
direction: 1
hardware_interface:
loop_hz: 50 # hz
joints:
- left_wheel
- right_wheel

View File

@ -0,0 +1,18 @@
Usage
Hoverboard port can be set as a "port" parameter in the node namespace. Check hoverboard.launch and 4x4.launch for examples of 2WD and 4WD configurations. 4WD config is purely for illustration on how to use several ports, but was not tested with two hoverboards.
roslaunch hoverboard_driver hoverboard.launch
Use any tool (keyboard_teleop, rqt) to send speed commands to hoverboard_velocity_controller/cmd_vel.
In 4x4 configuration, if you want to control both axis with the same cmd_vel, you can apply remapping as following:
<launch>
<group ns="front">
<param name="port" type="str" value="/dev/ttyTHS1"/>
<remap from="/front/hoverboard_velocity_controller/cmd_vel" to="/cmd_vel"/>
<include file="$(find hoverboard_driver)/launch/hoverboard.launch" />
</group>
<group ns="rear">
<param name="port" type="str" value="/dev/ttyTHS2"/>
<remap from="/rear/hoverboard_velocity_controller/cmd_vel" to="/cmd_vel"/>
<include file="$(find hoverboard_driver)/launch/hoverboard.launch" />
</group>
</launch>

View File

@ -0,0 +1,10 @@
#pragma once
#define DEFAULT_PORT "/dev/ttyTHS1"
#define ENCODER_MIN 0
#define ENCODER_MAX 9000
#define ENCODER_LOW_WRAP_FACTOR 0.3
#define ENCODER_HIGH_WRAP_FACTOR 0.7
#define TICKS_PER_ROTATION 90

View File

@ -0,0 +1,74 @@
#include <ros/ros.h>
#include <controller_manager/controller_manager.h>
#include <hardware_interface/joint_command_interface.h>
#include <hardware_interface/joint_state_interface.h>
#include <hardware_interface/robot_hw.h>
#include <realtime_tools/realtime_buffer.h>
#include <std_msgs/Float64.h>
#include <std_msgs/Bool.h>
#include <dynamic_reconfigure/server.h>
#include <string>
#include "hoverboard_driver/HoverboardConfig.h"
#include "hoverboard_driver/pid.h"
#include "protocol.h"
class HoverboardAPI;
class Hoverboard : public hardware_interface::RobotHW {
public:
Hoverboard();
~Hoverboard();
void read();
void write(const ros::Time& time, const ros::Duration& period);
void tick();
private:
void protocol_recv (char c);
void on_encoder_update (int16_t right, int16_t left);
hardware_interface::JointStateInterface joint_state_interface;
hardware_interface::VelocityJointInterface velocity_joint_interface;
// The units for wheels are radians (pos), radians per second (vel,cmd), and Netwton metres (eff)
struct Joint {
std_msgs::Float64 pos;
std_msgs::Float64 vel;
std_msgs::Float64 eff;
std_msgs::Float64 cmd;
} joints[2];
// Publishers
ros::NodeHandle nh;
ros::Publisher vel_pub[2];
ros::Publisher pos_pub[2];
ros::Publisher cmd_pub[2];
ros::Publisher voltage_pub;
ros::Publisher temp_pub;
ros::Publisher connected_pub;
double wheel_radius;
double max_velocity = 0.0;
int direction_correction = 1;
std::string port;
ros::Time last_read;
// Last known encoder values
int16_t last_wheelcountR;
int16_t last_wheelcountL;
// Count of full encoder wraps
int multR;
int multL;
// Thresholds for calculating the wrap
int low_wrap;
int high_wrap;
// Hoverboard protocol
int port_fd;
int msg_len = 0;
char prev_byte = 0;
uint16_t start_frame = 0;
char* p;
SerialFeedback msg, prev_msg;
PID pids[2];
};

View File

@ -0,0 +1,134 @@
// This file is based on work by Franz Pucher, Diffbot, (2020), GitHub repository, https://github.com/fjp/diffbot
#include <ros/duration.h>
#include <control_toolbox/pid.h>
// Dynamic reconfigure
#include <dynamic_reconfigure/server.h>
#include "hoverboard_driver/HoverboardConfig.h"
#include <boost/thread/mutex.hpp>
class PID : public control_toolbox::Pid
{
public:
/**
* @brief Construct a new PID object
*
* @param p The proportional gain.
* @param i The integral gain.
* @param d The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param out_min The min computed output.
* @param out_max The max computed output.
*/
PID(double p=0.0, double i=0.0, double d=0.0, double i_max=0.0, double i_min=0.0, bool antiwindup=false, double out_max=0.0, double out_min=0.0);
/**
* @brief Initialize the
*
* @param nh ROS node handle with possible namespace describing the purpose of the PID controller.
* @param kF The feed forward gain.
* @param kP The proportional gain.
* @param kI The integral gain.
* @param kD The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param antiwindup
* @param out_min The min computed output.
* @param out_max The max computed output.
*/
void init(ros::NodeHandle& nh, double f, double p, double i, double d, double i_max, double i_min, bool antiwindup, double out_max, double out_min);
/**
* @brief Compute PID output value from error using process value, set point and time period
*
* @param measured_value The process value measured by sensors.
* @param setpoint The desired target value.
* @param dt The delta time or period since the last call.
* @return double Computed PID output value.
*/
double operator()(const double &measured_value, const double &setpoint, const ros::Duration &dt);
/**
* @brief Get the FPID parameters
*
* @param f The feed forward gain.
* @param p The proportional gain.
* @param i The integral gain.
* @param d The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param antiwindup Enable or disable antiwindup check.
*/
void getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min);
void getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min, bool &antiwindup);
/**
* @brief Set the Parameters using the Gains object of control_toolbox::Pid
*
* @param f The feed forward gain.
* @param p The proportional gain.
* @param i The integral gain.
* @param d The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param antiwindup Enable or disable antiwindup check.
*/
void setParameters(double f, double p, double i, double d, double i_max, double i_min, bool antiwindup = false);
/**
* @brief Set the Output Limits of the PID controller
*
* @param out_min
* @param out_max
*/
void setOutputLimits(double out_min, double out_max);
/**
* @brief Clam given value to upper and lower limits.
*
* @param value Input value that's possibly clamped.
* @param lower_limit Lower limit which the value must not exceed.
* @param upper_limit Upper limit which the value must not exceed.
* @return double Clamped value to range in between [lower_limit, upper_limit].
*/
double clamp(const double& value, const double& lower_limit, const double& upper_limit);
/**
* @brief Get the current error.
*
* @return double The current error computed from the measured and target value.
*/
inline double getError() const { return error_; };
/**
* @brief Start the dynamic reconfigure node and load the default values
* @param node - a node handle where dynamic reconfigure services will be published
*/
void initDynamicReconfig(ros::NodeHandle &node);
/**
* @brief Set Dynamic Reconfigure's gains to PID's values
*/
void updateDynamicReconfig();
void updateDynamicReconfig(Gains gains_config);
void updateDynamicReconfig(hoverboard_driver::HoverboardConfig config);
/**
* \brief Update the PID parameters from dynamics reconfigure
*/
void dynamicReconfigCallback(hoverboard_driver::HoverboardConfig &config, uint32_t /*level*/);
private:
double f_;
double error_;
double out_min_;
double out_max_;
// Dynamic reconfigure
bool dynamic_reconfig_initialized_;
typedef dynamic_reconfigure::Server<hoverboard_driver::HoverboardConfig> DynamicReconfigServer;
boost::shared_ptr<DynamicReconfigServer> param_reconfig_server_;
DynamicReconfigServer::CallbackType param_reconfig_callback_;
boost::recursive_mutex param_reconfig_mutex_;
};

View File

@ -0,0 +1,33 @@
// Protocol support file
// Based on Arduino sample https://github.com/EmanuelFeru/hoverboard-firmware-hack-FOC/blob/master/Arduino/hoverserial/hoverserial.ino
// From Emanuel FERU's hoverboard-firmware-hack-FOC firmware
#ifndef _FOC_PROTOCOL_H
#define _FOC_PROTOCOL_H
#define START_FRAME 0xABCD
// 串口命令信息
typedef struct {
uint16_t start; // 开始帧头
int16_t steer;
int16_t speed; //
uint16_t checksum;
} SerialCommand;
// 串口的返回信息
typedef struct {
uint16_t start; // 开始帧头
int16_t cmd1; // cmd1
int16_t cmd2; // cmd2
int16_t speedR_meas; // 右轮测速
int16_t speedL_meas; // 左轮测速
int16_t wheelR_cnt; // 右轮计数
int16_t wheelL_cnt; // 左轮计数
int16_t batVoltage; // 电池电压
int16_t boardTemp; // 板子温度
uint16_t cmdLed; // LED命令
uint16_t checksum; // 校验位
} SerialFeedback;
#endif

View File

@ -0,0 +1,10 @@
<launch>
<group ns="front">
<param name="port" type="str" value="/dev/ttyTHS1"/>
<include file="$(find hoverboard_driver)/launch/hoverboard.launch" />
</group>
<group ns="rear">
<param name="port" type="str" value="/dev/ttyTHS2"/>
<include file="$(find hoverboard_driver)/launch/hoverboard.launch" />
</group>
</launch>

View File

@ -0,0 +1,10 @@
<launch>
<!-- Include this in your launch file to change port -->
<!-- <param name="port" type="str" value="/dev/ttyTHS1"/> -->
<rosparam file="$(find hoverboard_driver)/config/hardware.yaml" command="load"/>
<rosparam file="$(find hoverboard_driver)/config/controllers.yaml" command="load"/>
<node name="hoverboard_driver" pkg="hoverboard_driver" type="hoverboard_driver" output="screen"/>
<node name="controller_spawner" pkg="controller_manager" type="spawner" respawn="false" output="screen"
args="hoverboard_joint_publisher hoverboard_velocity_controller" />
</launch>

View File

@ -0,0 +1,26 @@
<?xml version="1.0"?>
<package format="2">
<name>hoverboard_driver</name>
<version>0.0.1</version>
<description>Driver for hoverboard motors</description>
<maintainer email="road.and.track@gmail.com">road.and.track@gmail.com</maintainer>
<license>BSD</license>
<buildtool_depend>catkin</buildtool_depend>
<build_depend>roscpp</build_depend>
<build_depend>std_msgs</build_depend>
<build_depend>hardware_interface</build_depend>
<build_depend>controller_manager</build_depend>
<build_depend>control_toolbox</build_depend>
<build_depend>rosparam_shortcuts</build_depend>
<build_export_depend>roscpp</build_export_depend>
<build_export_depend>std_msgs</build_export_depend>
<exec_depend>roscpp</exec_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>hardware_interface</exec_depend>
<exec_depend>controller_manager</exec_depend>
<exec_depend>rosparam_shortcuts</exec_depend>
<exec_depend>control_toolbox</exec_depend>
<depend>dynamic_reconfigure</depend>
<export>
</export>
</package>

View File

@ -0,0 +1,277 @@
#include "config.h"
#include "hoverboard.h"
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <dynamic_reconfigure/server.h>
#include <rosparam_shortcuts/rosparam_shortcuts.h>
Hoverboard::Hoverboard() {
hardware_interface::JointStateHandle left_wheel_state_handle("left_wheel",
&joints[0].pos.data,
&joints[0].vel.data,
&joints[0].eff.data);
hardware_interface::JointStateHandle right_wheel_state_handle("right_wheel",
&joints[1].pos.data,
&joints[1].vel.data,
&joints[1].eff.data);
joint_state_interface.registerHandle (left_wheel_state_handle);
joint_state_interface.registerHandle (right_wheel_state_handle);
registerInterface(&joint_state_interface);
hardware_interface::JointHandle left_wheel_vel_handle(
joint_state_interface.getHandle("left_wheel"),
&joints[0].cmd.data);
hardware_interface::JointHandle right_wheel_vel_handle(
joint_state_interface.getHandle("right_wheel"),
&joints[1].cmd.data);
velocity_joint_interface.registerHandle (left_wheel_vel_handle);
velocity_joint_interface.registerHandle (right_wheel_vel_handle);
registerInterface(&velocity_joint_interface);
// These publishers are only for debugging purposes
vel_pub[0] = nh.advertise<std_msgs::Float64>("hoverboard/left_wheel/velocity", 3);
vel_pub[1] = nh.advertise<std_msgs::Float64>("hoverboard/right_wheel/velocity", 3);
pos_pub[0] = nh.advertise<std_msgs::Float64>("hoverboard/left_wheel/position", 3);
pos_pub[1] = nh.advertise<std_msgs::Float64>("hoverboard/right_wheel/position", 3);
cmd_pub[0] = nh.advertise<std_msgs::Float64>("hoverboard/left_wheel/cmd", 3);
cmd_pub[1] = nh.advertise<std_msgs::Float64>("hoverboard/right_wheel/cmd", 3);
voltage_pub = nh.advertise<std_msgs::Float64>("hoverboard/battery_voltage", 3);
temp_pub = nh.advertise<std_msgs::Float64>("hoverboard/temperature", 3);
connected_pub = nh.advertise<std_msgs::Bool>("hoverboard/connected", 3);
std::size_t error = 0;
error += !rosparam_shortcuts::get("hoverboard_driver", nh, "hoverboard_velocity_controller/wheel_radius", wheel_radius);
error += !rosparam_shortcuts::get("hoverboard_driver", nh, "hoverboard_velocity_controller/linear/x/max_velocity", max_velocity);
error += !rosparam_shortcuts::get("hoverboard_driver", nh, "robaka/direction", direction_correction);
rosparam_shortcuts::shutdownIfError("hoverboard_driver", error);
if (!rosparam_shortcuts::get("hoverboard_driver", nh, "port", port)) {
port = DEFAULT_PORT;
ROS_WARN("Port is not set in config, using default %s", port.c_str());
} else {
ROS_INFO("Using port %s", port.c_str());
}
// Convert m/s to rad/s
max_velocity /= wheel_radius;
low_wrap = ENCODER_LOW_WRAP_FACTOR*(ENCODER_MAX - ENCODER_MIN) + ENCODER_MIN;
high_wrap = ENCODER_HIGH_WRAP_FACTOR*(ENCODER_MAX - ENCODER_MIN) + ENCODER_MIN;
last_wheelcountR = last_wheelcountL = 0;
multR = multL = 0;
ros::NodeHandle nh_left(nh, "pid/left");
ros::NodeHandle nh_right(nh, "pid/right");
// Init PID controller
pids[0].init(nh_left, 1.0, 0.0, 0.0, 0.01, 1.5, -1.5, true, max_velocity, -max_velocity);
pids[0].setOutputLimits(-max_velocity, max_velocity);
pids[1].init(nh_right, 1.0, 0.0, 0.0, 0.01, 1.5, -1.5, true, max_velocity, -max_velocity);
pids[1].setOutputLimits(-max_velocity, max_velocity);
if ((port_fd = open(port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY)) < 0) {
ROS_FATAL("Cannot open serial port to hoverboard");
exit(-1);
}
// CONFIGURE THE UART -- connecting to the board
// The flags (defined in /usr/include/termios.h - see http://pubs.opengroup.org/onlinepubs/007908799/xsh/termios.h.html):
struct termios options;
tcgetattr(port_fd, &options);
options.c_cflag = B115200 | CS8 | CLOCAL | CREAD; //<Set baud rate
options.c_iflag = IGNPAR;
options.c_oflag = 0;
options.c_lflag = 0;
tcflush(port_fd, TCIFLUSH);
tcsetattr(port_fd, TCSANOW, &options);
}
Hoverboard::~Hoverboard() {
if (port_fd != -1)
close(port_fd);
}
void Hoverboard::read() {
if (port_fd != -1) {
unsigned char c;
int i = 0, r = 0;
while ((r = ::read(port_fd, &c, 1)) > 0 && i++ < 1024)
protocol_recv(c);
if (i > 0)
last_read = ros::Time::now();
if (r < 0 && errno != EAGAIN)
ROS_ERROR("Reading from serial %s failed: %d", port.c_str(), r);
}
if ((ros::Time::now() - last_read).toSec() > 1) {
ROS_FATAL("Timeout reading from serial %s failed", port.c_str());
//publish false when not receiving serial data
std_msgs::Bool b;
b.data = false;
connected_pub.publish(b);
} else {
//we must be connected - publish true
std_msgs::Bool b;
b.data = true;
connected_pub.publish(b);
}
}
// 协议读取函数
void Hoverboard::protocol_recv (char byte) {
start_frame = ((uint16_t)(byte) << 8) | (uint8_t)prev_byte;
// Read the start frame
if (start_frame == START_FRAME) {
p = (char*)&msg;
*p++ = prev_byte;
*p++ = byte;
msg_len = 2;
} else if (msg_len >= 2 && msg_len < sizeof(SerialFeedback)) {
// Otherwise just read the message content until the end
*p++ = byte;
msg_len++;
}
if (msg_len == sizeof(SerialFeedback)) {
uint16_t checksum = (uint16_t)(
msg.start ^
msg.cmd1 ^
msg.cmd2 ^
msg.speedR_meas ^
msg.speedL_meas ^
msg.wheelR_cnt ^
msg.wheelL_cnt ^
msg.batVoltage ^
msg.boardTemp ^
msg.cmdLed);
// 校验帧头和校验位
if (msg.start == START_FRAME && msg.checksum == checksum) {
std_msgs::Float64 f;
// 获取电压信息并发布
f.data = (double)msg.batVoltage/100.0;
voltage_pub.publish(f);
// 获取板子信息
f.data = (double)msg.boardTemp/10.0;
temp_pub.publish(f);
// 转换 RPM to RAD/S
joints[0].vel.data = direction_correction * (abs(msg.speedL_meas) * 0.10472);
joints[1].vel.data = direction_correction * (abs(msg.speedR_meas) * 0.10472);
// 发布x和y方向的速度
vel_pub[0].publish(joints[0].vel);
vel_pub[1].publish(joints[1].vel);
// Process encoder values and update odometry
on_encoder_update (msg.wheelR_cnt, msg.wheelL_cnt);
} else {
ROS_WARN("Hoverboard checksum mismatch: %d vs %d", msg.checksum, checksum);
}
msg_len = 0;
}
prev_byte = byte;
}
void Hoverboard::write(const ros::Time& time, const ros::Duration& period) {
if (port_fd == -1) {
ROS_ERROR("Attempt to write on closed serial");
return;
}
// Inform interested parties about the commands we've got
// 把命令发送出去(速度信息发布)
cmd_pub[0].publish(joints[0].cmd);
cmd_pub[1].publish(joints[1].cmd);
// 计算PID输出
double pid_outputs[2];
pid_outputs[0] = pids[0](joints[0].vel.data, joints[0].cmd.data, period);
pid_outputs[1] = pids[1](joints[1].vel.data, joints[1].cmd.data, period);
// Convert PID outputs in RAD/S to RPM
double set_speed[2] = {
pid_outputs[0] / 0.10472,
pid_outputs[1] / 0.10472
};
// Calculate steering from difference of left and right
// 根据左右差计算转向
const double speed = (set_speed[0] + set_speed[1]) / 2.0;
const double steer = (set_speed[0] - speed)*2.0;
SerialCommand command;
command.start = (uint16_t)START_FRAME;
command.steer = (int16_t)steer;
command.speed = (int16_t)speed;
command.checksum = (uint16_t)(command.start ^ command.steer ^ command.speed);
int rc = ::write(port_fd, (const void*)&command, sizeof(command));
if (rc < 0) {
ROS_ERROR("Error writing to hoverboard serial port");
}
}
void Hoverboard::on_encoder_update (int16_t right, int16_t left) {
double posL = 0.0, posR = 0.0;
// Calculate wheel position in ticks, factoring in encoder wraps
if (right < low_wrap && last_wheelcountR > high_wrap)
multR++;
else if (right > high_wrap && last_wheelcountR < low_wrap)
multR--;
posR = right + multR*(ENCODER_MAX-ENCODER_MIN);
last_wheelcountR = right;
if (left < low_wrap && last_wheelcountL > high_wrap)
multL++;
else if (left > high_wrap && last_wheelcountL < low_wrap)
multL--;
posL = left + multL*(ENCODER_MAX-ENCODER_MIN);
last_wheelcountL = left;
// When the board shuts down and restarts, wheel ticks are reset to zero so the robot can be suddently lost
// This section accumulates ticks even if board shuts down and is restarted
static double lastPosL = 0.0, lastPosR = 0.0;
static double lastPubPosL = 0.0, lastPubPosR = 0.0;
static bool nodeStartFlag = true;
//IF there has been a pause in receiving data AND the new number of ticks is close to zero, indicates a board restard
//(the board seems to often report 1-3 ticks on startup instead of zero)
//reset the last read ticks to the startup values
if((ros::Time::now() - last_read).toSec() > 0.2
&& abs(posL) < 5 && abs(posR) < 5){
lastPosL = posL;
lastPosR = posR;
}
double posLDiff = 0;
double posRDiff = 0;
//if node is just starting keep odom at zeros
if(nodeStartFlag){
nodeStartFlag = false;
}else{
posLDiff = posL - lastPosL;
posRDiff = posR - lastPosR;
}
lastPubPosL += posLDiff;
lastPubPosR += posRDiff;
lastPosL = posL;
lastPosR = posR;
// Convert position in accumulated ticks to position in radians
joints[0].pos.data = 2.0*M_PI * lastPubPosL/(double)TICKS_PER_ROTATION;
joints[1].pos.data = 2.0*M_PI * lastPubPosR/(double)TICKS_PER_ROTATION;
pos_pub[0].publish(joints[0].pos);
pos_pub[1].publish(joints[1].pos);
}

View File

@ -0,0 +1,31 @@
#include <ros/ros.h>
#include <controller_manager/controller_manager.h>
#include <dynamic_reconfigure/server.h>
#include "hoverboard.h"
int main(int argc, char **argv) {
ros::init(argc, argv, "hoverboard_driver");
Hoverboard hoverboard;
controller_manager::ControllerManager cm(&hoverboard);
ros::AsyncSpinner spinner(1);
spinner.start();
ros::Time prev_time = ros::Time::now();
ros::Rate rate(100.0);
while (ros::ok()) {
const ros::Time time = ros::Time::now();
const ros::Duration period = time - prev_time;
prev_time = time;
hoverboard.read();
cm.update(time, period);
hoverboard.write(time, period);
rate.sleep();
}
return 0;
}

View File

@ -0,0 +1,161 @@
// Based on work by Franz Pucher, Diffbot, (2020), GitHub repository, https://github.com/fjp/diffbot
#include "hoverboard_driver/pid.h"
PID::PID(double p, double i, double d, double i_max, double i_min, bool antiwindup, double out_max, double out_min)
: control_toolbox::Pid()
, dynamic_reconfig_initialized_(false)
{
f_ = 0.0;
initPid(p, i, d, i_max, i_min, antiwindup);
error_ = 0.0;
out_min_ = out_min;
out_max_ = out_max;
}
void PID::init(ros::NodeHandle& nh, double f, double p, double i, double d, double i_max, double i_min, bool antiwindup, double out_max, double out_min)
{
ROS_INFO("Initialize PID");
f_ = f;
initPid(p, i, d, i_max, i_min, antiwindup);
error_ = 0.0;
out_min_ = out_min;
out_max_ = out_max;
initDynamicReconfig(nh);
Gains gains = getGains();
ROS_INFO_STREAM("Initialized PID: F=" << f << ", P=" << gains.p_gain_ << ", I=" << gains.i_gain_ << ", D=" << gains.d_gain_ << ", out_min=" << out_min_ << ", out_max=" << out_max_);
}
double PID::operator()(const double &measured_value, const double &setpoint, const ros::Duration &dt)
{
// Compute error terms
error_ = setpoint - measured_value;
ROS_DEBUG_STREAM_THROTTLE(1, "Error: " << error_);
// Reset the i_error in case the p_error and the setpoint is zero
// Otherwise there will always be a constant i_error_ that won't vanish
if (0.0 == setpoint && 0.0 == error_)
{
// reset() will reset
// p_error_last_ = 0.0;
// p_error_ = 0.0;
// i_error_ = 0.0;
// d_error_ = 0.0;
// cmd_ = 0.0;
reset();
}
// Use control_toolbox::Pid::computeCommand()
double output = computeCommand(error_, dt);
ROS_DEBUG_STREAM_THROTTLE(1, "PID computed command: " << output);
// Compute final output including feed forward term
output = f_ * setpoint + output;
//output = clamp(output, out_min_, out_max_);
return output;
}
void PID::getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min)
{
bool antiwindup;
getParameters(f, p, i, d, i_max, i_min, antiwindup);
}
void PID::getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min, bool &antiwindup)
{
f = f_;
// Call getGains from control_toolbox
getGains(p, i, d, i_max, i_min, antiwindup);
}
void PID::setParameters(double f, double p, double i, double d, double i_max, double i_min, bool antiwindup)
{
f_ = f;
setGains(p, i, d, i_max, i_min, antiwindup);
Gains gains = getGains();
ROS_INFO_STREAM("Update PID Gains: F=" << f << ", P=" << gains.p_gain_ << ", I=" << gains.i_gain_ << ", D=" << gains.d_gain_ << ", out_min=" << out_min_ << ", out_max=" << out_max_);
}
void PID::setOutputLimits(double output_max, double output_min)
{
out_max_ = output_max;
out_min_ = output_min;
ROS_INFO_STREAM("Update PID output limits: lower=" << out_min_ << ", upper=" << out_max_);
}
double PID::clamp(const double& value, const double& lower_limit, const double& upper_limit)
{
if (value > upper_limit)
{
ROS_DEBUG_STREAM_THROTTLE(1, "Clamp " << value << " to upper limit " << upper_limit);
return upper_limit;
}
else if (value < lower_limit)
{
ROS_DEBUG_STREAM_THROTTLE(1, "Clamp " << value << " to lower limit " << upper_limit);
return lower_limit;
}
return value;
}
void PID::initDynamicReconfig(ros::NodeHandle &node)
{
ROS_INFO_STREAM_NAMED("pid","Initializing dynamic reconfigure in namespace "
<< node.getNamespace());
// Start dynamic reconfigure server
param_reconfig_server_.reset(new DynamicReconfigServer(param_reconfig_mutex_, node));
dynamic_reconfig_initialized_ = true;
// Set Dynamic Reconfigure's gains to Pid's values
updateDynamicReconfig();
// Set callback
param_reconfig_callback_ = boost::bind(&PID::dynamicReconfigCallback, this, _1, _2);
param_reconfig_server_->setCallback(param_reconfig_callback_);
ROS_INFO_NAMED("pid", "Initialized dynamic reconfigure");
}
void PID::updateDynamicReconfig()
{
// Make sure dynamic reconfigure is initialized
if(!dynamic_reconfig_initialized_)
return;
// Get starting values
hoverboard_driver::HoverboardConfig config;
config.f = f_;
// Get starting values
getGains(config.p, config.i, config.d, config.i_clamp_max, config.i_clamp_min, config.antiwindup);
updateDynamicReconfig(config);
}
void PID::updateDynamicReconfig(hoverboard_driver::HoverboardConfig config)
{
// Make sure dynamic reconfigure is initialized
if(!dynamic_reconfig_initialized_)
return;
// Set starting values, using a shared mutex with dynamic reconfig
param_reconfig_mutex_.lock();
param_reconfig_server_->updateConfig(config);
param_reconfig_mutex_.unlock();
}
void PID::dynamicReconfigCallback(hoverboard_driver::HoverboardConfig &config, uint32_t /*level*/)
{
ROS_DEBUG_STREAM_NAMED("pid","Dynamics reconfigure callback recieved.");
// Set the gains
setParameters(config.f, config.p, config.i, config.d, config.i_clamp_max, config.i_clamp_min, config.antiwindup);
}

View File

@ -0,0 +1,76 @@
cmake_minimum_required(VERSION 3.16)
project(hoverboard_driver LANGUAGES CXX)
if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
add_compile_options(-Wall -Wextra)
endif()
# find dependencies
set(THIS_PACKAGE_INCLUDE_DEPENDS
hardware_interface
pluginlib
rclcpp
rclcpp_lifecycle
control_toolbox
)
# find dependencies
find_package(ament_cmake REQUIRED)
foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS})
find_package(${Dependency} REQUIRED)
endforeach()
## COMPILE
add_library(
hoverboard_driver
SHARED
hardware/hoverboard_driver.cpp
hardware/pid.cpp
)
target_compile_features(hoverboard_driver PUBLIC cxx_std_17)
target_include_directories(hoverboard_driver PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/hardware/include>
$<INSTALL_INTERFACE:include/hoverboard_driver>
)
ament_target_dependencies(
hoverboard_driver PUBLIC
${THIS_PACKAGE_INCLUDE_DEPENDS}
)
# Causes the visibility macros to use dllexport rather than dllimport,
# which is appropriate when building the dll but not consuming it.
target_compile_definitions(${PROJECT_NAME} PRIVATE "HOVERBOARD_DRIVER_BUILDING_DLL")
# Export hardware plugins
pluginlib_export_plugin_description_file(hardware_interface hoverboard_driver.xml)
# INSTALL
install(
DIRECTORY hardware/include/
DESTINATION include/hoverboard_driver
)
install(
DIRECTORY description/launch description/ros2_control description/urdf
DESTINATION share/hoverboard_driver
)
install(
DIRECTORY bringup/launch bringup/config
DESTINATION share/hoverboard_driver
)
install(TARGETS hoverboard_driver
EXPORT export_hoverboard_driver
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
if(BUILD_TESTING)
find_package(ament_cmake_gtest REQUIRED)
endif()
## EXPORTS
ament_export_targets(export_hoverboard_driver HAS_LIBRARY_TARGET)
ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS})
ament_package()

View File

@ -0,0 +1,43 @@
# Hints
Connect Hoverboard PCB (USART3, GND, TX, RX, no VCC!!) to USB-TTL converter (or UART interface of your SBC).
Set the serial port according to your setup in hoverboard_driver.ros2_control.xacro file
# Launch
```
ros2 launch hoverboard_driver diffbot.launch.py
```
# Classes
The entire package consists of three main classes:
1. hoverboard_driver_node
2. hoverboard_driver
3. pid
whereas the first two are implemented in the same file named hoverboard_driver.cpp (blame on me)
## hoverboard_driver
This is the main class and implements the hardare interface for diff_drove_controller itself. This class is responsible for read/write to hardware.
## hoverboard_driver_node
This class is a member/attribute of hoverboard_driver class and acts as a helper class. As hoverboard_driver itselfs derives from hardware_interface::SystemInterface class, hoverboard_driver class is not able to define publishers or settable parameters. To overcome this, hoverboard_driver_node has been introduced. This class derives from rclcpp::Node and can therefore publish topics and also can define reconfigurable parameters.
This class publishes state of Hoverboard PCB like velocity, pose, command, voltage, temperature, battery level,current consumption of each motor and the state of serial interface(connected, not connected).
Also this class provides dynamic parameters for PID controller
## pid
The PID class is defined as /attribute of overboard_Driver class (array, of PIDs, one per wheel).
It works in general but it's not active now as I idn't find proper PID settings so far.
To activate, change this code section in hoverboard_driver.cpp
``` // Convert PID outputs in RAD/S to RPM
//double set_speed[2] = {
// pid_outputs[0] / 0.10472,
// pid_outputs[1] / 0.10472};
double set_speed[2] = {
hw_commands_[left_wheel] / 0.10472,
hw_commands_[right_wheel] / 0.10472
};
```

View File

@ -0,0 +1,57 @@
controller_manager:
ros__parameters:
update_rate: 10 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
hoverboard_base_controller:
type: diff_drive_controller/DiffDriveController
hoverboard_base_controller:
ros__parameters:
left_wheel_names: ["left_wheel_joint"]
right_wheel_names: ["right_wheel_joint"]
wheel_separation: 0.32
#wheels_per_side: 1 # actually 2, but both are controlled by 1 signal
wheel_radius: 0.0825
wheel_separation_multiplier: 1.0
left_wheel_radius_multiplier: 1.0
right_wheel_radius_multiplier: 1.0
publish_rate: 50.0
odom_frame_id: odom
base_frame_id: base_link
pose_covariance_diagonal : [0.001, 0.001, 0.001, 0.001, 0.001, 0.01]
twist_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.01]
open_loop: true
enable_odom_tf: true
cmd_vel_timeout: 0.5
#publish_limited_velocity: true
use_stamped_vel: false
#velocity_rolling_window_size: 10
# Velocity and acceleration limits
# Whenever a min_* is unspecified, default to -max_*
linear.x.has_velocity_limits: true
linear.x.has_acceleration_limits: true
linear.x.has_jerk_limits: false
linear.x.max_velocity: 1.0
linear.x.min_velocity: -1.0
linear.x.max_acceleration: 1.0
linear.x.max_jerk: 0.0
linear.x.min_jerk: 0.0
angular.z.has_velocity_limits: true
angular.z.has_acceleration_limits: true
angular.z.has_jerk_limits: false
angular.z.max_velocity: 1.0
angular.z.min_velocity: -1.0
angular.z.max_acceleration: 1.0
angular.z.min_acceleration: -1.0
angular.z.max_jerk: 0.0
angular.z.min_jerk: 0.0

View File

@ -0,0 +1,122 @@
# Copyright 2020 ros2_control Development Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, RegisterEventHandler
from launch.conditions import IfCondition
from launch.event_handlers import OnProcessExit
from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
# Declare arguments
declared_arguments = []
declared_arguments.append(
DeclareLaunchArgument(
"gui",
default_value="true",
description="Start RViz2 automatically with this launch file.",
)
)
# Initialize Arguments
gui = LaunchConfiguration("gui")
# Get URDF via xacro
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
PathJoinSubstitution(
[FindPackageShare("hoverboard_driver"), "urdf", "diffbot.urdf.xacro"]
),
]
)
robot_description = {"robot_description": robot_description_content}
robot_controllers = PathJoinSubstitution(
[
FindPackageShare("hoverboard_driver"),
"config",
"hoverboard_controllers.yaml",
]
)
# rviz_config_file = PathJoinSubstitution(
# [FindPackageShare("ros2_control_demo_description"), "diffbot/rviz", "diffbot.rviz"]
# )
control_node = Node(
package="controller_manager",
executable="ros2_control_node",
parameters=[robot_description, robot_controllers],
output="both",
)
robot_state_pub_node = Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description],
remappings=[
("/hoverboard_base_controller/cmd_vel_unstamped", "/cmd_vel"),
],
)
#rviz_node = Node(
# package="rviz2",
# executable="rviz2",
# name="rviz2",
# output="log",
# arguments=["-d", rviz_config_file],
# condition=IfCondition(gui),
#)
joint_state_broadcaster_spawner = Node(
package="controller_manager",
executable="spawner",
arguments=["joint_state_broadcaster", "--controller-manager", "/controller_manager"],
)
robot_controller_spawner = Node(
package="controller_manager",
executable="spawner",
arguments=["hoverboard_base_controller", "--controller-manager", "/controller_manager"],
)
# Delay rviz start after `joint_state_broadcaster`
#delay_rviz_after_joint_state_broadcaster_spawner = RegisterEventHandler(
# event_handler=OnProcessExit(
# target_action=joint_state_broadcaster_spawner,
# on_exit=[rviz_node],
# )
#)
# Delay start of robot_controller after `joint_state_broadcaster`
delay_robot_controller_spawner_after_joint_state_broadcaster_spawner = RegisterEventHandler(
event_handler=OnProcessExit(
target_action=joint_state_broadcaster_spawner,
on_exit=[robot_controller_spawner],
)
)
nodes = [
control_node,
robot_state_pub_node,
joint_state_broadcaster_spawner,
# delay_rviz_after_joint_state_broadcaster_spawner,
delay_robot_controller_spawner_after_joint_state_broadcaster_spawner,
]
return LaunchDescription(declared_arguments + nodes)

View File

@ -0,0 +1,111 @@
# Copyright 2021 Stogl Robotics Consulting UG (haftungsbeschränkt)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition
from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
# Declare arguments
declared_arguments = []
declared_arguments.append(
DeclareLaunchArgument(
"description_package",
default_value="ros2_control_demo_description",
description="Description package with robot URDF/xacro files. Usually the argument \
is not set, it enables use of a custom description.",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"description_file",
default_value="diffbot.urdf.xacro",
description="URDF/XACRO description file with the robot.",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"gui",
default_value="true",
description="Start Rviz2 and Joint State Publisher gui automatically \
with this launch file.",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"prefix",
default_value='""',
description="Prefix of the joint names, useful for \
multi-robot setup. If changed than also joint names in the controllers' configuration \
have to be updated.",
)
)
# Initialize Arguments
description_package = LaunchConfiguration("description_package")
description_file = LaunchConfiguration("description_file")
gui = LaunchConfiguration("gui")
prefix = LaunchConfiguration("prefix")
# Get URDF via xacro
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
PathJoinSubstitution(
[FindPackageShare("hoverboard_driver"), "urdf", description_file]
),
" ",
"prefix:=",
prefix,
]
)
robot_description = {"robot_description": robot_description_content}
rviz_config_file = PathJoinSubstitution(
[FindPackageShare(description_package), "diffbot/rviz", "diffbot_view.rviz"]
)
joint_state_publisher_node = Node(
package="joint_state_publisher_gui",
executable="joint_state_publisher_gui",
condition=IfCondition(gui),
)
robot_state_publisher_node = Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description],
)
rviz_node = Node(
package="rviz2",
executable="rviz2",
name="rviz2",
output="log",
arguments=["-d", rviz_config_file],
condition=IfCondition(gui),
)
nodes = [
joint_state_publisher_node,
robot_state_publisher_node,
rviz_node,
]
return LaunchDescription(declared_arguments + nodes)

View File

@ -0,0 +1,27 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="hoverboard_driver_ros2_control" params="name prefix">
<ros2_control name="${name}" type="system">
<hardware>
<plugin>hoverboard_driver/hoverboard_driver</plugin>
<param name="wheel_radius">0.0825</param>
<param name="max_velocity">1.0</param>
<param name="device">/dev/serial/by-id/usb-FTDI_Quad_RS232-HS-if01-port0</param>
</hardware>
<joint name="${prefix}left_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="${prefix}right_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
</xacro:macro>
</robot>

View File

@ -0,0 +1,37 @@
<?xml version="1.0"?>
<robot>
<material name="black">
<color rgba="0.0 0.0 0.0 1.0"/>
</material>
<material name="blue">
<color rgba="0.0 0.0 0.8 1.0"/>
</material>
<material name="green">
<color rgba="0.0 0.8 0.0 1.0"/>
</material>
<material name="grey">
<color rgba="0.2 0.2 0.2 1.0"/>
</material>
<material name="orange">
<color rgba="${255/255} ${108/255} ${10/255} 1.0"/>
</material>
<material name="brown">
<color rgba="${222/255} ${207/255} ${195/255} 1.0"/>
</material>
<material name="red">
<color rgba="0.8 0.0 0.0 1.0"/>
</material>
<material name="white">
<color rgba="1.0 1.0 1.0 1.0"/>
</material>
</robot>

View File

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!-- Basic differential drive mobile base -->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="diffdrive_robot">
<xacro:arg name="prefix" default="" />
<xacro:include filename="$(find hoverboard_driver)/urdf/diffbot_description.urdf.xacro" />
<!-- Import Rviz colors -->
<xacro:include filename="$(find hoverboard_driver)/urdf/diffbot.materials.xacro" />
<!-- Import diffbot ros2_control description -->
<xacro:include filename="$(find hoverboard_driver)/ros2_control/hoverboard_driver.ros2_control.xacro" />
<xacro:diffbot prefix="$(arg prefix)" />
<xacro:hoverboard_driver_ros2_control
name="DiffBot" prefix="$(arg prefix)" />
</robot>

View File

@ -0,0 +1,184 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="diffbot" params="prefix">
<!-- Constants for robot dimensions -->
<xacro:property name="PI" value="3.1415926535897931"/>
<xacro:property name="base_mass" value="0.3" /> <!-- arbitrary value for base mass -->
<xacro:property name="base_width" value="0.1" />
<xacro:property name="base_length" value="0.1" />
<xacro:property name="base_height" value="0.05" />
<xacro:property name="wheel_mass" value="0.3" /> <!-- arbitrary value for wheel mass -->
<xacro:property name="wheel_len" value="0.020" />
<xacro:property name="wheel_radius" value="0.015" />
<xacro:property name="caster_wheel_mass" value="0.1" /> <!-- arbitrary value for caster wheel mass -->
<xacro:property name="caster_wheel_radius" value="0.015" />
<xacro:property name="z_offset" value="-${base_height/2}" /> <!-- Space btw top of beam and the each joint -->
<!-- Base Link -->
<link name="${prefix}base_link">
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="${base_width} ${base_length} ${base_height}"/>
</geometry>
</collision>
<visual>
<origin xyz="0 0 ${base_height/2}" rpy="0 0 0"/>
<geometry>
<box size="${base_width} ${base_length} ${base_height}"/>
</geometry>
<material name="orange"/>
</visual>
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="${base_mass}"/>
<inertia
ixx="${base_mass / 12.0 * (base_length*base_length + base_height*base_height)}" ixy="0.0" ixz="0.0"
iyy="${base_mass / 12.0 * (base_height*base_height + base_width*base_width)}" iyz="0.0"
izz="${base_mass / 12.0 * (base_width*base_width + base_length*base_length)}"/>
</inertial>
</link>
<joint name="${prefix}left_wheel_joint" type="continuous">
<parent link="${prefix}base_link"/>
<child link="${prefix}left_wheel"/>
<origin xyz="0 -${base_width/2} ${z_offset}" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<dynamics damping="0.2"/>
</joint>
<!-- left wheel Link -->
<link name="${prefix}left_wheel">
<collision>
<origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
<geometry>
<cylinder length="${wheel_len}" radius="${wheel_radius}"/>
</geometry>
</collision>
<visual>
<origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
<geometry>
<cylinder length="${wheel_len}" radius="${wheel_radius}"/>
</geometry>
<material name="black"/>
</visual>
<inertial>
<origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
<mass value="${wheel_mass}"/>
<inertia
ixx="${wheel_mass / 12.0 * (3*wheel_radius*wheel_radius + wheel_len*wheel_len)}" ixy="0.0" ixz="0.0"
iyy="${wheel_mass / 12.0 * (3*wheel_radius*wheel_radius + wheel_len*wheel_len)}" iyz="0.0"
izz="${wheel_mass / 2.0 * wheel_radius*wheel_radius}"/>
</inertial>
</link>
<joint name="${prefix}right_wheel_joint" type="continuous">
<parent link="${prefix}base_link"/>
<child link="${prefix}right_wheel"/>
<origin xyz="0 ${base_width/2} ${z_offset}" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<dynamics damping="0.2"/>
</joint>
<!-- right wheel Link -->
<link name="${prefix}right_wheel">
<collision>
<origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
<geometry>
<cylinder length="${wheel_len}" radius="${wheel_radius}"/>
</geometry>
</collision>
<visual>
<origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
<geometry>
<cylinder length="${wheel_len}" radius="${wheel_radius}"/>
</geometry>
<material name="black"/>
</visual>
<inertial>
<origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
<mass value="${wheel_mass}"/>
<inertia
ixx="${wheel_mass / 12.0 * (3*wheel_radius*wheel_radius + wheel_len*wheel_len)}" ixy="0.0" ixz="0.0"
iyy="${wheel_mass / 12.0 * (3*wheel_radius*wheel_radius + wheel_len*wheel_len)}" iyz="0.0"
izz="${wheel_mass / 2.0 * wheel_radius*wheel_radius}"/>
</inertial>
</link>
<joint name="${prefix}caster_frontal_wheel_joint" type="fixed">
<parent link="${prefix}base_link"/>
<child link="${prefix}caster_frontal_wheel"/>
<origin xyz="${base_width/2 - caster_wheel_radius} 0 ${z_offset}" rpy="0 0 0"/>
</joint>
<!-- caster frontal wheel Link -->
<link name="${prefix}caster_frontal_wheel">
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="${caster_wheel_radius}"/>
</geometry>
</collision>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="${caster_wheel_radius}"/>
</geometry>
<material name="white"/>
</visual>
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="${caster_wheel_mass}"/>
<inertia
ixx="${caster_wheel_mass * 0.4 * (caster_wheel_radius * caster_wheel_radius)}" ixy="0.0" ixz="0.0"
iyy="${caster_wheel_mass * 0.4 * (caster_wheel_radius * caster_wheel_radius)}" iyz="0.0"
izz="${caster_wheel_mass * 0.4 * (caster_wheel_radius * caster_wheel_radius)}"/>
</inertial>
</link>
<joint name="${prefix}caster_rear_wheel_joint" type="fixed">
<parent link="${prefix}base_link"/>
<child link="${prefix}caster_rear_wheel"/>
<origin xyz="${-base_width/2 + caster_wheel_radius} 0 ${z_offset}" rpy="0 0 0"/>
</joint>
<!-- caster rear wheel Link -->
<link name="${prefix}caster_rear_wheel">
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="${caster_wheel_radius}"/>
</geometry>
</collision>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="${caster_wheel_radius}"/>
</geometry>
<material name="white"/>
</visual>
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="${caster_wheel_mass}"/>
<inertia
ixx="${caster_wheel_mass * 0.4 * (caster_wheel_radius * caster_wheel_radius)}" ixy="0.0" ixz="0.0"
iyy="${caster_wheel_mass * 0.4 * (caster_wheel_radius * caster_wheel_radius)}" iyz="0.0"
izz="${caster_wheel_mass * 0.4 * (caster_wheel_radius * caster_wheel_radius)}"/>
</inertial>
</link>
</xacro:macro>
</robot>

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,105 @@
*********
DiffBot
*********
*DiffBot*, or ''Differential Mobile Robot'', is a simple mobile base with differential drive.
The robot is basically a box moving according to differential drive kinematics.
For *example_2*, the hardware interface plugin is implemented having only one interface.
* The communication is done using proprietary API to communicate with the robot control box.
* Data for all joints is exchanged at once.
The *DiffBot* URDF files can be found in ``description/urdf`` folder.
.. include:: ../../doc/run_from_docker.rst
Tutorial steps
--------------------------
1. To check that *DiffBot* description is working properly use following launch commands
.. code-block:: shell
ros2 launch ros2_control_demo_example_2 view_robot.launch.py
.. warning::
Getting the following output in terminal is OK: ``Warning: Invalid frame ID "odom" passed to canTransform argument target_frame - frame does not exist``.
This happens because ``joint_state_publisher_gui`` node need some time to start.
.. image:: diffbot.png
:width: 400
:alt: Differential Mobile Robot
2. To start *DiffBot* example open a terminal, source your ROS2-workspace and execute its launch file with
.. code-block:: shell
ros2 launch ros2_control_demo_example_2 diffbot.launch.py
The launch file loads and starts the robot hardware, controllers and opens *RViz*.
In the starting terminal you will see a lot of output from the hardware implementation showing its internal states.
This excessive printing is only added for demonstration. In general, printing to the terminal should be avoided as much as possible in a hardware interface implementation.
If you can see an orange box in *RViz* everything has started properly.
Still, to be sure, let's introspect the control system before moving *DiffBot*.
3. Check if the hardware interface loaded properly, by opening another terminal and executing
.. code-block:: shell
ros2 control list_hardware_interfaces
You should get
.. code-block:: shell
command interfaces
left_wheel_joint/velocity [available] [claimed]
right_wheel_joint/velocity [available] [claimed]
state interfaces
left_wheel_joint/position
left_wheel_joint/velocity
right_wheel_joint/position
right_wheel_joint/velocity
The ``[claimed]`` marker on command interfaces means that a controller has access to command *DiffBot*.
4. Check if controllers are running
.. code-block:: shell
ros2 control list_controllers
You should get
.. code-block:: shell
diffbot_base_controller[diff_drive_controller/DiffDriveController] active
joint_state_broadcaster[joint_state_broadcaster/JointStateBroadcaster] active
5. If everything is fine, now you can send a command to *Diff Drive Controller* using ROS 2 CLI interface:
.. code-block:: shell
ros2 topic pub --rate 30 /diffbot_base_controller/cmd_vel_unstamped geometry_msgs/msg/Twist "linear:
x: 0.7
y: 0.0
z: 0.0
angular:
x: 0.0
y: 0.0
z: 1.0"
You should now see an orange box circling in *RViz*.
Also, you should see changing states in the terminal where launch file is started.
.. code-block:: shell
[DiffBotSystemHardware]: Got command 43.33333 for 'left_wheel_joint'!
[DiffBotSystemHardware]: Got command 50.00000 for 'right_wheel_joint'!
Files used for this demos
--------------------------

View File

@ -0,0 +1,547 @@
// Copyright 2021 ros2_control Development Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "hoverboard_driver/hoverboard_driver.hpp"
#include <chrono>
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <vector>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include "hardware_interface/types/hardware_interface_type_values.hpp"
#include "rclcpp/rclcpp.hpp"
namespace hoverboard_driver
{
hoverboard_driver_node::hoverboard_driver_node() : Node("hoverboard_driver_node")
{
// These publishers are only for debugging purposes
vel_pub[left_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/left_wheel/velocity", 3);
vel_pub[right_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/right_wheel/velocity", 3);
pos_pub[left_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/left_wheel/position", 3);
pos_pub[right_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/right_wheel/position", 3);
cmd_pub[left_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/left_wheel/cmd", 3);
cmd_pub[right_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/right_wheel/cmd", 3);
voltage_pub = this->create_publisher<std_msgs::msg::Float64>("hoverboard/battery_voltage", 3);
temp_pub = this->create_publisher<std_msgs::msg::Float64>("hoverboard/temperature", 3);
curr_pub[left_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/left_wheel/dc_current", 3);
curr_pub[right_wheel] = this->create_publisher<std_msgs::msg::Float64>("hoverboard/right_wheel/dc_current", 3);
connected_pub = this->create_publisher<std_msgs::msg::Bool>("hoverboard/connected", 3);
declare_parameter("f", 10.2);
declare_parameter("p", 1.0);
declare_parameter("i", 0.1);
declare_parameter("d", 1.0);
declare_parameter("i_clamp_min", -10.0);
declare_parameter("i_clamp_max", 10.0);
declare_parameter("antiwindup", false);
get_parameter("f", pid_config.f);
get_parameter("p", pid_config.p);
get_parameter("i", pid_config.i);
get_parameter("d", pid_config.d);
get_parameter("i_clamp_min", pid_config.i_clamp_min);
get_parameter("i_clamp_max", pid_config.i_clamp_max);
get_parameter("antiwindup", pid_config.antiwindup);
// register parameter change callback handle
callback_handle_ = this->add_on_set_parameters_callback(
std::bind(&hoverboard_driver_node::parametersCallback, this, std::placeholders::_1));
}
void hoverboard_driver_node::publish_vel(int wheel, double message)
{
std_msgs::msg::Float64 f;
f.data = message;
vel_pub[wheel]->publish(f);
}
void hoverboard_driver_node::publish_pos(int wheel, double message)
{
std_msgs::msg::Float64 f;
f.data = message;
pos_pub[wheel]->publish(f);
}
void hoverboard_driver_node::publish_cmd(int wheel, double message)
{
std_msgs::msg::Float64 f;
f.data = message;
cmd_pub[wheel]->publish(f);
}
void hoverboard_driver_node::publish_curr(int wheel, double message)
{
std_msgs::msg::Float64 f;
f.data = message;
curr_pub[wheel]->publish(f);
}
void hoverboard_driver_node::publish_voltage(double message)
{
std_msgs::msg::Float64 f;
f.data = message;
voltage_pub->publish(f);
}
void hoverboard_driver_node::publish_temp(double message)
{
std_msgs::msg::Float64 f;
f.data = message;
temp_pub->publish(f);
}
void hoverboard_driver_node::publish_connected(bool message)
{
std_msgs::msg::Bool f;
f.data = message;
connected_pub->publish(f);
}
rcl_interfaces::msg::SetParametersResult hoverboard_driver_node::parametersCallback(
const std::vector<rclcpp::Parameter> &parameters)
{
rcl_interfaces::msg::SetParametersResult result;
result.successful = true;
result.reason = "success";
// Here update class attributes, do some actions, etc.
for (const auto &param : parameters)
{
if (param.get_name() == "p")
{
pid_config.p = param.as_double();
RCLCPP_INFO(get_logger(), "new value for PID P: %f", pid_config.p);
}
if (param.get_name() == "i")
{
pid_config.i = param.as_double();
RCLCPP_INFO(get_logger(), "new value for PID I: %f", pid_config.i);
}
if (param.get_name() == "d")
{
pid_config.d = param.as_double();
RCLCPP_INFO(get_logger(), "new value for PID D: %f", pid_config.d);
}
if (param.get_name() == "f")
{
pid_config.p = param.as_double();
RCLCPP_INFO(get_logger(), "new value for PID F: %f", pid_config.f);
}
if (param.get_name() == "i_clamp_min")
{
pid_config.i_clamp_min = param.as_double();
RCLCPP_INFO(get_logger(), "new value for PID I_CLAMP_MIN: %f", pid_config.i_clamp_min);
}
if (param.get_name() == "i_clamp_max")
{
pid_config.i_clamp_max = param.as_double();
RCLCPP_INFO(get_logger(), "new value for PID I_CLAMP_MAX: %f", pid_config.i_clamp_max);
}
if (param.get_name() == "antiwindup")
{
pid_config.antiwindup = param.as_bool();
RCLCPP_INFO(get_logger(), "new value for PID ANTIWINDUP: %i", pid_config.antiwindup);
}
// HINT: the PID configuration itself will be set by timer callback of hoverboard_driver class
}
return result;
}
hardware_interface::CallbackReturn hoverboard_driver::on_init(
const hardware_interface::HardwareInfo &info)
{
if (
hardware_interface::SystemInterface::on_init(info) !=
hardware_interface::CallbackReturn::SUCCESS)
{
return hardware_interface::CallbackReturn::ERROR;
}
// read parameter from hoverboard_driver.ros2_control.xacro file
wheel_radius = std::stod(info_.hardware_parameters["wheel_radius"]);
max_velocity = std::stod(info_.hardware_parameters["max_velocity"]);
port = info_.hardware_parameters["device"];
hw_positions_.resize(info_.joints.size(), std::numeric_limits<double>::quiet_NaN());
hw_velocities_.resize(info_.joints.size(), std::numeric_limits<double>::quiet_NaN());
hw_commands_.resize(info_.joints.size(), std::numeric_limits<double>::quiet_NaN());
for (const hardware_interface::ComponentInfo &joint : info_.joints)
{
// DiffBotSystem has exactly two states and one command interface on each joint
if (joint.command_interfaces.size() != 1)
{
RCLCPP_FATAL(
rclcpp::get_logger("HoverBoardSystemHardware"),
"Joint '%s' has %zu command interfaces found. 1 expected.", joint.name.c_str(),
joint.command_interfaces.size());
return hardware_interface::CallbackReturn::ERROR;
}
if (joint.command_interfaces[0].name != hardware_interface::HW_IF_VELOCITY)
{
RCLCPP_FATAL(
rclcpp::get_logger("HoverBoardSystemHardware"),
"Joint '%s' have %s command interfaces found. '%s' expected.", joint.name.c_str(),
joint.command_interfaces[0].name.c_str(), hardware_interface::HW_IF_VELOCITY);
return hardware_interface::CallbackReturn::ERROR;
}
if (joint.state_interfaces.size() != 2)
{
RCLCPP_FATAL(
rclcpp::get_logger("HoverBoardSystemHardware"),
"Joint '%s' has %zu state interface. 2 expected.", joint.name.c_str(),
joint.state_interfaces.size());
return hardware_interface::CallbackReturn::ERROR;
}
if (joint.state_interfaces[0].name != hardware_interface::HW_IF_POSITION)
{
RCLCPP_FATAL(
rclcpp::get_logger("HoverBoardSystemHardware"),
"Joint '%s' have '%s' as first state interface. '%s' expected.", joint.name.c_str(),
joint.state_interfaces[0].name.c_str(), hardware_interface::HW_IF_POSITION);
return hardware_interface::CallbackReturn::ERROR;
}
if (joint.state_interfaces[1].name != hardware_interface::HW_IF_VELOCITY)
{
RCLCPP_FATAL(
rclcpp::get_logger("HoverBoardSystemHardware"),
"Joint '%s' have '%s' as second state interface. '%s' expected.", joint.name.c_str(),
joint.state_interfaces[1].name.c_str(), hardware_interface::HW_IF_VELOCITY);
return hardware_interface::CallbackReturn::ERROR;
}
}
hardware_publisher = std::make_shared<hoverboard_driver_node>(); // fire up the publisher node
return hardware_interface::CallbackReturn::SUCCESS;
}
std::vector<hardware_interface::StateInterface> hoverboard_driver::export_state_interfaces()
{
std::vector<hardware_interface::StateInterface> state_interfaces;
for (auto i = 0u; i < info_.joints.size(); i++)
{
state_interfaces.emplace_back(hardware_interface::StateInterface(
info_.joints[i].name, hardware_interface::HW_IF_POSITION, &hw_positions_[i]));
state_interfaces.emplace_back(hardware_interface::StateInterface(
info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, &hw_velocities_[i]));
}
return state_interfaces;
}
std::vector<hardware_interface::CommandInterface> hoverboard_driver::export_command_interfaces()
{
std::vector<hardware_interface::CommandInterface> command_interfaces;
for (auto i = 0u; i < info_.joints.size(); i++)
{
command_interfaces.emplace_back(hardware_interface::CommandInterface(
info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, &hw_commands_[i]));
}
return command_interfaces;
}
hardware_interface::CallbackReturn hoverboard_driver::on_activate(
const rclcpp_lifecycle::State & /*previous_state*/)
{
RCLCPP_INFO(rclcpp::get_logger("hoverboard_driver"), "Using port %s", port.c_str());
// Convert m/s to rad/s
max_velocity /= wheel_radius;
low_wrap = ENCODER_LOW_WRAP_FACTOR * (ENCODER_MAX - ENCODER_MIN) + ENCODER_MIN;
high_wrap = ENCODER_HIGH_WRAP_FACTOR * (ENCODER_MAX - ENCODER_MIN) + ENCODER_MIN;
last_wheelcountR = last_wheelcountL = 0;
multR = multL = 0;
first_read_pass_ = true;
// Init PID controller
pids[0].init(hardware_publisher->pid_config.f, hardware_publisher->pid_config.p,
hardware_publisher->pid_config.i, hardware_publisher->pid_config.d,
hardware_publisher->pid_config.i_clamp_max, hardware_publisher->pid_config.i_clamp_min,
hardware_publisher->pid_config.antiwindup, max_velocity, -max_velocity);
pids[0].setOutputLimits(-max_velocity, max_velocity);
pids[1].init(hardware_publisher->pid_config.f, hardware_publisher->pid_config.p,
hardware_publisher->pid_config.i, hardware_publisher->pid_config.d,
hardware_publisher->pid_config.i_clamp_max, hardware_publisher->pid_config.i_clamp_min,
hardware_publisher->pid_config.antiwindup, max_velocity, -max_velocity);
pids[1].setOutputLimits(-max_velocity, max_velocity);
if ((port_fd = open(port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY)) < 0)
{
RCLCPP_FATAL(rclcpp::get_logger("hoverboard_driver"), "Cannot open serial port to hoverboard");
exit(-1);
}
// CONFIGURE THE UART -- connecting to the board
// The flags (defined in /usr/include/termios.h - see http://pubs.opengroup.org/onlinepubs/007908799/xsh/termios.h.html):
struct termios options;
tcgetattr(port_fd, &options);
options.c_cflag = B115200 | CS8 | CLOCAL | CREAD; //<Set baud rate
options.c_iflag = IGNPAR;
options.c_oflag = 0;
options.c_lflag = 0;
tcflush(port_fd, TCIFLUSH);
tcsetattr(port_fd, TCSANOW, &options);
RCLCPP_INFO(rclcpp::get_logger("hoverboard_driver"), "Successfully activated!");
return hardware_interface::CallbackReturn::SUCCESS;
}
hardware_interface::CallbackReturn hoverboard_driver::on_deactivate(
const rclcpp_lifecycle::State & /*previous_state*/)
{
if (port_fd != -1)
close(port_fd);
RCLCPP_INFO(rclcpp::get_logger("hoverboard_driver"), "Successfully deactivated!");
return hardware_interface::CallbackReturn::SUCCESS;
}
hardware_interface::return_type hoverboard_driver::read(
const rclcpp::Time &time, const rclcpp::Duration &period)
{
// to be able to compare times, we need to set last_read time to a correct time source
// set the actual time as last_read, when it hasn't been set before (first attempt to read from harware)
if (first_read_pass_ == true)
{
last_read = time;
first_read_pass_ = false;
}
if (port_fd != -1)
{
unsigned char c;
int i = 0, r = 0;
while ((r = ::read(port_fd, &c, 1)) > 0 && i++ < 1024)
protocol_recv(time, c);
if (i > 0)
last_read = time;
if (r < 0 && errno != EAGAIN)
RCLCPP_ERROR(rclcpp::get_logger("hoverboard_driver"), "Reading from serial %s failed: %d", port.c_str(), r);
}
if ((time - last_read).seconds() > 1)
{
// ROS_FATAL("Timeout reading from serial %s failed", port.c_str());
// publish false when not receiving serial data
hardware_publisher->publish_connected(false);
}
else
{
// we must be connected - publish true
hardware_publisher->publish_connected(true);
}
return hardware_interface::return_type::OK;
}
void hoverboard_driver::protocol_recv(const rclcpp::Time &time, char byte)
{
start_frame = ((uint16_t)(byte) << 8) | (uint8_t)prev_byte;
// Read the start frame
if (start_frame == START_FRAME)
{
p = (char *)&msg;
*p++ = prev_byte;
*p++ = byte;
msg_len = 2;
}
else if (msg_len >= 2 && msg_len < sizeof(SerialFeedback))
{
// Otherwise just read the message content until the end
*p++ = byte;
msg_len++;
}
if (msg_len == sizeof(SerialFeedback))
{
uint16_t checksum = (uint16_t)(msg.start ^
msg.cmd1 ^
msg.cmd2 ^
msg.speedR_meas ^
msg.speedL_meas ^
msg.wheelR_cnt ^
msg.wheelL_cnt ^
msg.left_dc_curr ^
msg.right_dc_curr ^
msg.batVoltage ^
msg.boardTemp ^
msg.cmdLed);
if (msg.start == START_FRAME && msg.checksum == checksum)
{
hardware_publisher->publish_voltage((double)msg.batVoltage / 100.0);
hardware_publisher->publish_temp((double)msg.boardTemp / 10.0);
;
hardware_publisher->publish_curr(left_wheel, (double)msg.left_dc_curr / 100.0);
hardware_publisher->publish_curr(right_wheel, (double)msg.right_dc_curr / 100.0);
// Convert RPM to RAD/S
hw_velocities_[left_wheel] = direction_correction * (abs(msg.speedL_meas) * 0.10472);
hw_velocities_[right_wheel] = direction_correction * (abs(msg.speedR_meas) * 0.10472);
hardware_publisher->publish_vel(left_wheel, hw_velocities_[left_wheel]);
hardware_publisher->publish_vel(right_wheel, hw_velocities_[right_wheel]);
// Process encoder values and update odometry
on_encoder_update(time, msg.wheelR_cnt, msg.wheelL_cnt);
}
else
{
RCLCPP_WARN(rclcpp::get_logger("hoverboard_driver"), "Hoverboard checksum mismatch: %d vs %d", msg.checksum, checksum);
}
msg_len = 0;
}
prev_byte = byte;
}
hardware_interface::return_type hoverboard_driver::hoverboard_driver::write(
const rclcpp::Time &time, const rclcpp::Duration &period)
{
if (port_fd == -1)
{
RCLCPP_ERROR(rclcpp::get_logger("hoverboard_driver"), "Attempt to write on closed serial");
return hardware_interface::return_type::ERROR;
}
// Inform interested parties about the commands we've got
hardware_publisher->publish_cmd(left_wheel, hw_commands_[left_wheel]);
hardware_publisher->publish_cmd(right_wheel, hw_commands_[right_wheel]);
// Set PID Parameters
pids[0].setParameters(hardware_publisher->pid_config.f, hardware_publisher->pid_config.p,
hardware_publisher->pid_config.i, hardware_publisher->pid_config.d,
hardware_publisher->pid_config.i_clamp_max, hardware_publisher->pid_config.i_clamp_min,
hardware_publisher->pid_config.antiwindup);
pids[1].setParameters(hardware_publisher->pid_config.f, hardware_publisher->pid_config.p,
hardware_publisher->pid_config.i, hardware_publisher->pid_config.d,
hardware_publisher->pid_config.i_clamp_max, hardware_publisher->pid_config.i_clamp_min,
hardware_publisher->pid_config.antiwindup);
// calculate PID values
double pid_outputs[2];
pid_outputs[0] = pids[0](hw_velocities_[left_wheel], hw_commands_[left_wheel], period);
pid_outputs[1] = pids[1](hw_velocities_[left_wheel], hw_commands_[right_wheel], period);
// Convert PID outputs in RAD/S to RPM
//double set_speed[2] = {
// pid_outputs[0] / 0.10472,
// pid_outputs[1] / 0.10472};
double set_speed[2] = {
hw_commands_[left_wheel] / 0.10472,
hw_commands_[right_wheel] / 0.10472
};
// Calculate steering from difference of left and right
const double speed = (set_speed[0] + set_speed[1]) / 2.0;
const double steer = (set_speed[0] - speed) * 2.0;
SerialCommand command;
command.start = (uint16_t)START_FRAME;
command.steer = (int16_t)steer;
command.speed = (int16_t)speed;
command.checksum = (uint16_t)(command.start ^ command.steer ^ command.speed);
int rc = ::write(port_fd, (const void *)&command, sizeof(command));
if (rc < 0)
{
RCLCPP_ERROR(rclcpp::get_logger("hoverboard_driver"), "Error writing to hoverboard serial port");
}
return hardware_interface::return_type::OK;
}
void hoverboard_driver::on_encoder_update(const rclcpp::Time &time, int16_t right, int16_t left)
{
double posL = 0.0, posR = 0.0;
// Calculate wheel position in ticks, factoring in encoder wraps
if (right < low_wrap && last_wheelcountR > high_wrap)
multR++;
else if (right > high_wrap && last_wheelcountR < low_wrap)
multR--;
posR = right + multR * (ENCODER_MAX - ENCODER_MIN);
last_wheelcountR = right;
if (left < low_wrap && last_wheelcountL > high_wrap)
multL++;
else if (left > high_wrap && last_wheelcountL < low_wrap)
multL--;
posL = left + multL * (ENCODER_MAX - ENCODER_MIN);
last_wheelcountL = left;
// When the board shuts down and restarts, wheel ticks are reset to zero so the robot can be suddently lost
// This section accumulates ticks even if board shuts down and is restarted
static double lastPosL = 0.0, lastPosR = 0.0;
static double lastPubPosL = 0.0, lastPubPosR = 0.0;
static bool nodeStartFlag = true;
// IF there has been a pause in receiving data AND the new number of ticks is close to zero, indicates a board restard
//(the board seems to often report 1-3 ticks on startup instead of zero)
// reset the last read ticks to the startup values
if ((time - last_read).seconds() > 0.2 && abs(posL) < 5 && abs(posR) < 5)
{
lastPosL = posL;
lastPosR = posR;
}
double posLDiff = 0;
double posRDiff = 0;
// if node is just starting keep odom at zeros
if (nodeStartFlag)
{
nodeStartFlag = false;
}
else
{
posLDiff = posL - lastPosL;
posRDiff = posR - lastPosR;
}
lastPubPosL += posLDiff;
lastPubPosR += posRDiff;
lastPosL = posL;
lastPosR = posR;
// Convert position in accumulated ticks to position in radians
hw_positions_[left_wheel] = 2.0 * M_PI * lastPubPosL / (double)TICKS_PER_ROTATION;
hw_positions_[right_wheel] = 2.0 * M_PI * lastPubPosR / (double)TICKS_PER_ROTATION;
hardware_publisher->publish_pos(left_wheel, hw_positions_[left_wheel]);
hardware_publisher->publish_pos(right_wheel, hw_positions_[right_wheel]);
}
} // namespace hoverboard_driver
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(
hoverboard_driver::hoverboard_driver, hardware_interface::SystemInterface)

View File

@ -0,0 +1,9 @@
#pragma once
#define ENCODER_MIN 0
#define ENCODER_MAX 9000
#define ENCODER_LOW_WRAP_FACTOR 0.3
#define ENCODER_HIGH_WRAP_FACTOR 0.7
#define TICKS_PER_ROTATION 90

View File

@ -0,0 +1,188 @@
// Copyright 2021 ros2_control Development Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef HOVERBOARD_DRIVER_HPP_
#define HOVERBOARD_DRIVER_HPP_
#include <memory>
#include <string>
#include <vector>
#include "hardware_interface/handle.hpp"
#include "hardware_interface/hardware_info.hpp"
#include "hardware_interface/system_interface.hpp"
#include "hardware_interface/types/hardware_interface_return_values.hpp"
#include "rclcpp/clock.hpp"
#include "rclcpp/duration.hpp"
#include "rclcpp/macros.hpp"
#include "rclcpp/time.hpp"
#include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp"
#include "rclcpp_lifecycle/state.hpp"
#include "hoverboard_driver/config.hpp"
#include "hoverboard_driver/protocol.hpp"
#include "hoverboard_driver/pid.hpp"
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/float64.hpp"
#include "std_msgs/msg/bool.hpp"
#include "rcl_interfaces/msg/set_parameters_result.hpp"
namespace hoverboard_driver
{
enum
{
/// @brief left wheel
left_wheel,
/// @brief right wheel
right_wheel
};
/// @brief Helper class to get a node interface. This class is used to build publishers and dynamic parameters
/// for hoverboard_driver class.
class hoverboard_driver_node : public rclcpp::Node
{
public:
hoverboard_driver_node();
/// @brief publish velocity data for debugging
/// @param wheel left or right wheel
/// @param message value to publish
void publish_vel(int wheel, double message);
/// @brief publish pose data for debugging
/// @param wheel left or right wheel
/// @param message value to publish
void publish_pos(int wheel, double message);
/// @brief publish command data for debugging
/// @param wheel left or right wheel
/// @param message value to publish
void publish_cmd(int wheel, double message);
/// @brief publish battery voltage
/// @param message value to publish
void publish_voltage(double message);
/// @brief publish current (power consumption) of motor
/// @param wheel left or right wheel
/// @param message value to publish
void publish_curr(int wheel, double message);
/// @brief publish PCB temperature
/// @param message value to publish
void publish_temp(double message);
/// @brief publish state of PCB (on or off)
/// @param message value to publish
void publish_connected(bool message);
/// @brief parameter callback method.
/// @param parameters
/// @return
rcl_interfaces::msg::SetParametersResult parametersCallback(
const std::vector<rclcpp::Parameter> &parameters);
/// @brief PID configuration structure
struct
{
double p;
double i;
double d;
double f;
double i_clamp_min;
double i_clamp_max;
bool antiwindup;
} pid_config;
private:
// Publishers
rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr vel_pub[2];
rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr pos_pub[2];
rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr cmd_pub[2];
rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr voltage_pub;
rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr curr_pub[2];
rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr temp_pub;
rclcpp::Publisher<std_msgs::msg::Bool>::SharedPtr connected_pub;
// Parameter Callback handle
OnSetParametersCallbackHandle::SharedPtr callback_handle_;
};
/// @brief Hardware Interface class to communicate with Hoverboard PCB
class hoverboard_driver : public hardware_interface::SystemInterface
{
public:
RCLCPP_SHARED_PTR_DEFINITIONS(hoverboard_driver);
hardware_interface::CallbackReturn on_init(
const hardware_interface::HardwareInfo &info) override;
std::vector<hardware_interface::StateInterface> export_state_interfaces() override;
std::vector<hardware_interface::CommandInterface> export_command_interfaces() override;
hardware_interface::CallbackReturn on_activate(
const rclcpp_lifecycle::State &previous_state) override;
hardware_interface::CallbackReturn on_deactivate(
const rclcpp_lifecycle::State &previous_state) override;
hardware_interface::return_type read(
const rclcpp::Time &time, const rclcpp::Duration &period) override;
// HOVERBOARD_DRIVER_PUBLIC
hardware_interface::return_type write(
const rclcpp::Time &time, const rclcpp::Duration &period) override;
std::shared_ptr<hoverboard_driver_node> hardware_publisher; // make the publisher node a member
private:
// Store the command for the simulated robot
std::vector<double> hw_commands_;
std::vector<double> hw_positions_;
std::vector<double> hw_velocities_;
void protocol_recv(const rclcpp::Time &time, char c);
void on_encoder_update(const rclcpp::Time &time, int16_t right, int16_t left);
double wheel_radius;
double max_velocity = 0.0;
int direction_correction = 1;
std::string port;
rclcpp::Time last_read;
bool first_read_pass_;
// Last known encoder values
int16_t last_wheelcountR;
int16_t last_wheelcountL;
// Count of full encoder wraps
int multR;
int multL;
// Thresholds for calculating the wrap
int low_wrap;
int high_wrap;
// Hoverboard protocol
int port_fd;
unsigned long msg_len = 0;
char prev_byte = 0;
uint16_t start_frame = 0;
char *p;
SerialFeedback msg, prev_msg;
PID pids[2];
};
} // namespace hoverboard_driver
#endif // HOVERBOARD_DRIVER_HPP_

View File

@ -0,0 +1,131 @@
#include <rclcpp/duration.hpp>
#include <control_toolbox/pid.hpp>
#include <boost/thread/mutex.hpp>
class PID : public control_toolbox::Pid
{
public:
/**
* @brief Construct a new PID object
*
* @param p The proportional gain.
* @param i The integral gain.
* @param d The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param out_min The min computed output.
* @param out_max The max computed output.
*/
PID(double p=0.0, double i=0.0, double d=0.0, double i_max=0.0, double i_min=0.0, bool antiwindup=false, double out_max=0.0, double out_min=0.0);
/**
* @brief Initialize the
*
* @param kF The feed forward gain.
* @param kP The proportional gain.
* @param kI The integral gain.
* @param kD The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param antiwindup
* @param out_min The min computed output.
* @param out_max The max computed output.
*/
void init(double f, double p, double i, double d, double i_max, double i_min, bool antiwindup, double out_max, double out_min);
/**
* @brief Compute PID output value from error using process value, set point and time period
*
* @param measured_value The process value measured by sensors.
* @param setpoint The desired target value.
* @param dt The delta time or period since the last call.
* @return double Computed PID output value.
*/
double operator()(const double &measured_value, const double &setpoint, const rclcpp::Duration &dt);
/**
* @brief Get the FPID parameters
*
* @param f The feed forward gain.
* @param p The proportional gain.
* @param i The integral gain.
* @param d The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param antiwindup Enable or disable antiwindup check.
*/
void getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min);
void getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min, bool &antiwindup);
/**
* @brief Set the Parameters using the Gains object of control_toolbox::Pid
*
* @param f The feed forward gain.
* @param p The proportional gain.
* @param i The integral gain.
* @param d The derivative gain.
* @param i_max The max integral windup.
* @param i_min The min integral windup.
* @param antiwindup Enable or disable antiwindup check.
*/
void setParameters(double f, double p, double i, double d, double i_max, double i_min, bool antiwindup = false);
/**
* @brief Set the Output Limits of the PID controller
*
* @param out_min
* @param out_max
*/
void setOutputLimits(double out_min, double out_max);
/**
* @brief Clam given value to upper and lower limits.
*
* @param value Input value that's possibly clamped.
* @param lower_limit Lower limit which the value must not exceed.
* @param upper_limit Upper limit which the value must not exceed.
* @return double Clamped value to range in between [lower_limit, upper_limit].
*/
double clamp(const double& value, const double& lower_limit, const double& upper_limit);
/**
* @brief Get the current error.
*
* @return double The current error computed from the measured and target value.
*/
inline double getError() const { return error_; };
/**
* @brief Start the dynamic reconfigure node and load the default values
* @param node - a node handle where dynamic reconfigure services will be published
*/
// void initDynamicReconfig(ros::NodeHandle &node);
/**
* @brief Set Dynamic Reconfigure's gains to PID's values
*/
//void updateDynamicReconfig();
//void updateDynamicReconfig(Gains gains_config);
//void updateDynamicReconfig(hoverboard_driver::HoverboardConfig config);
/**
* \brief Update the PID parameters from dynamics reconfigure
*/
//void dynamicReconfigCallback(hoverboard_driver::HoverboardConfig &config, uint32_t /*level*/);
private:
double f_;
double error_;
double out_min_;
double out_max_;
rclcpp::Node::SharedPtr node_;
// Dynamic reconfigure
bool dynamic_reconfig_initialized_;
// typedef dynamic_reconfigure::Server<hoverboard_driver::HoverboardConfig> DynamicReconfigServer;
//boost::shared_ptr<DynamicReconfigServer> param_reconfig_server_;
//DynamicReconfigServer::CallbackType param_reconfig_callback_;
// boost::recursive_mutex param_reconfig_mutex_;
};

View File

@ -0,0 +1,30 @@
#ifndef _FOC_PROTOCOL_H
#define _FOC_PROTOCOL_H
#define START_FRAME 0xABCD
typedef struct {
uint16_t start;
int16_t steer;
int16_t speed;
uint16_t checksum;
} SerialCommand;
typedef struct {
uint16_t start;
int16_t cmd1;
int16_t cmd2;
int16_t speedR_meas;
int16_t speedL_meas;
int16_t wheelR_cnt;
int16_t wheelL_cnt;
int16_t left_dc_curr;
int16_t right_dc_curr;
int16_t batVoltage;
int16_t boardTemp;
uint16_t cmdLed;
uint16_t checksum;
} SerialFeedback;
#endif

View File

@ -0,0 +1,107 @@
#include "hoverboard_driver/pid.hpp"
PID::PID(double p, double i, double d, double i_max, double i_min, bool antiwindup, double out_max, double out_min)
: control_toolbox::Pid()
{
f_ = 0.0;
initPid(p, i, d, i_max, i_min, antiwindup);
error_ = 0.0;
out_min_ = out_min;
out_max_ = out_max;
}
void PID::init(double f, double p, double i, double d, double i_max, double i_min, bool antiwindup, double out_max, double out_min)
{
RCLCPP_INFO(rclcpp::get_logger("hoverboard_driver"), "Initialize PID");
f_ = f;
initPid(p, i, d, i_max, i_min, antiwindup);
error_ = 0.0;
out_min_ = out_min;
out_max_ = out_max;
Gains gains = getGains();
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("hoverboard_driver"), "Initialized PID: F=" << f << ", P=" << gains.p_gain_ << ", I=" << gains.i_gain_ << ", D=" << gains.d_gain_ << ", out_min=" << out_min_ << ", out_max=" << out_max_);
}
double PID::operator()(const double &measured_value, const double &setpoint, const rclcpp::Duration &dt)
{
// Compute error terms
error_ = setpoint - measured_value;
// rclcpp::Clock clock; // NOTA: Puede mamar
// RCLCPP_DEBUG_STREAM_THROTTLE(
// node_->get_logger(), clock, 1000000000, "Error: " << error_);
// Reset the i_error in case the p_error and the setpoint is zero
// Otherwise there will always be a constant i_error_ that won't vanish
if (0.0 == setpoint && 0.0 == error_)
{
// reset() will reset
// p_error_last_ = 0.0;
// p_error_ = 0.0;
// i_error_ = 0.0;
// d_error_ = 0.0;
// cmd_ = 0.0;
reset();
}
// Use control_toolbox::Pid::computeCommand()
double output = computeCommand(error_, dt.nanoseconds());
// RCLCPP_DEBUG_STREAM_THROTTLE(node_->get_logger(), clock, dt.nanoseconds(),
// "PID computed command: " << output);
// Compute final output including feed forward term
output = f_ * setpoint + output;
//output = clamp(output, out_min_, out_max_);
return output;
}
void PID::getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min)
{
bool antiwindup;
getParameters(f, p, i, d, i_max, i_min, antiwindup);
}
void PID::getParameters(double &f, double &p, double &i, double &d, double &i_max, double &i_min, bool &antiwindup)
{
f = f_;
// Call getGains from control_toolbox
getGains(p, i, d, i_max, i_min, antiwindup);
}
void PID::setParameters(double f, double p, double i, double d, double i_max, double i_min, bool antiwindup)
{
f_ = f;
setGains(p, i, d, i_max, i_min, antiwindup);
Gains gains = getGains();
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("hoverboard_driver"), "Update PID Gains: F=" << f << ", P=" << gains.p_gain_ << ", I=" << gains.i_gain_ << ", D=" << gains.d_gain_ << ", out_min=" << out_min_ << ", out_max=" << out_max_);
}
void PID::setOutputLimits(double output_max, double output_min)
{
out_max_ = output_max;
out_min_ = output_min;
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("hoverboard_driver"), "Update PID output limits: lower=" << out_min_ << ", upper=" << out_max_);
}
double PID::clamp(const double& value, const double& lower_limit, const double& upper_limit)
{
rclcpp::Clock clock;
if (value > upper_limit){
RCLCPP_DEBUG_STREAM_THROTTLE(
rclcpp::get_logger("hoverboard_driver"), clock, 1000000000,
"Clamp " << value << " to upper limit " << upper_limit);
return upper_limit;
}else if (value < lower_limit){
RCLCPP_DEBUG_STREAM_THROTTLE(
rclcpp::get_logger("hoverboard_driver"), clock, 1000000000,
"Clamp " << value << " to lower limit " << upper_limit);
return lower_limit;
}
return value;
}

View File

@ -0,0 +1,9 @@
<library path="hoverboard_driver">
<class name="hoverboard_driver/hoverboard_driver"
type="hoverboard_driver::hoverboard_driver"
base_class_type="hardware_interface::SystemInterface">
<description>
The ros2_control DiffBot example using a system hardware interface-type. It uses velocity command and position state interface. The example is the starting point to implement a hardware interface for differential-drive mobile robots.
</description>
</class>
</library>

View File

@ -0,0 +1,42 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>hoverboard_driver</name>
<version>0.0.0</version>
<description>Demo package of `ros2_control` hardware for DiffBot.</description>
<maintainer email="denis.stogl@stoglrobotics.de">Dr.-Ing. Denis Štogl</maintainer>
<maintainer email="bence.magyar.robotics@gmail.com">Bence Magyar</maintainer>
<maintainer email="christoph.froehlich@ait.ac.at">Christoph Froehlich</maintainer>
<author email="denis.stogl@stoglrobotics.de">Dr.-Ing. Denis Štogl</author>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>hardware_interface</depend>
<depend>pluginlib</depend>
<depend>rclcpp</depend>
<depend>rclcpp_lifecycle</depend>
<exec_depend>controller_manager</exec_depend>
<exec_depend>diff_drive_controller</exec_depend>
<exec_depend>joint_state_broadcaster</exec_depend>
<exec_depend>joint_state_publisher_gui</exec_depend>
<exec_depend>robot_state_publisher</exec_depend>
<!--<exec_depend>ros2_control_demo_description</exec_depend> -->
<exec_depend>ros2_controllers_test_nodes</exec_depend>
<exec_depend>ros2controlcli</exec_depend>
<exec_depend>ros2launch</exec_depend>
<exec_depend>rviz2</exec_depend>
<exec_depend>xacro</exec_depend>
<exec_depend>control_toolbox</exec_depend>
<test_depend>ament_cmake_gtest</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>