From 56c43ff1cd389266ab0a66c61b34010c053db244 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Tue, 25 Apr 2017 22:44:18 -0600 Subject: [PATCH 01/32] removed keyboard depends --- CMakeLists.txt | 118 ++++++++++++++++++++++++------------------------- package.xml | 5 ++- 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d7f605..b77f227 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,6 @@ project(tf_keyboard_cal) set(CMAKE_CXX_FLAGS "-std=c++11 -Wall ${CMAKE_CXX_FLAGS}") find_package(catkin REQUIRED COMPONENTS - keyboard roscpp tf2 tf @@ -25,10 +24,11 @@ catkin_package( INCLUDE_DIRS include LIBRARIES - ${PROJECT_NAME}_manual_tf_alignment - ${PROJECT_NAME}_imarker_simple +# ${PROJECT_NAME}_manual_tf_alignment +# ${PROJECT_NAME}_imarker_simple CATKIN_DEPENDS - keyboard roscpp +# keyboard + roscpp tf2 tf rosparam_shortcuts @@ -49,43 +49,43 @@ include_directories( ) # Library -add_library(${PROJECT_NAME}_manual_tf_alignment - src/manual_tf_alignment.cpp -) -target_link_libraries(${PROJECT_NAME}_manual_tf_alignment - ${catkin_LIBRARIES} - ${Boost_LIBRARIES} -) +# add_library(${PROJECT_NAME}_manual_tf_alignment +# src/manual_tf_alignment.cpp +# ) +# target_link_libraries(${PROJECT_NAME}_manual_tf_alignment +# ${catkin_LIBRARIES} +# ${Boost_LIBRARIES} +# ) # Library -add_library(${PROJECT_NAME}_imarker_simple - src/imarker_simple.cpp -) -target_link_libraries(${PROJECT_NAME}_imarker_simple - ${catkin_LIBRARIES} - ${Boost_LIBRARIES} -) +# add_library(${PROJECT_NAME}_imarker_simple +# src/imarker_simple.cpp +# ) +# target_link_libraries(${PROJECT_NAME}_imarker_simple +# ${catkin_LIBRARIES} +# ${Boost_LIBRARIES} +# ) # Executable -add_executable(${PROJECT_NAME} - src/tf_keyboard.cpp -) -target_link_libraries(${PROJECT_NAME} - ${PROJECT_NAME}_manual_tf_alignment - ${catkin_LIBRARIES} -) +# add_executable(${PROJECT_NAME} +# src/tf_keyboard.cpp +# ) +# target_link_libraries(${PROJECT_NAME} +# ${PROJECT_NAME}_manual_tf_alignment +# ${catkin_LIBRARIES} +# ) # Executable -add_executable(${PROJECT_NAME}_demo_tf_listener - src/demo_tf_listener.cpp -) -# Rename C++ executable without namespace -set_target_properties(${PROJECT_NAME}_demo_tf_listener - PROPERTIES OUTPUT_NAME demo_tf_listener PREFIX "") -# Specify libraries to link a library or executable target against -target_link_libraries(${PROJECT_NAME}_demo_tf_listener - ${catkin_LIBRARIES} -) +# add_executable(${PROJECT_NAME}_demo_tf_listener +# src/demo_tf_listener.cpp +# ) +# # Rename C++ executable without namespace +# set_target_properties(${PROJECT_NAME}_demo_tf_listener +# PROPERTIES OUTPUT_NAME demo_tf_listener PREFIX "") +# # Specify libraries to link a library or executable target against +# target_link_libraries(${PROJECT_NAME}_demo_tf_listener +# ${catkin_LIBRARIES} +# ) ############# ## Testing ## @@ -98,27 +98,27 @@ roslint_cpp() ## Install ## ############# -# Install libraries -install(TARGETS ${PROJECT_NAME}_manual_tf_alignment ${PROJECT_NAME}_imarker_simple - LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}) - -# Install header files -install(DIRECTORY include/${PROJECT_NAME}/ DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}) - -# Install executables -install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_demo_tf_listener - LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} - RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -) - -# Install scripts -install(PROGRAMS - scripts/tf_interactive_marker.py - DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -) - -# Install launch files -install(DIRECTORY - launch - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} -) +# # Install libraries +# install(TARGETS ${PROJECT_NAME}_manual_tf_alignment ${PROJECT_NAME}_imarker_simple +# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}) + +# # Install header files +# install(DIRECTORY include/${PROJECT_NAME}/ DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}) + +# # Install executables +# install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_demo_tf_listener +# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} +# RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +# ) + +# # Install scripts +# install(PROGRAMS +# scripts/tf_interactive_marker.py +# DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +# ) + +# # Install launch files +# install(DIRECTORY +# launch +# DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} +# ) diff --git a/package.xml b/package.xml index 7b1b94b..4869cf7 100644 --- a/package.xml +++ b/package.xml @@ -10,14 +10,14 @@ BSD https://github.com/davetcoleman/tf_keyboard_cal - https://github.com/davetcoleman/tf_keyboard_cal/issues + https://github.com/davetcoleman/tf_keyboard_cal/issues https://github.com/davetcoleman/tf_keyboard_cal/ Dave Coleman Andy McEvoy catkin - keyboard + rviz_visual_tools roscpp tf2 @@ -30,5 +30,6 @@ interactive_markers + From f76dca3ff54d971ee3e53c5c18503bfa518b2dde Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Tue, 25 Apr 2017 22:51:39 -0600 Subject: [PATCH 02/32] start work on rviz tool --- plugin_description.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 plugin_description.xml diff --git a/plugin_description.xml b/plugin_description.xml new file mode 100644 index 0000000..d824824 --- /dev/null +++ b/plugin_description.xml @@ -0,0 +1,16 @@ + + + + A panel widget that allows you to create and control TFs. + + + + + + A tool to capture keyboard and mouse input. + + + + From 249c622aad0905b04b658c755deed961bd2d6774 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Wed, 26 Apr 2017 23:27:35 -0600 Subject: [PATCH 03/32] simple panel working --- CMakeLists.txt | 84 +++++++++++++++++++++++++++++++++++-- package.xml | 1 + plugin_description.xml | 7 ---- src/tf_keyboard_cal_gui.cpp | 78 ++++++++++++++++++++++++++++++++++ src/tf_keyboard_cal_gui.h | 73 ++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 src/tf_keyboard_cal_gui.cpp create mode 100644 src/tf_keyboard_cal_gui.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b77f227..6a946f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ find_package(catkin REQUIRED COMPONENTS rviz_visual_tools tf_conversions interactive_markers + rviz ) find_package(Eigen REQUIRED) @@ -22,8 +23,9 @@ find_package(Boost REQUIRED) catkin_package( INCLUDE_DIRS - include + include LIBRARIES + ${PROJECT_NAME}_gui # ${PROJECT_NAME}_manual_tf_alignment # ${PROJECT_NAME}_imarker_simple CATKIN_DEPENDS @@ -37,9 +39,33 @@ catkin_package( Eigen ) -########### -## Build ## -########### +# Qt 4 or 5 +if(rviz_QT_VERSION VERSION_LESS "5") + message(STATUS "Using Qt4 based on the rviz_QT_VERSION: ${rviz_QT_VERSION}") + find_package(Qt4 ${rviz_QT_VERSION} REQUIRED QtCore QtGui) + include(${QT_USE_FILE}) + macro(qt_wrap_ui) + qt4_wrap_ui(${ARGN}) + endmacro() + macro(qt_wrap_cpp) + qt4_wrap_cpp(${ARGN}) + endmacro() +else() + message(STATUS "Using Qt5 based on the rviz_QT_VERSION: ${rviz_QT_VERSION}") + find_package(Qt5 ${rviz_QT_VERSION} REQUIRED Core Widgets) + set(QT_LIBRARIES Qt5::Widgets) + macro(qt_wrap_ui) + qt5_wrap_ui(${ARGN}) + endmacro() + macro(qt_wrap_cpp) + qt5_wrap_cpp(${ARGN}) + endmacro() +endif() +## Prefer the Qt signals and slots to avoid defining "emit", "slots", +## etc because they can conflict with boost signals, so define QT_NO_KEYWORDS here. +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +add_definitions(-DQT_NO_KEYWORDS) include_directories( include @@ -48,6 +74,25 @@ include_directories( ${EIGEN_INCLUDE_DIRS} ) +########### +## Build ## +########### + +## specify which header files need to be run through "moc", +## Qt's meta-object compiler. +qt_wrap_cpp(MOC_FILES + src/tf_keyboard_cal_gui.h +) + +## specify the list of source files, including the output of +## the previous command which is stored in ``${MOC_FILES}``. +set(SOURCE_FILES + src/tf_keyboard_cal_gui.cpp +) + +add_library(${PROJECT_NAME}_gui ${SOURCE_FILES}) +target_link_libraries(${PROJECT_NAME}_gui ${rviz_DEFAULT_PLUGIN_LIBRARIES} ${QT_LIBRARIES} ${catkin_LIBRARIES}) + # Library # add_library(${PROJECT_NAME}_manual_tf_alignment # src/manual_tf_alignment.cpp @@ -122,3 +167,34 @@ roslint_cpp() # launch # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} # ) + +############# +## Install ## +############# + +# Install libraries +install( + TARGETS + ${PROJECT_NAME}_gui + LIBRARY DESTINATION + ${CATKIN_PACKAGE_LIB_DESTINATION} + ) + +# Install header files +install(DIRECTORY include/${PROJECT_NAME}/ DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}) + +# Install shared resources +install(DIRECTORY launch DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}) +install(DIRECTORY resources DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}) + +# Install xml plugin config +install(FILES + plugin_description.xml + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} + ) + +# Install executables +install(TARGETS + LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} + RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} + ) diff --git a/package.xml b/package.xml index 4869cf7..bf43023 100644 --- a/package.xml +++ b/package.xml @@ -18,6 +18,7 @@ catkin + rviz rviz_visual_tools roscpp tf2 diff --git a/plugin_description.xml b/plugin_description.xml index d824824..62513d4 100644 --- a/plugin_description.xml +++ b/plugin_description.xml @@ -6,11 +6,4 @@ A panel widget that allows you to create and control TFs. - - - - A tool to capture keyboard and mouse input. - - - diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp new file mode 100644 index 0000000..6160d14 --- /dev/null +++ b/src/tf_keyboard_cal_gui.cpp @@ -0,0 +1,78 @@ +/**************************************************************************************************** + * Software License Agreement (BSD License) + * + * Copyright 2017, Andy McEvoy + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ****************************************************************************************************/ + +/* + * Author(s) : Andy McEvoy ( mcevoy.andy@gmail.com ) + * Desc : Rviz display panel for dynamically manipulating TFs + * Created : 26 - April - 2017 + */ +#include + + +#include +#include + +#include "tf_keyboard_cal_gui.h" + + +namespace tf_keyboard_cal +{ +TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) +{ + std::cout << "\033[1;36m" << "TFKeyboardCalGui" << "\033[0m" << std::endl; + // create a test button + btn_test_ = new QPushButton(this); + btn_test_->setText("Test"); + connect(btn_test_, SIGNAL(clicked()), this, SLOT(testFnc())); + + // Vertical Layout + QVBoxLayout* layout = new QVBoxLayout; + layout->addWidget(btn_test_); + setLayout(layout); + std::cout << "\033[1;36m" << "end" << "\033[0m" << std::endl; +} + +void TFKeyboardCalGui::testFnc() +{ + +} + +void TFKeyboardCalGui::save(rviz::Config config) const +{ + rviz::Panel::save(config); +} + +void TFKeyboardCalGui::load(const rviz::Config& config) +{ + rviz::Panel::load(config); +} + +} // end namespace tf_keyboard_cal + +#include +PLUGINLIB_EXPORT_CLASS(tf_keyboard_cal::TFKeyboardCalGui, rviz::Panel) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h new file mode 100644 index 0000000..4642bd1 --- /dev/null +++ b/src/tf_keyboard_cal_gui.h @@ -0,0 +1,73 @@ +/**************************************************************************************************** + * Software License Agreement (BSD License) + * + * Copyright 2017, Andy McEvoy + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ****************************************************************************************************/ + +/* + * Author(s) : Andy McEvoy ( mcevoy.andy@gmail.com ) + * Desc : Rviz display panel for dynamically manipulating TFs + * Created : 25 - April - 2017 + */ + +#ifndef TF_KEYBOARD_CAL_GUI_H +#define TF_KEYBOARD_CAL_GUI_H + +#ifndef Q_MOC_RUN +#include +#include +#endif + +#include +#include + +class QLineEdit; // What are these for? +class QSpinBox; + +namespace tf_keyboard_cal +{ + +class TFKeyboardCalGui : public rviz::Panel +{ + Q_OBJECT +public: + TFKeyboardCalGui(QWidget *parent = 0); + + virtual void load(const rviz::Config &config); + virtual void save(rviz::Config config) const; + +public Q_SLOTS: + +protected Q_SLOTS: + + void testFnc(); + +protected: + QPushButton *btn_test_; +}; + +} // end namespace tf_keyboard_cal + +#endif From 59b3417a0c5a7d2c2a38594100a1c1191d321947 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Thu, 27 Apr 2017 23:08:34 -0600 Subject: [PATCH 04/32] basic tab layout and create tab --- src/tf_keyboard_cal_gui.cpp | 73 ++++++++++++++++++++++++++++--------- src/tf_keyboard_cal_gui.h | 56 ++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 6160d14..507d345 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -39,37 +39,76 @@ #include "tf_keyboard_cal_gui.h" - namespace tf_keyboard_cal { TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) { - std::cout << "\033[1;36m" << "TFKeyboardCalGui" << "\033[0m" << std::endl; - // create a test button - btn_test_ = new QPushButton(this); - btn_test_->setText("Test"); - connect(btn_test_, SIGNAL(clicked()), this, SLOT(testFnc())); - - // Vertical Layout - QVBoxLayout* layout = new QVBoxLayout; - layout->addWidget(btn_test_); - setLayout(layout); - std::cout << "\033[1;36m" << "end" << "\033[0m" << std::endl; + tabWidget_ = new QTabWidget; + tabWidget_->addTab(new createTFTab(), tr("Add / Remove")); + tabWidget_->addTab(new manipulateTFTab(), tr("Manipulate")); + + QVBoxLayout *main_layout = new QVBoxLayout; + main_layout->addWidget(tabWidget_); + setLayout(main_layout); + } -void TFKeyboardCalGui::testFnc() +createTFTab::createTFTab(QWidget *parent) : QWidget(parent) { + QLabel *from_label = new QLabel(tr("from:")); + QLabel *to_label = new QLabel(tr("to:")); + + from_ = new QLineEdit; + from_->setPlaceholderText("from TF"); + connect(from_, SIGNAL(textChanged(const QString &)), this, SLOT(fromTextChanged(const QString &))); + to_ = new QLineEdit; + to_->setPlaceholderText("to TF"); + connect(to_, SIGNAL(textChanged(const QString &)), this, SLOT(toTextChanged(const QString &))); + + create_tf_btn_ = new QPushButton(this); + create_tf_btn_->setText("Create TF"); + connect(create_tf_btn_, SIGNAL(clicked()), this, SLOT(createNewTF())); + + QHBoxLayout *from_row = new QHBoxLayout; + from_row->addWidget(from_label); + from_row->addWidget(from_); + + QHBoxLayout *to_row = new QHBoxLayout; + to_row->addWidget(to_label); + to_row->addWidget(to_); + + QVBoxLayout *main_layout = new QVBoxLayout; + main_layout->addLayout(from_row); + main_layout->addLayout(to_row); + main_layout->addWidget(create_tf_btn_); + setLayout(main_layout); } -void TFKeyboardCalGui::save(rviz::Config config) const +void createTFTab::createNewTF() { - rviz::Panel::save(config); + ROS_DEBUG_STREAM_NAMED("createNewTF","create new TF button pressed"); } -void TFKeyboardCalGui::load(const rviz::Config& config) +void createTFTab::fromTextChanged(QString text) { - rviz::Panel::load(config); + from_tf_name_ = text.toStdString(); + ROS_DEBUG_STREAM_NAMED("fromTextChanged","from: " << from_tf_name_); +} + +void createTFTab::toTextChanged(QString text) +{ + to_tf_name_ = text.toStdString(); + ROS_DEBUG_STREAM_NAMED("toTextChanged","to: " << to_tf_name_); +} + +manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) +{ + QLabel *some_text = new QLabel(tr("Some Text")); + + QVBoxLayout *main_layout = new QVBoxLayout; + main_layout->addWidget(some_text); + setLayout(main_layout); } } // end namespace tf_keyboard_cal diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 4642bd1..f8b910a 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -40,32 +40,64 @@ #include #endif +#include #include -#include - -class QLineEdit; // What are these for? -class QSpinBox; +#include +#include namespace tf_keyboard_cal { class TFKeyboardCalGui : public rviz::Panel { - Q_OBJECT + Q_OBJECT public: - TFKeyboardCalGui(QWidget *parent = 0); + explicit TFKeyboardCalGui(QWidget *parent = 0); + +private: + QTabWidget *tabWidget_; + + +}; - virtual void load(const rviz::Config &config); - virtual void save(rviz::Config config) const; -public Q_SLOTS: +/** + * Tab for creating, saving, loading, removing and deleting TFs + */ +class createTFTab : public QWidget +{ + Q_OBJECT + +public: + explicit createTFTab(QWidget *parent = 0); protected Q_SLOTS: + void createNewTF(); + void fromTextChanged(QString text); + void toTextChanged(QString text); + +private: + std::string from_tf_name_; + std::string to_tf_name_; + + QLineEdit *from_; + QLineEdit *to_; + + QPushButton *create_tf_btn_; +}; + + - void testFnc(); -protected: - QPushButton *btn_test_; +/** + * Tab for manipulating TFs + */ +class manipulateTFTab : public QWidget +{ + Q_OBJECT + +public: + explicit manipulateTFTab(QWidget *parent = 0); }; } // end namespace tf_keyboard_cal From a15281731c28c60199aff493900b36e0f8774cb8 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Sat, 29 Apr 2017 10:38:31 -0600 Subject: [PATCH 05/32] format of add/remove tab --- src/tf_keyboard_cal_gui.cpp | 60 ++++++++++++++++++++++++++++++++----- src/tf_keyboard_cal_gui.h | 28 ++++++++++++----- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 507d345..413c162 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -36,6 +36,7 @@ #include #include +#include #include "tf_keyboard_cal_gui.h" @@ -46,18 +47,20 @@ TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) tabWidget_ = new QTabWidget; tabWidget_->addTab(new createTFTab(), tr("Add / Remove")); tabWidget_->addTab(new manipulateTFTab(), tr("Manipulate")); + tabWidget_->addTab(new saveLoadTFTab(), tr("Save / Load")); QVBoxLayout *main_layout = new QVBoxLayout; main_layout->addWidget(tabWidget_); - setLayout(main_layout); - + setLayout(main_layout); } createTFTab::createTFTab(QWidget *parent) : QWidget(parent) { + + // TF controls QLabel *from_label = new QLabel(tr("from:")); QLabel *to_label = new QLabel(tr("to:")); - + from_ = new QLineEdit; from_->setPlaceholderText("from TF"); connect(from_, SIGNAL(textChanged(const QString &)), this, SLOT(fromTextChanged(const QString &))); @@ -69,7 +72,17 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) create_tf_btn_ = new QPushButton(this); create_tf_btn_->setText("Create TF"); connect(create_tf_btn_, SIGNAL(clicked()), this, SLOT(createNewTF())); - + + remove_tf_btn_ = new QPushButton(this); + remove_tf_btn_->setText("Remove TF"); + connect(remove_tf_btn_, SIGNAL(clicked()), this, SLOT(removeTF())); + + active_tfs_ = new QComboBox; + active_tfs_->addItem(tr("Select TF to delete")); + + // Layout + QGroupBox *add_section = new QGroupBox(tr("Add TF")); + QHBoxLayout *from_row = new QHBoxLayout; from_row->addWidget(from_label); from_row->addWidget(from_); @@ -78,16 +91,38 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) to_row->addWidget(to_label); to_row->addWidget(to_); + QHBoxLayout *remove_row = new QHBoxLayout; + remove_row->addWidget(active_tfs_); + remove_row->addWidget(remove_tf_btn_); + + QVBoxLayout *add_controls = new QVBoxLayout; + add_controls->addLayout(from_row); + add_controls->addLayout(to_row); + add_controls->addWidget(create_tf_btn_); + add_section->setLayout(add_controls); + + QGroupBox *remove_section = new QGroupBox(tr("Remove TF")); + + QVBoxLayout *remove_controls = new QVBoxLayout; + remove_controls->addLayout(remove_row); + remove_section->setLayout(remove_controls); + QVBoxLayout *main_layout = new QVBoxLayout; - main_layout->addLayout(from_row); - main_layout->addLayout(to_row); - main_layout->addWidget(create_tf_btn_); + main_layout->addWidget(add_section); + main_layout->addWidget(remove_section); setLayout(main_layout); } void createTFTab::createNewTF() { - ROS_DEBUG_STREAM_NAMED("createNewTF","create new TF button pressed"); + ROS_DEBUG_STREAM_NAMED("createNewTF","create new TF button pressed."); + ROS_DEBUG_STREAM_NAMED("createNewTF","from:to = " << from_tf_name_ << ":" << to_tf_name_); +} + +void createTFTab::removeTF() +{ + std::string tf_id = active_tfs_->currentText().toStdString(); + ROS_DEBUG_STREAM_NAMED("removeTF","remove TF: " << active_tfs_->currentIndex() << ", " << tf_id); } void createTFTab::fromTextChanged(QString text) @@ -111,6 +146,15 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) setLayout(main_layout); } +saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) +{ + QLabel *some_text = new QLabel(tr("Some Text")); + + QVBoxLayout *main_layout = new QVBoxLayout; + main_layout->addWidget(some_text); + setLayout(main_layout); +} + } // end namespace tf_keyboard_cal #include diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index f8b910a..ca113ae 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -44,6 +44,8 @@ #include #include #include +#include +#include namespace tf_keyboard_cal { @@ -55,12 +57,9 @@ class TFKeyboardCalGui : public rviz::Panel explicit TFKeyboardCalGui(QWidget *parent = 0); private: - QTabWidget *tabWidget_; - - + QTabWidget *tabWidget_; }; - /** * Tab for creating, saving, loading, removing and deleting TFs */ @@ -73,21 +72,23 @@ class createTFTab : public QWidget protected Q_SLOTS: void createNewTF(); + void removeTF(); + void fromTextChanged(QString text); void toTextChanged(QString text); private: std::string from_tf_name_; std::string to_tf_name_; - + QLineEdit *from_; QLineEdit *to_; QPushButton *create_tf_btn_; -}; - - + QPushButton *remove_tf_btn_; + QComboBox *active_tfs_; +}; /** * Tab for manipulating TFs @@ -100,6 +101,17 @@ class manipulateTFTab : public QWidget explicit manipulateTFTab(QWidget *parent = 0); }; +/** + * Tab for saving and loading TFs + */ +class saveLoadTFTab : public QWidget +{ + Q_OBJECT + +public: + explicit saveLoadTFTab(QWidget *parent = 0); +}; + } // end namespace tf_keyboard_cal #endif From 89f76b6b487cb9f3ad9f53c9cee84b220786753f Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Sat, 29 Apr 2017 23:01:00 -0600 Subject: [PATCH 06/32] work on manipulation tab --- src/tf_keyboard_cal_gui.cpp | 75 ++++++++++++++++++++++++++++++++++--- src/tf_keyboard_cal_gui.h | 28 +++++++++++++- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 413c162..be9dc64 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -61,9 +61,10 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) QLabel *from_label = new QLabel(tr("from:")); QLabel *to_label = new QLabel(tr("to:")); - from_ = new QLineEdit; - from_->setPlaceholderText("from TF"); - connect(from_, SIGNAL(textChanged(const QString &)), this, SLOT(fromTextChanged(const QString &))); + from_ = new QComboBox; + from_->setEditable(true); + from_->addItem(tr("Select existing or add new TF")); + connect(from_, SIGNAL(editTextChanged(const QString &)), this, SLOT(fromTextChanged(const QString &))); to_ = new QLineEdit; to_->setPlaceholderText("to TF"); @@ -139,13 +140,77 @@ void createTFTab::toTextChanged(QString text) manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) { - QLabel *some_text = new QLabel(tr("Some Text")); + QLabel *tf_label = new QLabel(tr("tf:")); + tf_ = new QComboBox; + tf_->addItem(tr("Select TF")); + + QLabel *xyz_delta_label = new QLabel(tr("xyz delta (m):")); + xyz_delta_box_ = new QLineEdit; + xyz_delta_box_->setText("0.1"); + connect(xyz_delta_box_, SIGNAL(textChanged(const QString &)), this, SLOT(setXYZDelta(const QString &))); + + QLabel *rpy_delta_label = new QLabel(tr("rpy delta (deg):")); + rpy_delta_box_ = new QLineEdit; + rpy_delta_box_->setText("5.0"); + connect(rpy_delta_box_, SIGNAL(textChanged(const QString &)), this, SLOT(setRPYDelta(const QString &))); + + QGroupBox *tf_ctrl_section = new QGroupBox(tr("Manipulation Data")); + + QHBoxLayout *tf_row = new QHBoxLayout; + tf_row->addWidget(tf_label); + tf_row->addWidget(tf_); + + QHBoxLayout *xyz_delta_row = new QHBoxLayout; + xyz_delta_row->addWidget(xyz_delta_label); + xyz_delta_row->addWidget(xyz_delta_box_); + + QHBoxLayout *rpy_delta_row = new QHBoxLayout; + rpy_delta_row->addWidget(rpy_delta_label); + rpy_delta_row->addWidget(rpy_delta_box_); + + QVBoxLayout *tf_controls = new QVBoxLayout; + tf_controls->addLayout(tf_row); + tf_controls->addLayout(xyz_delta_row); + tf_controls->addLayout(rpy_delta_row); + tf_ctrl_section->setLayout(tf_controls); QVBoxLayout *main_layout = new QVBoxLayout; - main_layout->addWidget(some_text); + main_layout->addWidget(tf_ctrl_section); setLayout(main_layout); } +void manipulateTFTab::setXYZDelta(QString text) +{ + xyz_delta_ = text.toDouble(); + ROS_DEBUG_STREAM_NAMED("set_xyz_delta","text = " << text.toStdString() << ", value = " << xyz_delta_); + + if (std::abs(xyz_delta_) > MAX_XYZ_DELTA) + { + ROS_WARN_STREAM_NAMED("setXYZDelta","Tried to set XYZ delta outside of limits. (+/-" << MAX_XYZ_DELTA << ")"); + xyz_delta_ > 0 ? xyz_delta_ = MAX_XYZ_DELTA : xyz_delta_ = -1.0 * MAX_XYZ_DELTA; + QString value = QString::number(xyz_delta_); + xyz_delta_box_->setText(value); + } + + ROS_DEBUG_STREAM_NAMED("set_xyz_delta","setting xyz_delta_ = " << xyz_delta_); +} + +void manipulateTFTab::setRPYDelta(QString text) +{ + rpy_delta_ = text.toDouble(); + ROS_DEBUG_STREAM_NAMED("set_rpy_delta","text = " << text.toStdString() << ", value = " << rpy_delta_); + + if (std::abs(rpy_delta_) > MAX_RPY_DELTA) + { + ROS_WARN_STREAM_NAMED("setRPYDelta","Tried to set RPY delta outside of limits. (+/-" << MAX_RPY_DELTA << ")"); + rpy_delta_ > 0 ? rpy_delta_ = MAX_RPY_DELTA : rpy_delta_ = -1.0 * MAX_RPY_DELTA; + QString value = QString::number(rpy_delta_); + rpy_delta_box_->setText(value); + } + + ROS_DEBUG_STREAM_NAMED("set_rpy_delta","setting rpy_delta_ = " << rpy_delta_); +} + saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) { QLabel *some_text = new QLabel(tr("Some Text")); diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index ca113ae..3d1badc 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -46,6 +46,7 @@ #include #include #include +#include namespace tf_keyboard_cal { @@ -81,7 +82,7 @@ protected Q_SLOTS: std::string from_tf_name_; std::string to_tf_name_; - QLineEdit *from_; + QComboBox *from_; QLineEdit *to_; QPushButton *create_tf_btn_; @@ -99,6 +100,31 @@ class manipulateTFTab : public QWidget public: explicit manipulateTFTab(QWidget *parent = 0); + +protected Q_SLOTS: + void incrementDOF(); + void setXYZDelta(QString text); + void setRPYDelta(QString text); + +private: + + static constexpr double MAX_XYZ_DELTA = 10.0; + static constexpr double MAX_RPY_DELTA = 180.0; + + double x_; + double y_; + double z_; + double roll_; + double pitch_; + double yaw_; + double xyz_delta_; + double rpy_delta_; + + QComboBox *tf_; + + QLineEdit *xyz_delta_box_; + QLineEdit *rpy_delta_box_; + }; /** From 7896388d8ee183cc1a19e8b5866af6b029f62e35 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Mon, 1 May 2017 23:05:40 -0600 Subject: [PATCH 07/32] work on increment layout --- src/tf_keyboard_cal_gui.cpp | 73 +++++++++++++++++++++++++++++++++++++ src/tf_keyboard_cal_gui.h | 17 ++++----- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index be9dc64..47fa536 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -140,6 +140,10 @@ void createTFTab::toTextChanged(QString text) manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) { + + xyz_delta_ = 0.1; + rpy_delta_ = 1.0; + QLabel *tf_label = new QLabel(tr("tf:")); tf_ = new QComboBox; tf_->addItem(tr("Select TF")); @@ -174,9 +178,78 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) tf_controls->addLayout(rpy_delta_row); tf_ctrl_section->setLayout(tf_controls); + // Set up xyr rpy increment controls + QGroupBox *tf_increment_section = new QGroupBox(tr("Increment TF")); + QVBoxLayout *tf_increment_controls = new QVBoxLayout; + + std::vector labels = { "x (m):", "y (m):", "z (m):", "r (deg):", "p (deg):", "y (deg):"}; + for (std::size_t i = 0; i < labels.size(); i++) + { + dof_values_.push_back(0.0); + + QLabel *label = new QLabel(tr(labels[i].c_str())); + + QPushButton *minus = new QPushButton; + minus->setText("-"); + minus->setProperty("sign", -1.0); + minus->setProperty("dof", (int)i); + connect(minus, SIGNAL(clicked()), this, SLOT(incrementDOF())); + + QLineEdit *value = new QLineEdit; + value->setText("0.0"); + value->setProperty("dof", (int)i); + dof_box_values_.push_back(value); + connect(dof_box_values_[i], SIGNAL(textChanged(const QString &)), this, SLOT(setIncrementValue(const QString &))); + + QPushButton *plus = new QPushButton; + plus->setText("+"); + plus->setProperty("sign", 1.0); + plus->setProperty("dof", (int)i); + connect(plus, SIGNAL(clicked()), this, SLOT(incrementDOF())); + + QHBoxLayout *row = new QHBoxLayout; + row->addWidget(label); + row->addWidget(minus); + row->addWidget(value); + row->addWidget(plus); + tf_increment_controls->addLayout(row); + } + + tf_increment_section->setLayout(tf_increment_controls); + + // set main layout QVBoxLayout *main_layout = new QVBoxLayout; main_layout->addWidget(tf_ctrl_section); + main_layout->addWidget(tf_increment_section); setLayout(main_layout); + +} + +void manipulateTFTab::setIncrementValue(QString text) +{ + double value = text.toDouble(); + int dof = (sender()->property("dof")).toInt(); + + dof_values_[dof] = value; + dof_box_values_[dof]->setText(QString::number(dof_values_[dof])); + + ROS_DEBUG_STREAM_NAMED("setIncrementValue","text = " << text.toStdString() << + ", value = " << value << ", dof = " << dof); +} + +void manipulateTFTab::incrementDOF() +{ + int dof = (sender()->property("dof")).toInt(); + double sign = (sender()->property("sign")).toDouble(); + + if (dof == 0 || dof == 1 || dof == 2) + dof_values_[dof] += (double)sign * xyz_delta_; + else + dof_values_[dof] += (double)sign * rpy_delta_; + + dof_box_values_[dof]->setText(QString::number(dof_values_[dof])); + + ROS_DEBUG_STREAM_NAMED("incrementDOF","dof = " << dof << ", sign = " << sign); } void manipulateTFTab::setXYZDelta(QString text) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 3d1badc..8b0dbc6 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -103,20 +103,17 @@ class manipulateTFTab : public QWidget protected Q_SLOTS: void incrementDOF(); + void setIncrementValue(QString text); + void setXYZDelta(QString text); void setRPYDelta(QString text); private: - static constexpr double MAX_XYZ_DELTA = 10.0; - static constexpr double MAX_RPY_DELTA = 180.0; - - double x_; - double y_; - double z_; - double roll_; - double pitch_; - double yaw_; + static constexpr double MAX_XYZ_DELTA = 100.0; + static constexpr double MAX_RPY_DELTA = 360.0; + + std::vector dof_values_; double xyz_delta_; double rpy_delta_; @@ -124,6 +121,8 @@ protected Q_SLOTS: QLineEdit *xyz_delta_box_; QLineEdit *rpy_delta_box_; + + std::vector dof_box_values_; }; From b4a2dda6d3a3754a5347a84f6e0c34a68ae8ddf6 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Tue, 2 May 2017 22:36:08 -0600 Subject: [PATCH 08/32] added save/load tab, gridlayout on increment tab --- src/tf_keyboard_cal_gui.cpp | 60 ++++++++++++++++++++++++++++++------- src/tf_keyboard_cal_gui.h | 12 ++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 47fa536..280d916 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -37,6 +37,8 @@ #include #include #include +#include +#include #include "tf_keyboard_cal_gui.h" @@ -180,7 +182,7 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) // Set up xyr rpy increment controls QGroupBox *tf_increment_section = new QGroupBox(tr("Increment TF")); - QVBoxLayout *tf_increment_controls = new QVBoxLayout; + QGridLayout *grid_layout = new QGridLayout(); std::vector labels = { "x (m):", "y (m):", "z (m):", "r (deg):", "p (deg):", "y (deg):"}; for (std::size_t i = 0; i < labels.size(); i++) @@ -207,15 +209,13 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) plus->setProperty("dof", (int)i); connect(plus, SIGNAL(clicked()), this, SLOT(incrementDOF())); - QHBoxLayout *row = new QHBoxLayout; - row->addWidget(label); - row->addWidget(minus); - row->addWidget(value); - row->addWidget(plus); - tf_increment_controls->addLayout(row); + grid_layout->addWidget(label, i, 1); + grid_layout->addWidget(minus, i, 2); + grid_layout->addWidget(value, i, 3); + grid_layout->addWidget(plus, i, 4); } - tf_increment_section->setLayout(tf_increment_controls); + tf_increment_section->setLayout(grid_layout); // set main layout QVBoxLayout *main_layout = new QVBoxLayout; @@ -286,13 +286,53 @@ void manipulateTFTab::setRPYDelta(QString text) saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) { - QLabel *some_text = new QLabel(tr("Some Text")); + load_btn_ = new QPushButton(tr("Load TFs from File"), this); + connect(load_btn_ , SIGNAL(clicked()), this, SLOT(load())); + + save_btn_ = new QPushButton(tr("Save TFs to File"), this); + connect(save_btn_ , SIGNAL(clicked()), this, SLOT(save())); + + QGroupBox *file_section = new QGroupBox(tr("Save/Load TF Files")); + + QVBoxLayout *file_layout = new QVBoxLayout; + file_layout->addWidget(load_btn_); + file_layout->addWidget(save_btn_); + + file_section->setLayout(file_layout); QVBoxLayout *main_layout = new QVBoxLayout; - main_layout->addWidget(some_text); + main_layout->addWidget(file_section); setLayout(main_layout); } +void saveLoadTFTab::load() +{ + QString filters("YAML files (*.yaml);;All files (*.*)"); + QString default_filter("YAML files (*.yaml)"); + + QString directory = + QFileDialog::getOpenFileName(0, "Load TF File", QDir::currentPath(), filters, &default_filter); + + full_load_path_ = directory.toStdString(); + ROS_DEBUG_STREAM_NAMED("load","load_file = " << full_load_path_); +} + +void saveLoadTFTab::save() +{ + QString filters("YAML files (*.yaml);;All files (*.*)"); + QString default_filter("YAML files (*.yaml)"); + + QString directory = + QFileDialog::getSaveFileName(0, "Save TF File", QDir::currentPath(), filters, &default_filter); + + + full_save_path_ = directory.toStdString(); + + // TODO: check for file extension and automatically append if left off + + ROS_DEBUG_STREAM_NAMED("load","save_file = " << full_save_path_); +} + } // end namespace tf_keyboard_cal #include diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 8b0dbc6..6cc59aa 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -47,6 +47,7 @@ #include #include #include +#include namespace tf_keyboard_cal { @@ -135,6 +136,17 @@ class saveLoadTFTab : public QWidget public: explicit saveLoadTFTab(QWidget *parent = 0); + +protected Q_SLOTS: + void load(); + void save(); + +private: + QPushButton *load_btn_; + QPushButton *save_btn_; + + std::string full_save_path_; + std::string full_load_path_; }; } // end namespace tf_keyboard_cal From a0f647bb87189949a0c299a108595ad0a7653df5 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Wed, 10 May 2017 00:04:48 -0600 Subject: [PATCH 09/32] added hooks to remote_receiver --- CMakeLists.txt | 6 +- .../tf_keyboard_cal}/tf_keyboard_cal_gui.h | 13 +++- include/tf_keyboard_cal/tf_remote_receiver.h | 63 +++++++++++++++++++ src/tf_keyboard_cal_gui.cpp | 11 +++- src/tf_remote_receiver.cpp | 59 +++++++++++++++++ 5 files changed, 147 insertions(+), 5 deletions(-) rename {src => include/tf_keyboard_cal}/tf_keyboard_cal_gui.h (96%) create mode 100644 include/tf_keyboard_cal/tf_remote_receiver.h create mode 100644 src/tf_remote_receiver.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a946f6..d9113b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,8 +90,12 @@ set(SOURCE_FILES src/tf_keyboard_cal_gui.cpp ) +add_library(${PROJECT_NAME}_receiver src/tf_remote_receiver.cpp) +target_link_libraries(${PROJECT_NAME}_receiver ${catkin_LIBRARIES}) + add_library(${PROJECT_NAME}_gui ${SOURCE_FILES}) -target_link_libraries(${PROJECT_NAME}_gui ${rviz_DEFAULT_PLUGIN_LIBRARIES} ${QT_LIBRARIES} ${catkin_LIBRARIES}) +target_link_libraries(${PROJECT_NAME}_gui ${PROJECT_NAME}_receiver ${rviz_DEFAULT_PLUGIN_LIBRARIES} ${QT_LIBRARIES} ${catkin_LIBRARIES}) + # Library # add_library(${PROJECT_NAME}_manual_tf_alignment diff --git a/src/tf_keyboard_cal_gui.h b/include/tf_keyboard_cal/tf_keyboard_cal_gui.h similarity index 96% rename from src/tf_keyboard_cal_gui.h rename to include/tf_keyboard_cal/tf_keyboard_cal_gui.h index 6cc59aa..6b3c19a 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/include/tf_keyboard_cal/tf_keyboard_cal_gui.h @@ -40,6 +40,8 @@ #include #endif +#include + #include #include #include @@ -57,9 +59,11 @@ class TFKeyboardCalGui : public rviz::Panel Q_OBJECT public: explicit TFKeyboardCalGui(QWidget *parent = 0); - + private: - QTabWidget *tabWidget_; + QTabWidget *tabWidget_; + + }; /** @@ -90,6 +94,8 @@ protected Q_SLOTS: QPushButton *remove_tf_btn_; QComboBox *active_tfs_; + + TFRemoteReceiver *remote_receiver_; }; /** @@ -124,7 +130,8 @@ protected Q_SLOTS: QLineEdit *rpy_delta_box_; std::vector dof_box_values_; - + + TFRemoteReceiver *remote_receiver_; }; /** diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h new file mode 100644 index 0000000..9119764 --- /dev/null +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -0,0 +1,63 @@ +/**************************************************************************************************** + * Software License Agreement (BSD License) + * + * Copyright 2017, Andy McEvoy + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ****************************************************************************************************/ +/* + * Author(s) : Andy McEvoy ( mcevoy.andy@gmail.com ) + * Desc : Object for wrapping tf control functionality + * Created : 09 - May - 2017 + */ + +#ifndef TF_REMOTE_RECEIVER_H +#define TF_REMOTE_RECEIVER_H + +#include + +namespace tf_keyboard_cal +{ + +class TFRemoteReceiver +{ +public: + // singleton + static TFRemoteReceiver& getInstance() + { + static TFRemoteReceiver instance; + return instance; + } + + void createTF(); + void removeTF(); + void updateTF(); + +private: + TFRemoteReceiver(); + +}; // end class TFRemoteReceiver + +} // end namespace tf_keyboard_cal + +#endif diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 280d916..5cbde8c 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -40,7 +40,7 @@ #include #include -#include "tf_keyboard_cal_gui.h" +#include namespace tf_keyboard_cal { @@ -114,18 +114,23 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) main_layout->addWidget(add_section); main_layout->addWidget(remove_section); setLayout(main_layout); + + remote_receiver_ = &TFRemoteReceiver::getInstance(); } void createTFTab::createNewTF() { ROS_DEBUG_STREAM_NAMED("createNewTF","create new TF button pressed."); ROS_DEBUG_STREAM_NAMED("createNewTF","from:to = " << from_tf_name_ << ":" << to_tf_name_); + //TFRemoteReceiver::getInstance().createTF(); + remote_receiver_->createTF(); } void createTFTab::removeTF() { std::string tf_id = active_tfs_->currentText().toStdString(); ROS_DEBUG_STREAM_NAMED("removeTF","remove TF: " << active_tfs_->currentIndex() << ", " << tf_id); + remote_receiver_->removeTF(); } void createTFTab::fromTextChanged(QString text) @@ -222,6 +227,8 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) main_layout->addWidget(tf_ctrl_section); main_layout->addWidget(tf_increment_section); setLayout(main_layout); + + remote_receiver_ = &TFRemoteReceiver::getInstance(); } @@ -250,6 +257,7 @@ void manipulateTFTab::incrementDOF() dof_box_values_[dof]->setText(QString::number(dof_values_[dof])); ROS_DEBUG_STREAM_NAMED("incrementDOF","dof = " << dof << ", sign = " << sign); + remote_receiver_->updateTF(); } void manipulateTFTab::setXYZDelta(QString text) @@ -303,6 +311,7 @@ saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) QVBoxLayout *main_layout = new QVBoxLayout; main_layout->addWidget(file_section); setLayout(main_layout); + } void saveLoadTFTab::load() diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp new file mode 100644 index 0000000..456030b --- /dev/null +++ b/src/tf_remote_receiver.cpp @@ -0,0 +1,59 @@ +/**************************************************************************************************** + * Software License Agreement (BSD License) + * + * Copyright 2017, Andy McEvoy + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ****************************************************************************************************/ +/* + * Author(s) : Andy McEvoy ( mcevoy.andy@gmail.com ) + * Desc : implementation of TFRemoteReceiver. See H file for documentation. + * Created : 09 - May - 2017 + */ + +#include + +namespace tf_keyboard_cal +{ + +TFRemoteReceiver::TFRemoteReceiver() +{ + std::cout << "\033[1;36m" << "remote receiver initialized" << "\033[0m" << std::endl; +} + +void TFRemoteReceiver::createTF() +{ + std::cout << "\033[1;36m" << "create tf" << "\033[0m" << std::endl; +} + +void TFRemoteReceiver::removeTF() +{ + std::cout << "\033[1;36m" << "remove tf" << "\033[0m" << std::endl; +} + +void TFRemoteReceiver::updateTF() +{ + std::cout << "\033[1;36m" << "update tf" << "\033[0m" << std::endl; +} + +} // end namespace tf_keyboard_cal From 181b4e82593f6c79c244263c42f639b4553af2c2 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Thu, 11 May 2017 01:14:19 -0600 Subject: [PATCH 10/32] playing with pubs and msgs --- CMakeLists.txt | 7 +++-- include/tf_keyboard_cal/tf_remote_receiver.h | 12 +++++++++ package.xml | 4 +++ src/tf_keyboard_cal_gui.cpp | 5 ++-- .../tf_keyboard_cal_gui.h | 4 +-- src/tf_remote_receiver.cpp | 26 +++++++++++++++++++ 6 files changed, 52 insertions(+), 6 deletions(-) rename {include/tf_keyboard_cal => src}/tf_keyboard_cal_gui.h (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index d9113b8..4317440 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,8 @@ find_package(catkin REQUIRED COMPONENTS tf_conversions interactive_markers rviz + std_msgs + geometry_msgs ) find_package(Eigen REQUIRED) @@ -29,8 +31,9 @@ catkin_package( # ${PROJECT_NAME}_manual_tf_alignment # ${PROJECT_NAME}_imarker_simple CATKIN_DEPENDS -# keyboard - roscpp + # keyboard + message_runtime + roscpp tf2 tf rosparam_shortcuts diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index 9119764..0d197e0 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -36,6 +36,9 @@ #include +#include + + namespace tf_keyboard_cal { @@ -55,6 +58,15 @@ class TFRemoteReceiver private: TFRemoteReceiver(); + + Eigen::Vector3d translation_; + Eigen::Vector3d rotation_; + std::string from_; + std::string to_; + + ros::NodeHandle nh_; + ros::Publisher create_tf_pub_; + ros::Publisher remove_tf_pub_; }; // end class TFRemoteReceiver diff --git a/package.xml b/package.xml index bf43023..b468773 100644 --- a/package.xml +++ b/package.xml @@ -29,6 +29,10 @@ roslint tf_conversions interactive_markers + message_generation + message_runtime + std_msgs + geometry_msgs diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 5cbde8c..0a38699 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -40,7 +40,8 @@ #include #include -#include +// TODO: Why doesn't example put this in the include dir? +#include "tf_keyboard_cal_gui.h" namespace tf_keyboard_cal { @@ -53,7 +54,7 @@ TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) QVBoxLayout *main_layout = new QVBoxLayout; main_layout->addWidget(tabWidget_); - setLayout(main_layout); + setLayout(main_layout); } createTFTab::createTFTab(QWidget *parent) : QWidget(parent) diff --git a/include/tf_keyboard_cal/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h similarity index 99% rename from include/tf_keyboard_cal/tf_keyboard_cal_gui.h rename to src/tf_keyboard_cal_gui.h index 6b3c19a..c22f5d2 100644 --- a/include/tf_keyboard_cal/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -59,11 +59,11 @@ class TFKeyboardCalGui : public rviz::Panel Q_OBJECT public: explicit TFKeyboardCalGui(QWidget *parent = 0); + + void publishTFs(); private: QTabWidget *tabWidget_; - - }; /** diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index 456030b..b33210e 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -33,17 +33,43 @@ #include +#include + namespace tf_keyboard_cal { TFRemoteReceiver::TFRemoteReceiver() + : nh_("~") { std::cout << "\033[1;36m" << "remote receiver initialized" << "\033[0m" << std::endl; + translation_[0] = 1.0; + translation_[1] = 1.0; + translation_[2] = 1.0; + + rotation_[0] = 0.7; + rotation_[1] = 0.7; + rotation_[2] = 0.7; + + from_ = "world"; + to_ = "robot"; + + create_tf_pub_ = nh_.advertise("create_rviz_tf", 10); + remove_tf_pub_ = nh_.advertise("remove_rviz_tf", 10); + } void TFRemoteReceiver::createTF() { std::cout << "\033[1;36m" << "create tf" << "\033[0m" << std::endl; + + geometry_msgs::TransformStamped new_tf_msg; + unsigned long int id = 1; + new_tf_msg.header.seq = id; + new_tf_msg.header.frame_id = from_; + new_tf_msg.child_frame_id = to_; + + create_tf_pub_.publish(new_tf_msg); + } void TFRemoteReceiver::removeTF() From 63dd0ac2abb421450f2cd63d4a4365cdd711b1a6 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Mon, 15 May 2017 22:56:55 -0600 Subject: [PATCH 11/32] work on add/remove comboboxes --- include/tf_keyboard_cal/tf_remote_receiver.h | 9 +--- src/tf_keyboard_cal_gui.cpp | 47 ++++++++++++++++++-- src/tf_keyboard_cal_gui.h | 16 ++++++- src/tf_remote_receiver.cpp | 23 ++-------- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index 0d197e0..c02dca4 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -35,7 +35,7 @@ #define TF_REMOTE_RECEIVER_H #include - +#include #include @@ -52,18 +52,13 @@ class TFRemoteReceiver return instance; } - void createTF(); + void createTF(std::size_t id, std::string from, std::string to); void removeTF(); void updateTF(); private: TFRemoteReceiver(); - Eigen::Vector3d translation_; - Eigen::Vector3d rotation_; - std::string from_; - std::string to_; - ros::NodeHandle nh_; ros::Publisher create_tf_pub_; ros::Publisher remove_tf_pub_; diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 0a38699..333334a 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -48,7 +48,10 @@ namespace tf_keyboard_cal TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) { tabWidget_ = new QTabWidget; - tabWidget_->addTab(new createTFTab(), tr("Add / Remove")); + createTFTab *new_create_tab = new createTFTab(); + new_create_tab->setActiveTFComboBox(); + tabWidget_->addTab(new_create_tab, tr("Add / Remove")); + // tabWidget_->addTab(new createTFTab(), tr("Add / Remove")); tabWidget_->addTab(new manipulateTFTab(), tr("Manipulate")); tabWidget_->addTab(new saveLoadTFTab(), tr("Save / Load")); @@ -57,6 +60,11 @@ TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) setLayout(main_layout); } +void createTFTab::setActiveTFComboBox() +{ + ROS_DEBUG_STREAM_NAMED("setActiveTFComboBox","test"); +} + createTFTab::createTFTab(QWidget *parent) : QWidget(parent) { @@ -116,6 +124,8 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) main_layout->addWidget(remove_section); setLayout(main_layout); + id_ = 0; + remote_receiver_ = &TFRemoteReceiver::getInstance(); } @@ -123,14 +133,45 @@ void createTFTab::createNewTF() { ROS_DEBUG_STREAM_NAMED("createNewTF","create new TF button pressed."); ROS_DEBUG_STREAM_NAMED("createNewTF","from:to = " << from_tf_name_ << ":" << to_tf_name_); - //TFRemoteReceiver::getInstance().createTF(); - remote_receiver_->createTF(); + + tf_data new_tf; + new_tf.id_ = id_++; + new_tf.from_ = from_tf_name_; + new_tf.to_ = to_tf_name_; + std::string text = std::to_string(new_tf.id_) + ": " + new_tf.from_ + "-" + new_tf.to_; + new_tf.name_ = QString::fromStdString(text); + + active_tf_list_.push_back(new_tf); + + active_tfs_->clear(); + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + active_tfs_->addItem(active_tf_list_[i].name_); + } + + remote_receiver_->createTF(new_tf.id_, new_tf.from_, new_tf.to_); } void createTFTab::removeTF() { std::string tf_id = active_tfs_->currentText().toStdString(); ROS_DEBUG_STREAM_NAMED("removeTF","remove TF: " << active_tfs_->currentIndex() << ", " << tf_id); + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + if (active_tf_list_[i].name_ == active_tfs_->currentText()) + { + ROS_DEBUG_STREAM_NAMED("removeTF","remove index = " << i); + active_tf_list_.erase(active_tf_list_.begin() + i); + break; + } + } + + active_tfs_->clear(); + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + active_tfs_->addItem(active_tf_list_[i].name_); + } + remote_receiver_->removeTF(); } diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index c22f5d2..2ac34f3 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -64,6 +64,7 @@ class TFKeyboardCalGui : public rviz::Panel private: QTabWidget *tabWidget_; + QComboBox *active_tfs_; }; /** @@ -72,9 +73,10 @@ class TFKeyboardCalGui : public rviz::Panel class createTFTab : public QWidget { Q_OBJECT - + public: explicit createTFTab(QWidget *parent = 0); + void setActiveTFComboBox(); protected Q_SLOTS: void createNewTF(); @@ -84,9 +86,21 @@ protected Q_SLOTS: void toTextChanged(QString text); private: + + struct tf_data{ + std::size_t id_; + std::string from_; + std::string to_; + QString name_; + }; + + std::vector< tf_data > active_tf_list_; + std::string from_tf_name_; std::string to_tf_name_; + std::size_t id_; + QComboBox *from_; QLineEdit *to_; diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index b33210e..8485e44 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -33,8 +33,6 @@ #include -#include - namespace tf_keyboard_cal { @@ -42,34 +40,21 @@ TFRemoteReceiver::TFRemoteReceiver() : nh_("~") { std::cout << "\033[1;36m" << "remote receiver initialized" << "\033[0m" << std::endl; - translation_[0] = 1.0; - translation_[1] = 1.0; - translation_[2] = 1.0; - - rotation_[0] = 0.7; - rotation_[1] = 0.7; - rotation_[2] = 0.7; - - from_ = "world"; - to_ = "robot"; create_tf_pub_ = nh_.advertise("create_rviz_tf", 10); remove_tf_pub_ = nh_.advertise("remove_rviz_tf", 10); - } -void TFRemoteReceiver::createTF() +void TFRemoteReceiver::createTF(std::size_t id, std::string from, std::string to) { std::cout << "\033[1;36m" << "create tf" << "\033[0m" << std::endl; - + ROS_DEBUG_STREAM_NAMED("createTF","from: " << from << ", to: " << to); geometry_msgs::TransformStamped new_tf_msg; - unsigned long int id = 1; new_tf_msg.header.seq = id; - new_tf_msg.header.frame_id = from_; - new_tf_msg.child_frame_id = to_; + new_tf_msg.header.frame_id = from; + new_tf_msg.child_frame_id = to; create_tf_pub_.publish(new_tf_msg); - } void TFRemoteReceiver::removeTF() From f29f830e27ce913b2c67efd0a3b305e58f0090be Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Wed, 17 May 2017 09:34:29 -0600 Subject: [PATCH 12/32] work on populating gui elements with correct data --- CMakeLists.txt | 1 + include/tf_keyboard_cal/tf_remote_receiver.h | 7 +++ launch/rviz_demo.launch | 8 ++- launch/rviz_demo.rviz | 51 ++++++++++-------- package.xml | 1 + src/tf_keyboard_cal_gui.cpp | 57 ++++++++++++-------- src/tf_keyboard_cal_gui.h | 52 +++++++++--------- src/tf_remote_receiver.cpp | 11 +++- 8 files changed, 113 insertions(+), 75 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4317440..6ee9e42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ set(CMAKE_CXX_FLAGS "-std=c++11 -Wall ${CMAKE_CXX_FLAGS}") find_package(catkin REQUIRED COMPONENTS roscpp tf2 + tf2_msgs tf roslib roslint diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index c02dca4..68671c1 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -37,6 +37,7 @@ #include #include #include +#include namespace tf_keyboard_cal @@ -57,11 +58,17 @@ class TFRemoteReceiver void updateTF(); private: + + void tf_callback(const tf2_msgs::TFMessage &msg); + TFRemoteReceiver(); ros::NodeHandle nh_; ros::Publisher create_tf_pub_; ros::Publisher remove_tf_pub_; + ros::Subscriber tf_sub_; + + }; // end class TFRemoteReceiver diff --git a/launch/rviz_demo.launch b/launch/rviz_demo.launch index 1768a98..3ea192d 100644 --- a/launch/rviz_demo.launch +++ b/launch/rviz_demo.launch @@ -1,6 +1,10 @@ - + - + + + + + diff --git a/launch/rviz_demo.rviz b/launch/rviz_demo.rviz index 3806e6c..f9d21a9 100644 --- a/launch/rviz_demo.rviz +++ b/launch/rviz_demo.rviz @@ -7,7 +7,7 @@ Panels: - /Global Options1 - /Status1 Splitter Ratio: 0.5 - Tree Height: 353 + Tree Height: 308 - Class: rviz/Selection Name: Selection - Class: rviz/Tool Properties @@ -16,7 +16,7 @@ Panels: - /2D Nav Goal1 - /Publish Point1 Name: Tool Properties - Splitter Ratio: 0.588679 + Splitter Ratio: 0.588679016 - Class: rviz/Views Expanded: - /Current View1 @@ -27,6 +27,8 @@ Panels: Name: Time SyncMode: 0 SyncSource: "" + - Class: tf_keyboard_cal/TFKeyboardCalGui + Name: TFKeyboardCalGui Visualization Manager: Class: "" Displays: @@ -36,7 +38,7 @@ Visualization Manager: Color: 160; 160; 164 Enabled: true Line Style: - Line Width: 0.03 + Line Width: 0.0299999993 Value: Lines Name: Grid Normal Cell Count: 0 @@ -53,7 +55,7 @@ Visualization Manager: Frame Timeout: 15 Frames: All Enabled: false - thing: + base: Value: true world: Value: true @@ -63,8 +65,8 @@ Visualization Manager: Show Axes: true Show Names: true Tree: - world: - thing: + base: + world: {} Update Interval: 0 Value: true @@ -79,17 +81,18 @@ Visualization Manager: Value: true - Alpha: 1 Axes Length: 0.5 - Axes Radius: 0.05 + Axes Radius: 0.0500000007 Class: rviz/Pose Color: 255; 25; 0 Enabled: false - Head Length: 0.3 - Head Radius: 0.1 + Head Length: 0.300000012 + Head Radius: 0.100000001 Name: PoseStamped Shaft Length: 1 - Shaft Radius: 0.05 + Shaft Radius: 0.0500000007 Shape: Axes Topic: /posestamped + Unreliable: false Value: false Enabled: true Global Options: @@ -117,36 +120,40 @@ Visualization Manager: Class: rviz/Orbit Distance: 2.64484 Enable Stereo Rendering: - Stereo Eye Separation: 0.06 + Stereo Eye Separation: 0.0599999987 Stereo Focal Distance: 1 Swap Stereo Eyes: false Value: false Focal Point: - X: 0.408868 - Y: 0.78209 - Z: 0.395786 + X: 0.408868015 + Y: 0.782090008 + Z: 0.395785987 + Focal Shape Fixed Size: true + Focal Shape Size: 0.0500000007 Name: Current View - Near Clip Distance: 0.01 - Pitch: 0.270398 + Near Clip Distance: 0.00999999978 + Pitch: 0.270397991 Target Frame: Value: Orbit (rviz) - Yaw: 4.89858 + Yaw: 4.89858007 Saved: ~ Window Geometry: Displays: collapsed: false - Height: 678 + Height: 1056 Hide Left Dock: false Hide Right Dock: true - QMainWindow State: 000000ff00000000fd00000004000000000000013c000001f7fc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000006400fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c0061007900730100000043000001f7000000de00fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000010f000001f7fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a005600690065007700730000000043000001f7000000b800fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e100000197000000030000055400000044fc0100000002fb0000000800540069006d00650100000000000005540000025800fffffffb0000000800540069006d0065010000000000000450000000000000000000000412000001f700000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + QMainWindow State: 000000ff00000000fd00000004000000000000016d00000390fc0200000009fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000006400fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c0061007900730100000028000001c3000000dd00fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000002000540046004b006500790062006f00610072006400430061006c00470075006901000001f1000001c7000001c700ffffff000000010000010f000001f7fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a005600690065007700730000000043000001f7000000b000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e100000197000000030000078000000044fc0100000002fb0000000800540069006d00650100000000000007800000030000fffffffb0000000800540069006d006501000000000000045000000000000000000000060d0000039000000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 Selection: collapsed: false + TFKeyboardCalGui: + collapsed: false Time: collapsed: false Tool Properties: collapsed: false Views: collapsed: true - Width: 1364 - X: 0 - Y: 27 + Width: 1920 + X: 1920 + Y: 24 diff --git a/package.xml b/package.xml index b468773..c77825d 100644 --- a/package.xml +++ b/package.xml @@ -22,6 +22,7 @@ rviz_visual_tools roscpp tf2 + tf2_msgs tf roslib rosparam_shortcuts diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 333334a..1b5dd93 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -45,24 +45,29 @@ namespace tf_keyboard_cal { + +std::vector< tf_data > active_tf_list_; + TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) -{ - tabWidget_ = new QTabWidget; - createTFTab *new_create_tab = new createTFTab(); - new_create_tab->setActiveTFComboBox(); - tabWidget_->addTab(new_create_tab, tr("Add / Remove")); - // tabWidget_->addTab(new createTFTab(), tr("Add / Remove")); - tabWidget_->addTab(new manipulateTFTab(), tr("Manipulate")); - tabWidget_->addTab(new saveLoadTFTab(), tr("Save / Load")); +{ + tab_widget_ = new QTabWidget; + tab_widget_->addTab(new createTFTab(), tr("Add / Remove")); + + new_manipulate_tab_ = new manipulateTFTab(); + connect(tab_widget_, SIGNAL(tabBarClicked(int)), this, SLOT(updateTFList())); + tab_widget_->addTab(new_manipulate_tab_, tr("Manipulate")); + // tab_widget_->addTab(new manipulateTFTab(), tr("Manipulate")); + + tab_widget_->addTab(new saveLoadTFTab(), tr("Save / Load")); QVBoxLayout *main_layout = new QVBoxLayout; - main_layout->addWidget(tabWidget_); + main_layout->addWidget(tab_widget_); setLayout(main_layout); } -void createTFTab::setActiveTFComboBox() +void TFKeyboardCalGui::updateTFList() { - ROS_DEBUG_STREAM_NAMED("setActiveTFComboBox","test"); + new_manipulate_tab_->updateTFList(); } createTFTab::createTFTab(QWidget *parent) : QWidget(parent) @@ -91,7 +96,7 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) active_tfs_ = new QComboBox; active_tfs_->addItem(tr("Select TF to delete")); - + // Layout QGroupBox *add_section = new QGroupBox(tr("Add TF")); @@ -148,7 +153,7 @@ void createTFTab::createNewTF() { active_tfs_->addItem(active_tf_list_[i].name_); } - + remote_receiver_->createTF(new_tf.id_, new_tf.from_, new_tf.to_); } @@ -188,30 +193,28 @@ void createTFTab::toTextChanged(QString text) } manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) -{ - - xyz_delta_ = 0.1; - rpy_delta_ = 1.0; - +{ QLabel *tf_label = new QLabel(tr("tf:")); - tf_ = new QComboBox; - tf_->addItem(tr("Select TF")); + active_tfs_ = new QComboBox; + active_tfs_->addItem(tr("Select TF")); QLabel *xyz_delta_label = new QLabel(tr("xyz delta (m):")); xyz_delta_box_ = new QLineEdit; xyz_delta_box_->setText("0.1"); + xyz_delta_ = 0.1; connect(xyz_delta_box_, SIGNAL(textChanged(const QString &)), this, SLOT(setXYZDelta(const QString &))); QLabel *rpy_delta_label = new QLabel(tr("rpy delta (deg):")); rpy_delta_box_ = new QLineEdit; rpy_delta_box_->setText("5.0"); + rpy_delta_ = 5.0; connect(rpy_delta_box_, SIGNAL(textChanged(const QString &)), this, SLOT(setRPYDelta(const QString &))); QGroupBox *tf_ctrl_section = new QGroupBox(tr("Manipulation Data")); QHBoxLayout *tf_row = new QHBoxLayout; tf_row->addWidget(tf_label); - tf_row->addWidget(tf_); + tf_row->addWidget(active_tfs_); QHBoxLayout *xyz_delta_row = new QHBoxLayout; xyz_delta_row->addWidget(xyz_delta_label); @@ -248,7 +251,7 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) value->setText("0.0"); value->setProperty("dof", (int)i); dof_box_values_.push_back(value); - connect(dof_box_values_[i], SIGNAL(textChanged(const QString &)), this, SLOT(setIncrementValue(const QString &))); + connect(dof_box_values_[i], SIGNAL(textEdited(const QString &)), this, SLOT(setIncrementValue(const QString &))); QPushButton *plus = new QPushButton; plus->setText("+"); @@ -274,13 +277,21 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) } +void manipulateTFTab::updateTFList() +{ + active_tfs_->clear(); + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + active_tfs_->addItem(active_tf_list_[i].name_); + } +} + void manipulateTFTab::setIncrementValue(QString text) { double value = text.toDouble(); int dof = (sender()->property("dof")).toInt(); dof_values_[dof] = value; - dof_box_values_[dof]->setText(QString::number(dof_values_[dof])); ROS_DEBUG_STREAM_NAMED("setIncrementValue","text = " << text.toStdString() << ", value = " << value << ", dof = " << dof); diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 2ac34f3..3c732ea 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -53,18 +53,11 @@ namespace tf_keyboard_cal { - -class TFKeyboardCalGui : public rviz::Panel -{ - Q_OBJECT -public: - explicit TFKeyboardCalGui(QWidget *parent = 0); - - void publishTFs(); - -private: - QTabWidget *tabWidget_; - QComboBox *active_tfs_; +struct tf_data{ + std::size_t id_; + std::string from_; + std::string to_; + QString name_; }; /** @@ -76,8 +69,7 @@ class createTFTab : public QWidget public: explicit createTFTab(QWidget *parent = 0); - void setActiveTFComboBox(); - + protected Q_SLOTS: void createNewTF(); void removeTF(); @@ -86,16 +78,6 @@ protected Q_SLOTS: void toTextChanged(QString text); private: - - struct tf_data{ - std::size_t id_; - std::string from_; - std::string to_; - QString name_; - }; - - std::vector< tf_data > active_tf_list_; - std::string from_tf_name_; std::string to_tf_name_; @@ -121,7 +103,8 @@ class manipulateTFTab : public QWidget public: explicit manipulateTFTab(QWidget *parent = 0); - + void updateTFList(); + protected Q_SLOTS: void incrementDOF(); void setIncrementValue(QString text); @@ -138,7 +121,7 @@ protected Q_SLOTS: double xyz_delta_; double rpy_delta_; - QComboBox *tf_; + QComboBox *active_tfs_; QLineEdit *xyz_delta_box_; QLineEdit *rpy_delta_box_; @@ -170,6 +153,23 @@ protected Q_SLOTS: std::string full_load_path_; }; +/** + * Main class + */ +class TFKeyboardCalGui : public rviz::Panel +{ + Q_OBJECT +public: + explicit TFKeyboardCalGui(QWidget *parent = 0); + +protected Q_SLOTS: + void updateTFList(); + +private: + QTabWidget *tab_widget_; + manipulateTFTab *new_manipulate_tab_; +}; + } // end namespace tf_keyboard_cal #endif diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index 8485e44..13cf184 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -41,8 +41,15 @@ TFRemoteReceiver::TFRemoteReceiver() { std::cout << "\033[1;36m" << "remote receiver initialized" << "\033[0m" << std::endl; - create_tf_pub_ = nh_.advertise("create_rviz_tf", 10); - remove_tf_pub_ = nh_.advertise("remove_rviz_tf", 10); + create_tf_pub_ = nh_.advertise("/create_rviz_tf", 10); + remove_tf_pub_ = nh_.advertise("/remove_rviz_tf", 10); + tf_sub_ = nh_.subscribe("/tf", 1, &TFRemoteReceiver::tf_callback, this); +} + +void TFRemoteReceiver::tf_callback(const tf2_msgs::TFMessage &msg) +{ + ROS_DEBUG_STREAM_THROTTLE_NAMED(1, "tf_callback", "tf callback..." << msg.transforms.size()); + } void TFRemoteReceiver::createTF(std::size_t id, std::string from, std::string to) From 47861cd54dff4bd72da6534e61093112ea532c8e Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Wed, 17 May 2017 17:06:54 -0600 Subject: [PATCH 13/32] work on update tf msg --- include/tf_keyboard_cal/tf_remote_receiver.h | 2 +- src/tf_keyboard_cal_gui.cpp | 39 +++++++++++++++++--- src/tf_keyboard_cal_gui.h | 3 +- src/tf_remote_receiver.cpp | 12 +----- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index 68671c1..445c569 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -53,7 +53,7 @@ class TFRemoteReceiver return instance; } - void createTF(std::size_t id, std::string from, std::string to); + void createTF(geometry_msgs::TransformStamped new_tf_msg); void removeTF(); void updateTF(); diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 1b5dd93..6e3340f 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -139,22 +139,32 @@ void createTFTab::createNewTF() ROS_DEBUG_STREAM_NAMED("createNewTF","create new TF button pressed."); ROS_DEBUG_STREAM_NAMED("createNewTF","from:to = " << from_tf_name_ << ":" << to_tf_name_); + // create new tf tf_data new_tf; new_tf.id_ = id_++; new_tf.from_ = from_tf_name_; new_tf.to_ = to_tf_name_; + for (std::size_t i = 0; i < 6; i++) + { + new_tf.values_[i] = 0.0; + } std::string text = std::to_string(new_tf.id_) + ": " + new_tf.from_ + "-" + new_tf.to_; new_tf.name_ = QString::fromStdString(text); - active_tf_list_.push_back(new_tf); - + + // repopulate dropdown box active_tfs_->clear(); for (std::size_t i = 0; i < active_tf_list_.size(); i++) { active_tfs_->addItem(active_tf_list_[i].name_); } - remote_receiver_->createTF(new_tf.id_, new_tf.from_, new_tf.to_); + // publish new tf + geometry_msgs::TransformStamped new_tf_msg; + new_tf_msg.header.seq = new_tf.id_; + new_tf_msg.header.frame_id = new_tf.from_; + new_tf_msg.child_frame_id = new_tf.to_; + remote_receiver_->createTF(new_tf_msg); } void createTFTab::removeTF() @@ -251,7 +261,7 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) value->setText("0.0"); value->setProperty("dof", (int)i); dof_box_values_.push_back(value); - connect(dof_box_values_[i], SIGNAL(textEdited(const QString &)), this, SLOT(setIncrementValue(const QString &))); + connect(dof_box_values_[i], SIGNAL(textEdited(const QString &)), this, SLOT(updateTFValues(const QString &))); QPushButton *plus = new QPushButton; plus->setText("+"); @@ -286,15 +296,32 @@ void manipulateTFTab::updateTFList() } } -void manipulateTFTab::setIncrementValue(QString text) +void manipulateTFTab::updateTFValues(QString text) { double value = text.toDouble(); int dof = (sender()->property("dof")).toInt(); dof_values_[dof] = value; - ROS_DEBUG_STREAM_NAMED("setIncrementValue","text = " << text.toStdString() << + ROS_DEBUG_STREAM_NAMED("updateTFValues","text = " << text.toStdString() << ", value = " << value << ", dof = " << dof); + + std::string tf_id = active_tfs_->currentText().toStdString(); + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + if (active_tf_list_[i].name_ == active_tfs_->currentText()) + { + active_tf_list_[i].values_[dof] = value; + + ROS_DEBUG_STREAM_NAMED("updateTFValues","tf index = " << i); + for (std::size_t j = 0; j < 6; j++) + { + std::cout << active_tf_list_[i].values_[j] << std::endl; + } + break; + } + } + } void manipulateTFTab::incrementDOF() diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 3c732ea..c091c7f 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -58,6 +58,7 @@ struct tf_data{ std::string from_; std::string to_; QString name_; + double values_[6]; }; /** @@ -107,7 +108,7 @@ class manipulateTFTab : public QWidget protected Q_SLOTS: void incrementDOF(); - void setIncrementValue(QString text); + void updateTFValues(QString text); void setXYZDelta(QString text); void setRPYDelta(QString text); diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index 13cf184..1ea22f9 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -48,19 +48,11 @@ TFRemoteReceiver::TFRemoteReceiver() void TFRemoteReceiver::tf_callback(const tf2_msgs::TFMessage &msg) { - ROS_DEBUG_STREAM_THROTTLE_NAMED(1, "tf_callback", "tf callback..." << msg.transforms.size()); - + // get names of published tfs } -void TFRemoteReceiver::createTF(std::size_t id, std::string from, std::string to) +void TFRemoteReceiver::createTF(geometry_msgs::TransformStamped new_tf_msg) { - std::cout << "\033[1;36m" << "create tf" << "\033[0m" << std::endl; - ROS_DEBUG_STREAM_NAMED("createTF","from: " << from << ", to: " << to); - geometry_msgs::TransformStamped new_tf_msg; - new_tf_msg.header.seq = id; - new_tf_msg.header.frame_id = from; - new_tf_msg.child_frame_id = to; - create_tf_pub_.publish(new_tf_msg); } From 34d534889b629f7dfdea689802db6a11583bc61e Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Thu, 18 May 2017 09:58:18 -0600 Subject: [PATCH 14/32] create/remove/update gui elements working --- include/tf_keyboard_cal/tf_remote_receiver.h | 11 +- src/tf_keyboard_cal_gui.cpp | 110 ++++++++++++------- src/tf_keyboard_cal_gui.h | 15 ++- src/tf_remote_receiver.cpp | 17 +-- 4 files changed, 98 insertions(+), 55 deletions(-) diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index 445c569..f54a218 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -53,9 +53,9 @@ class TFRemoteReceiver return instance; } - void createTF(geometry_msgs::TransformStamped new_tf_msg); - void removeTF(); - void updateTF(); + void createTF(geometry_msgs::TransformStamped create_tf_msg); + void removeTF(geometry_msgs::TransformStamped remove_tf_msg); + void updateTF(geometry_msgs::TransformStamped update_tf_msg); private: @@ -65,11 +65,10 @@ class TFRemoteReceiver ros::NodeHandle nh_; ros::Publisher create_tf_pub_; - ros::Publisher remove_tf_pub_; + ros::Publisher remove_tf_pub_; + ros::Publisher update_tf_pub_; ros::Subscriber tf_sub_; - - }; // end class TFRemoteReceiver } // end namespace tf_keyboard_cal diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 6e3340f..5008706 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -40,6 +40,8 @@ #include #include +#include + // TODO: Why doesn't example put this in the include dir? #include "tf_keyboard_cal_gui.h" @@ -160,15 +162,34 @@ void createTFTab::createNewTF() } // publish new tf - geometry_msgs::TransformStamped new_tf_msg; - new_tf_msg.header.seq = new_tf.id_; - new_tf_msg.header.frame_id = new_tf.from_; - new_tf_msg.child_frame_id = new_tf.to_; - remote_receiver_->createTF(new_tf_msg); + remote_receiver_->createTF(new_tf.getTFMsg()); +} + +geometry_msgs::TransformStamped tf_data::getTFMsg() +{ + geometry_msgs::TransformStamped msg; + msg.header.frame_id = from_; + msg.header.stamp = ros::Time::now(); + msg.child_frame_id = to_; + msg.transform.translation.x = values_[0]; + msg.transform.translation.y = values_[1]; + msg.transform.translation.z = values_[2]; + + tf::Quaternion q; + double deg_to_rad = 3.14159265 / 180.0; + q.setRPY(values_[3] * deg_to_rad, values_[4] * deg_to_rad, values_[5] * deg_to_rad); + + msg.transform.rotation.x = q[0]; + msg.transform.rotation.y = q[1]; + msg.transform.rotation.z = q[2]; + msg.transform.rotation.w = q[3]; + + return msg; } void createTFTab::removeTF() { + // find tf and remove from list std::string tf_id = active_tfs_->currentText().toStdString(); ROS_DEBUG_STREAM_NAMED("removeTF","remove TF: " << active_tfs_->currentIndex() << ", " << tf_id); for (std::size_t i = 0; i < active_tf_list_.size(); i++) @@ -176,18 +197,18 @@ void createTFTab::removeTF() if (active_tf_list_[i].name_ == active_tfs_->currentText()) { ROS_DEBUG_STREAM_NAMED("removeTF","remove index = " << i); + remote_receiver_->removeTF(active_tf_list_[i].getTFMsg()); active_tf_list_.erase(active_tf_list_.begin() + i); break; } } + // repopulate dropdown lists active_tfs_->clear(); for (std::size_t i = 0; i < active_tf_list_.size(); i++) { active_tfs_->addItem(active_tf_list_[i].name_); } - - remote_receiver_->removeTF(); } void createTFTab::fromTextChanged(QString text) @@ -207,6 +228,7 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) QLabel *tf_label = new QLabel(tr("tf:")); active_tfs_ = new QComboBox; active_tfs_->addItem(tr("Select TF")); + connect(active_tfs_, SIGNAL(activated(int)), this, SLOT(setQLineValues(int))); QLabel *xyz_delta_label = new QLabel(tr("xyz delta (m):")); xyz_delta_box_ = new QLineEdit; @@ -246,9 +268,7 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) std::vector labels = { "x (m):", "y (m):", "z (m):", "r (deg):", "p (deg):", "y (deg):"}; for (std::size_t i = 0; i < labels.size(); i++) - { - dof_values_.push_back(0.0); - + { QLabel *label = new QLabel(tr(labels[i].c_str())); QPushButton *minus = new QPushButton; @@ -260,8 +280,8 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) QLineEdit *value = new QLineEdit; value->setText("0.0"); value->setProperty("dof", (int)i); - dof_box_values_.push_back(value); - connect(dof_box_values_[i], SIGNAL(textEdited(const QString &)), this, SLOT(updateTFValues(const QString &))); + dof_qline_edits_.push_back(value); + connect(dof_qline_edits_[i], SIGNAL(textEdited(const QString &)), this, SLOT(editTFTextValue(const QString &))); QPushButton *plus = new QPushButton; plus->setText("+"); @@ -284,6 +304,22 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) setLayout(main_layout); remote_receiver_ = &TFRemoteReceiver::getInstance(); +} + +void manipulateTFTab::setQLineValues(int item_id) +{ + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + if (active_tf_list_[i].name_ == active_tfs_->currentText()) + { + ROS_DEBUG_STREAM_NAMED("setQLineValues","name_ = " << active_tf_list_[i].name_.toStdString()); + for (std::size_t j = 0; j < 6; j++) + { + dof_qline_edits_[j]->setText(QString::number(active_tf_list_[i].values_[j])); + } + break; + } + } } @@ -294,50 +330,50 @@ void manipulateTFTab::updateTFList() { active_tfs_->addItem(active_tf_list_[i].name_); } + + setQLineValues(active_tfs_->currentIndex()); } -void manipulateTFTab::updateTFValues(QString text) +void manipulateTFTab::editTFTextValue(QString text) { double value = text.toDouble(); int dof = (sender()->property("dof")).toInt(); - dof_values_[dof] = value; + updateTFValues(dof, value); +} + +void manipulateTFTab::incrementDOF() +{ + int dof = (sender()->property("dof")).toInt(); + double sign = (sender()->property("sign")).toDouble(); + + double value = dof_qline_edits_[dof]->text().toDouble(); - ROS_DEBUG_STREAM_NAMED("updateTFValues","text = " << text.toStdString() << - ", value = " << value << ", dof = " << dof); + if (dof == 0 || dof == 1 || dof == 2) + value += (double)sign * xyz_delta_; + else + value += (double)sign * rpy_delta_; - std::string tf_id = active_tfs_->currentText().toStdString(); + dof_qline_edits_[dof]->setText(QString::number(value)); + updateTFValues(dof, value); +} + +void manipulateTFTab::updateTFValues(int dof, double value) +{ for (std::size_t i = 0; i < active_tf_list_.size(); i++) { if (active_tf_list_[i].name_ == active_tfs_->currentText()) { + ROS_DEBUG_STREAM_NAMED("updateTFValues","name_ = " << active_tf_list_[i].name_.toStdString()); active_tf_list_[i].values_[dof] = value; - - ROS_DEBUG_STREAM_NAMED("updateTFValues","tf index = " << i); for (std::size_t j = 0; j < 6; j++) { - std::cout << active_tf_list_[i].values_[j] << std::endl; + ROS_DEBUG_STREAM_NAMED("updateTFValues", j << " = " << active_tf_list_[i].values_[j]); } + remote_receiver_->updateTF(active_tf_list_[i].getTFMsg()); break; } } - -} - -void manipulateTFTab::incrementDOF() -{ - int dof = (sender()->property("dof")).toInt(); - double sign = (sender()->property("sign")).toDouble(); - - if (dof == 0 || dof == 1 || dof == 2) - dof_values_[dof] += (double)sign * xyz_delta_; - else - dof_values_[dof] += (double)sign * rpy_delta_; - - dof_box_values_[dof]->setText(QString::number(dof_values_[dof])); - - ROS_DEBUG_STREAM_NAMED("incrementDOF","dof = " << dof << ", sign = " << sign); - remote_receiver_->updateTF(); } void manipulateTFTab::setXYZDelta(QString text) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index c091c7f..dd790b6 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -59,6 +59,8 @@ struct tf_data{ std::string to_; QString name_; double values_[6]; + + geometry_msgs::TransformStamped getTFMsg(); }; /** @@ -108,26 +110,31 @@ class manipulateTFTab : public QWidget protected Q_SLOTS: void incrementDOF(); - void updateTFValues(QString text); + void editTFTextValue(QString text); void setXYZDelta(QString text); void setRPYDelta(QString text); + + void setQLineValues(int item_id); private: + void updateTFValues(int dof, double value); + static constexpr double MAX_XYZ_DELTA = 100.0; static constexpr double MAX_RPY_DELTA = 360.0; - - std::vector dof_values_; + double xyz_delta_; double rpy_delta_; QComboBox *active_tfs_; + // TODO: on select, update values + QLineEdit *xyz_delta_box_; QLineEdit *rpy_delta_box_; - std::vector dof_box_values_; + std::vector dof_qline_edits_; TFRemoteReceiver *remote_receiver_; }; diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index 1ea22f9..e29d5ac 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -41,8 +41,9 @@ TFRemoteReceiver::TFRemoteReceiver() { std::cout << "\033[1;36m" << "remote receiver initialized" << "\033[0m" << std::endl; - create_tf_pub_ = nh_.advertise("/create_rviz_tf", 10); - remove_tf_pub_ = nh_.advertise("/remove_rviz_tf", 10); + create_tf_pub_ = nh_.advertise("/rviz_tf_create", 10); + remove_tf_pub_ = nh_.advertise("/rviz_tf_remove", 10); + update_tf_pub_ = nh_.advertise("/rviz_tf_update", 10); tf_sub_ = nh_.subscribe("/tf", 1, &TFRemoteReceiver::tf_callback, this); } @@ -51,19 +52,19 @@ void TFRemoteReceiver::tf_callback(const tf2_msgs::TFMessage &msg) // get names of published tfs } -void TFRemoteReceiver::createTF(geometry_msgs::TransformStamped new_tf_msg) +void TFRemoteReceiver::createTF(geometry_msgs::TransformStamped create_tf_msg) { - create_tf_pub_.publish(new_tf_msg); + create_tf_pub_.publish(create_tf_msg); } -void TFRemoteReceiver::removeTF() +void TFRemoteReceiver::removeTF(geometry_msgs::TransformStamped remove_tf_msg) { - std::cout << "\033[1;36m" << "remove tf" << "\033[0m" << std::endl; + remove_tf_pub_.publish(remove_tf_msg); } -void TFRemoteReceiver::updateTF() +void TFRemoteReceiver::updateTF(geometry_msgs::TransformStamped update_tf_msg) { - std::cout << "\033[1;36m" << "update tf" << "\033[0m" << std::endl; + update_tf_pub_.publish(update_tf_msg); } } // end namespace tf_keyboard_cal From b5b4ec035f878eeb6eda8fa5e7f6fb14888e2c42 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Thu, 18 May 2017 13:37:26 -0600 Subject: [PATCH 15/32] added tf publisher --- CMakeLists.txt | 12 +++ frames.gv | 8 ++ frames.pdf | Bin 0 -> 18512 bytes include/tf_keyboard_cal/rviz_tf_publisher.h | 74 ++++++++++++++++ src/rviz_tf_publisher.cpp | 92 ++++++++++++++++++++ src/tf_keyboard.cpp | 13 +-- 6 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 frames.gv create mode 100644 frames.pdf create mode 100644 include/tf_keyboard_cal/rviz_tf_publisher.h create mode 100644 src/rviz_tf_publisher.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ee9e42..d874e55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,16 @@ target_link_libraries(${PROJECT_NAME}_receiver ${catkin_LIBRARIES}) add_library(${PROJECT_NAME}_gui ${SOURCE_FILES}) target_link_libraries(${PROJECT_NAME}_gui ${PROJECT_NAME}_receiver ${rviz_DEFAULT_PLUGIN_LIBRARIES} ${QT_LIBRARIES} ${catkin_LIBRARIES}) +add_library(${PROJECT_NAME}_rviz_publisher + src/rviz_tf_publisher.cpp) +target_link_libraries(${PROJECT_NAME}_rviz_publisher + ${catkin_LIBRARIES}) + +add_executable(${PROJECT_NAME} + src/tf_keyboard.cpp) +target_link_libraries(${PROJECT_NAME} + ${PROJECT_NAME}_rviz_publisher + ${catkin_LIBRARIES}) # Library # add_library(${PROJECT_NAME}_manual_tf_alignment @@ -140,6 +150,8 @@ target_link_libraries(${PROJECT_NAME}_gui ${PROJECT_NAME}_receiver ${rviz_DEFAUL # ${catkin_LIBRARIES} # ) + + ############# ## Testing ## ############# diff --git a/frames.gv b/frames.gv new file mode 100644 index 0000000..c96c60a --- /dev/null +++ b/frames.gv @@ -0,0 +1,8 @@ +digraph G { +"world" -> "B"[label="Broadcaster: /tf_keyboard\nAverage rate: 30.203 Hz\nMost recent transform: 1495135847.188 ( 0.004 sec old)\nBuffer length: 4.933 sec\n"]; +"base" -> "world"[label="Broadcaster: /my_tf\nAverage rate: 10.181 Hz\nMost recent transform: 1495135847.260 ( -0.067 sec old)\nBuffer length: 4.813 sec\n"]; +edge [style=invis]; + subgraph cluster_legend { style=bold; color=black; label ="view_frames Result"; +"Recorded at time: 1495135847.193"[ shape=plaintext ] ; + }->"base"; +} \ No newline at end of file diff --git a/frames.pdf b/frames.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b6a3913ba4b420af4ef3fa7bf872605fd6d48dd6 GIT binary patch literal 18512 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 ytV#ui73lbhp!|Gb{DZ` literal 0 HcmV?d00001 diff --git a/include/tf_keyboard_cal/rviz_tf_publisher.h b/include/tf_keyboard_cal/rviz_tf_publisher.h new file mode 100644 index 0000000..48b6fe1 --- /dev/null +++ b/include/tf_keyboard_cal/rviz_tf_publisher.h @@ -0,0 +1,74 @@ +/**************************************************************************************************** + * Software License Agreement (BSD License) + * + * Copyright 2017, Andy McEvoy + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ****************************************************************************************************/ + +/* + * Author(s) : Andy McEvoy ( mcevoy.andy@gmail.com ) + * Desc : tf publisher for rviz transforms + * Created : 2017 - May - 18 + */ + + +#ifndef RVIZ_TF_PUBLISHER_H_ +#define RVIZ_TF_PUBLISHER_H_ + +#include +#include +#include +#include + +#include + +namespace tf_keyboard_cal +{ + +class RvizTFPublisher +{ + +public: + void publishTFs(); + RvizTFPublisher(); + +private: + + ros::NodeHandle nh_; + + void removeTF(geometry_msgs::TransformStamped remove_tf_msg); + void createTF(geometry_msgs::TransformStamped create_tf_msg); + void updateTF(geometry_msgs::TransformStamped update_tf_msg); + + ros::Subscriber create_tf_sub_; + ros::Subscriber remove_tf_sub_; + ros::Subscriber update_tf_sub_; + + std::vector< geometry_msgs::TransformStamped > active_tfs_; +}; // end class RvizTFPublisher + + +} // end namespace tf_keyboard_cal + +#endif diff --git a/src/rviz_tf_publisher.cpp b/src/rviz_tf_publisher.cpp new file mode 100644 index 0000000..f9c024a --- /dev/null +++ b/src/rviz_tf_publisher.cpp @@ -0,0 +1,92 @@ +/**************************************************************************************************** + * Software License Agreement (BSD License) + * + * Copyright 2017, Andy McEvoy + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ****************************************************************************************************/ + +/* + * Author(s) : Andy McEvoy ( mcevoy.andy@gmail.com ) + * Desc : implementation of rviz_tf_publisher.h. See *.h file for documentation. + * Created : 2017 - May - 18 + */ + +#include + +namespace tf_keyboard_cal +{ + +RvizTFPublisher::RvizTFPublisher() + : nh_("~") +{ + create_tf_sub_ = nh_.subscribe("/rviz_tf_create", 10, &RvizTFPublisher::createTF, this); + remove_tf_sub_ = nh_.subscribe("/rviz_tf_remove", 10, &RvizTFPublisher::removeTF, this); + update_tf_sub_ = nh_.subscribe("/rviz_tf_update", 10, &RvizTFPublisher::updateTF, this); +} + +void RvizTFPublisher::createTF(geometry_msgs::TransformStamped create_tf_msg) +{ + active_tfs_.push_back(create_tf_msg); +} + +void RvizTFPublisher::removeTF(geometry_msgs::TransformStamped remove_tf_msg) +{ + for (std::size_t i = 0; i < active_tfs_.size(); i++) + { + if (remove_tf_msg.child_frame_id.compare(active_tfs_[i].child_frame_id) == 0 && + remove_tf_msg.header.frame_id.compare(active_tfs_[i].header.frame_id) == 0) + { + active_tfs_.erase(active_tfs_.begin() + i); + break; + } + } +} + +void RvizTFPublisher::updateTF(geometry_msgs::TransformStamped update_tf_msg) +{ + ROS_DEBUG_STREAM_NAMED("updateTF","from: " << update_tf_msg.header.frame_id << ", to: " << update_tf_msg.child_frame_id); + for (std::size_t i = 0; i < active_tfs_.size(); i++) + { + if (update_tf_msg.child_frame_id.compare(active_tfs_[i].child_frame_id) == 0 && + update_tf_msg.header.frame_id.compare(active_tfs_[i].header.frame_id) == 0) + { + ROS_DEBUG_STREAM_NAMED("updateTF","i = " << i); + active_tfs_[i].transform = update_tf_msg.transform; + } + } +} + +void RvizTFPublisher::publishTFs() +{ + static tf::TransformBroadcaster br; + + for (std::size_t i = 0; i < active_tfs_.size(); i++) + { + tf::StampedTransform tf; + active_tfs_[i].header.stamp = ros::Time::now(); + tf::transformStampedMsgToTF(active_tfs_[i], tf); + br.sendTransform(tf); + } +} +} // end namespace tf_keyboard_cal diff --git a/src/tf_keyboard.cpp b/src/tf_keyboard.cpp index 6e71897..3abc783 100644 --- a/src/tf_keyboard.cpp +++ b/src/tf_keyboard.cpp @@ -36,24 +36,27 @@ Desc : Tweak a TF transform using a keyboard */ -#include +// #include +#include int main(int argc, char** argv) { ros::init(argc, argv, "tf_keyboard"); ROS_INFO_STREAM_NAMED("tf_keyboard","Starting keyboard control"); - ros::AsyncSpinner spinner(2); + ros::AsyncSpinner spinner(4); spinner.start(); - tf_keyboard_cal::ManualTFAlignment tf_align; - tf_align.printMenu(); + //tf_keyboard_cal::ManualTFAlignment tf_align; + tf_keyboard_cal::RvizTFPublisher tf_pub; + //tf_align.printMenu(); ros::Rate rate(30.0); // hz while ( ros::ok() ) { // publish transform to camera - tf_align.publishTF(); + //tf_align.publishTF(); + tf_pub.publishTFs(); rate.sleep(); } From d6a5a18626ccabb8984b5060b1571a90311a1f38 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 18 May 2017 13:50:03 -0600 Subject: [PATCH 16/32] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index fe56fd9..26464e9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +# NEW BRANCH + +``` +roslaunch tf_keyboard_cal rviz_demo.launch +rosrun tf_keyboard_cal tf_keyboard_cal +``` + +In `rviz` add the tf panel by: `Panel -> Add New Panel ->TFKeyboardCalGUI` + + # Manual TF Calibration Tools Move /tf frames around using your keyboard or interactive markers - a simple calibration-by-eye tool! From 103e644fb2301646d858820509a684c6e1d3ad8c Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Thu, 18 May 2017 21:42:18 -0600 Subject: [PATCH 17/32] from dropdown populated properly --- include/tf_keyboard_cal/tf_remote_receiver.h | 6 ++- launch/rviz_demo.launch | 3 +- src/tf_keyboard_cal_gui.cpp | 39 ++++++++++++++++++++ src/tf_remote_receiver.cpp | 20 ++++++++-- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index f54a218..bc72082 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -56,10 +56,12 @@ class TFRemoteReceiver void createTF(geometry_msgs::TransformStamped create_tf_msg); void removeTF(geometry_msgs::TransformStamped remove_tf_msg); void updateTF(geometry_msgs::TransformStamped update_tf_msg); + std::list getTFNames(); private: - void tf_callback(const tf2_msgs::TFMessage &msg); + void tfCallback(const tf2_msgs::TFMessage &msg); + TFRemoteReceiver(); @@ -69,6 +71,8 @@ class TFRemoteReceiver ros::Publisher update_tf_pub_; ros::Subscriber tf_sub_; + std::list< std::string > tf_names_; + }; // end class TFRemoteReceiver } // end namespace tf_keyboard_cal diff --git a/launch/rviz_demo.launch b/launch/rviz_demo.launch index 3ea192d..2103b2b 100644 --- a/launch/rviz_demo.launch +++ b/launch/rviz_demo.launch @@ -4,7 +4,8 @@ - + + diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 5008706..de62a5f 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -163,6 +163,25 @@ void createTFTab::createNewTF() // publish new tf remote_receiver_->createTF(new_tf.getTFMsg()); + + // update from list + from_->clear(); + from_->addItem(tr("Select existing or add new TF")); + std::list names; + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + names.push_back(active_tf_list_[i].from_); + names.push_back(active_tf_list_[i].to_); + } + + names.sort(); + names.unique(); + + std::list::iterator it; + for (it = names.begin(); it != names.end(); ++it) + { + from_->addItem(tr( (*it).c_str() )); + } } geometry_msgs::TransformStamped tf_data::getTFMsg() @@ -209,6 +228,26 @@ void createTFTab::removeTF() { active_tfs_->addItem(active_tf_list_[i].name_); } + + // update from list + from_->clear(); + from_->addItem(tr("Select existing or add new TF")); + std::list names; + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + names.push_back(active_tf_list_[i].from_); + names.push_back(active_tf_list_[i].to_); + } + + names.sort(); + names.unique(); + + std::list::iterator it; + for (it = names.begin(); it != names.end(); ++it) + { + from_->addItem(tr( (*it).c_str() )); + } + } void createTFTab::fromTextChanged(QString text) diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index e29d5ac..1156400 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -44,12 +44,19 @@ TFRemoteReceiver::TFRemoteReceiver() create_tf_pub_ = nh_.advertise("/rviz_tf_create", 10); remove_tf_pub_ = nh_.advertise("/rviz_tf_remove", 10); update_tf_pub_ = nh_.advertise("/rviz_tf_update", 10); - tf_sub_ = nh_.subscribe("/tf", 1, &TFRemoteReceiver::tf_callback, this); + tf_sub_ = nh_.subscribe("/tf", 1, &TFRemoteReceiver::tfCallback, this); } -void TFRemoteReceiver::tf_callback(const tf2_msgs::TFMessage &msg) -{ - // get names of published tfs +void TFRemoteReceiver::tfCallback(const tf2_msgs::TFMessage &msg) +{ + for (std::size_t i = 0; i < msg.transforms.size(); i++) + { + tf_names_.push_back(msg.transforms[i].child_frame_id); + tf_names_.push_back(msg.transforms[i].header.frame_id); + } + + tf_names_.sort(); + tf_names_.unique(); } void TFRemoteReceiver::createTF(geometry_msgs::TransformStamped create_tf_msg) @@ -67,4 +74,9 @@ void TFRemoteReceiver::updateTF(geometry_msgs::TransformStamped update_tf_msg) update_tf_pub_.publish(update_tf_msg); } +std::list TFRemoteReceiver::getTFNames() +{ + return tf_names_; +} + } // end namespace tf_keyboard_cal From feb8b18be391d077c28ddc784cb13e7c0160c578 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Fri, 19 May 2017 16:12:36 -0600 Subject: [PATCH 18/32] tf listeners setup and working --- include/tf_keyboard_cal/tf_remote_receiver.h | 12 +++--- src/tf_keyboard_cal_gui.cpp | 39 ++++++++++---------- src/tf_keyboard_cal_gui.h | 6 ++- src/tf_remote_receiver.cpp | 17 ++------- 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index bc72082..c7dbbf9 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -38,6 +38,7 @@ #include #include #include +#include namespace tf_keyboard_cal @@ -56,12 +57,9 @@ class TFRemoteReceiver void createTF(geometry_msgs::TransformStamped create_tf_msg); void removeTF(geometry_msgs::TransformStamped remove_tf_msg); void updateTF(geometry_msgs::TransformStamped update_tf_msg); - std::list getTFNames(); + std::vector getTFNames(); private: - - void tfCallback(const tf2_msgs::TFMessage &msg); - TFRemoteReceiver(); @@ -69,9 +67,11 @@ class TFRemoteReceiver ros::Publisher create_tf_pub_; ros::Publisher remove_tf_pub_; ros::Publisher update_tf_pub_; - ros::Subscriber tf_sub_; - std::list< std::string > tf_names_; + std::vector< std::string > tf_names_; + + tf2_ros::Buffer tf_buffer_; + tf2_ros::TransformListener *tf_listener_; }; // end class TFRemoteReceiver diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index de62a5f..960e054 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -53,23 +53,26 @@ std::vector< tf_data > active_tf_list_; TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) { tab_widget_ = new QTabWidget; - tab_widget_->addTab(new createTFTab(), tr("Add / Remove")); + connect(tab_widget_, SIGNAL(tabBarClicked(int)), this, SLOT(updateTabData())); + + new_create_tab_ = new createTFTab(); + tab_widget_->addTab(new_create_tab_, tr("Add / Remove")); new_manipulate_tab_ = new manipulateTFTab(); - connect(tab_widget_, SIGNAL(tabBarClicked(int)), this, SLOT(updateTFList())); tab_widget_->addTab(new_manipulate_tab_, tr("Manipulate")); - // tab_widget_->addTab(new manipulateTFTab(), tr("Manipulate")); - - tab_widget_->addTab(new saveLoadTFTab(), tr("Save / Load")); + + new_save_load_tab_ = new saveLoadTFTab(); + tab_widget_->addTab(new_save_load_tab_, tr("Save / Load")); QVBoxLayout *main_layout = new QVBoxLayout; main_layout->addWidget(tab_widget_); setLayout(main_layout); } -void TFKeyboardCalGui::updateTFList() +void TFKeyboardCalGui::updateTabData() { new_manipulate_tab_->updateTFList(); + new_create_tab_->updateFromList(); } createTFTab::createTFTab(QWidget *parent) : QWidget(parent) @@ -163,24 +166,22 @@ void createTFTab::createNewTF() // publish new tf remote_receiver_->createTF(new_tf.getTFMsg()); + + updateFromList(); +} + +void createTFTab::updateFromList() +{ + // give tf a chance to update + ros::Duration(0.25).sleep(); // update from list from_->clear(); from_->addItem(tr("Select existing or add new TF")); - std::list names; - for (std::size_t i = 0; i < active_tf_list_.size(); i++) - { - names.push_back(active_tf_list_[i].from_); - names.push_back(active_tf_list_[i].to_); - } - - names.sort(); - names.unique(); - - std::list::iterator it; - for (it = names.begin(); it != names.end(); ++it) + std::vector names = remote_receiver_->getTFNames(); + for (std::size_t i = 0; i < names.size(); i++) { - from_->addItem(tr( (*it).c_str() )); + from_->addItem(tr(names[i].c_str())); } } diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index dd790b6..82f81a5 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -72,6 +72,7 @@ class createTFTab : public QWidget public: explicit createTFTab(QWidget *parent = 0); + void updateFromList(); protected Q_SLOTS: void createNewTF(); @@ -171,11 +172,14 @@ class TFKeyboardCalGui : public rviz::Panel explicit TFKeyboardCalGui(QWidget *parent = 0); protected Q_SLOTS: - void updateTFList(); + void updateTabData(); private: QTabWidget *tab_widget_; + + createTFTab *new_create_tab_; manipulateTFTab *new_manipulate_tab_; + saveLoadTFTab *new_save_load_tab_; }; } // end namespace tf_keyboard_cal diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index 1156400..3b789ff 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -44,19 +44,8 @@ TFRemoteReceiver::TFRemoteReceiver() create_tf_pub_ = nh_.advertise("/rviz_tf_create", 10); remove_tf_pub_ = nh_.advertise("/rviz_tf_remove", 10); update_tf_pub_ = nh_.advertise("/rviz_tf_update", 10); - tf_sub_ = nh_.subscribe("/tf", 1, &TFRemoteReceiver::tfCallback, this); -} -void TFRemoteReceiver::tfCallback(const tf2_msgs::TFMessage &msg) -{ - for (std::size_t i = 0; i < msg.transforms.size(); i++) - { - tf_names_.push_back(msg.transforms[i].child_frame_id); - tf_names_.push_back(msg.transforms[i].header.frame_id); - } - - tf_names_.sort(); - tf_names_.unique(); + tf_listener_ = new tf2_ros::TransformListener(tf_buffer_); } void TFRemoteReceiver::createTF(geometry_msgs::TransformStamped create_tf_msg) @@ -74,8 +63,10 @@ void TFRemoteReceiver::updateTF(geometry_msgs::TransformStamped update_tf_msg) update_tf_pub_.publish(update_tf_msg); } -std::list TFRemoteReceiver::getTFNames() +std::vector TFRemoteReceiver::getTFNames() { + tf_buffer_._getFrameStrings(tf_names_); + return tf_names_; } From 4120ba1b4161bca65f5e77421fbe2f937e377ce5 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Fri, 19 May 2017 16:14:33 -0600 Subject: [PATCH 19/32] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26464e9..2120bff 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ``` roslaunch tf_keyboard_cal rviz_demo.launch -rosrun tf_keyboard_cal tf_keyboard_cal ``` In `rviz` add the tf panel by: `Panel -> Add New Panel ->TFKeyboardCalGUI` + # Manual TF Calibration Tools Move /tf frames around using your keyboard or interactive markers - a simple calibration-by-eye tool! From 3b77953e846609541e77481cfe11e67dd429f90d Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Sat, 20 May 2017 17:01:58 -0600 Subject: [PATCH 20/32] load working --- config/example_save_file.cvs | 11 ++++++ src/tf_keyboard_cal_gui.cpp | 68 +++++++++++++++++++++++++++++++++--- src/tf_keyboard_cal_gui.h | 6 ++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 config/example_save_file.cvs diff --git a/config/example_save_file.cvs b/config/example_save_file.cvs new file mode 100644 index 0000000..2d09f75 --- /dev/null +++ b/config/example_save_file.cvs @@ -0,0 +1,11 @@ +#ID FRAME_ID CHILD_FRAME_ID X Y Z ROLL PITCH YAW +# (comment line) +# space delimeter +0 a b 0.50 1.50 0.50 0.00 0.00 0.00 +1 b c 1.00 0.00 1.00 0.00 0.00 0.00 +2 b d 0.50 1.50 1.50 0.00 0.00 0.00 +3 d e 1.00 -3.00 1.00 0.00 0.00 0.00 +4 e f 1.00 3.00 1.00 0.00 0.00 0.00 +5 f g 1.00 -3.00 1.00 0.00 0.00 0.00 +6 g h 1.00 3.00 1.00 0.00 0.00 0.00 +7 h i 1.00 -3.00 1.00 0.00 0.00 0.00 diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 960e054..f3647d5 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -67,6 +67,8 @@ TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) QVBoxLayout *main_layout = new QVBoxLayout; main_layout->addWidget(tab_widget_); setLayout(main_layout); + + updateTabData(); } void TFKeyboardCalGui::updateTabData() @@ -468,24 +470,82 @@ saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) main_layout->addWidget(file_section); setLayout(main_layout); + remote_receiver_ = &TFRemoteReceiver::getInstance(); } void saveLoadTFTab::load() { - QString filters("YAML files (*.yaml);;All files (*.*)"); - QString default_filter("YAML files (*.yaml)"); + QString filters("csv files (*.cvs);;All files (*.*)"); + QString default_filter("csv files (*.cvs)"); QString directory = QFileDialog::getOpenFileName(0, "Load TF File", QDir::currentPath(), filters, &default_filter); full_load_path_ = directory.toStdString(); ROS_DEBUG_STREAM_NAMED("load","load_file = " << full_load_path_); + + std::ifstream in_file(full_load_path_); + std::string line; + + int id; + int result; + char from[256]; + char to[256]; + float x, y, z, roll, pitch, yaw; + + if (in_file.is_open()) + { + while (std::getline(in_file, line)) + { + ROS_DEBUG_STREAM_NAMED("load",line); + result = sscanf(line.c_str(), "%d %s %s %f %f %f %f %f %f", &id, from, to, &x, &y, &z, &roll, &pitch, &yaw); + ROS_DEBUG_STREAM_NAMED("load","result = " << result); + + if (result == 9) + { + ROS_DEBUG_STREAM_NAMED("load",id); + ROS_DEBUG_STREAM_NAMED("load",from); + ROS_DEBUG_STREAM_NAMED("load",to); + ROS_DEBUG_STREAM_NAMED("load",x); + ROS_DEBUG_STREAM_NAMED("load",y); + ROS_DEBUG_STREAM_NAMED("load",z); + ROS_DEBUG_STREAM_NAMED("load",roll); + ROS_DEBUG_STREAM_NAMED("load",pitch); + ROS_DEBUG_STREAM_NAMED("load",yaw); + + // create new tf + tf_data new_tf; + new_tf.id_ = id; + std::string frame_id(from); + new_tf.from_ = frame_id; + std::string child_frame_id(to); + new_tf.to_ = child_frame_id; + new_tf.values_[0] = x; + new_tf.values_[1] = y; + new_tf.values_[2] = z; + new_tf.values_[3] = roll; + new_tf.values_[4] = pitch; + new_tf.values_[5] = yaw; + + std::string text = std::to_string(new_tf.id_) + ": " + new_tf.from_ + "-" + new_tf.to_; + new_tf.name_ = QString::fromStdString(text); + + active_tf_list_.push_back(new_tf); + remote_receiver_->createTF(new_tf.getTFMsg()); + } + } + } + else + { + ROS_ERROR_STREAM_NAMED("load","Unable to open file."); + } + in_file.close(); } void saveLoadTFTab::save() { - QString filters("YAML files (*.yaml);;All files (*.*)"); - QString default_filter("YAML files (*.yaml)"); + QString filters("YAML files (*.cvs);;All files (*.*)"); + QString default_filter("YAML files (*.cvs)"); QString directory = QFileDialog::getSaveFileName(0, "Save TF File", QDir::currentPath(), filters, &default_filter); diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 82f81a5..1074d8d 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -51,6 +51,10 @@ #include #include +#include +#include +#include + namespace tf_keyboard_cal { struct tf_data{ @@ -160,6 +164,8 @@ protected Q_SLOTS: std::string full_save_path_; std::string full_load_path_; + + TFRemoteReceiver *remote_receiver_; }; /** From bfdd0e47220cc4720fa9e4d5085b186ec363a169 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Sat, 20 May 2017 18:18:37 -0600 Subject: [PATCH 21/32] save working --- ...save_file.cvs => example_save_file.rviztf} | 0 src/tf_keyboard_cal_gui.cpp | 51 +++++++++++++++---- 2 files changed, 41 insertions(+), 10 deletions(-) rename config/{example_save_file.cvs => example_save_file.rviztf} (100%) diff --git a/config/example_save_file.cvs b/config/example_save_file.rviztf similarity index 100% rename from config/example_save_file.cvs rename to config/example_save_file.rviztf diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index f3647d5..f4f3204 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -475,8 +475,8 @@ saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) void saveLoadTFTab::load() { - QString filters("csv files (*.cvs);;All files (*.*)"); - QString default_filter("csv files (*.cvs)"); + QString filters("rviz tf files (*.rviztf);;All files (*.*)"); + QString default_filter("rviz tf files (*.rviztf)"); QString directory = QFileDialog::getOpenFileName(0, "Load TF File", QDir::currentPath(), filters, &default_filter); @@ -497,9 +497,7 @@ void saveLoadTFTab::load() { while (std::getline(in_file, line)) { - ROS_DEBUG_STREAM_NAMED("load",line); result = sscanf(line.c_str(), "%d %s %s %f %f %f %f %f %f", &id, from, to, &x, &y, &z, &roll, &pitch, &yaw); - ROS_DEBUG_STREAM_NAMED("load","result = " << result); if (result == 9) { @@ -533,29 +531,62 @@ void saveLoadTFTab::load() active_tf_list_.push_back(new_tf); remote_receiver_->createTF(new_tf.getTFMsg()); } + else + { + ROS_INFO_STREAM_NAMED("load","skipping line: " << line); + } } } else { - ROS_ERROR_STREAM_NAMED("load","Unable to open file."); + ROS_ERROR_STREAM_NAMED("load","Unable to open file: " << full_load_path_); } in_file.close(); } void saveLoadTFTab::save() { - QString filters("YAML files (*.cvs);;All files (*.*)"); - QString default_filter("YAML files (*.cvs)"); + QString filters("rviz tf files (*.rviztf);;All files (*.*)"); + QString default_filter("rviz tf files (*.rviztf)"); QString directory = QFileDialog::getSaveFileName(0, "Save TF File", QDir::currentPath(), filters, &default_filter); - full_save_path_ = directory.toStdString(); - // TODO: check for file extension and automatically append if left off + // check if user specified file extension + std::size_t found = full_save_path_.find("."); + if (found == std::string::npos) + full_save_path_ += ".rviztf"; + + ROS_DEBUG_STREAM_NAMED("save","save_file = " << full_save_path_); + + std::ofstream out_file; + out_file.open(full_save_path_); + + std::string header = "#ID FRAME_ID CHILD_FRAME_ID X Y Z ROLL PITCH YAW\n# (comment line)\n# space delimeter"; + + if (out_file.is_open()) + { + out_file << header << std::endl; + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + out_file << active_tf_list_[i].id_ << " "; + out_file << active_tf_list_[i].from_ << " "; + out_file << active_tf_list_[i].to_; + for (std::size_t j = 0; j < 6; j++) + { + out_file << " " << active_tf_list_[i].values_[j]; + } + out_file << std::endl; + } + out_file.close(); + } + else + { + ROS_ERROR_STREAM_NAMED("save","Unable to open file: " << full_save_path_); + } - ROS_DEBUG_STREAM_NAMED("load","save_file = " << full_save_path_); } } // end namespace tf_keyboard_cal From 65e518253cbe7962a9746e0f4778f06afe70e0cf Mon Sep 17 00:00:00 2001 From: Dave Coleman Date: Sun, 28 May 2017 19:46:14 -0600 Subject: [PATCH 22/32] Fix eigen warning --- CMakeLists.txt | 14 ++++++++++---- src/tf_remote_receiver.cpp | 14 +++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d874e55..d189330 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,14 +21,20 @@ find_package(catkin REQUIRED COMPONENTS geometry_msgs ) -find_package(Eigen REQUIRED) +find_package(Eigen3 REQUIRED) + +# Eigen 3.2 (Wily) only provides EIGEN3_INCLUDE_DIR, not EIGEN3_INCLUDE_DIRS +if(NOT EIGEN3_INCLUDE_DIRS) + set(EIGEN3_INCLUDE_DIRS ${EIGEN3_INCLUDE_DIR}) +endif() + find_package(Boost REQUIRED) catkin_package( INCLUDE_DIRS include LIBRARIES - ${PROJECT_NAME}_gui + ${PROJECT_NAME}_gui # ${PROJECT_NAME}_manual_tf_alignment # ${PROJECT_NAME}_imarker_simple CATKIN_DEPENDS @@ -40,7 +46,7 @@ catkin_package( rosparam_shortcuts interactive_markers DEPENDS - Eigen + EIGEN3 ) # Qt 4 or 5 @@ -75,7 +81,7 @@ include_directories( include ${catkin_INCLUDE_DIRS} ${Boost_INCLUDE_DIR} - ${EIGEN_INCLUDE_DIRS} + ${EIGEN3_INCLUDE_DIRS} ) ########### diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index 3b789ff..d062289 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -1,21 +1,21 @@ /**************************************************************************************************** * Software License Agreement (BSD License) - * + * * Copyright 2017, Andy McEvoy - * + * * Redistribution and use in source and binary forms, with or without modification, are permitted * provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions * and the following disclaimer. - * + * * 2. Redistributions in binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other materials provided * with the distribution. - * + * * 3. Neither the name of the copyright holder nor the names of its contributors may be used to * endorse or promote products derived from this software without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR @@ -39,7 +39,7 @@ namespace tf_keyboard_cal TFRemoteReceiver::TFRemoteReceiver() : nh_("~") { - std::cout << "\033[1;36m" << "remote receiver initialized" << "\033[0m" << std::endl; + std::cout << "\033[1;36m" << "Remote receiver initialized" << "\033[0m" << std::endl; create_tf_pub_ = nh_.advertise("/rviz_tf_create", 10); remove_tf_pub_ = nh_.advertise("/rviz_tf_remove", 10); From 14a08934c02ed93ac959db60f909aa2b30c22dce Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Thu, 1 Jun 2017 22:54:26 -0600 Subject: [PATCH 23/32] added keyboard listener to manipulate tab, still need to implement functionality --- src/tf_keyboard_cal_gui.cpp | 6 ++++++ src/tf_keyboard_cal_gui.h | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index f4f3204..31328a5 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -348,6 +348,12 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) remote_receiver_ = &TFRemoteReceiver::getInstance(); } +void manipulateTFTab::keyPressEvent(QKeyEvent *event) +{ + // TODO: set to same keys as before, call update to tf when pressed. + ROS_DEBUG_STREAM_NAMED("keyPressEvent","pressed key"); +} + void manipulateTFTab::setQLineValues(int item_id) { for (std::size_t i = 0; i < active_tf_list_.size(); i++) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 1074d8d..ec73c41 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -43,6 +43,7 @@ #include #include +#include #include #include #include @@ -116,11 +117,16 @@ class manipulateTFTab : public QWidget protected Q_SLOTS: void incrementDOF(); void editTFTextValue(QString text); - + void setXYZDelta(QString text); void setRPYDelta(QString text); void setQLineValues(int item_id); + +protected: + void keyPressEvent(QKeyEvent *); + // TODO: Don't think I need release... + // void keyReleaseEvent(QKeyEvent *); private: From f10c95bdb1fc0859ca5d8db8a3360e0b3c994ea8 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Sat, 3 Jun 2017 00:02:45 -0600 Subject: [PATCH 24/32] keyboard manipulation when manip tab in focus --- src/tf_keyboard_cal_gui.cpp | 137 ++++++++++++++++++++++++++++++++---- src/tf_keyboard_cal_gui.h | 9 ++- 2 files changed, 133 insertions(+), 13 deletions(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 31328a5..29edc3b 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -53,7 +53,7 @@ std::vector< tf_data > active_tf_list_; TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) { tab_widget_ = new QTabWidget; - connect(tab_widget_, SIGNAL(tabBarClicked(int)), this, SLOT(updateTabData())); + connect(tab_widget_, SIGNAL(tabBarClicked(int)), this, SLOT(updateTabData(int))); new_create_tab_ = new createTFTab(); tab_widget_->addTab(new_create_tab_, tr("Add / Remove")); @@ -68,13 +68,19 @@ TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) main_layout->addWidget(tab_widget_); setLayout(main_layout); - updateTabData(); + updateTabData(0); } -void TFKeyboardCalGui::updateTabData() +void TFKeyboardCalGui::updateTabData(int index) { + ROS_DEBUG_STREAM_NAMED("updateTabData","index = " << index); new_manipulate_tab_->updateTFList(); new_create_tab_->updateFromList(); + + if (index == 1) // manipulate tab selected. + { + new_manipulate_tab_->setFocus(); + } } createTFTab::createTFTab(QWidget *parent) : QWidget(parent) @@ -137,6 +143,8 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) setLayout(main_layout); id_ = 0; + + this->setFocus(); remote_receiver_ = &TFRemoteReceiver::getInstance(); } @@ -344,14 +352,97 @@ manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) main_layout->addWidget(tf_ctrl_section); main_layout->addWidget(tf_increment_section); setLayout(main_layout); - + remote_receiver_ = &TFRemoteReceiver::getInstance(); } void manipulateTFTab::keyPressEvent(QKeyEvent *event) { - // TODO: set to same keys as before, call update to tf when pressed. - ROS_DEBUG_STREAM_NAMED("keyPressEvent","pressed key"); + + double xyz_delta = 0.01; + double rpy_delta = 1.0; // degrees + + switch (event->key()) + { + case Qt::Key_A: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","x+"); + incrementDOF(0, 1.0); + break; + case Qt::Key_Q: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","x-"); + incrementDOF(0, -1.0); + break; + case Qt::Key_W: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","y+"); + incrementDOF(1, 1.0); + break; + case Qt::Key_S: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","y-"); + incrementDOF(1, -1.0); + break; + case Qt::Key_E: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","z+"); + incrementDOF(2, 1.0); + break; + case Qt::Key_D: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","z-"); + incrementDOF(2, -1.0); + break; + case Qt::Key_R: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","roll+"); + incrementDOF(3, 1.0); + break; + case Qt::Key_F: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","roll-"); + incrementDOF(3, -1.0); + break; + case Qt::Key_T: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","pitch+"); + incrementDOF(4, 1.0); + break; + case Qt::Key_G: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","pitch-"); + incrementDOF(4, -1.0); + break; + case Qt::Key_Y: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","yaw+"); + incrementDOF(5, 1.0); + break; + case Qt::Key_H: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","yaw-"); + incrementDOF(5, -1.0); + break; + case Qt::Key_U: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","fast"); + xyz_delta = 0.1; + rpy_delta = 5.0; // degrees + xyz_delta_box_->setText(QString::number(xyz_delta)); + rpy_delta_box_->setText(QString::number(rpy_delta)); + setXYZDelta(xyz_delta); + setRPYDelta(rpy_delta); + break; + case Qt::Key_I: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","medium"); + xyz_delta = 0.01; + rpy_delta = 1.0; // degrees + xyz_delta_box_->setText(QString::number(xyz_delta)); + rpy_delta_box_->setText(QString::number(rpy_delta)); + setXYZDelta(xyz_delta); + setRPYDelta(rpy_delta); + break; + case Qt::Key_O: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","slow"); + xyz_delta = 0.001; + rpy_delta = 0.5; // degrees + xyz_delta_box_->setText(QString::number(xyz_delta)); + rpy_delta_box_->setText(QString::number(rpy_delta)); + setXYZDelta(xyz_delta); + setRPYDelta(rpy_delta); + break; + default: + ROS_DEBUG_STREAM_NAMED("keyPressEvent","undefined key pressed"); + break; + } } void manipulateTFTab::setQLineValues(int item_id) @@ -395,6 +486,11 @@ void manipulateTFTab::incrementDOF() int dof = (sender()->property("dof")).toInt(); double sign = (sender()->property("sign")).toDouble(); + incrementDOF(dof, sign); +} + +void manipulateTFTab::incrementDOF(int dof, double sign) +{ double value = dof_qline_edits_[dof]->text().toDouble(); if (dof == 0 || dof == 1 || dof == 2) @@ -425,10 +521,18 @@ void manipulateTFTab::updateTFValues(int dof, double value) } void manipulateTFTab::setXYZDelta(QString text) -{ - xyz_delta_ = text.toDouble(); - ROS_DEBUG_STREAM_NAMED("set_xyz_delta","text = " << text.toStdString() << ", value = " << xyz_delta_); +{ + double xyz_delta = text.toDouble(); + ROS_DEBUG_STREAM_NAMED("set_xyz_delta","text = " << text.toStdString() << ", value = " << xyz_delta); + + setXYZDelta(xyz_delta); +} + +void manipulateTFTab::setXYZDelta(double xyz_delta) +{ + xyz_delta_ = xyz_delta; + if (std::abs(xyz_delta_) > MAX_XYZ_DELTA) { ROS_WARN_STREAM_NAMED("setXYZDelta","Tried to set XYZ delta outside of limits. (+/-" << MAX_XYZ_DELTA << ")"); @@ -438,13 +542,22 @@ void manipulateTFTab::setXYZDelta(QString text) } ROS_DEBUG_STREAM_NAMED("set_xyz_delta","setting xyz_delta_ = " << xyz_delta_); + } void manipulateTFTab::setRPYDelta(QString text) { - rpy_delta_ = text.toDouble(); - ROS_DEBUG_STREAM_NAMED("set_rpy_delta","text = " << text.toStdString() << ", value = " << rpy_delta_); + double rpy_delta = text.toDouble(); + + ROS_DEBUG_STREAM_NAMED("set_rpy_delta","text = " << text.toStdString() << ", value = " << rpy_delta); + + setRPYDelta(rpy_delta); +} +void manipulateTFTab::setRPYDelta(double rpy_delta) +{ + rpy_delta_ = rpy_delta; + if (std::abs(rpy_delta_) > MAX_RPY_DELTA) { ROS_WARN_STREAM_NAMED("setRPYDelta","Tried to set RPY delta outside of limits. (+/-" << MAX_RPY_DELTA << ")"); @@ -453,7 +566,7 @@ void manipulateTFTab::setRPYDelta(QString text) rpy_delta_box_->setText(value); } - ROS_DEBUG_STREAM_NAMED("set_rpy_delta","setting rpy_delta_ = " << rpy_delta_); + ROS_DEBUG_STREAM_NAMED("set_rpy_delta","setting rpy_delta_ = " << rpy_delta_); } saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index ec73c41..3f6f5fd 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -116,11 +116,16 @@ class manipulateTFTab : public QWidget protected Q_SLOTS: void incrementDOF(); + void incrementDOF(int dof, double sign); + void editTFTextValue(QString text); void setXYZDelta(QString text); void setRPYDelta(QString text); + void setXYZDelta(double xyz_delta); + void setRPYDelta(double rpy_delta); + void setQLineValues(int item_id); protected: @@ -131,6 +136,8 @@ protected Q_SLOTS: private: void updateTFValues(int dof, double value); + + // can't overload 'set static constexpr double MAX_XYZ_DELTA = 100.0; static constexpr double MAX_RPY_DELTA = 360.0; @@ -184,7 +191,7 @@ class TFKeyboardCalGui : public rviz::Panel explicit TFKeyboardCalGui(QWidget *parent = 0); protected Q_SLOTS: - void updateTabData(); + void updateTabData(int); private: QTabWidget *tab_widget_; From 36058de99712105cd7a75a1e19ff151adcd88f05 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Sun, 4 Jun 2017 22:55:59 -0600 Subject: [PATCH 25/32] start of imarker integration --- include/tf_keyboard_cal/tf_remote_receiver.h | 3 +- launch/rviz_demo.rviz | 22 +++- src/tf_keyboard_cal_gui.cpp | 120 +++++++++++++++++-- src/tf_keyboard_cal_gui.h | 22 +++- src/tf_remote_receiver.cpp | 2 +- 5 files changed, 150 insertions(+), 19 deletions(-) diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index c7dbbf9..c1d966a 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -40,7 +40,6 @@ #include #include - namespace tf_keyboard_cal { @@ -57,6 +56,8 @@ class TFRemoteReceiver void createTF(geometry_msgs::TransformStamped create_tf_msg); void removeTF(geometry_msgs::TransformStamped remove_tf_msg); void updateTF(geometry_msgs::TransformStamped update_tf_msg); + + std::vector getTFNames(); private: diff --git a/launch/rviz_demo.rviz b/launch/rviz_demo.rviz index f9d21a9..0bab9d6 100644 --- a/launch/rviz_demo.rviz +++ b/launch/rviz_demo.rviz @@ -6,6 +6,7 @@ Panels: Expanded: - /Global Options1 - /Status1 + - /InteractiveMarkers1 Splitter Ratio: 0.5 Tree Height: 308 - Class: rviz/Selection @@ -55,7 +56,7 @@ Visualization Manager: Frame Timeout: 15 Frames: All Enabled: false - base: + a: Value: true world: Value: true @@ -65,8 +66,8 @@ Visualization Manager: Show Axes: true Show Names: true Tree: - base: - world: + world: + a: {} Update Interval: 0 Value: true @@ -94,6 +95,15 @@ Visualization Manager: Topic: /posestamped Unreliable: false Value: false + - Class: rviz/InteractiveMarkers + Enable Transparency: true + Enabled: true + Name: InteractiveMarkers + Show Axes: false + Show Descriptions: true + Show Visual Aids: false + Update Topic: /tf_keyboard_cal_imarkers/update + Value: true Enabled: true Global Options: Background Color: 48; 48; 48 @@ -118,7 +128,7 @@ Visualization Manager: Views: Current: Class: rviz/Orbit - Distance: 2.64484 + Distance: 7.7095623 Enable Stereo Rendering: Stereo Eye Separation: 0.0599999987 Stereo Focal Distance: 1 @@ -132,10 +142,10 @@ Visualization Manager: Focal Shape Size: 0.0500000007 Name: Current View Near Clip Distance: 0.00999999978 - Pitch: 0.270397991 + Pitch: 0.590397716 Target Frame: Value: Orbit (rviz) - Yaw: 4.89858007 + Yaw: 5.51858854 Saved: ~ Window Geometry: Displays: diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 29edc3b..1781e84 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -33,16 +33,8 @@ */ #include - -#include -#include -#include -#include -#include - -#include - // TODO: Why doesn't example put this in the include dir? +// Looks like it doesn't work if *.h is in include dir? #include "tf_keyboard_cal_gui.h" namespace tf_keyboard_cal @@ -50,6 +42,8 @@ namespace tf_keyboard_cal std::vector< tf_data > active_tf_list_; +boost::shared_ptr imarker_server_; + TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) { tab_widget_ = new QTabWidget; @@ -68,6 +62,9 @@ TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) main_layout->addWidget(tab_widget_); setLayout(main_layout); + imarker_server_.reset(new interactive_markers::InteractiveMarkerServer("tf_keyboard_cal_imarkers", "", false)); + ros::Duration(0.25).sleep(); + updateTabData(0); } @@ -99,6 +96,9 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) to_->setPlaceholderText("to TF"); connect(to_, SIGNAL(textChanged(const QString &)), this, SLOT(toTextChanged(const QString &))); + add_imarker_ = new QCheckBox("i marker?", this); + add_imarker_->setCheckState(Qt::Unchecked); + create_tf_btn_ = new QPushButton(this); create_tf_btn_->setText("Create TF"); connect(create_tf_btn_, SIGNAL(clicked()), this, SLOT(createNewTF())); @@ -121,6 +121,10 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) to_row->addWidget(to_label); to_row->addWidget(to_); + QHBoxLayout *create_row = new QHBoxLayout; + create_row->addWidget(add_imarker_); + create_row->addWidget(create_tf_btn_); + QHBoxLayout *remove_row = new QHBoxLayout; remove_row->addWidget(active_tfs_); remove_row->addWidget(remove_tf_btn_); @@ -128,7 +132,7 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) QVBoxLayout *add_controls = new QVBoxLayout; add_controls->addLayout(from_row); add_controls->addLayout(to_row); - add_controls->addWidget(create_tf_btn_); + add_controls->addLayout(create_row); add_section->setLayout(add_controls); QGroupBox *remove_section = new QGroupBox(tr("Remove TF")); @@ -167,6 +171,14 @@ void createTFTab::createNewTF() new_tf.name_ = QString::fromStdString(text); active_tf_list_.push_back(new_tf); + // interactive marker + new_tf.imarker_ = false; + if (add_imarker_->isChecked()) + { + new_tf.imarker_ = true; + createNewIMarker(new_tf); + } + // repopulate dropdown box active_tfs_->clear(); for (std::size_t i = 0; i < active_tf_list_.size(); i++) @@ -180,6 +192,89 @@ void createTFTab::createNewTF() updateFromList(); } +void createTFTab::createNewIMarker(tf_data new_tf) +{ + ROS_DEBUG_STREAM_NAMED("createNewIMarker","creating interactive marker..."); + + // create a box to visualize the marker + visualization_msgs::Marker marker; + marker.type = visualization_msgs::Marker::CUBE; + marker.scale.x = 0.1; + marker.scale.y = 0.1; + marker.scale.z = 0.1; + marker.color.r = 0.5; + marker.color.g = 0.5; + marker.color.b = 0.5; + marker.color.a = 1.0; + + visualization_msgs::InteractiveMarker int_marker; + int_marker.header.frame_id = new_tf.from_; + int_marker.scale = 0.25; + int_marker.name = new_tf.name_.toStdString(); + + visualization_msgs::InteractiveMarkerControl box_control; + box_control.always_visible = true; + box_control.markers.push_back(marker); + box_control.interaction_mode = visualization_msgs::InteractiveMarkerControl::MOVE_ROTATE_3D; + int_marker.controls.push_back(box_control); + + // create the handles to control individual dofs + visualization_msgs::InteractiveMarkerControl control; + control.orientation.w = 1; + control.orientation.x = 1; + control.orientation.y = 0; + control.orientation.z = 0; + control.name = "rotate_x"; + control.interaction_mode = visualization_msgs::InteractiveMarkerControl::ROTATE_AXIS; + int_marker.controls.push_back(control); + control.name = "move_x"; + control.interaction_mode = visualization_msgs::InteractiveMarkerControl::MOVE_AXIS; + int_marker.controls.push_back(control); + + control.orientation.w = 1; + control.orientation.x = 0; + control.orientation.y = 1; + control.orientation.z = 0; + control.name = "rotate_y"; + control.interaction_mode = visualization_msgs::InteractiveMarkerControl::ROTATE_AXIS; + int_marker.controls.push_back(control); + control.name = "move_y"; + control.interaction_mode = visualization_msgs::InteractiveMarkerControl::MOVE_AXIS; + int_marker.controls.push_back(control); + + control.orientation.w = 1; + control.orientation.x = 0; + control.orientation.y = 0; + control.orientation.z = 1; + control.name = "rotate_z"; + control.interaction_mode = visualization_msgs::InteractiveMarkerControl::ROTATE_AXIS; + int_marker.controls.push_back(control); + control.name = "move_z"; + control.interaction_mode = visualization_msgs::InteractiveMarkerControl::MOVE_AXIS; + int_marker.controls.push_back(control); + + imarker_server_->insert(int_marker); + imarker_server_->setCallback(int_marker.name, boost::bind( &createTFTab::processIMarkerFeedback, this, _1) ); + imarker_server_->applyChanges(); +} + +void createTFTab::processIMarkerFeedback(const visualization_msgs::InteractiveMarkerFeedbackConstPtr &feedback) +{ + ROS_DEBUG_STREAM_NAMED("processIMarkerFeedback","Feedback from " << feedback->marker_name); + + QString imarker_name = QString::fromStdString(feedback->marker_name); + + for (std::size_t i = 0; i < active_tf_list_.size(); i++) + { + if (active_tf_list_[i].name_ == imarker_name) + { + ROS_DEBUG_STREAM_NAMED("removeTF","update index = " << i); + manipulateTFTab::updateTFValues(feedback->pose); + break; + } + } +} + void createTFTab::updateFromList() { // give tf a chance to update @@ -502,6 +597,11 @@ void manipulateTFTab::incrementDOF(int dof, double sign) updateTFValues(dof, value); } +void manipulateTFTab::updateTFValues(int list_index, geometry_msgs::Pose pose) +{ + // TODO: update euler angles, change pose -> transformstamped, call remote_receiver_, update gui text +} + void manipulateTFTab::updateTFValues(int dof, double value) { for (std::size_t i = 0; i < active_tf_list_.size(); i++) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 3f6f5fd..8d08e68 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -46,11 +46,22 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include +#include +#include + +#include + +#include +#include #include #include @@ -62,6 +73,7 @@ struct tf_data{ std::size_t id_; std::string from_; std::string to_; + bool imarker_; QString name_; double values_[6]; @@ -87,6 +99,9 @@ protected Q_SLOTS: void toTextChanged(QString text); private: + void processIMarkerFeedback(const visualization_msgs::InteractiveMarkerFeedbackConstPtr &feedback); + void createNewIMarker(tf_data new_tf); + std::string from_tf_name_; std::string to_tf_name_; @@ -95,6 +110,8 @@ protected Q_SLOTS: QComboBox *from_; QLineEdit *to_; + QCheckBox *add_imarker_; + QPushButton *create_tf_btn_; QPushButton *remove_tf_btn_; @@ -113,7 +130,8 @@ class manipulateTFTab : public QWidget public: explicit manipulateTFTab(QWidget *parent = 0); void updateTFList(); - + static void updateTFValues(int list_index, geometry_msgs::Pose pose); + protected Q_SLOTS: void incrementDOF(); void incrementDOF(int dof, double sign); @@ -129,6 +147,8 @@ protected Q_SLOTS: void setQLineValues(int item_id); protected: + + void keyPressEvent(QKeyEvent *); // TODO: Don't think I need release... // void keyReleaseEvent(QKeyEvent *); diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index d062289..997f58e 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -60,7 +60,7 @@ void TFRemoteReceiver::removeTF(geometry_msgs::TransformStamped remove_tf_msg) void TFRemoteReceiver::updateTF(geometry_msgs::TransformStamped update_tf_msg) { - update_tf_pub_.publish(update_tf_msg); + update_tf_pub_.publish(update_tf_msg); } std::vector TFRemoteReceiver::getTFNames() From a390e43bbaf3b2e4c1752e12940a3001699630b3 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Mon, 5 Jun 2017 22:21:02 -0600 Subject: [PATCH 26/32] tf follows imarker, need to implement reverse --- src/tf_keyboard_cal_gui.cpp | 21 +++++++++++++++++---- src/tf_keyboard_cal_gui.h | 3 +++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 1781e84..a032251 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -269,7 +269,8 @@ void createTFTab::processIMarkerFeedback(const visualization_msgs::InteractiveMa if (active_tf_list_[i].name_ == imarker_name) { ROS_DEBUG_STREAM_NAMED("removeTF","update index = " << i); - manipulateTFTab::updateTFValues(feedback->pose); + manipulateTFTab::updateTFValues(i, feedback->pose); + remote_receiver_->updateTF(active_tf_list_[i].getTFMsg()); break; } } @@ -553,8 +554,7 @@ void manipulateTFTab::setQLineValues(int item_id) } break; } - } - + } } void manipulateTFTab::updateTFList() @@ -597,9 +597,22 @@ void manipulateTFTab::incrementDOF(int dof, double sign) updateTFValues(dof, value); } -void manipulateTFTab::updateTFValues(int list_index, geometry_msgs::Pose pose) +void manipulateTFTab::updateTFValues(int idx, geometry_msgs::Pose pose) { // TODO: update euler angles, change pose -> transformstamped, call remote_receiver_, update gui text + active_tf_list_[idx].values_[0] = pose.position.x; + active_tf_list_[idx].values_[1] = pose.position.y; + active_tf_list_[idx].values_[2] = pose.position.z; + + double rad_to_deg = 180.0 / 3.14159265; + + Eigen::Quaterniond eigen_quaternion; + tf::quaternionMsgToEigen(pose.orientation, eigen_quaternion); + Eigen::Vector3d euler_angles = eigen_quaternion.toRotationMatrix().eulerAngles(2, 1, 0); + ROS_DEBUG_STREAM_NAMED("updateTFValues","euler_angles = " << euler_angles.transpose()); + active_tf_list_[idx].values_[3] = euler_angles[0] * rad_to_deg; + active_tf_list_[idx].values_[4] = euler_angles[1] * rad_to_deg; + active_tf_list_[idx].values_[5] = euler_angles[2] * rad_to_deg; } void manipulateTFTab::updateTFValues(int dof, double value) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 8d08e68..7940f21 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -63,6 +63,9 @@ #include #include +#include +#include + #include #include #include From 5c9626be7aedcd9da302bd01450fc2b3479d776b Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Tue, 6 Jun 2017 23:00:25 -0600 Subject: [PATCH 27/32] imarkers follow gui and keyboard cmds --- src/tf_keyboard_cal_gui.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index a032251..374cffc 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -175,6 +175,7 @@ void createTFTab::createNewTF() new_tf.imarker_ = false; if (add_imarker_->isChecked()) { + ROS_DEBUG_STREAM_NAMED("createNewTF","imarker = true"); new_tf.imarker_ = true; createNewIMarker(new_tf); } @@ -627,7 +628,22 @@ void manipulateTFTab::updateTFValues(int dof, double value) { ROS_DEBUG_STREAM_NAMED("updateTFValues", j << " = " << active_tf_list_[i].values_[j]); } - remote_receiver_->updateTF(active_tf_list_[i].getTFMsg()); + + geometry_msgs::TransformStamped tf_msg = active_tf_list_[i].getTFMsg(); + + if (active_tf_list_[i].imarker_) + { + ROS_DEBUG_STREAM_NAMED("updateTFValues","update imarker pose..."); + geometry_msgs::Pose imarker_pose; + imarker_pose.position.x = tf_msg.transform.translation.x; + imarker_pose.position.y = tf_msg.transform.translation.y; + imarker_pose.position.z = tf_msg.transform.translation.z; + imarker_pose.orientation = tf_msg.transform.rotation; + imarker_server_->setPose(active_tf_list_[i].name_.toStdString(), imarker_pose); + imarker_server_->applyChanges(); + } + + remote_receiver_->updateTF(tf_msg); break; } } From 8a570af503e04cfdbed94a37c374b2516c601a29 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Wed, 14 Jun 2017 23:12:06 -0600 Subject: [PATCH 28/32] fix imarker tf bug --- bug_notes.md | 17 +++++++++++++++++ src/tf_keyboard_cal_gui.cpp | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 bug_notes.md diff --git a/bug_notes.md b/bug_notes.md new file mode 100644 index 0000000..b2ad86f --- /dev/null +++ b/bug_notes.md @@ -0,0 +1,17 @@ +# Bugs and Issues + +Notes on bugs and issues seen during development. + +## 1. Interactive Marker Reset + +If you select `Enable Transparancy` under the Interactive Marker topic in Rviz BEFORE using the GUI buttons to adjust the marker's position, the marker will reset back to its origin. Once you adjust the marker through the GUI this problem seems to disappear. + +## 2. Interactive Markers Branching from non Interactive Marker + +If a TF is defined without an interactive marker and then a TF with is interactive marker is defined as a child of that, the interactive marker does not update properly when using the keyboard commands. + +There seems to be a disconnect between the values of the imarker and the active tfs list. + +### Fix + +Seems that `active_tf_list_.push_back(new_tf)` was being called before `.imarker_ = true` was called. This appears to have solved the errors. diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 374cffc..53048ba 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -169,16 +169,16 @@ void createTFTab::createNewTF() } std::string text = std::to_string(new_tf.id_) + ": " + new_tf.from_ + "-" + new_tf.to_; new_tf.name_ = QString::fromStdString(text); - active_tf_list_.push_back(new_tf); // interactive marker new_tf.imarker_ = false; if (add_imarker_->isChecked()) { - ROS_DEBUG_STREAM_NAMED("createNewTF","imarker = true"); new_tf.imarker_ = true; + ROS_DEBUG_STREAM_NAMED("createNewTF","imarker = " << new_tf.imarker_); createNewIMarker(new_tf); } + active_tf_list_.push_back(new_tf); // repopulate dropdown box active_tfs_->clear(); @@ -630,7 +630,7 @@ void manipulateTFTab::updateTFValues(int dof, double value) } geometry_msgs::TransformStamped tf_msg = active_tf_list_[i].getTFMsg(); - + ROS_DEBUG_STREAM_NAMED("updateTFValues","imarker_ = " << active_tf_list_[i].imarker_); if (active_tf_list_[i].imarker_) { ROS_DEBUG_STREAM_NAMED("updateTFValues","update imarker pose..."); From 44765330795e77c7103e88cb211a2a725589c123 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Fri, 16 Jun 2017 23:15:27 -0600 Subject: [PATCH 29/32] .rviztf to .tf and start work on basic imarker right-click menus --- config/example_imarker_menu.tf | 13 ++++ ..._save_file.rviztf => example_save_file.tf} | 0 src/tf_keyboard_cal_gui.cpp | 64 ++++++++++++++++--- src/tf_keyboard_cal_gui.h | 5 +- 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 config/example_imarker_menu.tf rename config/{example_save_file.rviztf => example_save_file.tf} (100%) diff --git a/config/example_imarker_menu.tf b/config/example_imarker_menu.tf new file mode 100644 index 0000000..7ff449b --- /dev/null +++ b/config/example_imarker_menu.tf @@ -0,0 +1,13 @@ +# example menu file +# (comment line) +# space delimeter +# level, menu title (note, supports only 1 sub level) +1, menu item A +2, sub entry a +2, sub entry b +2, sub entry c +1, menu item B +1, menu item C +1, menu item D +2, sub entry a +2, sub entry b \ No newline at end of file diff --git a/config/example_save_file.rviztf b/config/example_save_file.tf similarity index 100% rename from config/example_save_file.rviztf rename to config/example_save_file.tf diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 53048ba..66d5021 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -43,6 +43,7 @@ namespace tf_keyboard_cal std::vector< tf_data > active_tf_list_; boost::shared_ptr imarker_server_; +interactive_markers::MenuHandler imarker_menu_handler_; TFKeyboardCalGui::TFKeyboardCalGui(QWidget* parent) : rviz::Panel(parent) { @@ -82,7 +83,8 @@ void TFKeyboardCalGui::updateTabData(int index) createTFTab::createTFTab(QWidget *parent) : QWidget(parent) { - + menu_handler_set_ = false; + // TF controls QLabel *from_label = new QLabel(tr("from:")); QLabel *to_label = new QLabel(tr("to:")); @@ -98,6 +100,9 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) add_imarker_ = new QCheckBox("i marker?", this); add_imarker_->setCheckState(Qt::Unchecked); + + add_imarker_menu_ = new QCheckBox("menus?", this); + add_imarker_menu_->setCheckState(Qt::Unchecked); create_tf_btn_ = new QPushButton(this); create_tf_btn_->setText("Create TF"); @@ -123,6 +128,7 @@ createTFTab::createTFTab(QWidget *parent) : QWidget(parent) QHBoxLayout *create_row = new QHBoxLayout; create_row->addWidget(add_imarker_); + create_row->addWidget(add_imarker_menu_); create_row->addWidget(create_tf_btn_); QHBoxLayout *remove_row = new QHBoxLayout; @@ -176,7 +182,7 @@ void createTFTab::createNewTF() { new_tf.imarker_ = true; ROS_DEBUG_STREAM_NAMED("createNewTF","imarker = " << new_tf.imarker_); - createNewIMarker(new_tf); + createNewIMarker(new_tf, add_imarker_menu_->isChecked()); } active_tf_list_.push_back(new_tf); @@ -193,7 +199,7 @@ void createTFTab::createNewTF() updateFromList(); } -void createTFTab::createNewIMarker(tf_data new_tf) +void createTFTab::createNewIMarker(tf_data new_tf, bool has_menu) { ROS_DEBUG_STREAM_NAMED("createNewIMarker","creating interactive marker..."); @@ -256,6 +262,47 @@ void createTFTab::createNewIMarker(tf_data new_tf) imarker_server_->insert(int_marker); imarker_server_->setCallback(int_marker.name, boost::bind( &createTFTab::processIMarkerFeedback, this, _1) ); + + if (has_menu && !menu_handler_set_) + { + ROS_DEBUG_STREAM_NAMED("createNewIMarker","set menu"); + + QString filters("rviz tf files (*.tf);;All files (*.*)"); + QString default_filter("rviz tf files (*.tf)"); + + QString directory = + QFileDialog::getOpenFileName(0, "Load TF Menu File", QDir::currentPath(), filters, &default_filter); + + std::string full_load_path; + full_load_path = directory.toStdString(); + ROS_DEBUG_STREAM_NAMED("load","load_file = " << full_load_path); + + std::ifstream in_file(full_load_path); + std::string line; + int result; + int level; + char name[256]; + std::string menu_name(name); + + if (in_file.is_open()) + { + while (std::getline(in_file, line)) + { + ROS_DEBUG_STREAM_NAMED("createNewIMarker",line); + result = sscanf(line.c_str(), "%d, %[^\t\n]s", &level, name); + ROS_DEBUG_STREAM_NAMED("createNewIMarker","result = " << result); + if (result == 2) + { + std::string menu_name(name); + if (level == 1) + imarker_menu_handler_.insert(name, boost::bind(&createTFTab::processIMarkerFeedback, this, _1)); + } + } + } + imarker_menu_handler_.apply(*imarker_server_, int_marker.name); + menu_handler_set_ = true; + } + imarker_server_->applyChanges(); } @@ -723,8 +770,8 @@ saveLoadTFTab::saveLoadTFTab(QWidget *parent) : QWidget(parent) void saveLoadTFTab::load() { - QString filters("rviz tf files (*.rviztf);;All files (*.*)"); - QString default_filter("rviz tf files (*.rviztf)"); + QString filters("rviz tf files (*.tf);;All files (*.*)"); + QString default_filter("rviz tf files (*.tf)"); QString directory = QFileDialog::getOpenFileName(0, "Load TF File", QDir::currentPath(), filters, &default_filter); @@ -794,8 +841,8 @@ void saveLoadTFTab::load() void saveLoadTFTab::save() { - QString filters("rviz tf files (*.rviztf);;All files (*.*)"); - QString default_filter("rviz tf files (*.rviztf)"); + QString filters("rviz tf files (*.tf);;All files (*.*)"); + QString default_filter("rviz tf files (*.tf)"); QString directory = QFileDialog::getSaveFileName(0, "Save TF File", QDir::currentPath(), filters, &default_filter); @@ -805,7 +852,7 @@ void saveLoadTFTab::save() // check if user specified file extension std::size_t found = full_save_path_.find("."); if (found == std::string::npos) - full_save_path_ += ".rviztf"; + full_save_path_ += ".tf"; ROS_DEBUG_STREAM_NAMED("save","save_file = " << full_save_path_); @@ -834,7 +881,6 @@ void saveLoadTFTab::save() { ROS_ERROR_STREAM_NAMED("save","Unable to open file: " << full_save_path_); } - } } // end namespace tf_keyboard_cal diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 7940f21..d05edd3 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -103,7 +103,9 @@ protected Q_SLOTS: private: void processIMarkerFeedback(const visualization_msgs::InteractiveMarkerFeedbackConstPtr &feedback); - void createNewIMarker(tf_data new_tf); + void createNewIMarker(tf_data new_tf, bool has_menu); + + bool menu_handler_set_; std::string from_tf_name_; std::string to_tf_name_; @@ -114,6 +116,7 @@ protected Q_SLOTS: QLineEdit *to_; QCheckBox *add_imarker_; + QCheckBox *add_imarker_menu_; QPushButton *create_tf_btn_; QPushButton *remove_tf_btn_; From f480b0396c6a3995de00f6d905ec0dca83115acd Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Mon, 19 Jun 2017 23:02:26 -0600 Subject: [PATCH 30/32] work on creating imarker menus --- config/example_imarker_menu.tf | 26 +++++++++------- src/tf_keyboard_cal_gui.cpp | 57 +++++++++++++++++++++++----------- src/tf_keyboard_cal_gui.h | 4 +++ 3 files changed, 57 insertions(+), 30 deletions(-) diff --git a/config/example_imarker_menu.tf b/config/example_imarker_menu.tf index 7ff449b..f621495 100644 --- a/config/example_imarker_menu.tf +++ b/config/example_imarker_menu.tf @@ -1,13 +1,15 @@ # example menu file -# (comment line) -# space delimeter -# level, menu title (note, supports only 1 sub level) -1, menu item A -2, sub entry a -2, sub entry b -2, sub entry c -1, menu item B -1, menu item C -1, menu item D -2, sub entry a -2, sub entry b \ No newline at end of file +# (shebang anywhere in line comments line) +# FORMAT: (supports only 1 sub level, sub menus cannot have sub menus) +# (num sub menus), menu title +# submenu title +# very little error checking done... so check your input. +3, menu item A +sub entry 1 +sub entry 2 +sub entry 3 +0, menu item B +0, menu item C +2, menu item D +sub entry 1 +sub entry 2 \ No newline at end of file diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 66d5021..8ee7652 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -269,9 +269,11 @@ void createTFTab::createNewIMarker(tf_data new_tf, bool has_menu) QString filters("rviz tf files (*.tf);;All files (*.*)"); QString default_filter("rviz tf files (*.tf)"); - + + std::string pkg_path = ros::package::getPath("tf_keyboard_cal"); + QString directory = - QFileDialog::getOpenFileName(0, "Load TF Menu File", QDir::currentPath(), filters, &default_filter); + QFileDialog::getOpenFileName(0, "Load TF Menu File", QString::fromStdString(pkg_path), filters, &default_filter); std::string full_load_path; full_load_path = directory.toStdString(); @@ -279,28 +281,49 @@ void createTFTab::createNewIMarker(tf_data new_tf, bool has_menu) std::ifstream in_file(full_load_path); std::string line; - int result; - int level; - char name[256]; - std::string menu_name(name); - + + // std::string menu_name(name); + if (in_file.is_open()) { while (std::getline(in_file, line)) - { - ROS_DEBUG_STREAM_NAMED("createNewIMarker",line); - result = sscanf(line.c_str(), "%d, %[^\t\n]s", &level, name); - ROS_DEBUG_STREAM_NAMED("createNewIMarker","result = " << result); + { + // skip comments + boost::trim(line); + if (line.find("#") != std::string::npos) + { + continue; + } + + int result; + int num_sub_menus; + char menu_name[256]; + + result = sscanf(line.c_str(), "%d, %[^\t\n\r]s", &num_sub_menus, menu_name); + if (result == 2) { - std::string menu_name(name); - if (level == 1) - imarker_menu_handler_.insert(name, boost::bind(&createTFTab::processIMarkerFeedback, this, _1)); + ROS_DEBUG_STREAM_NAMED("createNewIMarker","create main item: " << menu_name); + } + else + { + ROS_WARN_STREAM_NAMED("createNewIMarker","skipping mal formed line: " << line); + } + + for (int i = 0; i < num_sub_menus; i++) + { + std::getline(in_file, line); + result = sscanf(line.c_str(), "%[^\t\n\r]s", menu_name); + + if (result == 1) + { + ROS_DEBUG_STREAM_NAMED("createNewIMarker","create sub menu: " << menu_name); + } } } } - imarker_menu_handler_.apply(*imarker_server_, int_marker.name); - menu_handler_set_ = true; + // imarker_menu_handler_.apply(*imarker_server_, int_marker.name); + // menu_handler_set_ = true; } imarker_server_->applyChanges(); @@ -408,13 +431,11 @@ void createTFTab::removeTF() void createTFTab::fromTextChanged(QString text) { from_tf_name_ = text.toStdString(); - ROS_DEBUG_STREAM_NAMED("fromTextChanged","from: " << from_tf_name_); } void createTFTab::toTextChanged(QString text) { to_tf_name_ = text.toStdString(); - ROS_DEBUG_STREAM_NAMED("toTextChanged","to: " << to_tf_name_); } manipulateTFTab::manipulateTFTab(QWidget *parent) : QWidget(parent) diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index d05edd3..96d15a8 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -37,6 +37,7 @@ #ifndef Q_MOC_RUN #include +#include #include #endif @@ -67,9 +68,12 @@ #include #include +#include #include #include +#include + namespace tf_keyboard_cal { struct tf_data{ From 75875d2a4c06057067c9c5e2fac4c36ac409ce50 Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Tue, 20 Jun 2017 22:14:22 -0600 Subject: [PATCH 31/32] menu pubs done --- bug_notes.md | 7 +++ include/tf_keyboard_cal/tf_remote_receiver.h | 8 +++- src/tf_keyboard_cal_gui.cpp | 45 +++++++++++++++++--- src/tf_keyboard_cal_gui.h | 2 +- src/tf_remote_receiver.cpp | 33 ++++++++++++++ 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/bug_notes.md b/bug_notes.md index b2ad86f..8ab2a88 100644 --- a/bug_notes.md +++ b/bug_notes.md @@ -15,3 +15,10 @@ There seems to be a disconnect between the values of the imarker and the active ### Fix Seems that `active_tf_list_.push_back(new_tf)` was being called before `.imarker_ = true` was called. This appears to have solved the errors. + +## 3. Large Decimals + +On the `manipulate` tab, dragging the interactive marker causes large decimals. Need to display these only to 2 or 3 significant digits. + +## 4. When `has menu` is ticked, tick `imarker` checkbox. + diff --git a/include/tf_keyboard_cal/tf_remote_receiver.h b/include/tf_keyboard_cal/tf_remote_receiver.h index c1d966a..df21e3d 100644 --- a/include/tf_keyboard_cal/tf_remote_receiver.h +++ b/include/tf_keyboard_cal/tf_remote_receiver.h @@ -39,6 +39,7 @@ #include #include #include +#include namespace tf_keyboard_cal { @@ -56,7 +57,8 @@ class TFRemoteReceiver void createTF(geometry_msgs::TransformStamped create_tf_msg); void removeTF(geometry_msgs::TransformStamped remove_tf_msg); void updateTF(geometry_msgs::TransformStamped update_tf_msg); - + void addIMarkerMenuPub(int menu_index, std::string menu_name); + void publishIMarkerMenuSelection(int menu_index); std::vector getTFNames(); @@ -71,7 +73,9 @@ class TFRemoteReceiver std::vector< std::string > tf_names_; - tf2_ros::Buffer tf_buffer_; + std::vector< std::pair > menu_pubs_; + + tf2_ros::Buffer tf_buffer_; tf2_ros::TransformListener *tf_listener_; }; // end class TFRemoteReceiver diff --git a/src/tf_keyboard_cal_gui.cpp b/src/tf_keyboard_cal_gui.cpp index 8ee7652..2767eb7 100644 --- a/src/tf_keyboard_cal_gui.cpp +++ b/src/tf_keyboard_cal_gui.cpp @@ -281,6 +281,7 @@ void createTFTab::createNewIMarker(tf_data new_tf, bool has_menu) std::ifstream in_file(full_load_path); std::string line; + int menu_idx = 1; // std::string menu_name(name); @@ -298,12 +299,31 @@ void createTFTab::createNewIMarker(tf_data new_tf, bool has_menu) int result; int num_sub_menus; char menu_name[256]; + std::string menu_str; + std::string sub_menu_str; + + interactive_markers::MenuHandler::EntryHandle sub_menu_handle; result = sscanf(line.c_str(), "%d, %[^\t\n\r]s", &num_sub_menus, menu_name); if (result == 2) { - ROS_DEBUG_STREAM_NAMED("createNewIMarker","create main item: " << menu_name); + + if (num_sub_menus == 0) + { + //ROS_DEBUG_STREAM_NAMED("createNewIMarker","create main item: " << menu_idx << ", " << menu_name); + imarker_menu_handler_.insert(menu_name, boost::bind( &createTFTab::processIMarkerFeedback, this, _1)); + menu_str = menu_name; + remote_receiver_->addIMarkerMenuPub(menu_idx, menu_str); + menu_idx++; + } + else + { + //ROS_DEBUG_STREAM_NAMED("createNewIMarker","create sub menu: " << menu_idx << ", " << menu_name); + sub_menu_handle = imarker_menu_handler_.insert(menu_name); + menu_str = menu_name; + menu_idx++; + } } else { @@ -317,14 +337,20 @@ void createTFTab::createNewIMarker(tf_data new_tf, bool has_menu) if (result == 1) { - ROS_DEBUG_STREAM_NAMED("createNewIMarker","create sub menu: " << menu_name); + //ROS_DEBUG_STREAM_NAMED("createNewIMarker","create sub menu: " << menu_idx << ", " << menu_name); + imarker_menu_handler_.insert(sub_menu_handle, menu_name, boost::bind( &createTFTab::processIMarkerFeedback, this, _1)); + sub_menu_str = menu_name; + remote_receiver_->addIMarkerMenuPub(menu_idx, menu_str + "_" + sub_menu_str); + menu_idx++; } } } - } - // imarker_menu_handler_.apply(*imarker_server_, int_marker.name); - // menu_handler_set_ = true; + } + menu_handler_set_ = true; } + + if (has_menu) + imarker_menu_handler_.apply(*imarker_server_, int_marker.name); imarker_server_->applyChanges(); } @@ -339,12 +365,17 @@ void createTFTab::processIMarkerFeedback(const visualization_msgs::InteractiveMa { if (active_tf_list_[i].name_ == imarker_name) { - ROS_DEBUG_STREAM_NAMED("removeTF","update index = " << i); + ROS_DEBUG_STREAM_NAMED("processIMarkerFeedback","update index = " << i); manipulateTFTab::updateTFValues(i, feedback->pose); remote_receiver_->updateTF(active_tf_list_[i].getTFMsg()); break; } - } + } + + if (feedback->event_type == visualization_msgs::InteractiveMarkerFeedback::MENU_SELECT) + { + remote_receiver_->publishIMarkerMenuSelection(feedback->menu_entry_id); + } } void createTFTab::updateFromList() diff --git a/src/tf_keyboard_cal_gui.h b/src/tf_keyboard_cal_gui.h index 96d15a8..a0af796 100644 --- a/src/tf_keyboard_cal_gui.h +++ b/src/tf_keyboard_cal_gui.h @@ -108,7 +108,7 @@ protected Q_SLOTS: private: void processIMarkerFeedback(const visualization_msgs::InteractiveMarkerFeedbackConstPtr &feedback); void createNewIMarker(tf_data new_tf, bool has_menu); - + bool menu_handler_set_; std::string from_tf_name_; diff --git a/src/tf_remote_receiver.cpp b/src/tf_remote_receiver.cpp index 997f58e..87f7a64 100644 --- a/src/tf_remote_receiver.cpp +++ b/src/tf_remote_receiver.cpp @@ -70,4 +70,37 @@ std::vector TFRemoteReceiver::getTFNames() return tf_names_; } +void TFRemoteReceiver::addIMarkerMenuPub(int menu_index, std::string menu_name) +{ + // replace ' ' with '_' + for (std::size_t i = 0; i < menu_name.size(); i++) + { + if (menu_name[i] == ' ') + menu_name[i] = '_'; + } + + + std::string new_topic_name = "/imarker/" + menu_name; + ros::Publisher new_pub = nh_.advertise(new_topic_name, 1); + + std::pair new_menu_pub(menu_index, new_pub); + menu_pubs_.push_back(new_menu_pub); +} + +void TFRemoteReceiver::publishIMarkerMenuSelection(int menu_index) +{ + std_msgs::Bool msg; + msg.data = true; + + for (std::size_t i = 0; i < menu_pubs_.size(); i++) + { + if (menu_pubs_[i].first == menu_index) + { + menu_pubs_[i].second.publish(msg); + break; + } + } +} + + } // end namespace tf_keyboard_cal From 349c77c844a381a25a1d6b92f8eb75ac4d6b174d Mon Sep 17 00:00:00 2001 From: Andy McEvoy Date: Tue, 27 Jun 2017 23:49:14 -0600 Subject: [PATCH 32/32] begin clean up for new package, begin documentation --- CMakeLists.txt | 70 ---- README.md => README.old | 0 bug_notes.md | 3 + config/tf_keyboard_world_to_thing.yaml | 11 - frames.gv | 8 - frames.pdf | Bin 18512 -> 0 bytes include/tf_keyboard_cal/imarker_simple.h | 102 ----- include/tf_keyboard_cal/manual_tf_alignment.h | 115 ------ launch/rviz_demo.launch | 3 - launch/tf_im_world_to_thing.launch | 8 - launch/tf_keyboard_world_to_thing.launch | 8 - resources/interactive_marker_screenshot.png | Bin 28273 -> 0 bytes resources/keyboard_screenshot.png | Bin 70358 -> 0 bytes scripts/tf_interactive_marker.py | 370 ------------------ src/demo_tf_listener.cpp | 153 -------- src/imarker_simple.cpp | 133 ------- src/manual_tf_alignment.cpp | 279 ------------- src/rviz_tf_publisher.cpp | 1 + src/tf_keyboard.cpp | 5 - 19 files changed, 4 insertions(+), 1265 deletions(-) rename README.md => README.old (100%) delete mode 100644 config/tf_keyboard_world_to_thing.yaml delete mode 100644 frames.gv delete mode 100644 frames.pdf delete mode 100644 include/tf_keyboard_cal/imarker_simple.h delete mode 100644 include/tf_keyboard_cal/manual_tf_alignment.h delete mode 100644 launch/tf_im_world_to_thing.launch delete mode 100644 launch/tf_keyboard_world_to_thing.launch delete mode 100644 resources/interactive_marker_screenshot.png delete mode 100644 resources/keyboard_screenshot.png delete mode 100755 scripts/tf_interactive_marker.py delete mode 100644 src/demo_tf_listener.cpp delete mode 100644 src/imarker_simple.cpp delete mode 100644 src/manual_tf_alignment.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d189330..2594cdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,47 +117,6 @@ target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_rviz_publisher ${catkin_LIBRARIES}) -# Library -# add_library(${PROJECT_NAME}_manual_tf_alignment -# src/manual_tf_alignment.cpp -# ) -# target_link_libraries(${PROJECT_NAME}_manual_tf_alignment -# ${catkin_LIBRARIES} -# ${Boost_LIBRARIES} -# ) - -# Library -# add_library(${PROJECT_NAME}_imarker_simple -# src/imarker_simple.cpp -# ) -# target_link_libraries(${PROJECT_NAME}_imarker_simple -# ${catkin_LIBRARIES} -# ${Boost_LIBRARIES} -# ) - -# Executable -# add_executable(${PROJECT_NAME} -# src/tf_keyboard.cpp -# ) -# target_link_libraries(${PROJECT_NAME} -# ${PROJECT_NAME}_manual_tf_alignment -# ${catkin_LIBRARIES} -# ) - -# Executable -# add_executable(${PROJECT_NAME}_demo_tf_listener -# src/demo_tf_listener.cpp -# ) -# # Rename C++ executable without namespace -# set_target_properties(${PROJECT_NAME}_demo_tf_listener -# PROPERTIES OUTPUT_NAME demo_tf_listener PREFIX "") -# # Specify libraries to link a library or executable target against -# target_link_libraries(${PROJECT_NAME}_demo_tf_listener -# ${catkin_LIBRARIES} -# ) - - - ############# ## Testing ## ############# @@ -169,35 +128,6 @@ roslint_cpp() ## Install ## ############# -# # Install libraries -# install(TARGETS ${PROJECT_NAME}_manual_tf_alignment ${PROJECT_NAME}_imarker_simple -# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}) - -# # Install header files -# install(DIRECTORY include/${PROJECT_NAME}/ DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}) - -# # Install executables -# install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_demo_tf_listener -# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} -# RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -# ) - -# # Install scripts -# install(PROGRAMS -# scripts/tf_interactive_marker.py -# DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -# ) - -# # Install launch files -# install(DIRECTORY -# launch -# DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} -# ) - -############# -## Install ## -############# - # Install libraries install( TARGETS diff --git a/README.md b/README.old similarity index 100% rename from README.md rename to README.old diff --git a/bug_notes.md b/bug_notes.md index 8ab2a88..90c58f6 100644 --- a/bug_notes.md +++ b/bug_notes.md @@ -22,3 +22,6 @@ On the `manipulate` tab, dragging the interactive marker causes large decimals. ## 4. When `has menu` is ticked, tick `imarker` checkbox. +## 5. Multiple TFs with same name + +Need to not allow user to create multiple TFs with same `to` and `from`, or create loops in TF tree. diff --git a/config/tf_keyboard_world_to_thing.yaml b/config/tf_keyboard_world_to_thing.yaml deleted file mode 100644 index 6842d09..0000000 --- a/config/tf_keyboard_world_to_thing.yaml +++ /dev/null @@ -1,11 +0,0 @@ -initial_x: 0.5 -initial_y: 0.5 -initial_z: 0.5 -initial_roll: 0.0 -initial_pitch: 0.0 -initial_yaw: 0.0 -from: world -to: thing -file_package: tf_keyboard_cal -file_name: /config/tf_keyboard_world_to_thing.yaml -topic_name: /keyboard_world_to_thing/keydown diff --git a/frames.gv b/frames.gv deleted file mode 100644 index c96c60a..0000000 --- a/frames.gv +++ /dev/null @@ -1,8 +0,0 @@ -digraph G { -"world" -> "B"[label="Broadcaster: /tf_keyboard\nAverage rate: 30.203 Hz\nMost recent transform: 1495135847.188 ( 0.004 sec old)\nBuffer length: 4.933 sec\n"]; -"base" -> "world"[label="Broadcaster: /my_tf\nAverage rate: 10.181 Hz\nMost recent transform: 1495135847.260 ( -0.067 sec old)\nBuffer length: 4.813 sec\n"]; -edge [style=invis]; - subgraph cluster_legend { style=bold; color=black; label ="view_frames Result"; -"Recorded at time: 1495135847.193"[ shape=plaintext ] ; - }->"base"; -} \ No newline at end of file diff --git a/frames.pdf b/frames.pdf deleted file mode 100644 index b6a3913ba4b420af4ef3fa7bf872605fd6d48dd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18512 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 ytV#ui73lbhp!|Gb{DZ` diff --git a/include/tf_keyboard_cal/imarker_simple.h b/include/tf_keyboard_cal/imarker_simple.h deleted file mode 100644 index f00c374..0000000 --- a/include/tf_keyboard_cal/imarker_simple.h +++ /dev/null @@ -1,102 +0,0 @@ -/********************************************************************* - * Software License Agreement (BSD License) - * - * Copyright (c) 2017, PickNik LLC - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of PickNik LLC nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *********************************************************************/ - -/* Author: Dave Coleman - Desc: Use interactive markers in a C++ class via the external python node -*/ - -#ifndef TF_KEYBOARD_CAL_IMARKER_SIMPLE_H -#define TF_KEYBOARD_CAL_IMARKER_SIMPLE_H - -// ROS -#include - -#include - -#include -#include -#include -#include -#include - -namespace tf_keyboard_cal -{ -using visualization_msgs::InteractiveMarkerFeedback; -using visualization_msgs::InteractiveMarkerControl; - -typedef std::function -IMarkerCallback; - -class IMarkerSimple -{ -public: - -/** \brief Constructor */ -IMarkerSimple(); - -geometry_msgs::Pose& getPose(); - -void iMarkerCallback(const visualization_msgs::InteractiveMarkerFeedbackConstPtr &feedback); - -void sendUpdatedIMarkerPose(); - -void make6DofMarker(const geometry_msgs::Pose &pose); - -private: - -// -------------------------------------------------------- - -// The short name of this class -std::string name_ = "imarker_simple"; - -// A shared node handle -ros::NodeHandle nh_; - -geometry_msgs::Pose latest_pose_; - -// Interactive markers -std::shared_ptr imarker_server_; - -// Interactive markers -// interactive_markers::MenuHandler menu_handler_; -visualization_msgs::InteractiveMarker int_marker_; - -}; // end class - -// Create std pointers for this class -typedef std::shared_ptr IMarkerSimplePtr; -typedef std::shared_ptr IMarkerSimpleConstPtr; - -} // namespace tf_keyboard_cal -#endif // TF_KEYBOARD_CAL_IMARKER_SIMPLE_H diff --git a/include/tf_keyboard_cal/manual_tf_alignment.h b/include/tf_keyboard_cal/manual_tf_alignment.h deleted file mode 100644 index ce5bbe8..0000000 --- a/include/tf_keyboard_cal/manual_tf_alignment.h +++ /dev/null @@ -1,115 +0,0 @@ -/********************************************************************* - * Software License Agreement (BSD License) - * - * Copyright (c) 2015, PickNik LLC - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of PickNik LLC nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *********************************************************************/ - -/* - * Author : Andy McEvoy (mcevoy.andy@gmail.com), Dave Coleman - * Desc : Allows manual control of a TF through the keyboard - */ - -#ifndef TF_KEYBOARD_CAL__MANUAL_TF_ALIGNMENT_ -#define TF_KEYBOARD_CAL__MANUAL_TF_ALIGNMENT_ - -#include - -#include - -#include - -#include - -#include - -namespace tf_keyboard_cal -{ - -class ManualTFAlignment -{ -public: - - /* - * \brief - */ - ManualTFAlignment(); - - /* - * \brief - */ - void keyboardCallback(const keyboard::Key::ConstPtr& msg); - - /* - * \brief - */ - void printMenu(); - - /* - * \brief - */ - void publishTF(); - - /* - * \brief - */ - void setPose(Eigen::Vector3d translation, Eigen::Vector3d rotation); - - /* - * \brief - */ - void updateTF(int mode, double delta); - - /* - * \brief - */ - void writeTFToFile(); - - ros::NodeHandle nh_; - - // Name of class - std::string name_ = "manipulation_data"; - - Eigen::Vector3d translation_; - Eigen::Vector3d rotation_; - std::string save_path_; - int mode_; - double delta_; - std::string from_; - std::string to_; - std::string file_package_; - std::string file_name_; - std::string topic_name_; - ros::Subscriber keyboard_sub_; -}; - -} // end namespace - -#endif diff --git a/launch/rviz_demo.launch b/launch/rviz_demo.launch index 2103b2b..462f9ea 100644 --- a/launch/rviz_demo.launch +++ b/launch/rviz_demo.launch @@ -3,9 +3,6 @@ - - - diff --git a/launch/tf_im_world_to_thing.launch b/launch/tf_im_world_to_thing.launch deleted file mode 100644 index d03be37..0000000 --- a/launch/tf_im_world_to_thing.launch +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/launch/tf_keyboard_world_to_thing.launch b/launch/tf_keyboard_world_to_thing.launch deleted file mode 100644 index 697cfed..0000000 --- a/launch/tf_keyboard_world_to_thing.launch +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/resources/interactive_marker_screenshot.png b/resources/interactive_marker_screenshot.png deleted file mode 100644 index 5c664d2a92e6d773b695d966c1cd1f3b73b8b71c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28273 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 diff --git a/scripts/tf_interactive_marker.py b/scripts/tf_interactive_marker.py deleted file mode 100755 index da6772f..0000000 --- a/scripts/tf_interactive_marker.py +++ /dev/null @@ -1,370 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -""" -Created on 15/01/15 -@author: Sammy Pfeiffer - -# Software License Agreement (BSD License) -# -# Copyright (c) 2016, PAL Robotics, S.L. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -tf_interactive_marker.py contains a commandline -node that sets up a 6DOF Interactive Marker at /posestamped_im/update -and a PoseStamped topic at /posestamped and a -TF transform broadcaster with the pose of the interactive marker. -It prints on the commandline the current commandline transform and URDF -transform as you move the interactive marker. - -You can stop publishing transforms doing right click -and click on Stop Publish transform. - -As the help says: -Generate an interactive marker to setup your transforms. -Usage: -./tf_interactive_marker.py from_frame to frame position (x,y,z) orientation (r,p,y) or (x,y,z,w) -./tf_interactive_marker.py x y z r p y [deg] -./tf_interactive_marker.py x y z x y z w -For example (they do all the same): -./tf_interactive_marker.py base_footprint new_frame 1.0 0.0 1.0 0.0 0.0 90.0 deg -./tf_interactive_marker.py base_footprint new_frame 1.0 0.0 1.0 0.0 0.0 1.57 -./tf_interactive_marker.py base_footprint new_frame 1.0 0.0 1.0 0.0 0.7071 0.7071 0.0 - -The output looks like: - -Static transform publisher command (with roll pitch yaw): - rosrun tf static_transform_publisher 1.0 0.0 1.0 0.0 -0.0 1.57 base_footprint new_frame 50 - -Static transform publisher command (with quaternion): - rosrun tf static_transform_publisher 1.0 0.0 1.0 0.0 0.7068 0.7074 0.0 base_footprint new_frame 50 - -Roslaunch line of static transform publisher (rpy): - - -URDF format: - - -Example usage video: https://www.youtube.com/watch?v=C9BbFv-C9Zo - -""" - -# based on https://github.com/ros-visualization/visualization_tutorials/blob/indigo-devel/interactive_marker_tutorials/scripts/basic_controls.py -# and https://github.com/ros-visualization/visualization_tutorials/blob/indigo-devel/interactive_marker_tutorials/scripts/menu.py - -import sys -from copy import deepcopy -import rospy -from interactive_markers.interactive_marker_server import InteractiveMarkerServer -from interactive_markers.menu_handler import MenuHandler -from visualization_msgs.msg import InteractiveMarkerControl, Marker, InteractiveMarker, \ - InteractiveMarkerFeedback, InteractiveMarkerUpdate, InteractiveMarkerPose, MenuEntry -from geometry_msgs.msg import Point, Pose, PoseStamped, Vector3, Quaternion -import tf -from tf.transformations import quaternion_from_euler, euler_from_quaternion -from math import radians - - -class InteractiveMarkerPoseStampedPublisher(): - - def __init__(self, from_frame, to_frame, position, orientation): - self.server = InteractiveMarkerServer("posestamped_im") - o = orientation - r, p, y = euler_from_quaternion([o.w, o.x, o.y, o.z]) - rospy.loginfo("Publishing transform and PoseStamped from: " + - from_frame + " to " + to_frame + - " at " + str(position.x) + " " + str(position.y) + " " + str(position.z) + - " and orientation " + str(o.x) + " " + str(o.y) + - " " + str(o.z) + " " + str(o.w) + " or rpy " + - str(r) + " " + str(p) + " " + str(y)) - self.menu_handler = MenuHandler() - self.from_frame = from_frame - self.to_frame = to_frame - # Transform broadcaster - self.tf_br = tf.TransformBroadcaster() - - self.pub = rospy.Publisher('/posestamped', PoseStamped, queue_size=1) - rospy.loginfo("Publishing posestampeds at topic: " + str(self.pub.name)) - pose = Pose() - pose.position = position - pose.orientation = orientation - self.last_pose = pose - self.print_commands(pose) - self.makeGraspIM(pose) - - self.server.applyChanges() - - def processFeedback(self, feedback): - """ - :type feedback: InteractiveMarkerFeedback - """ - s = "Feedback from marker '" + feedback.marker_name - s += "' / control '" + feedback.control_name + "'" - - mp = "" - if feedback.mouse_point_valid: - mp = " at " + str(feedback.mouse_point.x) - mp += ", " + str(feedback.mouse_point.y) - mp += ", " + str(feedback.mouse_point.z) - mp += " in frame " + feedback.header.frame_id - - if feedback.event_type == InteractiveMarkerFeedback.BUTTON_CLICK: - rospy.logdebug(s + ": button click" + mp + ".") - - elif feedback.event_type == InteractiveMarkerFeedback.MENU_SELECT: - rospy.logdebug(s + ": menu item " + str(feedback.menu_entry_id) + " clicked" + mp + ".") - if feedback.menu_entry_id == 1: - rospy.logdebug("Start publishing transform...") - if self.tf_br is None: - self.tf_br = tf.TransformBroadcaster() - elif feedback.menu_entry_id == 2: - rospy.logdebug("Stop publishing transform...") - self.tf_br = None - - # When clicking this event triggers! - elif feedback.event_type == InteractiveMarkerFeedback.POSE_UPDATE: - rospy.logdebug(s + ": pose changed") - - elif feedback.event_type == InteractiveMarkerFeedback.MOUSE_DOWN: - rospy.logdebug(s + ": mouse down" + mp + ".") - - elif feedback.event_type == InteractiveMarkerFeedback.MOUSE_UP: - rospy.logdebug(s + ": mouse up" + mp + ".") - - # TODO: Print here the commands - # tf static transform - self.print_commands(feedback.pose) - - - self.last_pose = deepcopy(feedback.pose) - - self.server.applyChanges() - - def print_commands(self, pose, decimals=4): - p = pose.position - o = pose.orientation - - print "\n---------------------------" - print "Static transform publisher command (with roll pitch yaw):" - common_part = "rosrun tf static_transform_publisher " - pos_part = str(round(p.x, decimals)) + " " + str(round(p.y, decimals)) + " "+ str(round(p.z, decimals)) - r, p, y = euler_from_quaternion([o.w, o.x, o.y, o.z]) - ori_part = str(round(r, decimals)) + " " + str(round(p, decimals)) + " " + str(round(y, decimals)) - static_tf_cmd = common_part + pos_part + " " + ori_part + " " + self.from_frame + " " + self.to_frame + " 50" - print " " + static_tf_cmd - print - print "Static transform publisher command (with quaternion):" - ori_q = str(round(o.x, decimals)) + " " + str(round(o.y, decimals)) + " " + str(round(o.z, decimals)) + " " + str(round(o.w, decimals)) - static_tf_cmd = common_part + pos_part + " " + ori_q + " " + self.from_frame + " " + self.to_frame + " 50" - print " " + static_tf_cmd - print - - print "Roslaunch line of static transform publisher (rpy):" - node_name = "from_" + self.from_frame + "_to_" + self.to_frame + "_static_tf" - roslaunch_part = ' ' - print roslaunch_part - print - - print "URDF format:" # - print ' ' - print "\n---------------------------" - - def makeArrow(self, msg): - marker = Marker() - - # An arrow that's squashed to easier view the orientation on roll - marker.type = Marker.ARROW - marker.scale.x = 0.15 - marker.scale.y = 0.05 - marker.scale.z = 0.05 - marker.color.r = 1.0 - marker.color.g = 0.0 - marker.color.b = 0.0 - marker.color.a = 1.0 - - return marker - - def makeBoxControl(self, msg): - control = InteractiveMarkerControl() - control.always_visible = True - control.markers.append(self.makeArrow(msg)) - msg.controls.append(control) - return control - - def makeGraspIM(self, pose): - """ - :type pose: Pose - """ - int_marker = InteractiveMarker() - int_marker.header.frame_id = self.from_frame - int_marker.pose = pose - int_marker.scale = 0.3 - - int_marker.name = "6dof_eef" - int_marker.description = "transform from " + self.from_frame + " to " + self.to_frame - - # insert a box, well, an arrow - self.makeBoxControl(int_marker) - int_marker.controls[0].interaction_mode = InteractiveMarkerControl.MOVE_ROTATE_3D - - control = InteractiveMarkerControl() - control.orientation.w = 1 - control.orientation.x = 1 - control.orientation.y = 0 - control.orientation.z = 0 - control.name = "rotate_x" - control.interaction_mode = InteractiveMarkerControl.ROTATE_AXIS - int_marker.controls.append(control) - - control = InteractiveMarkerControl() - control.orientation.w = 1 - control.orientation.x = 1 - control.orientation.y = 0 - control.orientation.z = 0 - control.name = "move_x" - control.interaction_mode = InteractiveMarkerControl.MOVE_AXIS - int_marker.controls.append(control) - - control = InteractiveMarkerControl() - control.orientation.w = 1 - control.orientation.x = 0 - control.orientation.y = 1 - control.orientation.z = 0 - control.name = "rotate_z" - control.interaction_mode = InteractiveMarkerControl.ROTATE_AXIS - int_marker.controls.append(control) - - control = InteractiveMarkerControl() - control.orientation.w = 1 - control.orientation.x = 0 - control.orientation.y = 1 - control.orientation.z = 0 - control.name = "move_z" - control.interaction_mode = InteractiveMarkerControl.MOVE_AXIS - int_marker.controls.append(control) - - control = InteractiveMarkerControl() - control.orientation.w = 1 - control.orientation.x = 0 - control.orientation.y = 0 - control.orientation.z = 1 - control.name = "rotate_y" - control.interaction_mode = InteractiveMarkerControl.ROTATE_AXIS - int_marker.controls.append(control) - - control = InteractiveMarkerControl() - control.orientation.w = 1 - control.orientation.x = 0 - control.orientation.y = 0 - control.orientation.z = 1 - control.name = "move_y" - control.interaction_mode = InteractiveMarkerControl.MOVE_AXIS - int_marker.controls.append(control) - - control = InteractiveMarkerControl() - control.orientation.w = 1 - control.orientation.x = 0 - control.orientation.y = 0 - control.orientation.z = 1 - control.name = "move_3d" - control.interaction_mode = InteractiveMarkerControl.MOVE_3D - int_marker.controls.append(control) - - self.menu_handler.insert("Publish transform", - callback=self.processFeedback) - self.menu_handler.insert("Stop publishing transform", - callback=self.processFeedback) - - self.server.insert(int_marker, self.processFeedback) - self.menu_handler.apply(self.server, int_marker.name) - - def run(self): - r = rospy.Rate(20) - while not rospy.is_shutdown(): - if self.tf_br is not None: - pos = self.last_pose.position - ori = self.last_pose.orientation - self.tf_br.sendTransform( - (pos.x, pos.y, pos.z), - (ori.x, ori.y, ori.z, ori.w), - rospy.Time.now(), - self.to_frame, - self.from_frame - ) - ps = PoseStamped() - ps.pose = self.last_pose - ps.header.frame_id = self.from_frame - ps.header.stamp = rospy.Time.now() - self.pub.publish(ps) - r.sleep() - - -def usage(): - print "Generate an interactive marker to setup your transforms." - print "Usage:" - print sys.argv[0] + " from_frame to frame position (x,y,z) orientation (r,p,y) or (x,y,z,w)" - print sys.argv[0] + " x y z r p y [deg]" - print sys.argv[0] + " x y z x y z w" - print "For example (they all do the same):" - print sys.argv[0] + " base_footprint new_frame 1.0 0.0 1.0 0.0 0.0 90.0 deg" - print sys.argv[0] + " base_footprint new_frame 1.0 0.0 1.0 0.0 0.0 1.57" - print sys.argv[0] + " base_footprint new_frame 1.0 0.0 1.0 0.0 0.0 0.7071 0.7071" - print "You can stop publishing transforms in the right click menu of the interactive marker." - -if __name__ == "__main__": - rospy.init_node("tf_interactive_marker", anonymous=True) - cleaned_args = rospy.myargv(sys.argv) - if "-h" in cleaned_args or "--help" in cleaned_args: - usage() - exit() - if len(cleaned_args) not in [9, 10]: - print "Arguments error." - usage() - exit() - print len(cleaned_args) - from_frame = cleaned_args[1] - to_frame = cleaned_args[2] - px, py, pz = [float(item) for item in cleaned_args[3:6]] - position = Point(px, py, pz) - if len(cleaned_args) == 9: - r, p, y = [float(item) for item in cleaned_args[6:]] - quat = quaternion_from_euler(r, p, y) - orientation = Quaternion(quat[1], quat[2], quat[3], quat[0]) - elif cleaned_args[-1] == 'deg': - r, p, y = [radians(float(item)) for item in cleaned_args[6:-1]] - quat = quaternion_from_euler(r, p, y) - orientation = Quaternion(quat[1], quat[2], quat[3], quat[0]) - else: - x, y, z, w = [float(item) for item in cleaned_args[-4:]] - orientation = Quaternion(x, y, z, w) - - ig = InteractiveMarkerPoseStampedPublisher(from_frame, to_frame, - position, orientation) - ig.run() diff --git a/src/demo_tf_listener.cpp b/src/demo_tf_listener.cpp deleted file mode 100644 index 878b922..0000000 --- a/src/demo_tf_listener.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/********************************************************************* - * Software License Agreement (BSD License) - * - * Copyright (c) 2016, University of Colorado, Boulder - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of PickNik LLC nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *********************************************************************/ - -/* Author: Dave Coleman - Desc: -*/ - -#ifndef TF_KEYBOARD_CAL_DEMO_TF_LISTENER_H -#define TF_KEYBOARD_CAL_DEMO_TF_LISTENER_H - -// ROS -#include -#include - -// Conversions -#include -#include - -// Visualize -#include - -namespace tf_keyboard_cal -{ -class DemoTFListener -{ -public: - /** - * \brief Constructor - */ - DemoTFListener() : name_("demo_tf_listener"), nh_("~") - { - visual_tools_.reset(new rviz_visual_tools::RvizVisualTools("world", "/demo_tf_listener/markers")); - visual_tools_->deleteAllMarkers(); - - ROS_INFO_STREAM_NAMED(name_, "DemoTFListener Ready."); - } - - void runLoop() - { - // Allow time for listener to load - ros::Duration(1.0).sleep(); - - const bool verbose = false; - - ros::Rate rate(10.0); - while (ros::ok()) - { - Eigen::Affine3d pose; - - getPose("/world", "/thing", pose); - - // Show pose in console - if (verbose) - std::cout << "Position: \n" << pose.translation() << std::endl; - - // Visualize pose - visual_tools_->publishXArrow(pose); - - rate.sleep(); - } - } - - bool getPose(const std::string& from_frame, const std::string& to_frame, Eigen::Affine3d &pose) - { - tf::StampedTransform tf_transform; - try - { - tf_.lookupTransform(from_frame, to_frame, ros::Time(0), tf_transform); - } - catch (tf::TransformException ex) - { - ROS_ERROR("%s", ex.what()); - return false; - } - - // Convert to eigen - tf::transformTFToEigen(tf_transform, pose); - return true; - } - -private: - // -------------------------------------------------------- - // The short name of this class - std::string name_; - - // A shared node handle - ros::NodeHandle nh_; - - // For visualizing things in rviz - rviz_visual_tools::RvizVisualToolsPtr visual_tools_; - - tf::TransformListener tf_; - -}; // end class - -// Create boost pointers for this class -typedef boost::shared_ptr DemoTFListenerPtr; -typedef boost::shared_ptr DemoTFListenerConstPtr; - -} // namespace tf_keyboard_cal -#endif // TF_KEYBOARD_CAL_DEMO_TF_LISTENER_H - -int main(int argc, char** argv) -{ - // Initialize ROS - ros::init(argc, argv, "demo_tf_listener"); - ROS_INFO_STREAM_NAMED("main", "Starting DemoTFListener..."); - - // Allow the action server to recieve and send ros messages - ros::AsyncSpinner spinner(2); - spinner.start(); - - // Initialize main class - tf_keyboard_cal::DemoTFListener server; - server.runLoop(); - - // Shutdown - ROS_INFO_STREAM_NAMED("main", "Shutting down."); - ros::shutdown(); - - return 0; -} diff --git a/src/imarker_simple.cpp b/src/imarker_simple.cpp deleted file mode 100644 index 6326017..0000000 --- a/src/imarker_simple.cpp +++ /dev/null @@ -1,133 +0,0 @@ -/********************************************************************* - * Software License Agreement (BSD License) - * - * Copyright (c) 2017, PickNik LLC - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of PickNik LLC nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *********************************************************************/ - -/* Author: Dave Coleman - Desc: Use interactive markers in a C++ class via the external python node -*/ - -#include - -namespace tf_keyboard_cal -{ - IMarkerSimple::IMarkerSimple() - : nh_("~") - { - // Create Marker Server - const std::string imarker_topic = nh_.getNamespace() + "/imarker"; - imarker_server_.reset(new interactive_markers::InteractiveMarkerServer(imarker_topic, "", false)); - - // Initialize Pose - latest_pose_.orientation.w = 0; - - ros::Duration(2.0).sleep(); - - // Create imarker - make6DofMarker(latest_pose_); - - // Apply - imarker_server_->applyChanges(); - } - - geometry_msgs::Pose& IMarkerSimple::getPose() - { - return latest_pose_; - } - - void IMarkerSimple::iMarkerCallback(const visualization_msgs::InteractiveMarkerFeedbackConstPtr &feedback) - { - // Ignore if not pose update - if (feedback->event_type != visualization_msgs::InteractiveMarkerFeedback::POSE_UPDATE) - return; - - latest_pose_ = feedback->pose; - } - - void IMarkerSimple::sendUpdatedIMarkerPose() - { - imarker_server_->setPose(int_marker_.name, latest_pose_); - imarker_server_->applyChanges(); - } - - void IMarkerSimple::make6DofMarker(const geometry_msgs::Pose &pose) - { - ROS_INFO_STREAM_NAMED(name_, "Making 6dof interactive marker named " << name_); - - int_marker_.header.frame_id = "world"; - int_marker_.pose = pose; - int_marker_.scale = 0.2; - - int_marker_.name = name_; - - // int_marker_.controls[0].interaction_mode = InteractiveMarkerControl::MENU; - - InteractiveMarkerControl control; - control.orientation.w = 1; - control.orientation.x = 1; - control.orientation.y = 0; - control.orientation.z = 0; - control.name = "rotate_x"; - control.interaction_mode = InteractiveMarkerControl::ROTATE_AXIS; - int_marker_.controls.push_back(control); - control.name = "move_x"; - control.interaction_mode = InteractiveMarkerControl::MOVE_AXIS; - int_marker_.controls.push_back(control); - - control.orientation.w = 1; - control.orientation.x = 0; - control.orientation.y = 1; - control.orientation.z = 0; - control.name = "rotate_z"; - control.interaction_mode = InteractiveMarkerControl::ROTATE_AXIS; - int_marker_.controls.push_back(control); - control.name = "move_z"; - control.interaction_mode = InteractiveMarkerControl::MOVE_AXIS; - int_marker_.controls.push_back(control); - - control.orientation.w = 1; - control.orientation.x = 0; - control.orientation.y = 0; - control.orientation.z = 1; - control.name = "rotate_y"; - control.interaction_mode = InteractiveMarkerControl::ROTATE_AXIS; - int_marker_.controls.push_back(control); - control.name = "move_y"; - control.interaction_mode = InteractiveMarkerControl::MOVE_AXIS; - int_marker_.controls.push_back(control); - - imarker_server_->insert(int_marker_); - imarker_server_->setCallback(int_marker_.name, boost::bind(&IMarkerSimple::iMarkerCallback, this, _1)); - // menu_handler_.apply(*imarker_server_, int_marker_.name); - } - -} // namespace tf_keyboard_cal diff --git a/src/manual_tf_alignment.cpp b/src/manual_tf_alignment.cpp deleted file mode 100644 index c9eee6d..0000000 --- a/src/manual_tf_alignment.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/********************************************************************* - * Software License Agreement (BSD License) - * - * Copyright (c) 2015, PickNik LLC - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of PickNik LLC nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *********************************************************************/ - -/* - * Author : Andy McEvoy, Dave Coleman - * Desc : Allows manual control of a TF through the keyboard - */ - - -#include - -// Parameter loading -#include - -#include -#include -#include - -#include - -namespace tf_keyboard_cal -{ - -ManualTFAlignment::ManualTFAlignment() - : nh_("~") -{ - // set defaults - mode_ = 1; - delta_ = 0.010; - - // initial camera transform - double x, y, z, roll, pitch, yaw; - - // Get settings from rosparam - std::size_t error = 0; - error += !rosparam_shortcuts::get(name_, nh_, "initial_x", x); - error += !rosparam_shortcuts::get(name_, nh_, "initial_y", y); - error += !rosparam_shortcuts::get(name_, nh_, "initial_z", z); - error += !rosparam_shortcuts::get(name_, nh_, "initial_roll", roll); - error += !rosparam_shortcuts::get(name_, nh_, "initial_pitch", pitch); - error += !rosparam_shortcuts::get(name_, nh_, "initial_yaw", yaw); - error += !rosparam_shortcuts::get(name_, nh_, "file_package", file_package_); - error += !rosparam_shortcuts::get(name_, nh_, "file_name", file_name_); - error += !rosparam_shortcuts::get(name_, nh_, "topic_name", topic_name_); - error += !rosparam_shortcuts::get(name_, nh_, "from", from_); - error += !rosparam_shortcuts::get(name_, nh_, "to", to_); - rosparam_shortcuts::shutdownIfError(name_, error); - - setPose(Eigen::Vector3d(x, y, z), Eigen::Vector3d(roll, pitch, yaw)); - - // default, save in tf_keyboard_cal/data - std::string package_path = ros::package::getPath(file_package_); - save_path_ = package_path + file_name_; - - // listen to keyboard topic - keyboard_sub_ = nh_.subscribe(topic_name_, 100, - &ManualTFAlignment::keyboardCallback, this); - - // Echo info - ROS_INFO_STREAM_NAMED("manualTF","Listening to topic : " << topic_name_); - ROS_INFO_STREAM_NAMED("manualTF","Transform from : " << from_); - ROS_INFO_STREAM_NAMED("manualTF","Transform to : " << to_); - ROS_INFO_STREAM_NAMED("manualTF","Config File : " << save_path_); - ROS_INFO_STREAM_NAMED("manualTF","Initial transform : " << x << ", " << y << ", " << z << ", " << roll << ", " << pitch << ", " << yaw ); -} - -void ManualTFAlignment::keyboardCallback(const keyboard::Key::ConstPtr& msg) -{ - int entry = msg->code; - const double fine = 0.001; - const double coarse = 0.01; - const double very_coarse = 0.1; - - //std::cout << "key: " << entry << std::endl; - - switch(entry) - { - case 112: // - writeTFToFile(); - break; - case 117: // (very coarse delta) - std::cout << "Delta = very coarse (0.1)" << std::endl; - delta_ = very_coarse; - break; - case 105: // (coarse delta) - std::cout << "Delta = coarse (0.01)" << std::endl; - delta_ = coarse; - break; - case 111: // (fine delta) - std::cout << "Delta = fine (0.001)" << std::endl; - delta_ = fine; - break; - - // X axis - case 113: // up - updateTF(1, delta_); - break; - case 97: // down - updateTF(1, -delta_); - break; - - // y axis - case 119: // up - updateTF(2, delta_); - break; - case 115: // down - updateTF(2, -delta_); - break; - - // z axis - case 101: // up - updateTF(3, delta_); - break; - case 100: // down - updateTF(3, -delta_); - break; - - // roll - case 114: // up - updateTF(4, delta_); - break; - case 102: // down - updateTF(4, -delta_); - break; - - // pitch - case 116: // up - updateTF(5, delta_); - break; - case 103: // down - updateTF(5, -delta_); - break; - - // yaw - case 121: // up - updateTF(6, delta_); - break; - case 104: // down - updateTF(6, -delta_); - break; - - default: - // don't do anything - break; - } -} - -void ManualTFAlignment::printMenu() -{ - std::cout << "Manual alignment of camera to world CS:" << std::endl; - std::cout << "=======================================" << std::endl; - std::cout << "MOVE: X Y Z R P YAW " << std::endl; - std::cout << "------------------------" << std::endl; - std::cout << "up q w e r t y " << std::endl; - std::cout << "down a s d f g h " << std::endl; - std::cout << std::endl; - std::cout << "Fast: u " << std::endl; - std::cout << "Med: i " << std::endl; - std::cout << "Slow: o " << std::endl; - std::cout << "Save: p " << std::endl; -} - -void ManualTFAlignment::publishTF() -{ - static tf::TransformBroadcaster br; - tf::Transform transform; - tf::Quaternion q; - - // set camera pose translation - transform.setOrigin( tf::Vector3( translation_[0], - translation_[1], - translation_[2]) ); - - // set camera pose rotation - q.setRPY(rotation_[0], rotation_[1], rotation_[2]); - transform.setRotation(q); - - // publish - br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), from_ , to_)); -} - -void ManualTFAlignment::setPose(Eigen::Vector3d translation, Eigen::Vector3d rotation) -{ - translation_ = translation; - rotation_ = rotation; -} - -void ManualTFAlignment::updateTF(int mode, double delta) -{ - ROS_DEBUG_STREAM_NAMED("tf_alignment","mode = " << mode << ", delta = " << delta); - - switch(mode) - { - case 1: - translation_ += Eigen::Vector3d(delta, 0, 0); - break; - case 2: - translation_ += Eigen::Vector3d(0, delta, 0); - break; - case 3: - translation_ += Eigen::Vector3d(0, 0, delta); - break; - case 4: - rotation_ += Eigen::Vector3d(delta, 0, 0); - break; - case 5: - rotation_ += Eigen::Vector3d(0, delta, 0); - break; - case 6: - rotation_ += Eigen::Vector3d(0, 0, delta); - break; - default: - // don't do anything - break; - } -} - - -void ManualTFAlignment::writeTFToFile() -{ - std::ofstream file (save_path_.c_str()); //, std::ios::app); - ROS_INFO_STREAM_NAMED("tf_align.write","Writing transformation to file " << save_path_); - - if (!file.is_open()) - ROS_ERROR_STREAM_NAMED("tf_align.write","Output file could not be opened: " << save_path_); - else - { - ROS_INFO_STREAM_NAMED("tf_align.write","Initial transform : " << translation_[0] << ", " << translation_[1] - << ", " << translation_[2] << ", " << rotation_[0] << ", " << rotation_[1] - << ", " << rotation_[2] ); - - file << "initial_x: " << translation_[0] << std::endl; - file << "initial_y: " << translation_[1] << std::endl; - file << "initial_z: " << translation_[2] << std::endl; - file << "initial_roll: " << rotation_[0] << std::endl; - file << "initial_pitch: " << rotation_[1] << std::endl; - file << "initial_yaw: " << rotation_[2] << std::endl; - file << "from: " << from_ << std::endl; - file << "to: " << to_ << std::endl; - file << "file_package: " << file_package_ << std::endl; - file << "file_name: " << file_name_ << std::endl; - file << "topic_name: " << topic_name_ << std::endl; - } - file.close(); - -} - -} diff --git a/src/rviz_tf_publisher.cpp b/src/rviz_tf_publisher.cpp index f9c024a..52edd34 100644 --- a/src/rviz_tf_publisher.cpp +++ b/src/rviz_tf_publisher.cpp @@ -79,6 +79,7 @@ void RvizTFPublisher::updateTF(geometry_msgs::TransformStamped update_tf_msg) void RvizTFPublisher::publishTFs() { + ROS_DEBUG_STREAM_THROTTLE_NAMED(1,"publishTFs","publishing TFs: " << active_tfs_.size()); static tf::TransformBroadcaster br; for (std::size_t i = 0; i < active_tfs_.size(); i++) diff --git a/src/tf_keyboard.cpp b/src/tf_keyboard.cpp index 3abc783..ed26c52 100644 --- a/src/tf_keyboard.cpp +++ b/src/tf_keyboard.cpp @@ -47,17 +47,12 @@ int main(int argc, char** argv) ros::AsyncSpinner spinner(4); spinner.start(); - //tf_keyboard_cal::ManualTFAlignment tf_align; tf_keyboard_cal::RvizTFPublisher tf_pub; - //tf_align.printMenu(); ros::Rate rate(30.0); // hz while ( ros::ok() ) { - // publish transform to camera - //tf_align.publishTF(); tf_pub.publishTFs(); - rate.sleep(); }