From 31231c71b86b82f2b11aa4b2afec5cd78e7f4450 Mon Sep 17 00:00:00 2001 From: lab Date: Fri, 13 Aug 2021 14:43:56 +0200 Subject: [PATCH] Load plugin from lab15-19 --- CMakeLists.txt | 204 ++++++++++++++++++ launch/user_plugin.launch | 15 ++ package.xml | 35 +++ plugin.xml | 15 ++ scripts/english.py | 65 ++++++ scripts/enums/__init__.py | 0 scripts/enums/clutch_state.py | 6 + scripts/enums/stop_state.py | 6 + scripts/language.py | 61 ++++++ scripts/polish.py | 63 ++++++ scripts/qt_wrapper.py | 338 +++++++++++++++++++++++++++++ scripts/restrictions.py | 8 + scripts/robot_info.py | 32 +++ scripts/ros_wrapper.py | 269 +++++++++++++++++++++++ scripts/user_info.py | 37 ++++ scripts/user_plugin.py | 259 ++++++++++++++++++++++ scripts/user_widget.py | 18 ++ setup.py | 8 + ui/Angular.png | Bin 0 -> 910 bytes ui/Cancel.jpg | Bin 0 -> 17228 bytes ui/Distance.png | Bin 0 -> 1643 bytes ui/Linear.png | Bin 0 -> 488 bytes ui/Ok.jpg | Bin 0 -> 13269 bytes ui/user_view.ui | 395 ++++++++++++++++++++++++++++++++++ 24 files changed, 1834 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 launch/user_plugin.launch create mode 100644 package.xml create mode 100644 plugin.xml create mode 100644 scripts/english.py create mode 100644 scripts/enums/__init__.py create mode 100644 scripts/enums/clutch_state.py create mode 100644 scripts/enums/stop_state.py create mode 100644 scripts/language.py create mode 100644 scripts/polish.py create mode 100644 scripts/qt_wrapper.py create mode 100644 scripts/restrictions.py create mode 100644 scripts/robot_info.py create mode 100644 scripts/ros_wrapper.py create mode 100644 scripts/user_info.py create mode 100755 scripts/user_plugin.py create mode 100644 scripts/user_widget.py create mode 100644 setup.py create mode 100644 ui/Angular.png create mode 100644 ui/Cancel.jpg create mode 100644 ui/Distance.png create mode 100644 ui/Linear.png create mode 100644 ui/Ok.jpg create mode 100644 ui/user_view.ui diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d193766 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,204 @@ +cmake_minimum_required(VERSION 2.8.3) +project(safety_user_plugin) + +## Compile as C++11, supported in ROS Kinetic and newer +# add_compile_options(-std=c++11) + +## Find catkin macros and libraries +## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) +## is used, also find other catkin packages +find_package(catkin REQUIRED COMPONENTS + rospy + rqt_gui + rqt_gui_py + std_msgs + geometry_msgs +) + +## System dependencies are found with CMake's conventions +# find_package(Boost REQUIRED COMPONENTS system) + + +## Uncomment this if the package has a setup.py. This macro ensures +## modules and global scripts declared therein get installed +## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html +catkin_python_setup() + +################################################ +## Declare ROS messages, services and actions ## +################################################ + +## To declare and build messages, services or actions from within this +## package, follow these steps: +## * Let MSG_DEP_SET be the set of packages whose message types you use in +## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...). +## * In the file package.xml: +## * add a build_depend tag for "message_generation" +## * add a build_depend and a run_depend tag for each package in MSG_DEP_SET +## * If MSG_DEP_SET isn't empty the following dependency has been pulled in +## but can be declared for certainty nonetheless: +## * add a run_depend tag for "message_runtime" +## * In this file (CMakeLists.txt): +## * add "message_generation" and every package in MSG_DEP_SET to +## find_package(catkin REQUIRED COMPONENTS ...) +## * add "message_runtime" and every package in MSG_DEP_SET to +## catkin_package(CATKIN_DEPENDS ...) +## * uncomment the add_*_files sections below as needed +## and list every .msg/.srv/.action file to be processed +## * uncomment the generate_messages entry below +## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) + +## Generate messages in the 'msg' folder +# add_message_files( +# FILES +# RestrictionsMsg.msg +# RobotInfoMsg.msg +# ) + +## Generate services in the 'srv' folder +# add_service_files( +# FILES +# Service1.srv +# Service2.srv +# ) + +## Generate actions in the 'action' folder +# add_action_files( +# FILES +# Action1.action +# Action2.action +# ) + +## Generate added messages and services with any dependencies listed here +# generate_messages( +# DEPENDENCIES +# std_msgs +# geometry_msgs +# ) + +################################################ +## Declare ROS dynamic reconfigure parameters ## +################################################ + +## To declare and build dynamic reconfigure parameters within this +## package, follow these steps: +## * In the file package.xml: +## * add a build_depend and a run_depend tag for "dynamic_reconfigure" +## * In this file (CMakeLists.txt): +## * add "dynamic_reconfigure" to +## find_package(catkin REQUIRED COMPONENTS ...) +## * uncomment the "generate_dynamic_reconfigure_options" section below +## and list every .cfg file to be processed + +## Generate dynamic reconfigure parameters in the 'cfg' folder +# generate_dynamic_reconfigure_options( +# cfg/DynReconf1.cfg +# cfg/DynReconf2.cfg +# ) + +################################### +## catkin specific configuration ## +################################### +## The catkin_package macro generates cmake config files for your package +## Declare things to be passed to dependent projects +## INCLUDE_DIRS: uncomment this if your package contains header files +## LIBRARIES: libraries you create in this project that dependent projects also need +## CATKIN_DEPENDS: catkin_packages dependent projects also need +## DEPENDS: system dependencies of this project that dependent projects also need +catkin_package( + CATKIN_DEPENDS message_runtime +# INCLUDE_DIRS include +# LIBRARIES safety_master_plugin +# CATKIN_DEPENDS rospy std_msgs +# DEPENDS system_lib +) + +########### +## Build ## +########### + +## Specify additional locations of header files +## Your package locations should be listed before other locations +include_directories( +# include + ${catkin_INCLUDE_DIRS} +) + +## Declare a C++ library +# add_library(${PROJECT_NAME} +# src/${PROJECT_NAME}/safety_master_plugin.cpp +# ) + +## Add cmake target dependencies of the library +## as an example, code may need to be generated before libraries +## either from message generation or dynamic reconfigure +# add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) + + +## Declare a C++ executable +## With catkin_make all packages are built within a single CMake context +## The recommended prefix ensures that target names across packages don't collide +# add_executable(${PROJECT_NAME}_node src/safety_master_plugin_node.cpp) + +## Rename C++ executable without prefix +## The above recommended prefix causes long target names, the following renames the +## target back to the shorter version for ease of user use +## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node" +# set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "") + +## Add cmake target dependencies of the executable +## same as for the library above +# add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) + +## Specify libraries to link a library or executable target against +# target_link_libraries(${PROJECT_NAME}_node +# ${catkin_LIBRARIES} +# ) + +############# +## Install ## +############# + +# all install targets should use catkin DESTINATION variables +# See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html + +## Mark executable scripts (Python etc.) for installation +## in contrast to setup.py, you can choose the destination +install(PROGRAMS +# scripts/my_python_script + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +## Mark executables and/or libraries for installation +# install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_node +# ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} +# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} +# RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +# ) + +## Mark cpp header files for installation +# install(DIRECTORY include/${PROJECT_NAME}/ +# DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} +# FILES_MATCHING PATTERN "*.h" +# PATTERN ".svn" EXCLUDE +# ) + +## Mark other files for installation (e.g. launch and bag files, etc.) +# install(FILES +# # myfile1 +# # myfile2 +# DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} +# ) + +############# +## Testing ## +############# + +## Add gtest based cpp test target and link libraries +# catkin_add_gtest(${PROJECT_NAME}-test test/test_safety_master_plugin.cpp) +# if(TARGET ${PROJECT_NAME}-test) +# target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}) +# endif() + +## Add folders to be run by python nosetests +# catkin_add_nosetests(test) diff --git a/launch/user_plugin.launch b/launch/user_plugin.launch new file mode 100644 index 0000000..4a14bf8 --- /dev/null +++ b/launch/user_plugin.launch @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/package.xml b/package.xml new file mode 100644 index 0000000..6255728 --- /dev/null +++ b/package.xml @@ -0,0 +1,35 @@ + + + safety_user_plugin + 0.0.0 + The safety_user_plugin package + + Aleksander Bojda + + MIT + + catkin + + rospy + rqt_gui + rqt_gui_py + std_msgs + rosaria_msgs + + rospy + rqt_gui + rqt_gui_py + std_msgs + rosaria_msgs + + rospy + rqt_gui + rqt_gui_py + std_msgs + message_runtime + rosaria_msgs + + + + + diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..0b2b84d --- /dev/null +++ b/plugin.xml @@ -0,0 +1,15 @@ + + + + Safety system user plugin + + + + + folder + + + system-help + + + \ No newline at end of file diff --git a/scripts/english.py b/scripts/english.py new file mode 100644 index 0000000..e8af3d3 --- /dev/null +++ b/scripts/english.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# coding=utf-8 + +from language import LanguageTexts + +class EnglishTexts(LanguageTexts): + + # Warning popup for robot unlocking + warning_text_1 = "ATTENTION" + warning_text_2 = "Are You sure You want to unlock the robot?" + + # Button texts + select_text = 'Select' + + stop_robot_text = "STOP robot" + unlock_robot_text = "UNLOCK robot" + + disengage_clutch_text = "Disengage clutch" + engage_clutch_text = "Engage clutch" + + release_robot_text = 'Release robot' + + + # Logger info texts + robot_selected_text = 'PIONIER{0} selected!' + robot_released_text = 'Robot released' + stopped_and_engaged_text = "Robot will be stopped and clutch engaged" + + master_stop_clicked_text = 'MasterSTOP button pressed. Stopping the robots' + master_stop_released_text = 'MasterSTOP button released. Robot can be unlocked' + + robot_stopped_text = 'Robot stopped' + robot_started_text = 'Robot started' + + clutch_disengaged_text = 'Disengaging the clutch' + clutch_engaged_text = 'Engaging the clutch' + + obstacle_detected_text = 'Obstacle detected. Robot stopped' + obstacle_removed_text = 'Obstacle removed' + + + # Logger error/problem messages + selection_error_text = 'Select the PIONEER from robots list first' + cannot_start_masterstop_text = "Robot can't be started. MasterSTOP pressed!" + cannot_start_obstacle_text = "Robot can't be started. Obstacle detected!" + cannot_select_robot_text = "Robot can't be selected. It is already selected by another group" + connection_to_robot_lost_text = 'Connection lost with robot PIONIER{0}' + connection_to_master_lost_text = 'Connection lost with masterSTOP. Ask the teacher to run it' + + + # Description labels + linear_text = '

Linear velocity

' + angular_text = '

Angular velocity

' + id_text = '

Robot ID

' + battery_text = '

Battery state

' + masterstop_text = '

Master Stop

' + obstacle_text = '

Obstacle Stop

' + robot_text = '

Robot state

' + restrictions_text = '

Restrictions

' + history_text = '

Message history

' + + + + def __init__(self): + pass \ No newline at end of file diff --git a/scripts/enums/__init__.py b/scripts/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/enums/clutch_state.py b/scripts/enums/clutch_state.py new file mode 100644 index 0000000..9479b62 --- /dev/null +++ b/scripts/enums/clutch_state.py @@ -0,0 +1,6 @@ + + +class ClutchState: + ENGAGED = 4 + DISENGAGED = 5 + UNKNOWN = 6 \ No newline at end of file diff --git a/scripts/enums/stop_state.py b/scripts/enums/stop_state.py new file mode 100644 index 0000000..17d5939 --- /dev/null +++ b/scripts/enums/stop_state.py @@ -0,0 +1,6 @@ + + +class StopState: + STOPPED = 1 + RUNNING = 2 + UNKNOWN = 3 \ No newline at end of file diff --git a/scripts/language.py b/scripts/language.py new file mode 100644 index 0000000..63aceb6 --- /dev/null +++ b/scripts/language.py @@ -0,0 +1,61 @@ + +class LanguageTexts: + + # Warning popup for robot unlocking + warning_text_1 = "Text not reimplemented!" + warning_text_2 = "Text not reimplemented!" + + # Button texts + select_text = "Text not reimplemented!" + + stop_robot_text = "Text not reimplemented!" + unlock_robot_text = "Text not reimplemented!" + + disengage_clutch_text = "Text not reimplemented!" + engage_clutch_text = "Text not reimplemented!" + + release_robot_text = "Text not reimplemented!" + + + # Logger info texts + robot_selected_text = "Text not reimplemented!" + robot_released_text = "Text not reimplemented!" + stopped_and_engaged_text = "Text not reimplemented!" + + master_stop_clicked_text = "Text not reimplemented!" + master_stop_released_text = "Text not reimplemented!" + + robot_stopped_text = "Text not reimplemented!" + robot_started_text = "Text not reimplemented!" + + clutch_disengaged_text = "Text not reimplemented!" + clutch_engaged_text = "Text not reimplemented!" + + obstacle_detected_text = "Text not reimplemented!" + obstacle_removed_text = "Text not reimplemented!" + + + # Logger error/problem messages + selection_error_text = "Text not reimplemented!" + cannot_start_masterstop_text = "Text not reimplemented!" + cannot_start_obstacle_text = "Text not reimplemented!" + cannot_select_robot_text = "Text not reimplemented!" + connection_to_robot_lost_text = "Text not reimplemented!" + connection_to_master_lost_text = "Text not reimplemented!" + + + # Description labels + angular_text = "Text not reimplemented!" + linear_text = "Text not reimplemented!" + id_text = "Text not reimplemented!" + battery_text = "Text not reimplemented!" + masterstop_text = "Text not reimplemented!" + obstacle_text = "Text not reimplemented!" + robot_text = "Text not reimplemented!" + restrictions_text = "Text not reimplemented!" + history_text = "Text not reimplemented!" + + + + def __init__(self): + pass \ No newline at end of file diff --git a/scripts/polish.py b/scripts/polish.py new file mode 100644 index 0000000..2348d57 --- /dev/null +++ b/scripts/polish.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# coding=utf-8 + +from language import LanguageTexts + +class PolishTexts(LanguageTexts): + + # Warning popup for robot unlocking + warning_text_1 = "UWAGA" + warning_text_2 = "Na pewno chcesz odblokować robota?" + + # Button texts + select_text = 'Wybierz' + + stop_robot_text = "Zatrzymaj robota" + unlock_robot_text = "Odblokuj robota" + + disengage_clutch_text = "Rozłącz sprzęgło" + engage_clutch_text = "Połącz sprzęgło" + + release_robot_text = 'Zwolnij robota' + + + # Logger info texts + robot_selected_text = 'PIONIER{0} wybrany!' + robot_released_text = 'Odłączam robota' + stopped_and_engaged_text = "Robot zostanie zatrzymany i sprzęgnięty" + + master_stop_clicked_text = 'Przycisk masterSTOP został naciśnięty. Zatrzymuje roboty' + master_stop_released_text = 'Przycisk masterSTOP odciśnięty. Mozesz uruchomić robota' + + robot_stopped_text = 'Robot zatrzymany' + robot_started_text = 'Robot wystartował' + + clutch_disengaged_text = 'Rozprzęgam sprzęgło' + clutch_engaged_text = 'Sprzęgam sprzegło' + + obstacle_detected_text = 'Przeszkoda wykryta. Zatrzymuję robota' + obstacle_removed_text = 'Przeszkoda usunięta' + + + # Logger error/problem messages + selection_error_text = 'Najpierw wybierz PIONIERA z listy robotów' + cannot_start_masterstop_text = 'Nie można wystartować robota. MasterSTOP wciśnięty!' + cannot_start_obstacle_text = 'Nie mozna wystartować. Przeszkoda w polu działania robota' + cannot_select_robot_text = 'Nie mozna wybrać robota. Robot został już wybrany przez inną grupę' + connection_to_robot_lost_text = 'Utrata połączenia z robotem PIONIER{0}' + connection_to_master_lost_text = 'Utrata połączenia z masterstopem. Poproś prowadzącego o jego uruchomienie' + + + # Description labels + linear_text = '

Prędkość liniowa

' + angular_text = '

Prędkość obrotowa

' + id_text = '

ID robota

' + battery_text = '

Stan baterii

' + masterstop_text = '

Stop mastera

' + obstacle_text = '

Stop przeszkody

' + robot_text = '

Stan robota

' + restrictions_text = '

Ograniczenia

' + history_text = '

Historia komunikatów

' + + def __init__(self): + pass \ No newline at end of file diff --git a/scripts/qt_wrapper.py b/scripts/qt_wrapper.py new file mode 100644 index 0000000..8d3faa0 --- /dev/null +++ b/scripts/qt_wrapper.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python +# coding=utf-8 + +import os +import rospy +import rospkg + +import math +import datetime + +from enums.clutch_state import ClutchState as CS +from enums.stop_state import StopState as SS + +from python_qt_binding.QtGui import QPixmap +from python_qt_binding.QtGui import QTextCursor +from python_qt_binding.QtWidgets import QMessageBox + +from polish import PolishTexts as PT +from english import EnglishTexts as ET + +class QtWrapper: + + def __init__(self,widget): + self.widget = widget + self.displayed_robots_id_list = [] + + lang = rospy.get_param('lang') + + if lang == 'eng': + self.language = ET + self.log_info('Started english version of plugin NEW') + elif lang == 'pol': + self.language = PT + self.log_info('Polska wersja pluginu uruchomiona') + else: + raise ValueError('language parameter has value different than "eng" or "pol"') + + self.clear_robot_info() + + ui_directory_path = '{0}/ui'.format(rospkg.RosPack().get_path('safety_user_plugin')) + + self.distance_pixmap = QPixmap('{0}/Distance.png'.format(ui_directory_path)) + self.angular_pixmap = QPixmap('{0}/Angular.png'.format(ui_directory_path)) + self.linear_pixmap = QPixmap('{0}/Linear.png'.format(ui_directory_path)) + + self.widget.distanceImage.setPixmap(self.distance_pixmap.scaled(66,46)) + self.widget.angularImage.setPixmap(self.angular_pixmap) + self.widget.linearImage.setPixmap(self.linear_pixmap.scaled(20,48)) + + self.ok_pixmap = QPixmap('{0}/Ok.jpg'.format(ui_directory_path)) + self.cancel_pixmap = QPixmap('{0}/Cancel.jpg'.format(ui_directory_path)) + + def disconnect_signals(self): + self.widget.clutchButton.clicked.disconnect() + self.widget.stopButton.clicked.disconnect() + self.widget.choiceButton.clicked.disconnect() + + def connect_robot_signals(self): + self.widget.clutchButton.clicked.connect(self.handle_clutchButton_clicked_callback) + self.widget.stopButton.clicked.connect(self.handle_stopButton_clicked) + self.widget.choiceButton.clicked.connect(self.handle_closeButton_clicked) + + def connect_signals(self): + self.widget.clutchButton.clicked.connect(self.handle_not_selected_error) + self.widget.stopButton.clicked.connect(self.handle_not_selected_error) + self.widget.choiceButton.clicked.connect(self.handle_openButton_clicked) + + + def handle_emitted_signal(self,method_name): + exec('self.{0}()'.format(method_name)) + + def handle_emitted_signal_with_list_argument(self,method_name,argument): + if method_name == 'update_selected_robot_info': + self.update_selected_robot_info(argument[0]) + + else: + method_with_argument = 'self.{0}({1})'.format(method_name,argument[0]) + exec(method_with_argument) + + + def handle_stopButton_clicked(self): + if self.robot_state == SS.RUNNING: + self.handle_stopButton_clicked_callback() + else: + reply = QMessageBox.warning(self.widget,self.language.warning_text_1,self.language.warning_text_2,QMessageBox.Yes,QMessageBox.No) + + if reply == QMessageBox.Yes: + self.handle_stopButton_clicked_callback() + + + def get_selected_robot_id(self): + current_text = self.widget.robotsList.currentText() + + if 'PIONIER' in current_text: + return int(current_text[7:]) + else: + return None + + def display_clutch_on(self): + # self.widget.clutchLabel.setPixmap(self.ok_pixmap.scaled(40,40)) + self.widget.clutchButton.setStyleSheet("QPushButton { color: black; background-color: green; font: bold 20px}") + self.widget.clutchButton.setText(self.language.disengage_clutch_text) + + def display_clutch_off(self): + # self.widget.clutchLabel.setPixmap(self.cancel_pixmap.scaled(40,40)) + self.widget.clutchButton.setStyleSheet("QPushButton { color: black; background-color: red; font: bold 20px}") + self.widget.clutchButton.setText(self.language.engage_clutch_text) + + def display_state_on(self): + self.widget.stateLabel.setPixmap(self.ok_pixmap.scaled(40,40)) + self.widget.stopButton.setStyleSheet("QPushButton { color: black; background-color: green; font: bold 20px}") + self.widget.stopButton.setText(self.language.stop_robot_text) + self.robot_state = SS.RUNNING + + def display_state_off(self): + self.widget.stateLabel.setPixmap(self.cancel_pixmap.scaled(40,40)) + self.widget.stopButton.setStyleSheet("QPushButton { color: black; background-color: red; font: bold 20px}") + self.widget.stopButton.setText(self.language.unlock_robot_text) + self.robot_state = SS.STOPPED + + def handle_not_selected_error(self): + self.log_error(self.language.selection_error_text) + + def handle_openButton_clicked(self): + selected_robot_id = self.get_selected_robot_id() + + if selected_robot_id != None: + self.handle_openButton_clicked_callback(selected_robot_id) + else: + self.log_error(self.language.selection_error_text) + + def handle_closeButton_clicked(self): + self.handle_closeButton_clicked_callback() + + def set_robot_selection_callback(self,callback): + self.handle_openButton_clicked_callback = callback + + def set_robot_release_callback(self,callback): + self.handle_closeButton_clicked_callback = callback + + def set_user_stop_state_updated_callback(self,callback): + self.handle_stopButton_clicked_callback = callback + + def set_clutch_switched_callback(self,callback): + self.handle_clutchButton_clicked_callback = callback + + def select_robot(self,robot_id): + self.disconnect_signals() + self.connect_robot_signals() + self.log_info(self.language.robot_selected_text.format(robot_id)) + self.log_info(self.language.stopped_and_engaged_text) + + self.widget.choiceButton.setText(self.language.release_robot_text) + + def release_robot(self): + self.disconnect_signals() + self.connect_signals() + self.log_info(self.language.robot_released_text) + self.clear_robot_info() + + def update_robots_list_gui(self,robots_id_list): + robots_id_list.sort() + id_strings_list = (('PIONIER'+str(x)) for x in robots_id_list) + + robots_to_add = [] + robots_to_remove = [] + + for robot_id in robots_id_list: + if robot_id not in self.displayed_robots_id_list: + robots_to_add.append(robot_id) + + for robot_id in self.displayed_robots_id_list: + if robot_id not in robots_id_list: + robots_to_remove.append(robot_id) + + for robot_id in robots_to_remove: + self.remove_robot_from_list(robot_id) + + for robot_id in robots_to_add: + self.add_robot_to_list(robot_id) + + + def remove_robot_from_list(self,robot_id): + count = self.widget.robotsList.count() + + for i in range(count): + if str(robot_id) in self.widget.robotsList.itemText(i): + self.widget.robotsList.removeItem(i) + self.displayed_robots_id_list.remove(robot_id) + return + + + def add_robot_to_list(self,robot_id): + self.widget.robotsList.addItem('PIONIER{0}'.format(robot_id)) + self.displayed_robots_id_list.append(robot_id) + + + def update_selected_robot_info(self,robot_info): + linear_vel = math.sqrt(robot_info.linear_velocity[0]**2 + robot_info.linear_velocity[1]**2) + + self.widget.idLabel.setText('PIONIER{0}'.format(robot_info.robot_id)) + self.widget.batteryLabel.setText("{:.2f}".format(robot_info.battery_voltage)) + self.widget.linearLabel.setText("{:.2f}".format(linear_vel)) + self.widget.angularLabel.setText("{:.2f}".format(robot_info.angular_velocity[2])) + + # self.log_info(str(robot_info.clutch == CS.ENGAGED)) + + if robot_info.clutch == CS.ENGAGED: + self.display_clutch_on() + else: + self.display_clutch_off() + + if robot_info.state == SS.RUNNING: + self.display_state_on() + else: + self.display_state_off() + + if robot_info.obstacle_detected == True: + self.widget.obstaclestopLabel.setPixmap(self.cancel_pixmap.scaled(40,40)) + else: + self.widget.obstaclestopLabel.setPixmap(self.ok_pixmap.scaled(40,40)) + + def master_stopped(self): + self.widget.masterstopLabel.setPixmap(self.cancel_pixmap.scaled(40,40)) + self.log_info(self.language.master_stop_clicked_text) + + def master_started(self): + self.widget.masterstopLabel.setPixmap(self.ok_pixmap.scaled(40,40)) + self.log_info(self.language.master_stop_released_text) + + def user_stopped(self): + self.log_info(self.language.robot_stopped_text) + self.display_state_off() + + def user_started(self): + self.log_info(self.language.robot_started_text) + self.display_state_on() + + def disengage_clutch(self): + self.log_info(self.language.clutch_disengaged_text) + self.display_clutch_off() + + def engage_clutch(self): + self.log_info(self.language.clutch_engaged_text) + self.display_clutch_on() + + def master_is_stopped_notify(self): + self.log_error(self.language.cannot_start_masterstop_text) + + def obstacle_is_detected_notify(self): + self.log_error(self.language.cannot_start_obstacle_text) + + def robot_selected_by_another_group_notify(self): + self.log_error(self.language.cannot_select_robot_text) + + def update_restrictions_gui(self,restrictions): + self.widget.distanceRestrictionLabel.setText("{:.2f}".format(restrictions.distance)) + self.widget.linearRestrictionLabel.setText("{:.2f}".format(restrictions.linear_velocity)) + self.widget.angularRestrictionLabel.setText("{:.2f}".format(restrictions.angular_velocity)) + + def log_info(self,info_text): + time = datetime.datetime.now().strftime('[%H:%M:%S]') + + cursor = self.widget.logsBrowser.textCursor() + cursor.movePosition(QTextCursor.End) + self.widget.logsBrowser.setTextCursor(cursor) + self.widget.logsBrowser.insertHtml(' {0} {1}
'.format(time,info_text)) + self.scroll_to_bottom() + # self.widget.logsBrowser.insertHtml(str(self.logger_counter) + '\t[INFO]\t' + info_text) + + def log_warning(self,warning_text): + time = datetime.datetime.now().strftime('[%H:%M:%S]') + + cursor = self.widget.logsBrowser.textCursor() + cursor.movePosition(QTextCursor.End) + self.widget.logsBrowser.setTextCursor(cursor) + self.widget.logsBrowser.textCursor().movePosition(QTextCursor.End) + self.widget.logsBrowser.insertHtml(' {0} {1}
'.format(time,warning_text)) + self.scroll_to_bottom() + # self.widget.logsBrowser.append(str(self.logger_counter) + '\t[WARN]\t' + warning_text) + + def log_error(self,error_text): + time = datetime.datetime.now().strftime('[%H:%M:%S]') + + cursor = self.widget.logsBrowser.textCursor() + cursor.movePosition(QTextCursor.End) + self.widget.logsBrowser.setTextCursor(cursor) + self.widget.logsBrowser.textCursor().movePosition(QTextCursor.End) + self.widget.logsBrowser.insertHtml(' {0} {1}
'.format(time,error_text)) + self.scroll_to_bottom() + # self.widget.logsBrowser.append(str(self.logger_counter) + '\t[ERROR]\t' + error_text) + + def connection_to_robot_lost(self,lost_robot_id): + self.clear_robot_info() + self.disconnect_signals() + self.connect_signals() + self.log_info(self.language.connection_to_robot_lost_text.format(lost_robot_id)) + + def connection_to_master_lost(self): + self.log_error(self.language.connection_to_master_lost_text) + + def obstacle_detected(self): + self.log_error(self.language.obstacle_detected_text) + # TODO Zmiana + + def obstacle_removed(self): + self.log_info(self.language.obstacle_removed_text) + + def scroll_to_bottom(self): + scrollbar = self.widget.logsBrowser.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def clear_robot_info(self): + self.widget.idLabel.setText('-') + self.widget.batteryLabel.setText('-') + self.widget.linearLabel.setText('-') + self.widget.angularLabel.setText('-') + self.widget.stateLabel.setText('-') + self.widget.masterstopLabel.setText('-') + self.widget.obstaclestopLabel.setText('-') + + self.widget.choiceButton.setText(self.language.select_text) + + self.widget.stopButton.setText('-') + self.widget.stopButton.setStyleSheet("") + + self.widget.clutchButton.setText('-') + self.widget.clutchButton.setStyleSheet("") + + self.widget.angularText.setText(self.language.angular_text) + self.widget.batteryText.setText(self.language.battery_text) + self.widget.idText.setText(self.language.id_text) + self.widget.linearText.setText(self.language.linear_text) + self.widget.masterStopText.setText(self.language.masterstop_text) + self.widget.obstacleText.setText(self.language.obstacle_text) + self.widget.robotText.setText(self.language.robot_text) + self.widget.historyLabel.setText(self.language.history_text) + self.widget.restrictionsLabel.setText(self.language.restrictions_text) diff --git a/scripts/restrictions.py b/scripts/restrictions.py new file mode 100644 index 0000000..bea4082 --- /dev/null +++ b/scripts/restrictions.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +class Restrictions: + + def __init__(self,restrictions_msg): + self.distance = restrictions_msg.distance.data + self.linear_velocity = restrictions_msg.linear_velocity.data + self.angular_velocity = restrictions_msg.angular_velocity.data \ No newline at end of file diff --git a/scripts/robot_info.py b/scripts/robot_info.py new file mode 100644 index 0000000..6669655 --- /dev/null +++ b/scripts/robot_info.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +from enums.clutch_state import ClutchState as CS +from enums.stop_state import StopState as SS + +class RobotInfo: + + def __init__(self,robot_id): + self.robot_id = robot_id + self.battery_voltage = None + self.linear_velocity = None + self.angular_velocity = None + self.state = None + self.clutch = None + self.obstacle_detected = None + + def update_robot_info(self,robot_info_msg): + self.robot_id = robot_info_msg.robot_id.data + self.battery_voltage = robot_info_msg.battery_voltage.data + self.linear_velocity = [robot_info_msg.twist.linear.x,robot_info_msg.twist.linear.y,robot_info_msg.twist.linear.z] + self.angular_velocity = [robot_info_msg.twist.angular.x,robot_info_msg.twist.angular.y,robot_info_msg.twist.angular.z] + self.obstacle_detected = robot_info_msg.obstacle_detected.data + + if robot_info_msg.state.data == True: + self.state = SS.RUNNING + else: + self.state = SS.STOPPED + + if robot_info_msg.clutch.data == True: + self.clutch = CS.ENGAGED + else: + self.clutch = CS.DISENGAGED \ No newline at end of file diff --git a/scripts/ros_wrapper.py b/scripts/ros_wrapper.py new file mode 100644 index 0000000..c7d5034 --- /dev/null +++ b/scripts/ros_wrapper.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +import rospy +import rospkg +import rostopic + +from std_msgs.msg import Bool +from rosaria_msgs.msg import RobotInfoMsg +from rosaria_msgs.msg import RestrictionsMsg + +from robot_info import RobotInfo +from restrictions import Restrictions + +from enums.clutch_state import ClutchState as CS +from enums.stop_state import StopState as SS + +class ROSWrapper: + + def __init__(self): + self.robots_list_update_callback = None + self.selected_robot_info_update_callback = None + self.master_stop_update_callback = None + self.restrictions_update_callback = None + + self.get_user_stop_state = None + self.get_clutch_state = None + + self.robot_info_subscriber = None + self.master_stop_subscriber = None + self.restrictions_subscriber = None + + self.user_stop_publisher = None + self.clutch_publisher = None + + self.periodic_timer = None + self.robots_list_timer = None + self.connection_timer = None + self.master_connection_timer = None + self.hz_update_timer = rospy.Timer(rospy.Duration(0.2),self.update_hz_values) + + self.robots_hz_subscribers_dict = {} + self.rostopics_hz_dict = {} + self.robots_hz_value = {} + + # def setup_node(self): + # rospy.init_node('safety_user_plugin', anonymous=True) + + def setup_subscribers_and_publishers(self,robot_id): + robot_name = 'PIONIER' + str(robot_id) + + self.robot_info_subscriber = rospy.Subscriber('/{0}/RosAria/robot_info'.format(robot_name), RobotInfoMsg, self.handle_selected_robot_info_update) + self.master_stop_subscriber = rospy.Subscriber('/PIONIER/master_stop', Bool, self.handle_master_stop_update) + self.restrictions_subscriber = rospy.Subscriber('/PIONIER/restrictions', RestrictionsMsg, self.handle_restrictions_update) + + self.user_stop_publisher = rospy.Publisher('/{0}/RosAria/user_stop'.format(robot_name),Bool,queue_size = 1) + self.clutch_publisher = rospy.Publisher('/{0}/RosAria/clutch'.format(robot_name),Bool,queue_size = 1) + + if self.periodic_timer != None: + self.shutdown_timer(self.periodic_timer) + self.periodic_timer = rospy.Timer(rospy.Duration(0.1),self.publish_periodic_update) + print("NEW") + if self.connection_timer != None: + self.shutdown_timer(self.connection_timer) + self.connection_timer = rospy.Timer(rospy.Duration(5),self.handle_robot_connection_lost) + + if self.master_connection_timer != None: + self.shutdown_timer(self.master_connection_timer) + self.master_connection_timer = rospy.Timer(rospy.Duration(1.5),self.handle_master_connection_lost) + + if self.hz_update_timer != None: + self.shutdown_timer(self.hz_update_timer) + self.hz_update_timer = rospy.Timer(rospy.Duration(0.2),self.update_hz_values) + + def unsubscribe(self,subscriber): + if subscriber != None: + subscriber.unregister() + + def unregister_publisher(self,publisher): + if publisher != None: + publisher.unregister() + + def shutdown_timer(self,timer): + if timer != None: + timer.shutdown() + + def publish_periodic_update(self,event): + stop_state = self.get_user_stop_state() + # clutch_state = self.get_clutch_state() + + if stop_state == SS.RUNNING: + self.user_started() + elif stop_state == SS.STOPPED: + self.user_stopped() + else: + raise ValueError('stop_state UNKNOWN when attempting to publish periodic update') + + # if clutch_state == CS.ENGAGED: + # self.engage_clutch() + # elif clutch_state == CS.DISENGAGED: + # self.disengage_clutch() + # else: + # raise ValueError('clutch_state UNKNOWN when attempting to publish periodic update') + + def update_hz_values(self,event): + for key in self.rostopics_hz_dict: + self.robots_hz_value[key] = self.rostopics_hz_dict[key].get_hz('/PIONIER{0}/RosAria/user_stop'.format(key)) + + + def handle_robot_connection_lost(self,event): + self.shutdown_timer(self.connection_timer) + self.robot_connection_lost_callback() + + def handle_master_connection_lost(self,event): + self.shutdown_timer(self.master_connection_timer) + self.master_connection_lost_callback() + + def cancel_subscribers_and_publishers(self): + self.shutdown_timer(self.periodic_timer) + self.unsubscribe(self.robot_info_subscriber) + self.unsubscribe(self.master_stop_subscriber) + self.unsubscribe(self.restrictions_subscriber) + self.unregister_publisher(self.user_stop_publisher) + self.unregister_publisher(self.clutch_publisher) + + self.periodic_timer = None + self.robot_info_subscriber = None + self.master_stop_subscriber = None + self.restrictions_subscriber = None + self.user_stop_publisher = None + self.clutch_publisher = None + + def restart_connection_timer(self): + if self.connection_timer != None: + self.connection_timer.shutdown() + self.connection_timer = rospy.Timer(rospy.Duration(1.5),self.handle_robot_connection_lost) + else: + raise RuntimeError('Attempting to restart connection_timer when it is not initialized') + + def restart_master_connection_timer(self): + if self.master_connection_timer != None: + self.master_connection_timer.shutdown() + self.master_connection_timer = rospy.Timer(rospy.Duration(1.5),self.handle_master_connection_lost) + else: + raise RuntimeError('Attempting to restart master_connection_timer when it is not initialized') + + def unregister_node(self): + self.cancel_subscribers_and_publishers() + rospy.signal_shutdown('Closing safety user plugin') + + def select_robot(self,robot_id): + self.setup_subscribers_and_publishers(robot_id) + + def setup_get_user_stop_state_function(self,function): + self.get_user_stop_state = function + + def setup_get_clutch_state_function(self,function): + self.get_clutch_state = function + + # ROSWrapper Callbacks + def get_robots_list(self,event): + robots_id_list = [] + + published_topics_list = rospy.get_published_topics(namespace='/') + published_topics = [] + + for list_ in published_topics_list: + published_topics.append(list_[0]) + + for topic in published_topics: + if topic.find('RosAria') ==-1 or topic.find('robot_info') == -1: + pass + else: + robot_number = topic.split('/') + robot_number = robot_number[1] + robot_number = robot_number[7:] + if len(robot_number) > 0: + robot_number = int(robot_number) + robots_id_list.append(robot_number) + + + # Adding new hz subscribers + for robot_id in robots_id_list: + if robot_id not in self.robots_hz_subscribers_dict: + # Add hz subscriber + self.rostopics_hz_dict[robot_id] = rostopic.ROSTopicHz(-1) + self.robots_hz_subscribers_dict[robot_id] = rospy.Subscriber('/PIONIER{0}/RosAria/user_stop'.format(robot_id),Bool, + self.rostopics_hz_dict[robot_id].callback_hz,callback_args='/PIONIER{0}/RosAria/user_stop'.format(robot_id)) + + # Removing old hz subscribers + for robot_key in self.robots_hz_subscribers_dict: + if robot_key not in robots_id_list: + # Remove old subscriber + self.unsubscribe(self.robots_hz_subscribers_dict[robot_id]) + self.robots_hz_subscribers_dict[robot_id] = None + self.rostopics_hz_dict[robot_id] = None + + + self.robots_list_update_callback(robots_id_list) + + + def release_robot(self): + self.unsubscribe(self.robot_info_subscriber) + self.shutdown_timer(self.periodic_timer) + self.shutdown_timer(self.connection_timer) + self.shutdown_timer(self.master_connection_timer) + + def handle_selected_robot_info_update(self,msg): + # Restarting connection timer to avoid raising robot_connection_lost_callback + self.restart_connection_timer() + + _robot_info = RobotInfo(0) + _robot_info.update_robot_info(msg) + self.selected_robot_info_update_callback(_robot_info) + + def handle_master_stop_update(self,msg): + # Restarting master connection timer to avoid raising master_connection_lost_callback + self.restart_master_connection_timer() + + master_stop_state = SS.UNKNOWN + if msg.data == True: + master_stop_state = SS.RUNNING + else: + master_stop_state = SS.STOPPED + + self.master_stop_update_callback(master_stop_state) + + def handle_restrictions_update(self,msg): + restrictions = Restrictions(msg) + self.restrictions_update_callback(restrictions) + + # UserPlugin Callbacks + def set_robots_list_update_callback(self,callback_function): + self.robots_list_update_callback = callback_function + self.robots_list_timer = rospy.Timer(rospy.Duration(0.5),self.get_robots_list) + + def set_selected_robot_info_update_callback(self,callback_function): + self.selected_robot_info_update_callback = callback_function + + def set_master_stop_update_callback(self,callback_function): + self.master_stop_update_callback = callback_function + + def set_restrictions_update_callback(self,callback_function): + self.restrictions_update_callback = callback_function + + def set_robot_connection_lost_callback(self,callback_function): + self.robot_connection_lost_callback = callback_function + + def set_master_connection_lost_callback(self,callback_function): + self.master_connection_lost_callback = callback_function + + def engage_clutch(self): + msg = Bool() + msg.data = True + self.clutch_publisher.publish(msg) + + def disengage_clutch(self): + msg = Bool() + msg.data = False + self.clutch_publisher.publish(msg) + + def user_started(self): + msg = Bool() + msg.data = True + self.user_stop_publisher.publish(msg) + + def user_stopped(self): + msg = Bool() + msg.data = False + self.user_stop_publisher.publish(msg) + diff --git a/scripts/user_info.py b/scripts/user_info.py new file mode 100644 index 0000000..68cd98d --- /dev/null +++ b/scripts/user_info.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +from robot_info import RobotInfo + +class UserInfo: + + def __init__(self): + self.selected_robot = None + self.robots_id_list = [] + self.user_stop_state = None + self.master_stop_state = None + self.clutch_state = None + + def select_robot(self,robot_id): + self.selected_robot = RobotInfo(robot_id) + + def release_robot(self): + self.selected_robot = None + + def update_robots_id_list(self,robots_id_list): + self.robots_id_list = robots_id_list + + def update_selected_robot_info(self,new_robot_info): + self.selected_robot.robot_id = new_robot_info.robot_id + self.selected_robot.battery_voltage = new_robot_info.battery_voltage + self.selected_robot.linear_velocity = new_robot_info.linear_velocity + self.selected_robot.angular_velocity = new_robot_info.angular_velocity + self.selected_robot.state = new_robot_info.state + self.selected_robot.clutch = new_robot_info.clutch + self.selected_robot.obstacle_detected = new_robot_info.obstacle_detected + self.clutch_state = new_robot_info.clutch + + def get_user_stop_state(self): + return self.user_stop_state + + def get_clutch_state(self): + return self.clutch_state \ No newline at end of file diff --git a/scripts/user_plugin.py b/scripts/user_plugin.py new file mode 100755 index 0000000..cc7169a --- /dev/null +++ b/scripts/user_plugin.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python + +import os + +from qt_gui.plugin import Plugin +from python_qt_binding.QtCore import QObject +from python_qt_binding.QtCore import pyqtSignal + +from qt_wrapper import QtWrapper +from ros_wrapper import ROSWrapper +from user_widget import UserWidget +from user_info import UserInfo + +from enums.clutch_state import ClutchState as CS +from enums.stop_state import StopState as SS + + +class CallbackHandler(QObject): + signal = pyqtSignal(str) + signal_with_list_argument = pyqtSignal(str,list) + + def __init__(self): + super(CallbackHandler, self).__init__() + + def connect(self,slot): + self.signal.connect(slot) + + def list_connect(self,slot): + self.signal_with_list_argument.connect(slot) + + +class UserPlugin(Plugin): + + def __init__(self,context): + super(UserPlugin, self).__init__(context) + self.setObjectName('User Plugin - L1.5 safety system') + # setStyleSheet("background-color:black;") + self.cbhandler = CallbackHandler() + + self._widget = UserWidget(context) + self._qt_wrapper = QtWrapper(self._widget) + self._ros_wrapper = ROSWrapper() + self._user_info = UserInfo() + + self._user_info.user_stop_state = SS.STOPPED + self._user_info.master_stop_state = SS.UNKNOWN + self._user_info.clutch_state = CS.UNKNOWN + + # Setup functions to get _user_info states from _ros_wrapper + self._ros_wrapper.setup_get_user_stop_state_function(self._user_info.get_user_stop_state) + self._ros_wrapper.setup_get_clutch_state_function(self._user_info.get_clutch_state) + + # self._ros_wrapper.setup_node() + self.set_callback_functions() + + # At the end! + self._qt_wrapper.connect_signals() + self._ros_wrapper.set_robots_list_update_callback(self.handle_robots_list_update) + self.cbhandler.signal.connect(self._qt_wrapper.handle_emitted_signal) + self.cbhandler.signal_with_list_argument.connect(self._qt_wrapper.handle_emitted_signal_with_list_argument) + + + + def call_qt_wrapper_method(self,method_name): + self.cbhandler.signal.emit(method_name) + + def call_qt_wrapper_method_with_argument(self,method_name,argument): + self.cbhandler.signal_with_list_argument.emit(method_name,argument) + + def shutdown_plugin(self): + self._ros_wrapper.unregister_node() + + def set_callback_functions(self): + self.set_Qt_callback_functions() + self.set_ROS_callback_functions() + + def set_ROS_callback_functions(self): + self._ros_wrapper.set_selected_robot_info_update_callback(self.handle_selected_robot_info_update) + self._ros_wrapper.set_master_stop_update_callback(self.handle_master_stop_update) + self._ros_wrapper.set_restrictions_update_callback(self.handle_restrictions_update) + self._ros_wrapper.set_robot_connection_lost_callback(self.handle_robot_connection_lost) + self._ros_wrapper.set_master_connection_lost_callback(self.handle_master_connection_lost) + + def set_Qt_callback_functions(self): + self._qt_wrapper.set_robot_selection_callback(self.handle_robot_selection) + self._qt_wrapper.set_robot_release_callback(self.handle_robot_release) + self._qt_wrapper.set_user_stop_state_updated_callback(self.handle_user_stop_state_updated) + self._qt_wrapper.set_clutch_switched_callback(self.handle_clutch_switched) + + + + + # ROS callback functions + def handle_robots_list_update(self,robots_id_list): + self._user_info.update_robots_id_list(robots_id_list) + self._qt_wrapper.update_robots_list_gui(robots_id_list) + + def handle_selected_robot_info_update(self,robot_info): + if robot_info != None: + self.update_selected_robot_info(robot_info) + else: + raise RuntimeError('Updated robot_info is NoneType object') + + def handle_master_stop_update(self,master_stop_state): + if (master_stop_state != self._user_info.master_stop_state): + self._user_info.master_stop_state = master_stop_state + if master_stop_state == SS.STOPPED: + self.master_stopped() + elif master_stop_state == SS.RUNNING: + self.master_started() + else: + raise ValueError('Undefined master_stop_state received') + + def handle_restrictions_update(self,restrictions): + self._qt_wrapper.update_restrictions_gui(restrictions) + + + # Qt callback functions + def handle_robot_selection(self,robot_id): + if self._user_info.selected_robot == None: + if self._ros_wrapper.robots_hz_value[robot_id] == None: + self.select_robot(robot_id) + else: + self._qt_wrapper.robot_selected_by_another_group_notify() + + else: + raise RuntimeError('User already selected robot') + + def handle_robot_release(self): + if self._user_info.selected_robot != None: + self.release_robot() + else: + raise RuntimeError('Cannot release. No robot selected') + + def handle_user_stop_state_updated(self): + if self._user_info.selected_robot.obstacle_detected == True: + self._qt_wrapper.obstacle_is_detected_notify() + else: + if self._user_info.master_stop_state == SS.STOPPED: + self._qt_wrapper.master_is_stopped_notify() + elif self._user_info.master_stop_state == SS.RUNNING: + if self._user_info.user_stop_state == SS.RUNNING: + self.user_stopped() + elif self._user_info.user_stop_state == SS.STOPPED: + self.user_started() + else: + raise ValueError('user_stop_state value undefined') + else: + raise ValueError('master_stop_state value undefined') + + def handle_clutch_switched(self): + if self._user_info.clutch_state == CS.ENGAGED: + self.disengage_clutch() + elif self._user_info.clutch_state == CS.DISENGAGED: + self.engage_clutch() + else: + raise ValueError('clutch_state value undefined') + + def handle_robot_connection_lost(self): + if self._user_info.selected_robot != None: + lost_robot_id = self._user_info.selected_robot.robot_id + self.connection_to_robot_lost(lost_robot_id) + else: + raise RuntimeError('Connection lost callback received when no robot was selected') + + def handle_master_connection_lost(self): + self.connection_to_master_lost() + + # Operations + def master_stopped(self): + self._user_info.master_stop_state = SS.STOPPED + + if self._user_info.selected_robot != None: + self.user_stopped() + self.call_qt_wrapper_method('master_stopped') + # self._qt_wrapper.master_stopped() + + def master_started(self): + self._user_info.master_stop_state = SS.RUNNING + + if self._user_info.selected_robot != None: + self.call_qt_wrapper_method('master_started') + # self._qt_wrapper.master_started() + + def user_stopped(self): + self._user_info.user_stop_state = SS.STOPPED + self._ros_wrapper.user_stopped() + self.call_qt_wrapper_method('user_stopped') + # self._qt_wrapper.user_stopped() + + def user_started(self): + self._user_info.user_stop_state = SS.RUNNING + self._ros_wrapper.user_started() + self.call_qt_wrapper_method('user_started') + # self._qt_wrapper.user_started() + + def engage_clutch(self): + self._user_info.clutch_state = CS.ENGAGED + self._ros_wrapper.engage_clutch() + self.call_qt_wrapper_method('engage_clutch') + # self._qt_wrapper.engage_clutch() + + def disengage_clutch(self): + self._user_info.clutch_state = CS.DISENGAGED + self._ros_wrapper.disengage_clutch() + self.call_qt_wrapper_method('disengage_clutch') + # self._qt_wrapper.disengage_clutch() + + def select_robot(self,robot_id): + self._ros_wrapper.select_robot(robot_id) + # self._qt_wrapper.select_robot(robot_id) + self.call_qt_wrapper_method_with_argument('select_robot',[robot_id]) + self._user_info.select_robot(robot_id) + + # Stop robot and engage clutch after taking control over it to put it in starting state + self.user_stopped() + self.engage_clutch() + + if self._user_info.master_stop_state == SS.RUNNING: + self.master_started() + elif self._user_info.master_stop_state == SS.STOPPED: + self.master_stopped() + + def release_robot(self): + self._ros_wrapper.release_robot() + self.call_qt_wrapper_method('release_robot') + # self._qt_wrapper.release_robot() + self._user_info.release_robot() + self._user_info.master_stop_state = SS.UNKNOWN + + def connection_to_robot_lost(self,lost_robot_id): + # pass + self._ros_wrapper.release_robot() + self._user_info.release_robot() + self.call_qt_wrapper_method_with_argument('connection_to_robot_lost',[lost_robot_id]) + # self._qt_wrapper.connection_to_robot_lost(lost_robot_id) + + def connection_to_master_lost(self): + if self._user_info.selected_robot != None: + self.master_stopped() + self.call_qt_wrapper_method('connection_to_master_lost') + + def obstacle_detected(self): + self.call_qt_wrapper_method('obstacle_detected') + self.user_stopped() + + def obstacle_removed(self): + self.call_qt_wrapper_method('obstacle_removed') + + def update_selected_robot_info(self,robot_info): + if self._user_info.selected_robot != None: + if robot_info.obstacle_detected == True and self._user_info.selected_robot.obstacle_detected == False: + self.obstacle_detected() + elif robot_info.obstacle_detected == False and self._user_info.selected_robot.obstacle_detected == True: + self.obstacle_removed() + + self._user_info.update_selected_robot_info(robot_info) + self.call_qt_wrapper_method_with_argument('update_selected_robot_info',[robot_info]) + # self._qt_wrapper.update_selected_robot_info(robot_info) \ No newline at end of file diff --git a/scripts/user_widget.py b/scripts/user_widget.py new file mode 100644 index 0000000..915a2eb --- /dev/null +++ b/scripts/user_widget.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +import os +import rospkg + +from python_qt_binding.QtWidgets import QWidget +from python_qt_binding import loadUi + +class UserWidget(QWidget): + + def __init__(self,context): + super(UserWidget, self).__init__() + ui_file = os.path.join(rospkg.RosPack().get_path('safety_user_plugin'), 'ui', 'user_view.ui') + loadUi(ui_file, self) + self.setObjectName('User Plugin - L1.5 safety system') + + if context.serial_number() > 1: + self.setWindowTitle(self.windowTitle() + (' (%d)' % context.serial_number())) + context.add_widget(self) \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..25facb1 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +from distutils.core import setup +from catkin_pkg.python_setup import generate_distutils_setup + +setup_args = generate_distutils_setup( + package_dir={'': 'scripts'} +) + +setup(**setup_args) \ No newline at end of file diff --git a/ui/Angular.png b/ui/Angular.png new file mode 100644 index 0000000000000000000000000000000000000000..6b85fc6c93e30909f86240873ae9f9b0415bc0c7 GIT binary patch literal 910 zcmV;919AL`P)y&l-crulu$EHVfSZHQ;WYlJ{WOkXch33f&;pQi9>{SCUce&q z5dI}z#Ku9ja?XQ+;UWAuBv3bh2eX4{rhY?N57{nkEkY-8Aov2Nu^Ka2hx>77o$2!1 zXL8PO_Zi6E8N;5|<$b}h0-uGC{=wa&fGTwEtNU*2?8mBpVB13D zB|JK6sG#*&t88MH{a1tj;RyCo2t3y$RIR_RUMt%STU#lmgw4*` z9t~U0`BRdx9|K51$%eAl^s@Yyc$BW1h^s7*Rgsy7z#J3{PF ztZ54DtswkC5xYX?ZEWY+*qEWN!@ogzONdQjZ;`F}roeUu;k`xdjSySHgB|fPLS3Wt z@ON8XpHz=C@e;D^;xkULok93OT{nl13#j*~-cZI4hO1RK__p|6xE;R~P>1xmjg1Q| zUM{{-JqyGoV+pTdMXx^kK0}?4rIfBzovkkI(PR;i<5SggF{e86w0}R(IUgEFw&E&Z z`l&jDi}07*qoM6N<$f}2dSN&o-= literal 0 HcmV?d00001 diff --git a/ui/Cancel.jpg b/ui/Cancel.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff6a442c64c575d3a084e71b271f5f99908602b1 GIT binary patch literal 17228 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�OXvVI|ceZ|q&_2e&jzx}{u1$9^ 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 literal 0 HcmV?d00001 diff --git a/ui/Distance.png b/ui/Distance.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c7a087efe6918f9c1290faf6307de990b7cdcc GIT binary patch literal 1643 zcmV-x29)`UP)2{teDSxPWDnir{G5mpwL`c;1l!kc3Jde*1%Hx%M{@CaAo!;L2?$Gz-*&RT2lv+~`2_TFo)E!&JVNSX>flRiFcdupXNh5mJE_~3scQZzTB6G`g;TLIgo z*HYUrH=^(Uh9uc`T|`J43rtS0gNY<4XL1fOWLJ#nJA6>i) zbOYU;ot+m|>ueQxCF>WMUu`2vvw-V?DYlnG1tKJkk#r;QBCt=9`VAlf_65cOcUO4- zoxs@E*47)Vb+`)b4~zwVskVWnlYr}hLu|jD$;*Bkk`4gw0mkRVe*q5xH`!iRWM9dL zo)E&X)jC`aLI^7Wk|e2WGbHI0;2L1E?asWeBoUG(15W`hIq{>wxwe0($LUgNZ*On2 zeJC&oxB}P<_z<`ugz$2bB>Mtq0XqY)1DDiGrliw=D}jS;zhA6h$l9syPWF9EZFhwFKdq%(*_A5@A2C?Z1A_Qb8} zx~b?ZV0YV()Z;*v7y~>ULO3jh(B9P4G!57gc&xd(dG8Ry>=44a#P51ak|g75?0+0^ zIdGuu56T&>B0|z=UwDb3f17EKPsM*Vy+~ zL=ryg%P2o0P6swm@BaX%*?zCam)3({LI|Jbo=yA{-YT-|f>pcNTF_zp<8n$#1xSgI z)C63T)6cj4T8%HP7v*OBMuo-QB0EgRtBa=51!{!Xn z+ivLYFNdZiNk->`t`Nd}O2h~ugl$X6-emO%Hy?CW+|~j}ItI9fNWyB9aRJym_gr>% zZ2-=u|AMZul?){v2iy!yEhYith!_YW_NI4Y{;g4+wzRZN>FMbil|HU0XP4$376OOb zey5z>3&f?$#0?bnT*R<8>*wrx`)R~Q%rRuXEC;>@cId0kK#hpO#CE`L#nEOl3P5Y_ zxxXgL!9+)2BuF|$Qd0r=Bq!gxT!UeZvU?&)=Mg8+)@I-XV24z<4KeOtTU;#Zg!I1C z_Us}D77)Yj49jg_SY&fOuuBD-TY!l<(d)M7RO6te3xL_c0k(f^CMH5Mtg(KNL`b?A zIFm@ix0(D5dS}=NN8qZlq)0ah=ma*&%L~L$Bg1}@^69w|1xc3yrxNoM-{o}$paaN+ zn-cR9BMy@05z~DK+Wx*+U$Ff*uplS9R8m7qfkOq7E(c~1bCxCZu33iaHXvhZJaBS< zJ2!xs08A&6@MAfn0)Xvq;M$ygfuxbLf^0-g;dhl%N-9X&DW9{v708y74S*$*HtA<4 z2Lc_oe=4O&^OcK$t5y+V`%hxpDvRQ_1fG_(UVnSq&sbSXn38ZWuo*C8wYr958SmI! z<6VJeY0+oIAWc@^N35S5WxIQgN|){Dh!yWFi#!2XD(SK;^*x-Cl5jsUnKRw?Ur?qR zZhJ9sBE6;lQNY!}tCD66*X1?!`vGDl@0fJ!0Q7Mz!1ja0py!pGcn@Oz@zZpKtY5P6 z5JXDCV&d73<8Alk^?eR?+n)fFfOg_xkPPF9Bjig-cS+i_QeT4!Nu!9z8Jd8$ViEvq z31l`VW}LsFH=rqZ4W*##S>Q2ZeYLqqUG|Js z&BNLDO52NV??p_m+(SG*FjUb6oYWTyu$Jd69m{1M0FovU&ooT}8hW5)E%Bx8il_J* pwhTI+Klq5aWgT`HvNQca<3IVN!W@qnz@z{G002ovPDHLkV1iZ`^*8_k literal 0 HcmV?d00001 diff --git a/ui/Linear.png b/ui/Linear.png new file mode 100644 index 0000000000000000000000000000000000000000..e2b6a3fb7873427e230d52e036150e36ab3808a2 GIT binary patch literal 488 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 literal 0 HcmV?d00001 diff --git a/ui/user_view.ui b/ui/user_view.ui new file mode 100644 index 0000000..fd526e6 --- /dev/null +++ b/ui/user_view.ui @@ -0,0 +1,395 @@ + + + Form + + + + 0 + 0 + 952 + 817 + + + + Form + + + true + + + + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + + + + + 0 + 0 + + + + + 16777215 + 60 + + + + Wybierz + + + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + QLayout::SetDefaultConstraint + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">ID robota</span></p></body></html> + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Stan baterii</span></p></body></html> + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Predkosc liniowa</span></p></body></html> + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Predkosc obrotowa</span></p></body></html> + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Stan robota</span></p></body></html> + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Stop mastera</span></p></body></html> + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Stop przeszkody</span></p></body></html> + + + + + + + + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + + + 15 + + + + + + 100 + 0 + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + + 100 + 0 + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + + 100 + 0 + + + + <html><head/><body><p align="center">-</p></body></html> + + + Qt::AlignCenter + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Ograniczenia</span></p></body></html> + + + + + + + TextLabel + + + Qt::AlignCenter + + + + + + + TextLabel + + + Qt::AlignCenter + + + + + + + TextLabel + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 50 + 20 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + + 0 + 0 + + + + - + + + + + + + + 0 + 0 + + + + - + + + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Histora komunikatów</span></p></body></html> + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + +