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
zcmagFbyQoy*Df5~iWVta9D)@u?obju0a~O*iUloRv=nyBUw2N{I_IqHHFM6)nVJ1O&))xj{`&^S^HZa(qAnAOQ=59DqrVfklq-uNUwf
z0KmY)#=-#npMpm~h=GNRkBN;#^k|?+_Go~GiH(hgjfaPY^Js>FiG_`WOAf%pr(h8x
zc&eyJ$!g*HIq5eQHJflj?IgR1s1oG+8jZ&v4ydJD{bLnO7yyjNCLX)we-~Uld;$zi
z94u_Y$NyExAM3=%!okMF!NtMG`d>`|uyM$7S%mN?9vi@CwfIdbtn}DO0Tmmh_B*?X
zGSqU-Ef_xehg!6-PUU^bzXiaP$Ctn)$07&F1M>PJnJ2O7m_t;^?1G8OUQy&rC4b%r
zeS02QqtzI)Ov#INZ{|i>bzl2=PEpR$eB>X%4ex;3G>s9ScseSoFHCU|*HkF1_MtH(
zHrDH;f{NXEA8VB!cgf(=gcmEHYhU>0>W3ajv996N)L+Fd9&3=NDUL!
z6+A(g;~*4B+}ImLQ%imK53tVjIw{4PjWH5<^bfE|E1&o7vmC9@X|}`Ix1}rFzrU}4
z$dk5nU2PbJ{sSG6g`&wF|#x>6hy>PwgRg$3{!^?IAxyFtH_$
z(yYK)!q7-(J}dDQpo1$23=!7H?U-GsSITy^Xqq74(E|X)*x>-I45?QQ9UZ-khr?jO
z){LkyZ1F`HWVVqzh$57o3aFCB0mM$Wc(UAE95+YQE^~JK3HGzhmFS}~G#0oB))3Wd
zjp4wsgIJq2K0A)i*XYQgO!vf{Cb)0Y7XCTsRUNTs52gH7Ha(;j<#;T}KeL7P^#n#j
zg`!m<_}3o|#Z=B^dV)z^Zl-Ps8lH`o
z%?ewhGL_!5xHukOG@9Aio6WL5>=_(pDbt6E7e!R_s
zv$(e?NFKJlMvDLQZ4JS$`^A@6r{izTD&)jk+7@5Zy-n^-{X0c#m0y^{d0k;D>PDux
z`IdUm*F!0JDDlgtJ6X2v+b9`>DA>iB@fjM#H0Y_JbHlKrGqqE}a^UxT-i8(Y(l60R$J1FaZpKm*^g|woA)&R15gOe%i+|XL|(>9Qy$LN
zM7--xp$_GPU@BK}CD9YnbSMPi~SO}10U-)s>^6rn}+RLat%3(L+pBHn-TIx+b
z(td5v2^4emvKwN1o2uB=-wJFQ8TB}c09#`Q5!xlk@qm7JNg_imE{1R!Tf`qa6qNSTn2|t7cAQ>4SWKorcL!UBz
zTID}|3S6#VlsrPsoY6f?_~0P;U(*yflCAbX)0I;8@k;xud&AHAqhKxC?btxilRmd*
z8aYqzqUbBq9JV7Y*!@5#J&a$ZN9TdmC>`32dB&vsF0p8%CB3($E-h?J`E<1ohy0HaBsmg*r#Xmf3ELt
z4w(MeiiZ22Mf%x<`xrhxwt3Ji^=%+1$|E+B7YP6^Jmj!=><6?Z_j;v+w5T-tm>Lp?-aW#$5
z>iIG>Bh)L!pn$&=oMd1K7p{w2wPUD4N1q2f-4+v9!l`2xSX1ZQdCl(l(!ekLpAI
zz=P9zVK@{|%SRU6-qzY=8-3kIO;Hl8_!OU0Y|RH8E;2DUT|~9fzw&~*5J`0`bkcpn
z(NO-qEi#nR;5#b08L`_qFjfxNl;l=E;>AW5fFQ^eo^lprxP4%bU`B>*1DmdzDle!m
zu^}6g0#LmEVPtv882OvS*PVF1%mk6ykaQy6PFaaJ?V0w}LuNQcRB9obQuHMbi$PzT
zV8;0J$$#x?0zP><8-@7mhOT>8jAUvtu6z9)ao;(o1@_7c#rj)mlx!gv%`Tl>gTCvP
z{nj}9m2Vo>O~Y)V!+(}a2DPcH#Y&P+IhFlO?kr;1f1~85Om&z7ttf|Uw3=s9rps;U
zRQs-{;S|}^EIU6_`y5Q(_pWE~p`gF-1q+4{7e_sODOPBO;1n%+YmV@l#JU`_<$~8C
z$A3;ZH&2e&P3eoL&Rfk$v`axO>64bi`plDd4ciGRs?F=7)dGCt3QkDOFJt>l
zo~}xz`neTjUv*15ZESKSOEv))x}m$F>{evi;Xdy+m#Ut2OL$}JvVWL;R#cers<>#t
z=_LeFpScpyv*L59zT!CpH!QbHM1=O#jOU-s)%1bnR(9maO%K}^CF*#}nzm74si14j+oz(0
z9XaBVY7zD(D=w|94+&=}#JXuXS72R?VG7~K3H^X#bK?iWDsw4qR!euvwau`RVfMe{
z(W$0g#FrfLyIIO$2r7DtTGpvI;dJS_OLbt|##L8Z!SE$ESH=)qld=4u8hY*Iqn~FVhzdmrWkVY-tyLHiG}$_vOJT
zQvsdY4dy)0+4q5`J_w~EHY9gR-#LdECKZEbY>?B_q32f)@>eIsdYvLJesi=%WcvKY
z=B64kyjPqNk?DE8=~h@`0>RF>@xNcgIb~@LogIQvzS7YIZhaRZxmJ0dfup&b%Q-Go
z--oZ4G1nc=uW+*MkMnTabC`bDA-mqxl?$^7wMyG^8ZQ*AOxta;jVKu)AXP0DzzwZZ
zW}LeE-+%pgo!cg%Yo@PtyX(6x4OHh}sc+P1CGM*!#AIetP2M
zN8ApoXv#3|D5Qp+Qx=|p+6wlQ4FrU=lS{B=ps6%G3t;b<)Kt8K$v`>E(P76vS9X8JP5;NXwX{Tc2#2nA(suH$rN
zr;wDfrW>oghCJuZ9Y!h7gZP^@w*lDBZTg3BFGj71sJRzaW=bscS5&|BUbpShqrZxJ
zvJNR_l>D6KS4D}p|LB`#h;ua+%Er(#hC!4H`wgiex~J4~_!ulJ-`Nr;z`sd
zPfASUoESd9x;tSye>~;A;lC1KW9aw9MFsVhIpZhmVmS4XQsIiy?@B`mgfWOPcO>H7
z2pCg0d5^a4^86MY<~zaD6gH?*JXhIvB)gzx93>~rSTs`H78FA32*3iWYgiYq@}xt#
zO*jah*k#d>y3-0-pm4VtB0xGuDQpUPO1-t_@aA*71Ab+htaD|={*>4Pzv6rLBG)o-FR%!}Yw%*;>SDJV=H=I3?N>!STt8v9acj}J#vZ`B~{!*-E=MUQrN{WNMM
zBa&tLFsu5ChdTmRgPai3&h#Sco8bla{!NQ?Zo=(@MEx3AnRK}f3Db{wh0
zPJJ{I+KTx!h;}DcGxUzxiGqm_FNjL$dzInO<+QffF9h-hj_Nh4LcVy{Dv>y_?%#H#
zVEAc!WmB3ph`ZEB2N_r^YR=flmIU^@wrwJkqh=k0p(28Cm6~?VTuI2h4{y^%^9w(>
zMw=B%b%RCcuQ&@%mN9zf0_+3v*lQfX?-sL5VCvyY%9frumRZBF0(MyuIQ;bQ_~2+=
zM%1e*NCUT(;!QB$ORytE0azQoSMa+0nN77$^#qT=R5@ic#bHN}220-(&Ddrlvg9qV
z(q-bleuETc3b%^m$M3&xHr9$Zsld89ADlS8i)k&=NSB;i>!D=yH@r~O8(N`_hRQH`
zVlI69e6;`WR$^KPdGWJfd$D37eOAC5mF~{HDG^2aZm%8=lNm{zo&iF`++kS9fnM
zk-kq(NK)cUokAqpa}V6{)d}>^A`KlZz-+OjLt6|ioJYXMat&YKhrscgode|Scdx4R
zE+w(jxRY}D{qL@nE3P1}qavY42XWT#W<0-GTn3iwe0j&O;i?g4wfq8DLjAD*1N4TR
z*2&(6(fsqRv3p5kJ_4PlG-%0lRLu|q$#1qR&v`aoGQitJ9e`nGu5|t=Yi)WwP!Bcq8-iP{s
zee<@)@@W@W_*NR+Nl4`VxSEOF0S$iZc(^0$4?pt5NsO}D>3z4`F}_wU)Cn=gOU=6>
zXQ!1Fl`=|z_*v+wcwEHtoxI%6AN13-w$=O?qB+wMr#b9t*|-+(*_GlGWcMgXpm8Og
zi2*#Wd;>nrm~eawmi)~|18jajmoMP7_}eL$HVEb
zwqjX#v2v&5aH!MIPqr2eK=PPqq8$4wMe1n-`7SK;PgT_d4Rd;Ae4^dW$u+}!sA;}(K9vN$XyZA-_?9cqT-H-)i4Fz$s)Il;NWwvaWhSXVWn#Fm;|T@^E^lu({>-(J
zr-eEY;@CPKIULVb8l~lQX|*~zWM;7@zssL_U!*06(ne%U^@i){=KB~fMgcw3$Rdo5
z8I<&eggE$RdG4~&1Q{vmAf?4OZTiUr36*{3LC|z^bm44Si&wc$<;2c1YGzNnV9zI9
zU-v+p^|lP$8an{dCo~L3^7DWZPat;+cXH0hWCAX;!?3oL@tW7F8UsxN5y8mu`Z5Tj
znYPZwc2SK!XNVd&gM-}&g>@SExF>FC#I`JAjjZ|PsizU97mo0Zwzxx8w&}dO=jd2Q
zLV`Cqmgs{HuNO!%8uxWkGcEcZpET@GA%iT$V+G7O+5^Sc-nHpz*kwj)>xYz;{LM+R
zHy#HUdl>I?js5+v$vJRyOM6r6{kv`7lbC+FY5YXi4O7?@_egTV
zaWYwNzH^KRFpF;@1}-O}PVRCY1advCn8z-%w4k9Fye*1MUonATmoJVYxZT(R@Fa8*
zMdH&T3IKpBA8QNXv4_@a)y|J|`X$`Ol7>#)qAKto+}0ZJ%Jt1!*<2OLW=&)Kx&?G*
zAtYp4X}!Fye^|-<4I#c$J(_xQcYS&uD(?!UR<$2$D>ccWi(&0aQq8(qSnk_!oHQ=1
znaklZ-O~oLQoJFKFi8w!nPqe$5zPOq>p4;)!h9EsHW1B6&xW}ww=%O6r2yZJHWJbR
z#eriJh27Afsaa9n_35&EsjkXTiEl6vI8a<`6%OKt(A7xO&h<=PZdXDG1A}l&6z8T|
z&AM5d)!Uj!jYvsAX-B3XAp=fKls&Zb?B2L|BmY$r3C3<
zVKxn`KytC?c%4AxpQqg;M3CK5N!_J@h!i~MW00^ZwBG(={5#FX%?5lu{H@QzQxf#b
z2RLf5Y1#Ev#wfTlef_C%&p>Lx==F`_lUDZ?s8>7hYm+Z6+9)l!XY{8di+UAvMpQer
zSnyf?dwqd`P^1yD=E?E62GWhdNAWB+SWggvG!i_db1i()Pm5+nuMOr&Ua)qD_DpSq
z5^ENP3RRsT>;sRvyCMj_}Fj{fQ`<#S6}$0Zbt>Xi4tUx
z1q9k(IIa)RknfueRXO4`sa4Dk(q{&8Uhnd{ihva88SQe(7-3qrP
zpzyb>J%Q%PR?%(al#$Arr|Xg!`0rXK8iRgap6|_7Ylkp;_?@1vJ(E>?RXzJ$zNbEc
zyRLYmi7Svd%I5u0)T_mSfr>!_XUK#4*{%5BJPw7I-0(&Hh2*lHP*#Q?Vb?E!zrRWR
ze(_nks>|AHsiAwOvQFa#_pLEUvDPtPSf2h5w~L&^UXF*U1+~%AF89^IN|43Nwf$z>
zO*-|~R~4+mRm^-iH_S6Bm}W%HlscoXeZmdeHnfXXk-Kg!#s(hzIO@jwDP$Id>mqC8
zUcW6otBxC!_1juJ+w3*$XWOlD)bp(M*-txW5&{R+37pNHcq#Yct#oC=<2-k*1Fp@i
z$CBUB62?}e8js$m4b`RdPRFF|uVJK#AfBri#`G5aGr7y#JZXigi!KeyvI6If20|0>
zj1+CHG0`k6Y1ZW2HE|J&9v{fTOZpK6P`Y{zwq|<%mw$y*BDHCH8Iht|d89pKxL!%}
zPMbqLFw5UebPpe=U6q%(U8|@qu`EN$TI1C>+)PB-{Py^hNz1Nx?=PfLZhwdIPSi(V
zNaZN|FDivC4Qd?!1ZEw*T(Fz`DsL+-QSz?FJEP2iR+m+i46>P>9Mz^dkLPA-!5(E!
zq**>im^zZaiH6|u=;Go62*3&e>>$85bprKAVF4G=hnsiK`pttD=HTC{XS}eoec6dj
z&S-6&hI?{uW)F6{h8qBb!YtVG4b`MCB*uI%zhwG!B=MP2VZ3N7|fz1^$XzxrDbScmV`5)AMv5NUUp@E6kcKWi0#8;dcV{GNn8#8R9=@TL?Hae-
z`+`R)SAUc|#o7-ARh5$XHGr!57jy0;Cb>!Gj~14IvAD0Q<*@oDnvpL4V)MHA_G*YESmvj)1JrYrsvzjlMZFx-(0
z!TzSp*`@NM8O5-QYut_Co;j`L7l<=qRkbwT@L<6z4Erzr!IztWF*6evg*Rw4iB^0X8>L`Q$8=Un0n}X(O(qpnwnSshof<
z!`FDJq`8xe=_1^RPw!lHR}uCvBjtW|H7-%Q3i&8|59H9mj=}s=3fQSU-XV_j(>r>h
z=f>7^3%dO`e(`HUaa>yRgmXo3Z~Y9<0b|**?bg
zM~p{vG3bg?Q1P-CO9}E*!)`ZxB6UK_L)n~#t@s~+OrBWT!Q@3m=j6vr&V9YMl#I*l
z@!9_(18@Zu@25x<0kKRQcxbiYZ)WEq8uT`zQPfxOuF`~n2kzm=>;ja94&jWX_Ad=v
zfk2gRFe}=T{ussbQKkBL9yyb1$k@%SZIu#|q7vQj4G5rep85@w$hFi39*l}0*^?M7p4gNr%+OW(Qtb)C~Puf2j
zPpcKvVr`^o7>k`&^rgZYy(hTmRND=mn(Q0s!!tN&P?w{Zht#gsLut2bm4M+U
z0TI;b0Uj_M%-{qv27wXTkpoF%vdN4*GRll{0V;_~71t&f`Ye@40{cp
z9eWKs_)vsqJqyORwyZEZ>Q2^E0-^*@;hNEHkyS*WMq`-R!|Y+OWfOa#?lz%T+yiq$
zsMuT_ztO%KfIfFvRZ%Rf+TO0(t(2LX)0vUjfoR;rMoJi0p7>=J^urh2aikTa*ITZo
zcdGqU?BiBIea#~@aiy}PqidSm(&+dc6g?(Y;xh&T|O7o77{wnaj!;YeurlMvtv^#RqZhN*2Xmm)CN^
z_KbO_DzNpzIN%}aU7-IaTMK|OJd!OME*-y7y>n2A|Km6&__LiZFO32EO_9m$R+o0#
z>&iJ^k!Qt_Qqci^;9m01D7+?e(!>b|tS2iJMDR^cgtVTgr~WAkD)ku%GF#tzh$
z*hsmpG>qhMddmNe`R|Yz&o6`Qo9FV=n|aOh<66b>wY)Od|1+X|1OEWTTn`Jc{sE$I
z9PpIs+2)*AqvX>QzEBVAuT(l-pl3FzX!aN1H_yC`P8lgYyOoOrb~j>#@#f4}#ai8x
zWM+$|Q633ou79=VnAiKw(UAN+@9sTcl%`3vHT`ijKP1Dr1ASh8ftiSAu_8kEy6g+$jo%vO8mQTP~(}?IU@=*Z0~j)(u^`PKaDb
z2HrAvE=s%gl)U~2;500ma`xa!^j$?Lr8R4}-oysV_3xFGqOQ|hr1w$_|7%B)TOB`}
z#D=nA!xIw;AP6N0A_xSDk^rlbfcDUek6i0sb;KBepk_K(d9QeQkAnJln17biIKG4>|*6|YaP{R$g9
zeDWk80`E3j@MYs8S)+Ban2J@%Lspv%zO;&m^;?*w9tj-QJGW`JG4?ype&Kv8Ua65j
zuNgUI{8}%T!>J`Wjn=b4W@=)ctli4~{*mYDNeIKM@TW?umECCg`YU{z^N>
zsBCSaQm$1w&nxXUsx1w*O>4LjClTUN$|dgxR|HRi^$9`bL^|ZvBr)|Nyx(6NAbHYk
zNolMNG#9*~d-`QL84v`jLHEQ7#BE&}}
zqMTtwESVX@Bl-KCe54J%+pGsYy}+-x2Txg1X_f*?oJx%6k@0g1!pMPR
zZwI@lr_tz_&A56I>ZkTT1jxr{oqUpI1!1}R`J7HaIr*=3n~pg~n1pTcAD}7eApWqz
zQ*Pn=MBl)u(_;w2;|E^2GT0rMTrd3!oRq|w)!6Qdsld&@cID+nA)Vh;fnf3)Q)G`d
zKc~y445i-F@0&SzDbTIcA(|Lid;Jg4r&H)cvtF8-u&k+HoSMZ1&h%7Tb`uQ0ivH;-Qs{O@#!)q3E==UCPLRf>+-}fMh#^)Kb3rf4
zPJUl$iZUg@4Nm%JT9TdNwymN$Dz
zZ*2D2_uT{&pa>04jDStgL5Q_Lni+Ov%35GwaliVFm*1=BXz!CbG-{9!7u#!dE_qLk
zkMiU1n^TsKtFXMXk0iKP?HpHa#fWA}PN|H)@IJ3ya+c&^udP_Fii!%Q;7In$l5s;d
z0_psND1?Td24Q|k8VbUsNJIcAByj-%z|%(zG93!8A2*YKLFCsk#B09o;s%<_xpN;$
z9?2e@KA`1&aki_C2Te{sXIpGuL%&N@xgU0%N68QNy@nCEul
zakUr!3P%Kk8b^a9u_n@w`KM#!duj*1p9JHdN&Te%d%D3S)3r0^UtP`HuXyU&YjWy=OVq^MVn)V~G%P6VzB%P%Fu;bCPdSSMP$cl#aUE$L<)j`DD%8izs+8^^AM
z_r(L0Ala^2oEHHNfQ(WF-QsC{4Fk@MEnSsOsfG2Gm)}Js-dT-kxcmp&+RXY5*_A8_
z@7|Gl!4P;pfM%$D2D6XrF)HZxl
zI6zLz%qk#3m!jNHE5zjKqKdSAKj-UT(zrF9xcm|TK^YfM`OA;DvAeLpURt+NKdVrecsb}bFjZwubPyVWh6I{mgYJU
z2xB^Zu7=B2j}gvNC;29FSof#;nGG8(cH%K^tYOMiVk#(<1vtQr;X?F=6dCx*)Ky2K
zIkj@DqYy=JkD}0-PCqgLPViDLgmISAmLi6bv>pqwuaZZm)1Pj%QaE})eXtS`8AB%M
zR@!gCd>R`XPFN-&uXGFEp9ys|^h~)w(HfR8%1AM0aC)!bvJgB`5NM%5zVTdoh1n~$
zxR!pkZBg3LX&StkwZGQ%zGYuOSs;$i9%+(7NyQ>_IM~bqVP|>6FbJo_B`FZI&umjQ
zMrnE&L3H@Bc;5n+39p(URj)f0U^L60g)}?Gzo*+$gDZe#cgar)0U>RF6R~@2WsTD{
zMspmYcCXHd!>q7B!__gEOW+Csu2MbrUjPjD9kg1S)aUB(rp}J~khdaSr>8fb5!t_w
zesS{8=@z)8-s$;NdNITqE*`a&_W|#1Jsa_UEx&f;nhC!apz*cKJC+GV{_oZioDMukC
zLlK7x!D;NsY?_YcD3#vF9Zz+^?$6NdbSAff3ZgNL3&2HyA+be3@W|c+2)+THm;)$q
z0n~49KlxdUouar+F!_c;)BtlguMQp=KNUgk|OoAM)k(Rh^d1ThiFU}@p$
zc7YL1iPSe191h0u`(#NOJnS(<9}f~+S>^T!2o@?1E5)<8H}91KriFW+pfo&pOj)>{
z8Xn{R?75inOdjD2^9sib$N2+)x!XmxgY|Uf1A`qEneNctE|u0osp7{n*+F*|J)$_r
zrW@skGL4WnsFaHAiGp4AaJ(EAK%ubhgM&i+*uNe`v~?;R8dD`Ol{L-^Rz~9MtG}
zbUiY4XfyN2vz(%xrVy!>{IF&)?EP1Mz5;lWesa5rXpXy$>F>+ghpd0if_2IqBObn3#7;L|vrF5~&Xb<7>xLyR0A4B+Q?_
z!XM#sfU{y6qS}F1{OGVEu|8nR$QhlJAz^0c#Ab7LEBbP!Y${#x_{sFMBr1RFNYA%aAS?CluRgl4ANtlmY6Uwo^v3hWWBnIbqaB37_H0pH#+tni-C)Bo00fz9R`Te~
zK&@?`2hcSd^fVgLZIR?3@l
zT%P(p@5A9}V1Cij{^_^D5@R$wP{SVGwxXmU9IQxYlbE_s+#Kenp%GUmxFJLRK$j^(
zaf_z(2@6<^5T2A0{WC6Y=K2lr!NEG1xa-322v1|-p6jr!#5v`E5rEVh?s2T&p>@jF
z-eZ<;W%mqhUZz6~MKunO(8uEivh}O#?>harwOXvVI|ceZ|q&_2e&jzx}{u1$9^
zG`&6gxnVidvZ)NcxERzcr$-rjglra7Glsa#>mfRRb(Tk0ErqgQYSI3%q6~xmm=9o|
zuC=NkQvHc6fD)o-n>N^1J1bBAT05v%9=XQqg*Cl=Q{3=yr2MRHMhKZ;65Qykvfha2
z9W-f|gW>ZM`&lNZ(kp15_OBgAk=0eH6#v3Jwz(b9Ua;cHPmlA-lPO`b$gdy&{{D8)
zb6k5>*R(fmz_S;F=k!vwj|_$H%>gV<$b^k1o`
zUMEIR(__B=M318Y9OKuyhtp=nekV*VN51#DXuFgzF{MIKid*0{3jOYFW((g*;Fiv6
zOWL(Fsg;Q*xW;ICP?O8A#Dp+U>tO=QVP+?$&0ikhW)BhhXtxjlI69mn
zV8rp=V@OJRKYHIV2%;BCxseD+1ia$$}dbyJ9_PVW_|
zl}paMW3AEA(eqaJZBUr=V(ZhPM8klZLJYiu{!i}ro`)&Ur!{guiaXoLxH
z_OGda7!^;Hh)Ow@eYI)UPoL&_E0BTnh1KdNw0Zg|6Pp%$3iqu!W+8=_mqn>zc6z{c
zt-usGwK0(bQ7o(vssUiR4{N-WYb7AqN1ma?vpBbu3Uoc6)bwx*NS#)3b2Kd~DMC`~
z6aXaN;xw%MfvhWRjSs;6De-nEj%jWNIo&dXH`ak)i^9NAE!DV>Cyecp%LY=}jUvhx-eYS}9_^S9ZjKC#ijM)S#}&y=_m41k(6I@W
ztrJc62xFn6i5@2UMTXK-b;FZT9y~8+m?W8gZ!0lQ`
z#R2BnU{e1Cy#y`l-*Yb_G#|20p6%sC?(FQ@CFz9cMN>w(9vw4$Olle0Zc0aJl`Y#Z
zb!0roUl==u1x#Lr+1!?ijW4*@vp5_vA{}S5HQ386E5Q~_MXK841EQbztzN(64g&k3pAfIGS^Pbq
z+RT@rEKK15gM)7}l=JbAEm|9pKu7oFmNi@o?M6yHFmsY=lO(qB+yWfw4Z6JnYC^*td2=L36c
zZ^O$Px1w;Mvvqa-J?Y0$+Cs+#Bdl4T5g^YourB{(nMy_dlChmQ>-f;Y0r}e6X`R;{
zCy#Xt*5u%#+mkn+CPti{NHazc{rrMIaRJFfnnuAYtFF!&=yHgXDD%4hm4=h5=ieOu
ziD=#=--USH@~S5Ik8-+AXovlD`Ll{)aob6D6pxVzws(*Bfae>nMU60+U1?47GaVfW
z;QQcfa{zntAz)D6rjxw-cJEJ^K&$cQ*6Ha#fRIoZZq>8RL!Bq@U90Tf6J1uAw-mmj
zYJn*t;wk2e@n=q7Q8J6A+pzvMjXXnFS)rS6L;8|gV<^sHJ3^$cteiD{a$~uaA~n#;
zs#~kSfiS1>zLtu1;S{FccdPynP+VHivd@GC-h?B|eMrVD!gSi;(nuDVvjOMaeU1t}
z&%|+?yLw}XCGb^kh2qJGlF8RBMH`px=~WZ?7QsL%beEm*ZVGd@zdoKRfp3;cQA@sm
zbOT|Q9;eEOz|BalWW**@fN=)zo8nQltzK_hmZIW5NvW|*VPZaI@-(r9Mo6u-8Roh#
zCtrTow=&&T<Ny8xn(hZ)yxA-#OCxjE
zU6)6}XqrlOnJ?j%f4qDWxU)drqW)%zZhuqQjryVp-3%=Nt|+r6Q#C`U00u?n(&HAm
zZdHUNhC(ntGbJKBm@?&w*VrqwD$o7_Qc}6V#HR72R@+0sT*o2TlGe~Tu_p+vr-oB@>6dPK!cVIVpRgQBjx>X-
zPgoJkR@L)D?-+~ygwg^hA4SN$Rln8rM+Za=e36q^+?3?Bmbt?|A#$Rr5%e)GEvMu)
zv+?wPSYSOgLJg@`Fj#6(mICCC&?)*Bx{#A1?2#6!`SGYJSRWN21kWd$-KZR@+3Yz+
zg&Pw>rVBB35izA;DFCk3&V-F+hZQ@QeSMwBg4LH4y8m!{;j|#vx$F<2P2q`IDV1Px
zb%s-MKoZkCiL1XYALBBQm)_LvOg1Sci1G5l4~-V%prL#EzFXf0Wkx29J$w)gZa0|N
zgD_zYr~ddis|;Twm-It}r)Q{=)9OmYNZ0uodOi(1ZaM$0s^ssgPp~CmOaX~RyNWnX
zi%00%_r}f9A?w!F&Km0K2{RztkIbGYjdd^`p-JVBrAKDZr8BD~WdQ(a%>PgR%hdw6
zA9?I1L&(pYU-$B9MRg*~_EK)m68z~+9i1PpW$-^n=%i6Z)ZFg96Bu$5D!FxUrHTUY
zASUn=FvmKI2VRc?nMFs%k^5bClB}9$^6;D7JG_MHhFaxnc~7ehSXh=B+{ni)y2rEG
zV7RS*KiM?rae#0i2S|IgwCA&Dmm#tyZ+R(2T3*LVuFu-tEs|Q>{Rul)K+O@=&5^(o
z%AiUbUP=?K`(QDs1y2hGx@eE%Qxr?mk8e*l+DA)xO&hUA_X
z!BIbd7c}
z4=z$|Hp_!(TL>2zS|!qQ%N|h>V{eV25#POO5i~5Cxvp+~ezMrF5#DId{k;~Gh*{NNgBAO_*9_CxW
zQ9h)z#1dySjk8M*>T4B$>r%O8x$Vj<@*G`0dvB4zX$8b!agte>(r|~J)aafWIRag`
zYWAO7{3tM**3C`IDjMx#lNBh1mAbbz@Y#@VLh=>CVN?0e1Tk?vSK%?yCDS(#Or>Im
zNCWzH;ajy`V-MY0`TrjY!I+R#U{r#QzQsVh&j%^NaO$*u30f%p*@?
z=qp@iGk?Lv{j%(CA;#9m*{{<;X{gC`h^)%>b@i)g<9h;~Hl7s=w^p4Z6t%69Wq9FSRRq2gpBejiV75L57Nef;{n!%?Ijlgd*r-pyPjx!ebF^(rh8)ah?y9jKs
zCC@}H>yIddisv9NP>eIUT|#TyaK%<|jzB;n<=`QCGr6=w>I*;f?Ehr`DSS|)l#DP9
znI=;cR6Lv9xm9!@SXlAD^XdC$=C5<2z3CK(YVh`4<}9`|e#Kv)2r6A7h($*8dRuFZ
z@V1a^TgaIT{=UX#@Eh|XZVP+z0IZzegCjBqyAQLAZt`5f?LK$x7Jeu}m?M0`?qRN)oD$&K2i!8>OMUziXEtkAAWt*V@hSa91
zeRBnHXmn_3_yU2`3z?x1;(ds_yj*k7gT@zJt2$UuQVIvnnl}pXS{0ctg}KPa3_tW^^DLQ|_i9!H@pw`ipD8u{K-*QGQ%U0N
zP;BrR=LFx`Mk5*mdB1y9ycR%=B&d}LzckKJI4Rm;=d)sP-kCPc%t!8!+VqR>#dt|W
zjrU@1p~Pb)JZ|ax1J>xo7sBP-!&=xecyM?M$-#gt0E09TcqO=6MJ5oUyj-sL-DMQ9
zK|Qvq`;_%d^W1wF&)-q;CL{G?{b6&U$9%WUcKu`;{g$U*0=Wfv>{w;g7
zke6zu4ChRpAwTaw$>jw*Uf%~y>^fMA;Qn{o=zq%yL5Uc8%pA=Bk@Qe(ZHO=N`U$2N
z=acXABJyPN#}iw31$27&)`nYvzq?oE_=~2FGb=!g(ApuN<|1d+?^>>HVT0tn_Z3>r
z&D-HiE!O-jASyg%k5tkTGkC+D@X`~m`h`vo6L*q)bScf~C53mkj$n_?58IpZ<&aUY
z+IV{FBVe$X{^KX|S}$6tY7f7-<`@5PMQoaG0R3Q^5(v8nUWy9lTZG9d3XBG=ZNNTS
zE|xjZJKop)XmwH2P0>x??;Q@_tthQo$_n4F1U9srR7&B!rWsn~Ca~nye5FK3VIQYl
zq{~iUjOGY!=RTogSn@QcVK8g@GLjv1jKq#n)!i7)x6DD#t@ftZAxp1!!Vl~Kh3|~A
zmifT-ZmC8(>@lVGZA-?p;OObsyTXCRhY#{cd+4|Cb3eo$Y%maTwNBz@q_3+pjzxa|
z+FpBfTE2Qkx>wnrE3ZBRO~1|SH)@koq~w~;>o|ZqY$XN*M`ZiEK3&W*Ct&V2@%t@=
zKpU&+17o~Q#7g#~UN=|qnDWff6*Jm$WK0hw3y5$?erFiqV8?{x)BQh
zdAk@U-hfiql?qP5l=1kwaa=ULFACf9&0+P~w^q|L8I>2Sxr^v(!auMJlRl<#|+Gc)22F{NdCl&d;cer`h~Asa2;Z=dhpsC0D)l!^03XTi9rV#Q>B
z(O6bYZ4rXD_ON2OD~|FQr8b3y3YxW-kBJjF2415g{1zxOjk8>13MX2Q&QY*2=s!R?
z)QIrV7n%6sgSKc~dQwg@hlraCnA^=0)fBM2nNT2j4Hd#6Y?$y(Fol;jsCJ`@wpOYC
z0sgugUeZ6ucm~OVJ?}iwx_tOE%~lIY$WflYym8o+q^F?pcZ+Xn{hIj2Ir!RVhaoZl
zauGbIZ$WcPlY^=Zg@J7ZGO6
z!cH-SyX9&|9Bg;~zP3F`|8M6W9+F3MZ-G2w@AAE^VD3+C{{bo^DCMUqpr@=9=M^p?
zAJ(b2K2f3VO4SN&e2p4O|I&QSyZY#Wkmwuz_`)@#H9*lYBa?ln_ViEu=~BF=P6*INBqeIR5lt#q4|0Ws2(nP<@PT
z`^8aI?VUt~N#U60hSAv)wtAg$TGzR@7H(3Tdn>HDZUhsbWn
z?GmByJp704hjkUzJdNDJZIw)cr6175=!wqCR;U|tAD#(u60Ks-5Hr%D=lOu7Xh!5R*|mPVx#{|fTs+U)V6r|ZQxZ0CIo^V2HSO4Bsk
zJn=q0C8^3)n+VTTqK6etptTBsN^~$JwkTRFAG=`+0suoWA-In*(f?ln#Q{40X~xSS
z1%R*sX_l7h@QAocB}ylGOC*wWB%Mh-$vKj9yi>1y{XWs(ufJUWIyA~KPFLv^Bo6u`
z0F$V+TT=ev@9Uy77E1g2pMOv1pbO+6k9V0~`XGvA`=7+3V)=v84+}x}4u>H6{XWed4^Pjm8gtex$qmYFY|27qw6H?jl!QrXK=(?T
zkd-8)*Sz}C&mN;GcNt4$HjEuel$k13kn
zW^(?OSeux*-TTRx*OyN7hW`K@qbDw?Cy^>pM{Yo+CBFIU*f=BZ*VA*>Ib;HEMbA^2
z`JCd`eFs201oR)nMfn{dHW{8@A3UO(#B07eMF|7Gh}hqhS}qVh0MQt=?S5YU2iYz><6GaE(FAG%Ewd#{!@`>u``X5rrrbZe~CK*~BL4FaQ7m
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>
+
+
+
+
+
+
+
+