mirror of
https://github.com/caperren/school_archives.git
synced 2025-11-09 21:51:15 +00:00
Added 2017-2018 mars rover repository.
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
cmake_minimum_required(VERSION 2.8.3)
|
||||
project(ground_station)
|
||||
|
||||
## 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
|
||||
)
|
||||
|
||||
## 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
|
||||
# Message1.msg
|
||||
# Message2.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 # Or other packages containing 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(
|
||||
# INCLUDE_DIRS include
|
||||
# LIBRARIES ground_station
|
||||
# CATKIN_DEPENDS rospy
|
||||
# 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}/ground_station.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/ground_station_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_ground_station.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)
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0"?>
|
||||
<package format="2">
|
||||
<name>ground_station</name>
|
||||
<version>0.0.0</version>
|
||||
<description>The ground_station package</description>
|
||||
|
||||
<!-- One maintainer tag required, multiple allowed, one person per tag -->
|
||||
<!-- Example: -->
|
||||
<!-- <maintainer email="jane.doe@example.com">Jane Doe</maintainer> -->
|
||||
<maintainer email="caperren@todo.todo">caperren</maintainer>
|
||||
|
||||
|
||||
<!-- One license tag required, multiple allowed, one license per tag -->
|
||||
<!-- Commonly used license strings: -->
|
||||
<!-- BSD, MIT, Boost Software License, GPLv2, GPLv3, LGPLv2.1, LGPLv3 -->
|
||||
<license>TODO</license>
|
||||
|
||||
|
||||
<!-- Url tags are optional, but multiple are allowed, one per tag -->
|
||||
<!-- Optional attribute type can be: website, bugtracker, or repository -->
|
||||
<!-- Example: -->
|
||||
<!-- <url type="website">http://wiki.ros.org/ground_station</url> -->
|
||||
|
||||
|
||||
<!-- Author tags are optional, multiple are allowed, one per tag -->
|
||||
<!-- Authors do not have to be maintainers, but could be -->
|
||||
<!-- Example: -->
|
||||
<!-- <author email="jane.doe@example.com">Jane Doe</author> -->
|
||||
|
||||
|
||||
<!-- The *depend tags are used to specify dependencies -->
|
||||
<!-- Dependencies can be catkin packages or system dependencies -->
|
||||
<!-- Examples: -->
|
||||
<!-- Use depend as a shortcut for packages that are both build and exec dependencies -->
|
||||
<!-- <depend>roscpp</depend> -->
|
||||
<!-- Note that this is equivalent to the following: -->
|
||||
<!-- <build_depend>roscpp</build_depend> -->
|
||||
<!-- <exec_depend>roscpp</exec_depend> -->
|
||||
<!-- Use build_depend for packages you need at compile time: -->
|
||||
<!-- <build_depend>message_generation</build_depend> -->
|
||||
<!-- Use build_export_depend for packages you need in order to build against this package: -->
|
||||
<!-- <build_export_depend>message_generation</build_export_depend> -->
|
||||
<!-- Use buildtool_depend for build tool packages: -->
|
||||
<!-- <buildtool_depend>catkin</buildtool_depend> -->
|
||||
<!-- Use exec_depend for packages you need at runtime: -->
|
||||
<!-- <exec_depend>message_runtime</exec_depend> -->
|
||||
<!-- Use test_depend for packages you need only for testing: -->
|
||||
<!-- <test_depend>gtest</test_depend> -->
|
||||
<!-- Use doc_depend for packages you need only for building documentation: -->
|
||||
<!-- <doc_depend>doxygen</doc_depend> -->
|
||||
<buildtool_depend>catkin</buildtool_depend>
|
||||
<build_depend>rospy</build_depend>
|
||||
<build_export_depend>rospy</build_export_depend>
|
||||
<exec_depend>rospy</exec_depend>
|
||||
|
||||
|
||||
<!-- The export tag contains other, unspecified, tags -->
|
||||
<export>
|
||||
<!-- Other tools can request additional information be placed here -->
|
||||
|
||||
</export>
|
||||
</package>
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
current_folder_name="scripts"
|
||||
current_folder_name_length=`expr length $current_folder_name`
|
||||
|
||||
launch_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
launch_dir_length=`expr length $launch_dir`
|
||||
|
||||
launch_dir_length_without_current_folder=$(($launch_dir_length-$current_folder_name_length))
|
||||
|
||||
script_launch_path="${launch_dir:0:$launch_dir_length_without_current_folder}/src"
|
||||
cd $script_launch_path
|
||||
|
||||
cp ~/key .
|
||||
|
||||
sleep 1
|
||||
|
||||
export DISPLAY=:0
|
||||
python ground_station.py
|
||||
@@ -0,0 +1,295 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
import rospy
|
||||
|
||||
from rover_arm.msg import ArmStatusMessage
|
||||
from rover_control.msg import GripperStatusMessage
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
ARM_STATUS_TOPIC = "/rover_arm/status"
|
||||
GRIPPER_STATUS_TOPIC = "/rover_control/gripper/status"
|
||||
|
||||
|
||||
COMMS_TO_STRING = {
|
||||
0: "NO STATUS",
|
||||
1: "COMMS OK",
|
||||
2: "NO DEVICE",
|
||||
4: "BUS ERROR",
|
||||
8: "GEN COMM ERROR",
|
||||
16: "PARAMETER ERROR",
|
||||
32: "LENGTH ERROR"
|
||||
}
|
||||
|
||||
TARGET_REACHED_BIT_POSITION = 1
|
||||
|
||||
STATUS_TO_STRING = {
|
||||
1: "TARGET REACHED",
|
||||
2: "ERROR RECOVERY",
|
||||
3: "RUN",
|
||||
4: "ENABLED",
|
||||
5: "FAULT STOP",
|
||||
6: "WARNING",
|
||||
7: "STO ACTIVE",
|
||||
8: "SERVO READY",
|
||||
10: "BRAKING",
|
||||
11: "HOMING",
|
||||
12: "INITIALIZED",
|
||||
13: "VOLT OK",
|
||||
15: "PERMANENT STOP"
|
||||
}
|
||||
|
||||
FAULT_TO_STRING = {
|
||||
1: "TRACKING ERROR",
|
||||
2: "OVER CURRENT",
|
||||
# 3: "COMMUNICATION ERROR", # Was showing even though things were working???
|
||||
4: "ENCODER FAILURE",
|
||||
5: "OVER TEMP",
|
||||
6: "UNDER VOLTAGE",
|
||||
7: "OVER VOLTAGE",
|
||||
8: "PROG OR MEM ERROR",
|
||||
9: "HARDWARE ERROR",
|
||||
10: "OVER VELOCITY",
|
||||
11: "INIT ERROR",
|
||||
12: "MOTION ERROR",
|
||||
13: "RANGE ERROR",
|
||||
14: "POWER STAGE FORCED OFF",
|
||||
15: "HOST COMM ERROR"
|
||||
}
|
||||
|
||||
GRIPPER_MODES = {
|
||||
0: "No Change",
|
||||
1: "Normal",
|
||||
2: "Pinch",
|
||||
3: "Wide ",
|
||||
4: "Scissor"
|
||||
}
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class ArmIndication(QtCore.QObject):
|
||||
base_position_updated__signal = QtCore.pyqtSignal(float)
|
||||
shoulder_position_updated__signal = QtCore.pyqtSignal(float)
|
||||
elbow_position_updated__signal = QtCore.pyqtSignal(float)
|
||||
roll_position_updated__signal = QtCore.pyqtSignal(float)
|
||||
wrist_pitch_position_updated__signal = QtCore.pyqtSignal(float)
|
||||
wrist_roll_position_updated__signal = QtCore.pyqtSignal(float)
|
||||
|
||||
base_comms_state_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
shoulder_comms_state_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
elbow_comms_state_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
roll_comms_state_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
wrist_pitch_comms_state_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
wrist_roll_comms_state_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
base_status_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
shoulder_status_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
elbow_status_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
roll_status_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
wrist_pitch_status_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
wrist_roll_status_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
base_faults_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
shoulder_faults_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
elbow_faults_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
roll_faults_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
wrist_pitch_faults_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
wrist_roll_faults_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
pinch_position_updated__signal = QtCore.pyqtSignal(int)
|
||||
forefinger_position_updated__signal = QtCore.pyqtSignal(int)
|
||||
thumb_position_updated__signal = QtCore.pyqtSignal(int)
|
||||
middlefinger_position_updated__signal = QtCore.pyqtSignal(int)
|
||||
|
||||
pinch_current_updated__signal = QtCore.pyqtSignal(int)
|
||||
forefinger_current_updated__signal = QtCore.pyqtSignal(int)
|
||||
thumb_current_updated__signal = QtCore.pyqtSignal(int)
|
||||
middlefinger_current_updated__signal = QtCore.pyqtSignal(int)
|
||||
|
||||
gripper_reported_mode_updated__signal = QtCore.pyqtSignal(str)
|
||||
gripper_reported_setpoint_updated__signal = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(ArmIndication, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.right_screen = self.shared_objects["screens"]["right_screen"]
|
||||
|
||||
self.base_position_lcd_number = self.right_screen.base_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.shoulder_position_lcd_number = self.right_screen.shoulder_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.elbow_position_lcd_number = self.right_screen.elbow_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.roll_position_lcd_number = self.right_screen.roll_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.wrist_pitch_position_lcd_number = self.right_screen.wrist_pitch_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.wrist_roll_position_lcd_number = self.right_screen.wrist_roll_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
|
||||
self.pinch_position_lcd_number = self.right_screen.pinch_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.forefinger_position_lcd_number = self.right_screen.forefinger_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.thumb_position_lcd_number = self.right_screen.thumb_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.middlefinger_position_lcd_number = self.right_screen.middlefinger_position_lcd_number # type: QtWidgets.QLCDNumber
|
||||
|
||||
self.pinch_current_lcd_number = self.right_screen.pinch_current_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.forefinger_current_lcd_number = self.right_screen.forefinger_current_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.thumb_current_lcd_number = self.right_screen.thumb_current_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.middlefinger_current_lcd_number = self.right_screen.middlefinger_current_lcd_number # type: QtWidgets.QLCDNumber
|
||||
|
||||
self.arm_controls_base_comms_label = self.right_screen.arm_controls_base_comms_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_base_status_label = self.right_screen.arm_controls_base_status_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_base_faults_label = self.right_screen.arm_controls_base_faults_label # type:QtWidgets.QLabel
|
||||
|
||||
self.arm_controls_shoulder_comms_label = self.right_screen.arm_controls_shoulder_comms_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_shoulder_status_label = self.right_screen.arm_controls_shoulder_status_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_shoulder_faults_label = self.right_screen.arm_controls_shoulder_faults_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_elbow_comms_label = self.right_screen.arm_controls_elbow_comms_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_elbow_status_label = self.right_screen.arm_controls_elbow_status_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_elbow_faults_label = self.right_screen.arm_controls_elbow_faults_label # type:QtWidgets.QLabel
|
||||
|
||||
self.arm_controls_roll_comms_label = self.right_screen.arm_controls_roll_comms_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_roll_status_label = self.right_screen.arm_controls_roll_status_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_roll_faults_label = self.right_screen.arm_controls_roll_faults_label # type:QtWidgets.QLabel
|
||||
|
||||
self.arm_controls_wrist_pitch_comms_label = self.right_screen.arm_controls_wrist_pitch_comms_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_wrist_pitch_status_label = self.right_screen.arm_controls_wrist_pitch_status_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_wrist_pitch_faults_label = self.right_screen.arm_controls_wrist_pitch_faults_label # type:QtWidgets.QLabel
|
||||
|
||||
self.arm_controls_wrist_roll_comms_label = self.right_screen.arm_controls_wrist_roll_comms_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_wrist_roll_status_label = self.right_screen.arm_controls_wrist_roll_status_label # type:QtWidgets.QLabel
|
||||
self.arm_controls_wrist_roll_faults_label = self.right_screen.arm_controls_wrist_roll_faults_label # type:QtWidgets.QLabel
|
||||
|
||||
self.gripper_reported_mode_label = self.right_screen.gripper_reported_mode_label # type:QtWidgets.QLabel
|
||||
self.gripper_reported_setpoint_lcd_number = self.right_screen.gripper_reported_setpoint_lcd_number # type: QtWidgets.QLCDNumber
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.arm_status_subscriber = rospy.Subscriber(ARM_STATUS_TOPIC, ArmStatusMessage, self.on_arm_status_update_received__callback)
|
||||
self.gripper_status_subscriver = rospy.Subscriber(GRIPPER_STATUS_TOPIC, GripperStatusMessage, self.on_gripper_status_update_received__callback)
|
||||
|
||||
# ########## Connect Signals and Slots ##########
|
||||
self.connect_signals_and_slots()
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.base_position_updated__signal.connect(self.base_position_lcd_number.display)
|
||||
self.shoulder_position_updated__signal.connect(self.shoulder_position_lcd_number.display)
|
||||
self.elbow_position_updated__signal.connect(self.elbow_position_lcd_number.display)
|
||||
self.roll_position_updated__signal.connect(self.roll_position_lcd_number.display)
|
||||
self.wrist_pitch_position_updated__signal.connect(self.wrist_pitch_position_lcd_number.display)
|
||||
self.wrist_roll_position_updated__signal.connect(self.wrist_roll_position_lcd_number.display)
|
||||
|
||||
self.base_comms_state_update_ready__signal.connect(self.arm_controls_base_comms_label.setText)
|
||||
self.shoulder_comms_state_update_ready__signal.connect(self.arm_controls_shoulder_comms_label.setText)
|
||||
self.elbow_comms_state_update_ready__signal.connect(self.arm_controls_elbow_comms_label.setText)
|
||||
self.roll_comms_state_update_ready__signal.connect(self.arm_controls_roll_comms_label.setText)
|
||||
self.wrist_pitch_comms_state_update_ready__signal.connect(self.arm_controls_wrist_pitch_comms_label.setText)
|
||||
self.wrist_roll_comms_state_update_ready__signal.connect(self.arm_controls_wrist_roll_comms_label.setText)
|
||||
|
||||
self.base_status_update_ready__signal.connect(self.arm_controls_base_status_label.setText)
|
||||
self.shoulder_status_update_ready__signal.connect(self.arm_controls_shoulder_status_label.setText)
|
||||
self.elbow_status_update_ready__signal.connect(self.arm_controls_elbow_status_label.setText)
|
||||
self.roll_status_update_ready__signal.connect(self.arm_controls_roll_status_label.setText)
|
||||
self.wrist_pitch_status_update_ready__signal.connect(self.arm_controls_wrist_pitch_status_label.setText)
|
||||
self.wrist_roll_status_update_ready__signal.connect(self.arm_controls_wrist_roll_status_label.setText)
|
||||
|
||||
self.base_faults_update_ready__signal.connect(self.arm_controls_base_faults_label.setText)
|
||||
self.shoulder_faults_update_ready__signal.connect(self.arm_controls_shoulder_faults_label.setText)
|
||||
self.elbow_faults_update_ready__signal.connect(self.arm_controls_elbow_faults_label.setText)
|
||||
self.roll_faults_update_ready__signal.connect(self.arm_controls_roll_faults_label.setText)
|
||||
self.wrist_pitch_faults_update_ready__signal.connect(self.arm_controls_wrist_pitch_faults_label.setText)
|
||||
self.wrist_roll_faults_update_ready__signal.connect(self.arm_controls_wrist_roll_faults_label.setText)
|
||||
|
||||
self.pinch_position_updated__signal.connect(self.pinch_position_lcd_number.display)
|
||||
self.forefinger_position_updated__signal.connect(self.forefinger_position_lcd_number.display)
|
||||
self.thumb_position_updated__signal.connect(self.thumb_position_lcd_number.display)
|
||||
self.middlefinger_position_updated__signal.connect(self.middlefinger_position_lcd_number.display)
|
||||
|
||||
self.pinch_current_updated__signal.connect(self.pinch_current_lcd_number.display)
|
||||
self.forefinger_current_updated__signal.connect(self.forefinger_current_lcd_number.display)
|
||||
self.thumb_current_updated__signal.connect(self.thumb_current_lcd_number.display)
|
||||
self.middlefinger_current_updated__signal.connect(self.middlefinger_current_lcd_number.display)
|
||||
|
||||
self.gripper_reported_mode_updated__signal.connect(self.gripper_reported_mode_label.setText)
|
||||
self.gripper_reported_setpoint_updated__signal.connect(self.gripper_reported_setpoint_lcd_number.display)
|
||||
|
||||
def on_arm_status_update_received__callback(self, data):
|
||||
self.base_comms_state_update_ready__signal.emit(self.process_comms_to_string(data.base_comm_status))
|
||||
self.shoulder_comms_state_update_ready__signal.emit(self.process_comms_to_string(data.shoulder_comm_status))
|
||||
self.elbow_comms_state_update_ready__signal.emit(self.process_comms_to_string(data.elbow_comm_status))
|
||||
self.roll_comms_state_update_ready__signal.emit(self.process_comms_to_string(data.roll_comm_status))
|
||||
self.wrist_pitch_comms_state_update_ready__signal.emit(
|
||||
self.process_comms_to_string(data.wrist_pitch_comm_status))
|
||||
self.wrist_roll_comms_state_update_ready__signal.emit(self.process_comms_to_string(data.wrist_roll_comm_status))
|
||||
|
||||
self.base_status_update_ready__signal.emit(self.process_statuses_to_string(data.base_status))
|
||||
self.shoulder_status_update_ready__signal.emit(self.process_statuses_to_string(data.shoulder_status))
|
||||
self.elbow_status_update_ready__signal.emit(self.process_statuses_to_string(data.elbow_status))
|
||||
self.roll_status_update_ready__signal.emit(self.process_statuses_to_string(data.roll_status))
|
||||
self.wrist_pitch_status_update_ready__signal.emit(self.process_statuses_to_string(data.wrist_pitch_status))
|
||||
self.wrist_roll_status_update_ready__signal.emit(self.process_statuses_to_string(data.wrist_roll_status))
|
||||
|
||||
self.base_faults_update_ready__signal.emit(self.process_faults_to_string(data.base_faults))
|
||||
self.shoulder_faults_update_ready__signal.emit(self.process_faults_to_string(data.shoulder_faults))
|
||||
self.elbow_faults_update_ready__signal.emit(self.process_faults_to_string(data.elbow_faults))
|
||||
self.roll_faults_update_ready__signal.emit(self.process_faults_to_string(data.roll_faults))
|
||||
self.wrist_pitch_faults_update_ready__signal.emit(self.process_faults_to_string(data.wrist_pitch_faults))
|
||||
self.wrist_roll_faults_update_ready__signal.emit(self.process_faults_to_string(data.wrist_roll_faults))
|
||||
|
||||
self.base_position_updated__signal.emit(data.base)
|
||||
self.shoulder_position_updated__signal.emit(data.shoulder)
|
||||
self.elbow_position_updated__signal.emit(data.elbow)
|
||||
self.roll_position_updated__signal.emit(data.roll)
|
||||
self.wrist_pitch_position_updated__signal.emit(data.wrist_pitch)
|
||||
self.wrist_roll_position_updated__signal.emit(data.wrist_roll)
|
||||
|
||||
def on_gripper_status_update_received__callback(self, data):
|
||||
data = data # type: GripperStatusMessage
|
||||
|
||||
self.pinch_position_updated__signal.emit(data.pinch_position_raw)
|
||||
self.forefinger_position_updated__signal.emit(data.forefinger_position_raw)
|
||||
self.thumb_position_updated__signal.emit(data.thumb_position_raw)
|
||||
self.middlefinger_position_updated__signal.emit(data.middlefinger_position_raw)
|
||||
|
||||
self.pinch_current_updated__signal.emit(data.pinch_current)
|
||||
self.forefinger_current_updated__signal.emit(data.forefinger_current)
|
||||
self.thumb_current_updated__signal.emit(data.thumb_current)
|
||||
self.middlefinger_current_updated__signal.emit(data.middlefinger_current)
|
||||
|
||||
self.gripper_reported_mode_updated__signal.emit(GRIPPER_MODES[data.current_mode])
|
||||
self.gripper_reported_setpoint_updated__signal.emit(data.current_finger_position)
|
||||
|
||||
@staticmethod
|
||||
def process_faults_to_string(faults):
|
||||
fault_output = ""
|
||||
|
||||
for bit_position in FAULT_TO_STRING:
|
||||
if (1 << bit_position) & faults:
|
||||
fault_output += FAULT_TO_STRING[bit_position] + "\n"
|
||||
|
||||
return fault_output[:-1]
|
||||
|
||||
@staticmethod
|
||||
def process_statuses_to_string(statuses):
|
||||
status_output = ""
|
||||
|
||||
for bit_position in STATUS_TO_STRING:
|
||||
if (1 << bit_position) & statuses:
|
||||
status_output += STATUS_TO_STRING[bit_position] + "\n"
|
||||
|
||||
return status_output[:-1]
|
||||
|
||||
@staticmethod
|
||||
def process_comms_to_string(comms):
|
||||
return COMMS_TO_STRING[comms] if comms in COMMS_TO_STRING else "UNKNOWN"
|
||||
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
from inputs import devices, GamePad
|
||||
from time import time
|
||||
|
||||
import rospy
|
||||
from rover_control.msg import DriveCommandMessage, TowerPanTiltControlMessage
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
GAME_CONTROLLER_NAME = "Microsoft X-Box One S pad"
|
||||
|
||||
DEFAULT_DRIVE_COMMAND_TOPIC = "/rover_control/command_control/ground_station_drive"
|
||||
DEFAULT_TOWER_PAN_TILT_COMMAND_TOPIC = "/rover_control/tower/pan_tilt/control"
|
||||
DEFAULT_CHASSIS_PAN_TILT_COMMAND_TOPIC = "/rover_control/chassis/pan_tilt/control"
|
||||
|
||||
DRIVE_COMMAND_HERTZ = 20
|
||||
|
||||
STICK_DEADBAND = 3500
|
||||
|
||||
STICK_MAX = 32768.0
|
||||
STICK_OFFSET = 0
|
||||
|
||||
THROTTLE_MIN = 0.05
|
||||
|
||||
PAUSE_STATE_CHANGE_TIME = 0.5
|
||||
|
||||
CAMERA_CHANGE_TIME = 0.2
|
||||
GUI_ELEMENT_CHANGE_TIME = 0.2
|
||||
CAMERA_TOGGLE_CHANGE_TIME = 0.35
|
||||
|
||||
TOWER_PAN_TILT_X_AXIS_SCALAR = 2
|
||||
TOWER_PAN_TILT_Y_AXIS_SCALAR = 15
|
||||
|
||||
CHASSIS_PAN_TILT_X_AXIS_SCALAR = 15
|
||||
CHASSIS_PAN_TILT_Y_AXIS_SCALAR = 15
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class LogitechJoystick(QtCore.QThread):
|
||||
def __init__(self):
|
||||
super(LogitechJoystick, self).__init__()
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
self.setup_controller_flag = True
|
||||
self.data_acquisition_and_broadcast_flag = True
|
||||
self.controller_acquired = False
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.gamepad = None # type: GamePad
|
||||
|
||||
self.controller_states = {
|
||||
"left_x_axis": 0,
|
||||
"left_y_axis": 0,
|
||||
"right_x_axis": 0,
|
||||
"right_y_axis": 0,
|
||||
|
||||
"left_trigger": 0,
|
||||
"right_trigger": 0,
|
||||
|
||||
"left_stick": 0,
|
||||
"right_right": 0,
|
||||
|
||||
"left_bumper": 0,
|
||||
"right_bumper": 0,
|
||||
|
||||
"d_pad_x": 0,
|
||||
"d_pad_y": 0,
|
||||
|
||||
"back": 0,
|
||||
"start": 0,
|
||||
|
||||
"a": 0,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"b": 0,
|
||||
}
|
||||
|
||||
self.raw_mapping_to_class_mapping = {
|
||||
"ABS_X": "left_x_axis",
|
||||
"ABS_Y": "left_y_axis",
|
||||
"ABS_RX": "right_x_axis",
|
||||
"ABS_RY": "right_y_axis",
|
||||
|
||||
"ABS_Z": "left_trigger",
|
||||
"ABS_RZ": "right_trigger",
|
||||
|
||||
"BTN_THUMBL": "left_stick",
|
||||
"BTN_THUMBR": "right_right",
|
||||
|
||||
"BTN_TL": "left_bumper",
|
||||
"BTN_TR": "right_bumper",
|
||||
|
||||
"ABS_HAT0X": "d_pad_x",
|
||||
"ABS_HAT0Y": "d_pad_y",
|
||||
|
||||
"BTN_SELECT": "back",
|
||||
"BTN_START": "start",
|
||||
|
||||
"BTN_SOUTH": "a",
|
||||
"BTN_NORTH": "x",
|
||||
"BTN_WEST": "y",
|
||||
"BTN_EAST": "b",
|
||||
}
|
||||
|
||||
self.ready = False
|
||||
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.setup_controller_flag:
|
||||
self.controller_acquired = self.__setup_controller()
|
||||
self.setup_controller_flag = False
|
||||
if self.data_acquisition_and_broadcast_flag:
|
||||
self.__get_controller_data()
|
||||
|
||||
def __setup_controller(self):
|
||||
for device in devices.gamepads:
|
||||
# print device
|
||||
if device.name == GAME_CONTROLLER_NAME:
|
||||
self.gamepad = device
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def __get_controller_data(self):
|
||||
if self.controller_acquired:
|
||||
events = self.gamepad.read()
|
||||
|
||||
for event in events:
|
||||
# print event.code, event.state
|
||||
if event.code in self.raw_mapping_to_class_mapping:
|
||||
self.controller_states[self.raw_mapping_to_class_mapping[event.code]] = event.state
|
||||
|
||||
self.ready = True
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class DriveAndCameraControlSender(QtCore.QThread):
|
||||
set_speed_limit__signal = QtCore.pyqtSignal(int)
|
||||
set_left_drive_output__signal = QtCore.pyqtSignal(int)
|
||||
set_right_drive_output__signal = QtCore.pyqtSignal(int)
|
||||
|
||||
change_gui_element_selection__signal = QtCore.pyqtSignal(int)
|
||||
change_camera_selection__signal = QtCore.pyqtSignal(int)
|
||||
toggle_selected_gui_camera__signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(DriveAndCameraControlSender, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.video_coordinator = self.shared_objects["threaded_classes"]["Video Coordinator"]
|
||||
self.right_screen = self.shared_objects["screens"]["right_screen"]
|
||||
self.rover_speed_limit_slider = self.right_screen.rover_speed_limit_slider # type: QtWidgets.QSlider
|
||||
self.left_drive_progress_bar = self.right_screen.left_drive_progress_bar # type: QtWidgets.QProgressBar
|
||||
self.right_drive_progress_bar = self.right_screen.right_drive_progress_bar # type: QtWidgets.QProgressBar
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
self.joystick = LogitechJoystick()
|
||||
|
||||
# ########## Class Variables ##########
|
||||
# Publishers
|
||||
self.drive_command_publisher = rospy.Publisher(DEFAULT_DRIVE_COMMAND_TOPIC, DriveCommandMessage, queue_size=1)
|
||||
self.tower_pan_tilt_command_publisher = rospy.Publisher(DEFAULT_TOWER_PAN_TILT_COMMAND_TOPIC,
|
||||
TowerPanTiltControlMessage, queue_size=1)
|
||||
self.chassis_pan_tilt_command_publisher = rospy.Publisher(DEFAULT_CHASSIS_PAN_TILT_COMMAND_TOPIC,
|
||||
TowerPanTiltControlMessage, queue_size=1)
|
||||
|
||||
self.current_pan_tilt_selection = "no_pan_tilt"
|
||||
|
||||
self.last_hat_x_was_movement = False
|
||||
self.last_hat_y_was_movement = False
|
||||
|
||||
self.wait_time = 1.0 / DRIVE_COMMAND_HERTZ
|
||||
|
||||
self.drive_paused = True
|
||||
|
||||
self.last_pause_state_time = time()
|
||||
self.last_gui_element_change_time = time()
|
||||
self.last_camera_change_time = time()
|
||||
self.last_camera_toggle_time = time()
|
||||
|
||||
self.speed_limit = 0.5
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting Joystick Thread")
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
self.check_and_set_pause_state()
|
||||
self.__update_and_publish()
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int(self.wait_time - time_diff), 0))
|
||||
|
||||
self.logger.debug("Stopping Joystick Thread")
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.set_left_drive_output__signal.connect(self.left_drive_progress_bar.setValue)
|
||||
self.set_right_drive_output__signal.connect(self.right_drive_progress_bar.setValue)
|
||||
|
||||
self.video_coordinator.pan_tilt_selection_changed__signal.connect(self.on_pan_tilt_selection_changed__slot)
|
||||
self.rover_speed_limit_slider.valueChanged.connect(self.on_speed_limit_slider_value_changed__slot)
|
||||
|
||||
def check_and_set_pause_state(self):
|
||||
thumb_pressed = self.joystick.controller_states["start"]
|
||||
if thumb_pressed and (time() - self.last_pause_state_time) > PAUSE_STATE_CHANGE_TIME:
|
||||
self.drive_paused = not self.drive_paused
|
||||
self.show_changed_pause_state()
|
||||
self.last_pause_state_time = time()
|
||||
|
||||
def __update_and_publish(self):
|
||||
self.publish_drive_command()
|
||||
self.publish_camera_control_commands()
|
||||
self.publish_pan_tilt_control_commands()
|
||||
|
||||
def publish_drive_command(self):
|
||||
if self.drive_paused:
|
||||
drive_message = DriveCommandMessage()
|
||||
else:
|
||||
drive_message = self.get_drive_message(self.speed_limit)
|
||||
|
||||
left_output = abs(drive_message.drive_twist.linear.x - drive_message.drive_twist.angular.z)
|
||||
right_output = abs(drive_message.drive_twist.linear.x + drive_message.drive_twist.angular.z)
|
||||
|
||||
self.set_speed_limit__signal.emit(self.speed_limit * 100)
|
||||
self.set_left_drive_output__signal.emit(left_output * 100)
|
||||
self.set_right_drive_output__signal.emit(right_output * 100)
|
||||
|
||||
self.drive_command_publisher.publish(drive_message)
|
||||
|
||||
def publish_camera_control_commands(self):
|
||||
trigger_pressed = self.joystick.controller_states["y"]
|
||||
three_pressed = self.joystick.controller_states["left_bumper"]
|
||||
four_pressed = self.joystick.controller_states["right_bumper"]
|
||||
five_pressed = self.joystick.controller_states["left_trigger"]
|
||||
six_pressed = self.joystick.controller_states["right_trigger"]
|
||||
|
||||
if (five_pressed or six_pressed) and (time() - self.last_camera_change_time) > CAMERA_CHANGE_TIME:
|
||||
change = -1 if five_pressed else 1
|
||||
self.change_camera_selection__signal.emit(change)
|
||||
self.last_camera_change_time = time()
|
||||
|
||||
if (three_pressed or four_pressed) and (time() - self.last_gui_element_change_time) > GUI_ELEMENT_CHANGE_TIME:
|
||||
change = -1 if three_pressed else 1
|
||||
self.change_gui_element_selection__signal.emit(change)
|
||||
self.last_gui_element_change_time = time()
|
||||
|
||||
if trigger_pressed and (time() - self.last_camera_toggle_time) > CAMERA_TOGGLE_CHANGE_TIME:
|
||||
self.toggle_selected_gui_camera__signal.emit()
|
||||
self.last_camera_toggle_time = time()
|
||||
|
||||
def publish_pan_tilt_control_commands(self):
|
||||
button_eight = self.joystick.controller_states["a"]
|
||||
hat_x = self.joystick.controller_states["d_pad_x"]
|
||||
hat_y = self.joystick.controller_states["d_pad_y"]
|
||||
|
||||
if (hat_x == 0 and not self.last_hat_x_was_movement) and (
|
||||
hat_y == 0 and not self.last_hat_y_was_movement) and not button_eight:
|
||||
return
|
||||
|
||||
self.last_hat_x_was_movement = True if hat_x != 0 else False
|
||||
self.last_hat_y_was_movement = True if hat_y != 0 else False
|
||||
|
||||
pan_tilt_message = TowerPanTiltControlMessage()
|
||||
|
||||
if button_eight:
|
||||
pan_tilt_message.should_center = 1
|
||||
|
||||
if self.current_pan_tilt_selection == "tower_pan_tilt":
|
||||
pan_tilt_message.relative_pan_adjustment = hat_x * TOWER_PAN_TILT_X_AXIS_SCALAR
|
||||
pan_tilt_message.relative_tilt_adjustment = -(hat_y * TOWER_PAN_TILT_Y_AXIS_SCALAR)
|
||||
self.tower_pan_tilt_command_publisher.publish(pan_tilt_message)
|
||||
|
||||
elif self.current_pan_tilt_selection == "chassis_pan_tilt":
|
||||
pan_tilt_message.relative_pan_adjustment = hat_x * CHASSIS_PAN_TILT_X_AXIS_SCALAR
|
||||
pan_tilt_message.relative_tilt_adjustment = -(hat_y * CHASSIS_PAN_TILT_Y_AXIS_SCALAR)
|
||||
self.chassis_pan_tilt_command_publisher.publish(pan_tilt_message)
|
||||
|
||||
def get_drive_message(self, speed_limit):
|
||||
drive_message = DriveCommandMessage()
|
||||
|
||||
left_y_axis = self.joystick.controller_states["left_y_axis"] if abs(self.joystick.controller_states["left_y_axis"]) > STICK_DEADBAND else 0
|
||||
right_y_axis = self.joystick.controller_states["right_y_axis"] if abs(self.joystick.controller_states["right_y_axis"]) > STICK_DEADBAND else 0
|
||||
|
||||
left_y_axis = speed_limit * (-(left_y_axis - STICK_OFFSET) / STICK_MAX)
|
||||
right_y_axis = speed_limit * (-(right_y_axis - STICK_OFFSET) / STICK_MAX)
|
||||
|
||||
drive_message.drive_twist.linear.x = (left_y_axis + right_y_axis) / 2.0
|
||||
drive_message.drive_twist.angular.z = (right_y_axis - left_y_axis) / 2.0
|
||||
|
||||
return drive_message
|
||||
|
||||
def on_pan_tilt_selection_changed__slot(self, selection):
|
||||
self.current_pan_tilt_selection = selection
|
||||
|
||||
def on_speed_limit_slider_value_changed__slot(self, value):
|
||||
self.speed_limit = value / 100.0
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def show_changed_pause_state(self):
|
||||
if self.drive_paused:
|
||||
self.left_drive_progress_bar.setStyleSheet("background-color:darkred;")
|
||||
self.right_drive_progress_bar.setStyleSheet("background-color:darkred;")
|
||||
else:
|
||||
self.left_drive_progress_bar.setStyleSheet("background-color: transparent;")
|
||||
self.right_drive_progress_bar.setStyleSheet("background-color: transparent;")
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,445 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
from inputs import devices, GamePad
|
||||
from time import time
|
||||
|
||||
import rospy
|
||||
from rover_arm.msg import ArmControlMessage
|
||||
from rover_control.msg import MiningControlMessage, GripperControlMessage
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
GAME_CONTROLLER_NAME = "Afterglow Gamepad for Xbox 360"
|
||||
|
||||
DRIVE_COMMAND_HERTZ = 20
|
||||
|
||||
GRIPPER_CONTROL_TOPIC = "/rover_control/gripper/control"
|
||||
RELATIVE_ARM_CONTROL_TOPIC = "/rover_arm/control/relative"
|
||||
MINING_CONTROL_TOPIC = "/rover_control/mining/control"
|
||||
|
||||
BASE_SCALAR = 0.003
|
||||
SHOULDER_SCALAR = 0.002
|
||||
ELBOW_SCALAR = 0.002
|
||||
ROLL_SCALAR = 0.003
|
||||
WRIST_PITCH_SCALAR = 0.003
|
||||
WRIST_ROLL_SCALAR = 0.006
|
||||
|
||||
GRIPPER_MOVEMENT_SCALAR = 1500
|
||||
|
||||
LEFT_X_AXIS_DEADZONE = 1500
|
||||
LEFT_Y_AXIS_DEADZONE = 1500
|
||||
|
||||
RIGHT_X_AXIS_DEADZONE = 1500
|
||||
RIGHT_Y_AXIS_DEADZONE = 1500
|
||||
|
||||
THUMB_STICK_MAX = 32768.0
|
||||
|
||||
MINING_LIFT_SCALAR = 5
|
||||
MINING_TILT_SCALAR = 5
|
||||
|
||||
COLOR_GREEN = "background-color:darkgreen; border: 1px solid black;"
|
||||
COLOR_NONE = "border: 1px solid black;"
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class XBOXController(QtCore.QThread):
|
||||
def __init__(self):
|
||||
super(XBOXController, self).__init__()
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
self.setup_controller_flag = True
|
||||
self.data_acquisition_and_broadcast_flag = True
|
||||
self.controller_acquired = False
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.gamepad = None # type: GamePad
|
||||
|
||||
self.controller_states = {
|
||||
"left_x_axis": 0,
|
||||
"left_y_axis": 0,
|
||||
"left_stick_button": 0,
|
||||
|
||||
"right_x_axis": 0,
|
||||
"right_y_axis": 0,
|
||||
"right_stick_button": 0,
|
||||
|
||||
"left_trigger": 0,
|
||||
"left_bumper": 0,
|
||||
|
||||
"right_trigger": 0,
|
||||
"right_bumper": 0,
|
||||
|
||||
"hat_x_axis": 0,
|
||||
"hat_y_axis": 0,
|
||||
|
||||
"back_button": 0,
|
||||
"start_button": 0,
|
||||
"xbox_button": 0,
|
||||
|
||||
"x_button": 0,
|
||||
"a_button": 0,
|
||||
"b_button": 0,
|
||||
"y_button": 0
|
||||
}
|
||||
|
||||
self.raw_mapping_to_class_mapping = {
|
||||
"ABS_X": "left_x_axis",
|
||||
"ABS_Y": "left_y_axis",
|
||||
"BTN_THUMBL": "left_stick_button",
|
||||
|
||||
"ABS_RX": "right_x_axis",
|
||||
"ABS_RY": "right_y_axis",
|
||||
"BTN_THUMBR": "right_stick_button",
|
||||
|
||||
"ABS_Z": "left_trigger",
|
||||
"BTN_TL": "left_bumper",
|
||||
|
||||
"ABS_RZ": "right_trigger",
|
||||
"BTN_TR": "right_bumper",
|
||||
|
||||
"ABS_HAT0X": "hat_x_axis",
|
||||
"ABS_HAT0Y": "hat_y_axis",
|
||||
|
||||
"BTN_SELECT": "back_button",
|
||||
"BTN_START": "start_button",
|
||||
"BTN_MODE": "xbox_button",
|
||||
|
||||
"BTN_NORTH": "x_button",
|
||||
"BTN_SOUTH": "a_button",
|
||||
"BTN_EAST": "b_button",
|
||||
"BTN_WEST": "y_button"
|
||||
}
|
||||
|
||||
self.ready = False
|
||||
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.setup_controller_flag:
|
||||
self.controller_acquired = self.__setup_controller()
|
||||
self.setup_controller_flag = False
|
||||
if self.data_acquisition_and_broadcast_flag:
|
||||
self.__get_controller_data()
|
||||
|
||||
def __setup_controller(self):
|
||||
for device in devices.gamepads:
|
||||
# print device
|
||||
if device.name == GAME_CONTROLLER_NAME:
|
||||
self.gamepad = device
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def __get_controller_data(self):
|
||||
if self.controller_acquired:
|
||||
events = self.gamepad.read()
|
||||
|
||||
for event in events:
|
||||
# For seeing codes you haven't added yet...
|
||||
# if event.code not in self.raw_mapping_to_class_mapping and event.code != "SYN_REPORT":
|
||||
# print event.code, ":", event.state
|
||||
|
||||
if event.code in self.raw_mapping_to_class_mapping:
|
||||
# print event.code, ":", event.state
|
||||
self.controller_states[self.raw_mapping_to_class_mapping[event.code]] = event.state
|
||||
|
||||
self.ready = True
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class EffectorsAndArmControlSender(QtCore.QThread):
|
||||
xbox_control_arm_stylesheet_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
xbox_control_mining_stylesheet_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
gripper_mode_normal_stylesheet_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
gripper_mode_pinch_stylesheet_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
gripper_mode_wide_stylesheet_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
gripper_mode_scissor_stylesheet_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
XBOX_CONTROL_STATES = [
|
||||
"ARM",
|
||||
"MINING"
|
||||
]
|
||||
|
||||
GRIPPER_CONTROL_MODES = {
|
||||
"NORMAL": 1,
|
||||
"TWO_FINGER_PINCH": 2,
|
||||
"WIDE": 3,
|
||||
"SCISSOR": 4
|
||||
}
|
||||
|
||||
PINCH_MODE_ABSOLUTE_SET_POSITION = 57740
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(EffectorsAndArmControlSender, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
self.right_screen = self.shared_objects["screens"]["right_screen"]
|
||||
self.xbox_mode_arm_label = self.right_screen.xbox_mode_arm_label # type: QtWidgets.QLabel
|
||||
self.xbox_mode_mining_label = self.right_screen.xbox_mode_mining_label # type: QtWidgets.QLabel
|
||||
|
||||
self.gripper_mode_normal_label = self.right_screen.gripper_mode_normal_label # type: QtWidgets.QLabel
|
||||
self.gripper_mode_pinch_label = self.right_screen.gripper_mode_pinch_label # type: QtWidgets.QLabel
|
||||
self.gripper_mode_wide_label = self.right_screen.gripper_mode_wide_label # type: QtWidgets.QLabel
|
||||
self.gripper_mode_scissor_label = self.right_screen.gripper_mode_scissor_label # type: QtWidgets.QLabel
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
self.controller = XBOXController()
|
||||
|
||||
# ########## Class Variables ##########
|
||||
|
||||
self.wait_time = 1.0 / DRIVE_COMMAND_HERTZ
|
||||
|
||||
self.gripper_control_publisher = rospy.Publisher(GRIPPER_CONTROL_TOPIC, GripperControlMessage, queue_size=1)
|
||||
|
||||
self.relative_arm_control_publisher = rospy.Publisher(RELATIVE_ARM_CONTROL_TOPIC, ArmControlMessage,
|
||||
queue_size=1)
|
||||
self.mining_control_publisher = rospy.Publisher(MINING_CONTROL_TOPIC, MiningControlMessage, queue_size=1)
|
||||
|
||||
self.xbox_current_control_state = self.XBOX_CONTROL_STATES.index("ARM")
|
||||
self.xbox_control_state_just_changed = False
|
||||
|
||||
self.last_xbox_button_state = 0
|
||||
self.last_left_bumper_state = 0
|
||||
self.last_right_bumper_state = 0
|
||||
self.last_back_button_state = 0
|
||||
|
||||
self.gripper_control_mode = 1
|
||||
self.gripper_control_mode_just_changed = False
|
||||
self.send_new_gripper_mode = False
|
||||
|
||||
self.GRIPPER_DISPLAY_SIGNAL_MAPPING = {
|
||||
self.GRIPPER_CONTROL_MODES["NORMAL"]: {
|
||||
"SET": [self.gripper_mode_normal_stylesheet_update_ready__signal],
|
||||
"UNSET": [self.gripper_mode_pinch_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_wide_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_scissor_stylesheet_update_ready__signal]
|
||||
},
|
||||
|
||||
self.GRIPPER_CONTROL_MODES["TWO_FINGER_PINCH"]: {
|
||||
"SET": [self.gripper_mode_pinch_stylesheet_update_ready__signal],
|
||||
"UNSET": [self.gripper_mode_normal_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_wide_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_scissor_stylesheet_update_ready__signal]
|
||||
},
|
||||
|
||||
self.GRIPPER_CONTROL_MODES["WIDE"]: {
|
||||
"SET": [self.gripper_mode_wide_stylesheet_update_ready__signal],
|
||||
"UNSET": [self.gripper_mode_pinch_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_normal_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_scissor_stylesheet_update_ready__signal]
|
||||
},
|
||||
|
||||
self.GRIPPER_CONTROL_MODES["SCISSOR"]: {
|
||||
"SET": [self.gripper_mode_scissor_stylesheet_update_ready__signal],
|
||||
"UNSET": [self.gripper_mode_pinch_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_wide_stylesheet_update_ready__signal,
|
||||
self.gripper_mode_normal_stylesheet_update_ready__signal],
|
||||
# "ABS_MOVE": self.PINCH_MODE_ABSOLUTE_SET_POSITION
|
||||
}
|
||||
}
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting Joystick Thread")
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
self.change_control_state_if_needed()
|
||||
|
||||
if self.xbox_current_control_state == self.XBOX_CONTROL_STATES.index("ARM"):
|
||||
self.send_gripper_home_on_back_press()
|
||||
self.process_and_send_arm_control()
|
||||
elif self.xbox_current_control_state == self.XBOX_CONTROL_STATES.index("MINING"):
|
||||
self.send_mining_commands()
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int(self.wait_time - time_diff), 0))
|
||||
|
||||
self.logger.debug("Stopping Joystick Thread")
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.xbox_control_arm_stylesheet_update_ready__signal.connect(self.xbox_mode_arm_label.setStyleSheet)
|
||||
self.xbox_control_mining_stylesheet_update_ready__signal.connect(self.xbox_mode_mining_label.setStyleSheet)
|
||||
|
||||
self.gripper_mode_normal_stylesheet_update_ready__signal.connect(self.gripper_mode_normal_label.setStyleSheet)
|
||||
self.gripper_mode_pinch_stylesheet_update_ready__signal.connect(self.gripper_mode_pinch_label.setStyleSheet)
|
||||
self.gripper_mode_wide_stylesheet_update_ready__signal.connect(self.gripper_mode_wide_label.setStyleSheet)
|
||||
self.gripper_mode_scissor_stylesheet_update_ready__signal.connect(self.gripper_mode_scissor_label.setStyleSheet)
|
||||
|
||||
def change_control_state_if_needed(self):
|
||||
xbox_state = self.controller.controller_states["xbox_button"]
|
||||
left_bumper_state = self.controller.controller_states["left_bumper"]
|
||||
right_bumper_state = self.controller.controller_states["right_bumper"]
|
||||
|
||||
if self.last_xbox_button_state == 0 and xbox_state == 1:
|
||||
self.xbox_current_control_state += 1
|
||||
self.xbox_current_control_state = self.xbox_current_control_state % len(self.XBOX_CONTROL_STATES)
|
||||
self.xbox_control_state_just_changed = True
|
||||
self.last_xbox_button_state = 1
|
||||
elif self.last_xbox_button_state == 1 and xbox_state == 0:
|
||||
self.last_xbox_button_state = 0
|
||||
|
||||
if self.xbox_control_state_just_changed:
|
||||
if self.xbox_current_control_state == self.XBOX_CONTROL_STATES.index("ARM"):
|
||||
self.xbox_control_arm_stylesheet_update_ready__signal.emit(COLOR_GREEN)
|
||||
self.xbox_control_mining_stylesheet_update_ready__signal.emit(COLOR_NONE)
|
||||
elif self.xbox_current_control_state == self.XBOX_CONTROL_STATES.index("MINING"):
|
||||
self.xbox_control_arm_stylesheet_update_ready__signal.emit(COLOR_NONE)
|
||||
self.xbox_control_mining_stylesheet_update_ready__signal.emit(COLOR_GREEN)
|
||||
self.xbox_control_state_just_changed = False
|
||||
|
||||
if self.xbox_current_control_state == self.XBOX_CONTROL_STATES.index("ARM"):
|
||||
if self.last_left_bumper_state == 0 and left_bumper_state == 1:
|
||||
# self.gripper_control_mode = ((self.gripper_control_mode - 1) % len(self.GRIPPER_CONTROL_MODES))
|
||||
# self.gripper_control_mode_just_changed = True
|
||||
self.last_left_bumper_state = 1
|
||||
elif self.last_left_bumper_state == 1 and left_bumper_state == 0:
|
||||
self.last_left_bumper_state = 0
|
||||
if self.last_right_bumper_state == 0 and right_bumper_state == 1:
|
||||
# self.gripper_control_mode = ((self.gripper_control_mode + 1) % len(self.GRIPPER_CONTROL_MODES))
|
||||
# self.gripper_control_mode_just_changed = True
|
||||
self.last_right_bumper_state = 1
|
||||
elif self.last_right_bumper_state == 1 and right_bumper_state == 0:
|
||||
self.last_right_bumper_state = 0
|
||||
|
||||
if self.gripper_control_mode_just_changed:
|
||||
signal_map = self.GRIPPER_DISPLAY_SIGNAL_MAPPING[self.gripper_control_mode + 1]
|
||||
|
||||
for signal in signal_map["SET"]:
|
||||
signal.emit(COLOR_GREEN)
|
||||
|
||||
for signal in signal_map["UNSET"]:
|
||||
signal.emit(COLOR_NONE)
|
||||
|
||||
if "ABS_MOVE" in signal_map:
|
||||
gripper_control_message = GripperControlMessage()
|
||||
gripper_control_message.gripper_mode = self.GRIPPER_CONTROL_MODES["SCISSOR"]
|
||||
gripper_control_message.gripper_position_absolute = signal_map["ABS_MOVE"]
|
||||
self.gripper_control_publisher.publish(gripper_control_message)
|
||||
else:
|
||||
self.send_new_gripper_mode = True
|
||||
|
||||
self.gripper_control_mode_just_changed = False
|
||||
|
||||
def process_and_send_arm_control(self):
|
||||
|
||||
arm_control_message = ArmControlMessage()
|
||||
|
||||
gripper_control_message = GripperControlMessage()
|
||||
|
||||
should_publish_arm = False
|
||||
should_publish_gripper = True if self.send_new_gripper_mode else False
|
||||
|
||||
if self.send_new_gripper_mode:
|
||||
gripper_control_message.gripper_position_absolute = 0
|
||||
else:
|
||||
gripper_control_message.gripper_position_absolute = -1
|
||||
|
||||
gripper_control_message.gripper_mode = self.gripper_control_mode + 1
|
||||
|
||||
left_trigger = self.controller.controller_states["left_trigger"]
|
||||
right_trigger = self.controller.controller_states["right_trigger"]
|
||||
|
||||
left_x_axis = self.controller.controller_states["left_x_axis"] if abs(self.controller.controller_states[
|
||||
"left_x_axis"]) > LEFT_X_AXIS_DEADZONE else 0
|
||||
left_y_axis = self.controller.controller_states["left_y_axis"] if abs(self.controller.controller_states[
|
||||
"left_y_axis"]) > LEFT_Y_AXIS_DEADZONE else 0
|
||||
right_y_axis = self.controller.controller_states["right_y_axis"] if abs(self.controller.controller_states[
|
||||
"right_y_axis"]) > RIGHT_Y_AXIS_DEADZONE else 0
|
||||
right_x_axis = self.controller.controller_states["right_x_axis"] if abs(self.controller.controller_states[
|
||||
"right_x_axis"]) > RIGHT_X_AXIS_DEADZONE else 0
|
||||
|
||||
# print left_x_axis, ":", left_y_axis, ":", right_x_axis, ":", right_y_axis
|
||||
|
||||
left_trigger_ratio = left_trigger / 255.0
|
||||
right_trigger_ratio = right_trigger / 255.0
|
||||
|
||||
if left_trigger > 0:
|
||||
should_publish_arm = True
|
||||
arm_control_message.base = ((left_x_axis / THUMB_STICK_MAX) * BASE_SCALAR) * left_trigger_ratio
|
||||
arm_control_message.shoulder = ((left_y_axis / THUMB_STICK_MAX) * SHOULDER_SCALAR) * left_trigger_ratio
|
||||
arm_control_message.elbow = (-(right_y_axis / THUMB_STICK_MAX) * ELBOW_SCALAR) * left_trigger_ratio
|
||||
arm_control_message.roll = (-(right_x_axis / THUMB_STICK_MAX) * ROLL_SCALAR) * left_trigger_ratio
|
||||
|
||||
elif right_trigger > 0:
|
||||
should_publish_arm = True
|
||||
should_publish_gripper = True
|
||||
|
||||
arm_control_message.wrist_roll = ((left_x_axis / THUMB_STICK_MAX) * BASE_SCALAR) * right_trigger_ratio
|
||||
|
||||
# ##### FIXME #####
|
||||
# Remove this once the arm is fixed
|
||||
# arm_control_message.wrist_pitch = (-(left_y_axis / THUMB_STICK_MAX) * WRIST_PITCH_SCALAR) * right_trigger_ratio
|
||||
# #################
|
||||
|
||||
gripper_control_message.gripper_position_relative = (-(right_y_axis / THUMB_STICK_MAX) * GRIPPER_MOVEMENT_SCALAR) * right_trigger_ratio
|
||||
|
||||
if should_publish_arm:
|
||||
self.relative_arm_control_publisher.publish(arm_control_message)
|
||||
|
||||
if should_publish_gripper:
|
||||
self.gripper_control_publisher.publish(gripper_control_message)
|
||||
self.send_new_gripper_mode = False
|
||||
|
||||
def send_gripper_home_on_back_press(self):
|
||||
gripper_control_message = GripperControlMessage()
|
||||
back_state = self.controller.controller_states["back_button"]
|
||||
|
||||
if self.last_back_button_state == 0 and back_state == 1:
|
||||
gripper_control_message.should_home = True
|
||||
gripper_control_message.gripper_mode = self.gripper_control_mode + 1
|
||||
self.gripper_control_publisher.publish(gripper_control_message)
|
||||
self.last_back_button_state = 1
|
||||
elif self.last_back_button_state == 1 and back_state == 0:
|
||||
self.last_back_button_state = 0
|
||||
|
||||
def send_mining_commands(self):
|
||||
|
||||
left_y_axis = self.controller.controller_states["left_y_axis"] if abs(
|
||||
self.controller.controller_states["left_y_axis"]) > LEFT_Y_AXIS_DEADZONE else 0
|
||||
right_y_axis = self.controller.controller_states["right_y_axis"] if abs(self.controller.controller_states[
|
||||
"right_y_axis"]) > RIGHT_Y_AXIS_DEADZONE else 0
|
||||
|
||||
if left_y_axis or right_y_axis:
|
||||
message = MiningControlMessage()
|
||||
|
||||
message.lift_set_absolute = 1024
|
||||
message.tilt_set_absolute = 1024
|
||||
|
||||
message.lift_set_relative = (-(left_y_axis / THUMB_STICK_MAX) * MINING_LIFT_SCALAR)
|
||||
message.tilt_set_relative = ((right_y_axis / THUMB_STICK_MAX) * MINING_TILT_SCALAR)
|
||||
message.cal_factor = -1
|
||||
|
||||
self.mining_control_publisher.publish(message)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,175 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore
|
||||
import logging
|
||||
from time import time
|
||||
import spnav
|
||||
|
||||
import rospy
|
||||
|
||||
from rover_control.msg import MiningControlMessage
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
THREAD_HERTZ = 100
|
||||
|
||||
MINING_CONTROL_TOPIC = "/rover_control/mining/control"
|
||||
|
||||
Y_ANGULAR_DEADBAND = 0.05
|
||||
Z_LINEAR_DEADBAND = 0.15
|
||||
|
||||
MINING_LIFT_SCALAR = 5
|
||||
MINING_TILT_SCALAR = 5
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class SpaceNavControlSender(QtCore.QThread):
|
||||
spacenav_state_update__signal = QtCore.pyqtSignal(object)
|
||||
|
||||
MINING_MODE = 0
|
||||
ARM_MODE = 1
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(SpaceNavControlSender, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.right_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.wait_time = 1.0 / THREAD_HERTZ
|
||||
|
||||
self.spnav_states = {
|
||||
"linear_x": 0,
|
||||
"linear_y": 0,
|
||||
"linear_z": 0,
|
||||
|
||||
"angular_x": 0,
|
||||
"angular_y": 0,
|
||||
"angular_z": 0,
|
||||
|
||||
"panel_pressed": 0,
|
||||
"fit_pressed": 0,
|
||||
|
||||
"shift_pressed": 0,
|
||||
"alt_pressed": 0,
|
||||
"ctrl_pressed": 0,
|
||||
"esc_pressed": 0,
|
||||
|
||||
"1_pressed": 0,
|
||||
"2_pressed": 0,
|
||||
|
||||
"minus_pressed": 0,
|
||||
"plus_pressed": 0,
|
||||
|
||||
"t_pressed": 0,
|
||||
"l_pressed": 0,
|
||||
"2d_pressed": 0,
|
||||
"r_pressed": 0,
|
||||
"f_pressed": 0
|
||||
}
|
||||
|
||||
self.event_mapping_to_button_mapping = {
|
||||
11: "panel_pressed",
|
||||
10: "fit_pressed",
|
||||
|
||||
8: "shift_pressed",
|
||||
7: "alt_pressed",
|
||||
9: "ctrl_pressed",
|
||||
6: "esc_pressed",
|
||||
|
||||
0: "1_pressed",
|
||||
1: "2_pressed",
|
||||
|
||||
13: "minus_pressed",
|
||||
12: "plus_pressed",
|
||||
|
||||
2: "t_pressed",
|
||||
3: "l_pressed",
|
||||
14: "2d_pressed",
|
||||
4: "r_pressed",
|
||||
5: "f_pressed"
|
||||
}
|
||||
|
||||
# ##### Mining Control #####
|
||||
self.mining_control_publisher = rospy.Publisher(MINING_CONTROL_TOPIC, MiningControlMessage, queue_size=1)
|
||||
|
||||
self.current_control_mode = self.MINING_MODE
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting SpaceNav Mouse Thread")
|
||||
spnav.spnav_open()
|
||||
|
||||
while self.run_thread_flag:
|
||||
|
||||
start_time = time()
|
||||
|
||||
self.process_spnav_events()
|
||||
self.check_control_mode_change()
|
||||
self.broadcast_control_state()
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
# self.msleep(max(int((self.wait_time - time_diff) * 1000), 0))
|
||||
|
||||
self.logger.debug("Stopping SpaceNav Mouse Thread")
|
||||
|
||||
def process_spnav_events(self):
|
||||
event = spnav.spnav_poll_event()
|
||||
|
||||
if event:
|
||||
# print event
|
||||
if event.ev_type == spnav.SPNAV_EVENT_MOTION:
|
||||
self.spnav_states["linear_x"] = event.translation[0] / 350.0
|
||||
self.spnav_states["linear_y"] = event.translation[2] / 350.0
|
||||
self.spnav_states["linear_z"] = event.translation[1] / 350.0
|
||||
|
||||
self.spnav_states["angular_x"] = event.rotation[2] / 350.0
|
||||
self.spnav_states["angular_y"] = -(event.rotation[0] / 350.0)
|
||||
self.spnav_states["angular_z"] = -(event.rotation[1] / 350.0)
|
||||
|
||||
# print "x", self.spnav_states["linear_x"], "\t", "y", self.spnav_states["linear_y"], "\t", "z", self.spnav_states["linear_z"]
|
||||
# print "x", self.spnav_states["angular_x"], "\t", "y", self.spnav_states["angular_y"], "\t", "z", self.spnav_states["angular_z"]
|
||||
else:
|
||||
self.spnav_states[self.event_mapping_to_button_mapping[event.bnum]] = event.press
|
||||
|
||||
def check_control_mode_change(self):
|
||||
if self.spnav_states["1_pressed"]:
|
||||
self.current_control_mode = self.MINING_MODE
|
||||
elif self.spnav_states["2_pressed"]:
|
||||
self.current_control_mode = self.ARM_MODE
|
||||
|
||||
def broadcast_control_state(self):
|
||||
if self.current_control_mode == self.MINING_MODE:
|
||||
self.send_mining_commands()
|
||||
# self.spacenav_state_update__signal.emit(self.spnav_states)
|
||||
elif self.current_control_mode == self.ARM_MODE:
|
||||
pass
|
||||
|
||||
|
||||
# print self.spnav_states["linear_z"], self.spnav_states["angular_y"]
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
pass
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,127 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore
|
||||
from os import makedirs, rename, walk, unlink
|
||||
from os.path import exists, getmtime
|
||||
from os import environ
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
MAX_NUM_LOG_FILES = 30
|
||||
|
||||
|
||||
#####################################
|
||||
# Logger Definition
|
||||
#####################################
|
||||
class Logger(QtCore.QObject):
|
||||
def __init__(self, console_output=True):
|
||||
super(Logger, self).__init__()
|
||||
|
||||
# ########## Local class variables ##########
|
||||
self.console_output = console_output
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# # ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Set variables with useful paths ##########
|
||||
self.appdata_base_directory = environ["HOME"] + "/.groundstation"
|
||||
self.log_directory = self.appdata_base_directory + "//logs"
|
||||
self.log_file_path = self.log_directory + "//log.txt"
|
||||
|
||||
# ########## Cleanup old log files ##########
|
||||
self.__cleanup_log_files()
|
||||
|
||||
# ########## Set up logger with desired settings ##########
|
||||
self.__setup_logger()
|
||||
|
||||
# ########## Place divider in log file to see new program launch ##########
|
||||
self.__add_startup_log_buffer_text()
|
||||
|
||||
def __setup_logger(self):
|
||||
# Get the appdata directory and make the log path if it doesn't exist
|
||||
if not exists(self.log_directory):
|
||||
makedirs(self.log_directory)
|
||||
|
||||
# Set the debugging level
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Make a formatter with the log line format wanted
|
||||
formatter = logging.Formatter(fmt='%(levelname)s : %(asctime)s : %(message)s', datefmt='%m/%d/%y %H:%M:%S')
|
||||
|
||||
# Set up a file handler so everything can be saved and attach it to the logger
|
||||
file_handler = logging.FileHandler(filename=self.log_file_path)
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
# Enable console output if requested
|
||||
if self.console_output:
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
def __cleanup_log_files(self):
|
||||
# This copies the existing log.txt file to an old version with a datetime stamp
|
||||
# It then checks if there are too many log files, and if so, deletes the oldest
|
||||
if exists(self.log_directory):
|
||||
# Get the number of log files
|
||||
num_log_files = self.__get_num_files_in_directory(self.log_directory)
|
||||
|
||||
# Check that we actually have log files
|
||||
if num_log_files > 0:
|
||||
date_time = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
# If we do, move the current logfile to a backup in the format old_log_datetime
|
||||
if exists(self.log_file_path):
|
||||
rename(self.log_file_path, self.log_directory + "\\old_log_" + date_time + ".txt")
|
||||
|
||||
# If we have more than the max log files delete the oldest one
|
||||
if num_log_files >= MAX_NUM_LOG_FILES:
|
||||
unlink(self.__get_name_of_oldest_file(self.log_directory))
|
||||
|
||||
def __add_startup_log_buffer_text(self):
|
||||
# Prints a header saying when the program started
|
||||
self.logger.info("########## Application Starting ##########")
|
||||
|
||||
@staticmethod
|
||||
def __get_name_of_oldest_file(input_path):
|
||||
oldest_file_path = None
|
||||
previous_oldest_time = 0
|
||||
|
||||
# Walk the directory passed in to get all folders and files
|
||||
for dir_path, dir_names, file_names in walk(input_path):
|
||||
# Go through all of the filenames found
|
||||
for file in file_names:
|
||||
# Recreate the full path and get the modified time of the file
|
||||
current_path = dir_path + "\\" + file
|
||||
time = getmtime(current_path)
|
||||
|
||||
# Default case for if the variables above have not been initially set
|
||||
if previous_oldest_time == 0:
|
||||
previous_oldest_time = time
|
||||
oldest_file_path = current_path
|
||||
|
||||
# Saves the oldest time and file path of the current file if it's older (lower timestamp) than the
|
||||
# last file saved in the variables
|
||||
if time < previous_oldest_time:
|
||||
previous_oldest_time = time
|
||||
oldest_file_path = current_path
|
||||
|
||||
# Returns the path to the oldest file after checking all the files
|
||||
return oldest_file_path
|
||||
|
||||
@staticmethod
|
||||
def __get_num_files_in_directory(input_path):
|
||||
# Walk the directory passed in to get all the files
|
||||
for _, _, file_names in walk(input_path):
|
||||
# Return the number of files found in the directory
|
||||
return len(file_names)
|
||||
@@ -0,0 +1,480 @@
|
||||
'''
|
||||
MappingSettings.py: Objected Orientated Google Maps for Python
|
||||
ReWritten by Chris Pham
|
||||
|
||||
Copyright OSURC, orginal code from GooMPy by Alec Singer and Simon D. Levy
|
||||
|
||||
This code is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
This code is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this code. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
import math
|
||||
import urllib2
|
||||
from io import StringIO, BytesIO
|
||||
import os
|
||||
import time
|
||||
import PIL.ImageDraw
|
||||
import PIL.Image
|
||||
import PIL.ImageFont
|
||||
import signing
|
||||
import RoverMapHelper as MapHelper
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
#####################################
|
||||
# Constants
|
||||
#####################################
|
||||
_KEYS = []
|
||||
# Number of pixels in half the earth's circumference at zoom = 21
|
||||
_EARTHPIX = 268435456
|
||||
# Number of decimal places for rounding coordinates
|
||||
_DEGREE_PRECISION = 6
|
||||
_PRECISION_FORMAT = '%.' + str(_DEGREE_PRECISION) + 'f'
|
||||
# Larget tile we can grab without paying
|
||||
_TILESIZE = 640
|
||||
# Fastest rate at which we can download tiles without paying
|
||||
_GRABRATE = 4
|
||||
# Pixel Radius of Earth for calculations
|
||||
_PIXRAD = _EARTHPIX / math.pi
|
||||
_DISPLAYPIX = _EARTHPIX / 2000
|
||||
|
||||
file_pointer = open('key', 'r')
|
||||
for i in file_pointer:
|
||||
_KEYS.append(i.rstrip())
|
||||
file_pointer.close()
|
||||
|
||||
|
||||
class GMapsStitcher(object):
|
||||
def __init__(self, width, height,
|
||||
latitude, longitude, zoom,
|
||||
maptype, radius_meters=None, num_tiles=4, debug=False):
|
||||
self.helper = MapHelper.MapHelper()
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.start_latitude = latitude
|
||||
self.start_longitude = longitude
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.zoom = zoom
|
||||
self.maptype = maptype
|
||||
self.radius_meters = radius_meters
|
||||
self.num_tiles = num_tiles
|
||||
self.display_image = self.helper.new_image(width, height)
|
||||
self.debug = debug
|
||||
|
||||
# Get the big image here
|
||||
self._fetch()
|
||||
self.center_display(latitude, longitude)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
This string returns when used in a print statement
|
||||
Useful for debugging and to print current state
|
||||
|
||||
returns STRING
|
||||
"""
|
||||
string_builder = ""
|
||||
string_builder += ("Center of the displayed map: %4f, %4f\n" %
|
||||
(self.center_x, self.center_y))
|
||||
string_builder += ("Center of the big map: %4fx%4f\n" %
|
||||
(self.start_longitude, self.start_longitude))
|
||||
string_builder += ("Current latitude is: %4f, %4f\n" %
|
||||
(self.longitude, self.latitude))
|
||||
string_builder += ("The top-left of the box: %dx%d\n" %
|
||||
(self.left_x, self.upper_y))
|
||||
string_builder += ("Number of tiles genreated: %dx%d\n" %
|
||||
(self.num_tiles, self.num_tiles))
|
||||
string_builder += "Map Type: %s\n" % (self.maptype)
|
||||
string_builder += "Zoom Level: %s\n" % (self.zoom)
|
||||
string_builder += ("Dimensions of Big Image: %dx%d\n" %
|
||||
(self.big_image.size[0], self.big_image.size[1]))
|
||||
string_builder += ("Dimensions of Displayed Image: %dx%d\n" %
|
||||
(self.width, self.height))
|
||||
string_builder += ("LatLong of Northwest Corner: %4f, %4f\n" %
|
||||
(self.northwest))
|
||||
string_builder += ("LatLong of Southeast Corner: %4f, %4f\n" %
|
||||
(self.southeast))
|
||||
return string_builder
|
||||
|
||||
def _grab_tile(self, longitude, latitude, sleeptime=0):
|
||||
"""
|
||||
This will return the tile at location longitude x latitude.
|
||||
Includes a sleep time to allow for free use if there is no API key
|
||||
|
||||
returns PIL.IMAGE OBJECT
|
||||
"""
|
||||
# Make the url string for polling
|
||||
# GET request header gets appended to the string
|
||||
urlbase = 'https://maps.googleapis.com/maps/api/staticmap?'
|
||||
urlbase += 'center=' + _PRECISION_FORMAT + ',' + _PRECISION_FORMAT + '&zoom=%d&maptype=%s'
|
||||
urlbase += '&size=%dx%d&format=png&key=%s'
|
||||
|
||||
# Fill the formatting
|
||||
specs = (self.helper.fast_round(latitude, _DEGREE_PRECISION),
|
||||
self.helper.fast_round(longitude, _DEGREE_PRECISION),
|
||||
self.zoom, self.maptype, _TILESIZE, _TILESIZE, _KEYS[0])
|
||||
filename = 'Resources/Maps/' + ((_PRECISION_FORMAT + '_' + _PRECISION_FORMAT + '_%d_%s_%d_%d_%s') % specs)
|
||||
filename += '.png'
|
||||
|
||||
# Tile Image object
|
||||
tile_object = None
|
||||
|
||||
if os.path.isfile(filename):
|
||||
tile_object = PIL.Image.open(filename)
|
||||
|
||||
# If file on filesystem
|
||||
else:
|
||||
# make the url
|
||||
url = urlbase % specs
|
||||
url = signing.sign_url(url, _KEYS[1])
|
||||
try:
|
||||
result = urllib2.urlopen(urllib2.Request(url)).read()
|
||||
except urllib2.HTTPError, e:
|
||||
print "Error accessing url for reason:", e
|
||||
print url
|
||||
return
|
||||
|
||||
tile_object = PIL.Image.open(BytesIO(result))
|
||||
if not os.path.exists('Resources/Maps'):
|
||||
os.mkdir('Resources/Maps')
|
||||
tile_object.save(filename)
|
||||
# Added to prevent timeouts on Google Servers
|
||||
time.sleep(sleeptime)
|
||||
|
||||
return tile_object
|
||||
|
||||
def _pixels_to_lon(self, iterator, lon_pixels):
|
||||
"""
|
||||
This converts pixels to degrees to be used in
|
||||
fetching squares and generate correct squares
|
||||
|
||||
returns FLOAT(degrees)
|
||||
"""
|
||||
# Magic Lines, no idea
|
||||
degrees = self.helper.pixels_to_degrees(
|
||||
(iterator - self.num_tiles / 2) * _TILESIZE, self.zoom)
|
||||
return math.degrees((lon_pixels + degrees - _EARTHPIX) / _PIXRAD)
|
||||
|
||||
def _pixels_to_lat(self, iterator, lat_pixels):
|
||||
"""
|
||||
This converts pixels to latitude using meridian projection
|
||||
to get the latitude to generate squares
|
||||
|
||||
returns FLOAT(degrees)
|
||||
"""
|
||||
# Magic Lines
|
||||
return math.degrees(math.pi / 2 - 2 * math.atan(math.exp(((lat_pixels +
|
||||
self.helper.pixels_to_degrees(
|
||||
(iterator - self.num_tiles /
|
||||
2) * _TILESIZE, self.zoom)) -
|
||||
_EARTHPIX) / _PIXRAD)))
|
||||
|
||||
def fetch_tiles(self):
|
||||
"""
|
||||
Function that handles fetching of files from init'd variables
|
||||
|
||||
returns PIL.IMAGE OBJECT, (WEST, NORTH), (EAST, SOUTH)
|
||||
|
||||
North/East/South/West are in FLOAT(degrees)
|
||||
"""
|
||||
# cap floats to precision amount
|
||||
self.latitude = self.helper.fast_round(self.latitude,
|
||||
_DEGREE_PRECISION)
|
||||
self.longitude = self.helper.fast_round(self.longitude,
|
||||
_DEGREE_PRECISION)
|
||||
|
||||
# number of tiles required to go from center
|
||||
# latitude to desired radius in meters
|
||||
if self.radius_meters is not None:
|
||||
self.num_tiles = (int(
|
||||
round(2 * self.helper.pixels_to_meters(
|
||||
self.latitude, self.zoom) /
|
||||
(_TILESIZE / 2. / self.radius_meters))))
|
||||
|
||||
lon_pixels = _EARTHPIX + self.longitude * math.radians(_PIXRAD)
|
||||
|
||||
sin_lat = math.sin(math.radians(self.latitude))
|
||||
lat_pixels = _EARTHPIX - _PIXRAD * math.log((1 + sin_lat) / (1 - sin_lat)) / 2
|
||||
self.big_size = self.num_tiles * _TILESIZE
|
||||
big_image = self.helper.new_image(self.big_size, self.big_size)
|
||||
|
||||
for j in range(self.num_tiles):
|
||||
lon = self._pixels_to_lon(j, lon_pixels)
|
||||
for k in range(self.num_tiles):
|
||||
lat = self._pixels_to_lat(k, lat_pixels)
|
||||
tile = self._grab_tile(lon, lat)
|
||||
big_image.paste(tile, (j * _TILESIZE, k * _TILESIZE))
|
||||
|
||||
west = self._pixels_to_lon(0, lon_pixels)
|
||||
east = self._pixels_to_lon(self.num_tiles - 1, lon_pixels)
|
||||
|
||||
north = self._pixels_to_lat(0, lat_pixels)
|
||||
south = self._pixels_to_lat(self.num_tiles - 1, lat_pixels)
|
||||
return big_image, (north, west), (south, east)
|
||||
|
||||
def move_pix(self, dx, dy):
|
||||
"""
|
||||
Function gets change in x and y (dx, dy)
|
||||
then displaces the displayed map that amount
|
||||
|
||||
NO RETURN
|
||||
"""
|
||||
self._constrain_x(dx)
|
||||
self._constrain_y(dy)
|
||||
self.update()
|
||||
|
||||
def _constrain_x(self, diff):
|
||||
"""
|
||||
Helper for move_pix
|
||||
"""
|
||||
new_value = self.left_x - diff
|
||||
|
||||
if ((not new_value > 0) and
|
||||
(new_value < self.big_image.size[0] - self.width)):
|
||||
return self.left_x
|
||||
else:
|
||||
return new_value
|
||||
|
||||
def _constrain_y(self, diff):
|
||||
"""
|
||||
Helper for move_pix
|
||||
"""
|
||||
new_value = self.upper_y - diff
|
||||
|
||||
if ((not new_value > 0) and
|
||||
(new_value < self.big_image.size[1] - self.height)):
|
||||
return self.upper_y
|
||||
else:
|
||||
return new_value
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Function remakes display image using top left corners
|
||||
"""
|
||||
self.display_image.paste(self.big_image, (-self.left_x, -self.upper_y))
|
||||
# self.display_image.resize((self.image_zoom, self.image_zoom))
|
||||
|
||||
def _fetch(self):
|
||||
"""
|
||||
Function generates big image
|
||||
"""
|
||||
self.big_image, self.northwest, self.southeast = self.fetch_tiles()
|
||||
|
||||
def move_latlon(self, lat, lon):
|
||||
"""
|
||||
Function to move the object/rover
|
||||
"""
|
||||
x, y = self._get_cartesian(lat, lon)
|
||||
self._constrain_x(self.center_x - x)
|
||||
self._constrain_y(self.center_y - y)
|
||||
self.update()
|
||||
|
||||
def _get_cartesian(self, lat, lon):
|
||||
"""
|
||||
Helper for getting the x, y given lat and lon
|
||||
|
||||
returns INT, INT (x, y)
|
||||
"""
|
||||
viewport_lat_nw, viewport_lon_nw = self.northwest
|
||||
viewport_lat_se, viewport_lon_se = self.southeast
|
||||
# print "Lat:", viewport_lat_nw, viewport_lat_se
|
||||
# print "Lon:", viewport_lon_nw, viewport_lon_se
|
||||
|
||||
viewport_lat_diff = viewport_lat_nw - viewport_lat_se
|
||||
viewport_lon_diff = viewport_lon_se - viewport_lon_nw
|
||||
|
||||
# print viewport_lon_diff, viewport_lat_diff
|
||||
|
||||
bigimage_width = self.big_image.size[0]
|
||||
bigimage_height = self.big_image.size[1]
|
||||
|
||||
pixel_per_lat = bigimage_height / viewport_lat_diff
|
||||
pixel_per_lon = bigimage_width / viewport_lon_diff
|
||||
# print "Pixel per:", pixel_per_lat, pixel_per_lon
|
||||
|
||||
new_lat_gps_range_percentage = (viewport_lat_nw - lat)
|
||||
new_lon_gps_range_percentage = (lon - viewport_lon_nw)
|
||||
# print lon, viewport_lon_se
|
||||
|
||||
x = new_lon_gps_range_percentage * pixel_per_lon
|
||||
y = new_lat_gps_range_percentage * pixel_per_lat
|
||||
|
||||
return int(x), int(y)
|
||||
|
||||
def add_gps_location(self, lat, lon, shape, size, fill):
|
||||
"""
|
||||
Function adds a shape at lat x lon
|
||||
"""
|
||||
x, y = self._get_cartesian(lat, lon)
|
||||
draw = PIL.ImageDraw.Draw(self.big_image)
|
||||
if shape is "ellipsis":
|
||||
draw.ellipsis((x - size, y - size, x + size, y + size), fill)
|
||||
else:
|
||||
draw.rectangle([x - size, y - size, x + size, y + size], fill)
|
||||
self.update()
|
||||
|
||||
def center_display(self, lat, lon):
|
||||
"""
|
||||
Function centers the display image
|
||||
"""
|
||||
x, y = self._get_cartesian(lat, lon)
|
||||
self.center_x = x
|
||||
self.center_y = y
|
||||
|
||||
self.left_x = (self.center_x - (self.width / 2))
|
||||
self.upper_y = (self.center_y - (self.height / 2))
|
||||
self.update()
|
||||
|
||||
# def update_rover_map_location(self, lat, lon):
|
||||
# print "I did nothing"
|
||||
|
||||
# def draw_circle(self, lat, lon, radius, fill):
|
||||
# print "I did nothing"
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
pass
|
||||
|
||||
|
||||
class OverlayImage(object):
|
||||
def __init__(self, latitude, longitude, northwest, southeast,
|
||||
big_width, big_height, width, height):
|
||||
self.northwest = northwest
|
||||
self.southeast = southeast
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.big_width = big_width
|
||||
self.big_height = big_height
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.big_image = None
|
||||
self.big_image_copy = None
|
||||
self.display_image = None
|
||||
self.display_image_copy = None
|
||||
self.indicator = None
|
||||
self.helper = MapHelper.MapHelper()
|
||||
|
||||
x, y = self._get_cartesian(latitude, longitude)
|
||||
self.center_x = x
|
||||
self.center_y = y
|
||||
|
||||
self.left_x = (self.center_x - (self.width / 2))
|
||||
self.upper_y = (self.center_y - (self.height / 2))
|
||||
|
||||
self.generate_image_files()
|
||||
self.write_once = True
|
||||
|
||||
# Text Drawing Variables
|
||||
self.font = cv2.FONT_HERSHEY_TRIPLEX
|
||||
self.font_thickness = 1
|
||||
self.font_baseline = 0
|
||||
|
||||
self.nav_coordinates_text_image = None
|
||||
|
||||
def generate_image_files(self):
|
||||
"""
|
||||
Creates big_image and display image sizes
|
||||
|
||||
Returns NONE
|
||||
"""
|
||||
self.big_image = self.helper.new_image(self.big_width, self.big_height,
|
||||
True)
|
||||
|
||||
self.big_image_copy = self.big_image.copy()
|
||||
|
||||
self.display_image = self.helper.new_image(self.width, self.height,
|
||||
True)
|
||||
|
||||
self.display_image_copy = self.display_image.copy()
|
||||
|
||||
self.load_rover_icon()
|
||||
self.indicator.save("location.png")
|
||||
|
||||
def _get_cartesian(self, lat, lon):
|
||||
"""
|
||||
Helper for getting the x, y given lat and lon
|
||||
|
||||
returns INT, INT (x, y)
|
||||
"""
|
||||
viewport_lat_nw, viewport_lon_nw = self.northwest
|
||||
viewport_lat_se, viewport_lon_se = self.southeast
|
||||
# print "Lat:", viewport_lat_nw, viewport_lat_se
|
||||
# print "Lon:", viewport_lon_nw, viewport_lon_se
|
||||
|
||||
viewport_lat_diff = viewport_lat_nw - viewport_lat_se
|
||||
viewport_lon_diff = viewport_lon_se - viewport_lon_nw
|
||||
|
||||
# print viewport_lon_diff, viewport_lat_diff
|
||||
|
||||
pixel_per_lat = self.big_height / viewport_lat_diff
|
||||
pixel_per_lon = self.big_width / viewport_lon_diff
|
||||
# print "Pixel per:", pixel_per_lat, pixel_per_lon
|
||||
|
||||
new_lat_gps_range_percentage = (viewport_lat_nw - lat)
|
||||
new_lon_gps_range_percentage = (lon - viewport_lon_nw)
|
||||
# print lon, viewport_lon_se
|
||||
|
||||
x = new_lon_gps_range_percentage * pixel_per_lon
|
||||
y = new_lat_gps_range_percentage * pixel_per_lat
|
||||
|
||||
return int(x), int(y)
|
||||
|
||||
def update_new_location(self, latitude, longitude,
|
||||
compass, navigation_list, landmark_list):
|
||||
self.big_image = self.big_image_copy.copy()
|
||||
self.display_image = self.display_image_copy.copy()
|
||||
|
||||
size = 5
|
||||
draw = PIL.ImageDraw.Draw(self.big_image)
|
||||
|
||||
for element in navigation_list:
|
||||
x, y = self._get_cartesian(float(element[1]), float(element[2]))
|
||||
draw.text((x + 10, y - 5), str(element[0]))
|
||||
draw.ellipse((x - size, y - size, x + size, y + size), fill=(element[3].red(), element[3].green(), element[3].blue()))
|
||||
|
||||
for element in landmark_list:
|
||||
x, y = self._get_cartesian(element[1], element[2])
|
||||
draw.text((x + 10, y - 5), str(element[0]))
|
||||
draw.ellipse((x - size, y - size, x + size, y + size), fill=(element[3].red(), element[3].green(), element[3].blue()))
|
||||
|
||||
self._draw_rover(latitude, longitude, compass)
|
||||
self.update(latitude, longitude)
|
||||
|
||||
return self.display_image
|
||||
|
||||
def load_rover_icon(self):
|
||||
self.indicator = PIL.Image.open("Resources/Images/rover.png").resize((40, 40))
|
||||
|
||||
def _draw_rover(self, lat, lon, angle=0):
|
||||
x, y = self._get_cartesian(lat, lon)
|
||||
|
||||
x -= 25 # Half the height of the icon
|
||||
y -= 25
|
||||
|
||||
rotated = self.indicator.copy()
|
||||
rotated = rotated.rotate(-angle, resample=PIL.Image.BICUBIC)
|
||||
# rotated.save("rotated.png")
|
||||
self.big_image.paste(rotated, (x, y), rotated)
|
||||
if self.write_once:
|
||||
# self.display_image.save("Something.png")
|
||||
self.write_once = False
|
||||
|
||||
def update(self, latitude, longitude):
|
||||
# self.left_x -= 50
|
||||
# self.upper_y -= 50
|
||||
self.display_image.paste(self.big_image, (-self.left_x, -self.upper_y))
|
||||
# self._draw_coordinate_text(latitude, longitude)
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
pass
|
||||
@@ -0,0 +1,375 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
from PIL.ImageQt import ImageQt
|
||||
from PIL import Image
|
||||
import PIL.ImageDraw
|
||||
import PIL.Image
|
||||
import PIL.ImageFont
|
||||
import numpy
|
||||
|
||||
import logging
|
||||
|
||||
import rospy
|
||||
from tf import transformations
|
||||
from scipy.interpolate import interp1d
|
||||
import math
|
||||
from sensor_msgs.msg import Imu
|
||||
|
||||
# Custom Imports
|
||||
import RoverMap
|
||||
from Resources.Settings import MappingSettings
|
||||
from sensor_msgs.msg import NavSatFix
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
# put some stuff here later so you can remember
|
||||
|
||||
GPS_POSITION_TOPIC = "/rover_odometry/fix"
|
||||
IMU_DATA_TOPIC = "/rover_odometry/imu/data"
|
||||
|
||||
MAP_WIDGET_WIDTH = float(1280)
|
||||
MAP_WIDGET_HEIGHT = float(720)
|
||||
MAP_WIDGET_RATIO = MAP_WIDGET_WIDTH / MAP_WIDGET_HEIGHT
|
||||
|
||||
|
||||
class RoverMapCoordinator(QtCore.QThread):
|
||||
pixmap_ready_signal = QtCore.pyqtSignal()
|
||||
change_waypoint_signal = QtCore.pyqtSignal()
|
||||
|
||||
zoom_level_enabled_state_update__signal = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(RoverMapCoordinator, self).__init__()
|
||||
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
self.mapping_label = self.left_screen.mapping_label # type:QtWidgets.QLabel
|
||||
self.navigation_label = self.left_screen.navigation_waypoints_table_widget
|
||||
self.landmark_label = self.left_screen.landmark_waypoints_table_widget
|
||||
self.map_selection_combo_box = self.left_screen.map_selection_combo_box # type: QtWidgets.QComboBox
|
||||
self.map_zoom_level_combo_box = self.left_screen.map_zoom_level_combo_box # type: QtWidgets.QComboBox
|
||||
|
||||
self.setings = QtCore.QSettings()
|
||||
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
self.gps_position_subscriber = rospy.Subscriber(GPS_POSITION_TOPIC, NavSatFix,
|
||||
self.gps_position_updated_callback)
|
||||
|
||||
self.run_thread_flag = True
|
||||
self.setup_map_flag = True
|
||||
|
||||
self.google_maps_object = None
|
||||
self.map_image = None
|
||||
self.map_image_copy = None
|
||||
self.overlay_image = None
|
||||
self.overlay_image_object = None
|
||||
|
||||
self.loading_image_pixmap = QtGui.QPixmap.fromImage(
|
||||
ImageQt(Image.open("Resources/Images/maps_loading.png").resize((1280, 720), Image.BICUBIC)))
|
||||
|
||||
self.map_pixmap = self.loading_image_pixmap
|
||||
self.last_map_pixmap_cache_key = None
|
||||
|
||||
self.longitude = 0
|
||||
self.latitude = 0
|
||||
self.last_heading = 0
|
||||
|
||||
self.imu_data = None
|
||||
self.new_imu_data = False
|
||||
|
||||
self.yaw = None
|
||||
self.pitch = None
|
||||
self.roll = None
|
||||
|
||||
self.euler_interpolator = interp1d([math.pi, -math.pi], [-180, 180])
|
||||
|
||||
self.imu_data_subscriber = rospy.Subscriber(IMU_DATA_TOPIC, Imu, self.on_imu_data_received)
|
||||
|
||||
self.zoom_pan_x_location = 0
|
||||
self.zoom_pan_y_location = 0
|
||||
|
||||
self.zoom_subtraction = 0
|
||||
|
||||
self.x_drag = 0
|
||||
self.y_drag = 0
|
||||
|
||||
self.x_drag_start = -1
|
||||
self.y_drag_start = -1
|
||||
|
||||
self.x_drag_end = -1
|
||||
self.y_drag_end = -1
|
||||
|
||||
self.map_selection_name = ""
|
||||
self.map_selection_latitude = 0
|
||||
self.map_selection_longitude = 0
|
||||
self.map_selection_zoom = 0
|
||||
|
||||
self.setup_mapping_locations()
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting Map Coordinator Thread")
|
||||
self.pixmap_ready_signal.emit() # This gets us the loading map
|
||||
|
||||
# self.precache_all_maps()
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.setup_map_flag:
|
||||
self._map_setup()
|
||||
self.setup_map_flag = False
|
||||
else:
|
||||
if self.new_imu_data:
|
||||
self.calculate_euler_from_imu()
|
||||
self.new_imu_data = False
|
||||
|
||||
self._get_map_image()
|
||||
self.msleep(10)
|
||||
|
||||
self.logger.debug("Stopping Map Coordinator Thread")
|
||||
|
||||
def setup_mapping_locations(self):
|
||||
locations = [location for location in MappingSettings.MAPPING_LOCATIONS]
|
||||
|
||||
self.map_selection_combo_box.addItems(locations)
|
||||
|
||||
self.update_settings_from_selection()
|
||||
self.map_selection_combo_box.setCurrentText(self.map_selection_name)
|
||||
|
||||
self.update_zoom_combo_box_items(
|
||||
MappingSettings.MAPPING_LOCATIONS[MappingSettings.LAST_SELECTION]["valid_zoom_options"],
|
||||
MappingSettings.LAST_ZOOM_LEVEL)
|
||||
|
||||
def update_settings_from_selection(self):
|
||||
self.map_selection_latitude = MappingSettings.MAPPING_LOCATIONS[MappingSettings.LAST_SELECTION]["latitude"]
|
||||
self.map_selection_longitude = MappingSettings.MAPPING_LOCATIONS[MappingSettings.LAST_SELECTION]["longitude"]
|
||||
self.map_selection_zoom = MappingSettings.LAST_ZOOM_LEVEL
|
||||
|
||||
self.map_selection_name = MappingSettings.LAST_SELECTION
|
||||
|
||||
def precache_all_maps(self):
|
||||
print "Caching all map options!!!"
|
||||
for map in MappingSettings.MAPPING_LOCATIONS:
|
||||
lat = MappingSettings.MAPPING_LOCATIONS[map]["latitude"]
|
||||
lon = MappingSettings.MAPPING_LOCATIONS[map]["longitude"]
|
||||
for zoom in MappingSettings.MAPPING_LOCATIONS[map]["valid_zoom_options"]:
|
||||
print "Caching map: %s at zoom %d" % (map, zoom)
|
||||
|
||||
try:
|
||||
RoverMap.GMapsStitcher(1280,
|
||||
720,
|
||||
lat,
|
||||
lon,
|
||||
zoom,
|
||||
'satellite',
|
||||
None, 20)
|
||||
except:
|
||||
print "Could not cache map: %s at zoom %d" % (map, zoom)
|
||||
print "Finished caching map: %s at zoom %d" % (map, zoom)
|
||||
|
||||
print "Map cache complete!"
|
||||
|
||||
def _map_setup(self):
|
||||
self.update_zoom_combo_box_items(
|
||||
MappingSettings.MAPPING_LOCATIONS[MappingSettings.LAST_SELECTION]["valid_zoom_options"],
|
||||
MappingSettings.LAST_ZOOM_LEVEL)
|
||||
|
||||
self.map_image = None
|
||||
self.map_pixmap = self.loading_image_pixmap
|
||||
self.pixmap_ready__slot()
|
||||
|
||||
self.google_maps_object = None
|
||||
self.google_maps_object = RoverMap.GMapsStitcher(1280,
|
||||
720,
|
||||
self.map_selection_latitude,
|
||||
self.map_selection_longitude,
|
||||
self.map_selection_zoom,
|
||||
'satellite',
|
||||
None, 20)
|
||||
self.overlay_image_object = None
|
||||
self.overlay_image_object = (
|
||||
RoverMap.OverlayImage(self.map_selection_latitude, self.map_selection_longitude,
|
||||
self.google_maps_object.northwest,
|
||||
self.google_maps_object.southeast,
|
||||
self.google_maps_object.big_image.size[0],
|
||||
self.google_maps_object.big_image.size[1],
|
||||
1280, 720))
|
||||
|
||||
def _get_map_image(self):
|
||||
while self.map_image is None:
|
||||
self.map_image = self.google_maps_object.display_image
|
||||
|
||||
if self.map_image:
|
||||
self.map_image_copy = self.map_image.copy()
|
||||
|
||||
self.update_overlay()
|
||||
|
||||
self.map_image = self.map_image_copy.copy()
|
||||
self.map_image.paste(self.overlay_image_object.display_image,
|
||||
(0, 0),
|
||||
self.overlay_image_object.display_image)
|
||||
|
||||
# ##### Zoom testing! #####
|
||||
if self.zoom_subtraction:
|
||||
self.zoom_subtraction = self.constrain(self.zoom_subtraction, 0, 520)
|
||||
|
||||
crop_x_start = ((self.zoom_subtraction * MAP_WIDGET_RATIO) / 2) - self.x_drag - self.x_drag_end
|
||||
crop_y_start = (self.zoom_subtraction / 2) - self.y_drag - self.y_drag_end
|
||||
crop_x_end = (MAP_WIDGET_WIDTH - (
|
||||
(self.zoom_subtraction * MAP_WIDGET_RATIO) / 2)) - self.x_drag - self.x_drag_end
|
||||
crop_y_end = (MAP_WIDGET_HEIGHT - (self.zoom_subtraction / 2)) - self.y_drag - self.y_drag_end
|
||||
crop_box = (int(crop_x_start), int(crop_y_start), int(crop_x_end), int(crop_y_end))
|
||||
|
||||
self.map_image = self.map_image.crop(crop_box)
|
||||
self.map_image = self.map_image.resize((1280, 720), resample=PIL.Image.BICUBIC)
|
||||
|
||||
# ##### Draw coordinates #####
|
||||
self._draw_coordinate_text(self.latitude, self.longitude)
|
||||
|
||||
qim = ImageQt(self.map_image)
|
||||
self.map_pixmap = QtGui.QPixmap.fromImage(qim)
|
||||
|
||||
# if self.map_pixmap.cacheKey() != self.last_map_pixmap_cache_key:
|
||||
# self.last_map_pixmap_cache_key = self.map_pixmap.cacheKey()
|
||||
self.pixmap_ready_signal.emit()
|
||||
|
||||
def _draw_coordinate_text(self, latitude, longitude):
|
||||
|
||||
if latitude == 0 and longitude == 0:
|
||||
location_text = "LAT: NO FIX\nLON: NO FIX"
|
||||
else:
|
||||
location_text = "LAT: %+014.9f\nLON: %+014.9f" % (latitude, longitude)
|
||||
|
||||
font = PIL.ImageFont.truetype("UbuntuMono-R", size=20)
|
||||
|
||||
new_image = PIL.Image.new('RGBA', (200, 45), "black")
|
||||
|
||||
draw = PIL.ImageDraw.Draw(new_image)
|
||||
|
||||
draw.multiline_text((5, 0), location_text, font=font)
|
||||
|
||||
self.map_image.paste(new_image, (0, 0))
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.pixmap_ready_signal.connect(self.pixmap_ready__slot)
|
||||
self.change_waypoint_signal.connect(self.update_overlay)
|
||||
|
||||
self.mapping_label.mouseReleaseEvent = self.__mouse_released_event
|
||||
self.mapping_label.wheelEvent = self.__mouse_wheel_event
|
||||
self.mapping_label.mouseMoveEvent = self.__mouse_move_event
|
||||
|
||||
self.map_selection_combo_box.currentTextChanged.connect(self.map_selection_changed__slot)
|
||||
self.map_zoom_level_combo_box.currentTextChanged.connect(self.zoom_selection_changed__slot)
|
||||
|
||||
def on_kill_threads_requested_slot(self):
|
||||
self.run_thread_flag = False
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal,
|
||||
kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested_slot)
|
||||
|
||||
def pixmap_ready__slot(self):
|
||||
self.mapping_label.setPixmap(self.map_pixmap)
|
||||
|
||||
def _get_table_elements(self, UI_element):
|
||||
temp_list = []
|
||||
count = UI_element.rowCount()
|
||||
for row in range(0, count):
|
||||
name = UI_element.item(row, 0).text()
|
||||
lat = float(UI_element.item(row, 1).text())
|
||||
lng = float(UI_element.item(row, 2).text())
|
||||
color = UI_element.item(row, 3).background().color()
|
||||
temp_tuple = (name, lat, lng, color)
|
||||
temp_list.append(temp_tuple)
|
||||
return temp_list
|
||||
|
||||
def update_overlay(self):
|
||||
if not numpy.isnan(self.latitude) and not numpy.isnan(self.longitude):
|
||||
latitude = float(self.latitude)
|
||||
longitude = float(self.longitude)
|
||||
|
||||
navigation_list = self._get_table_elements(self.navigation_label)
|
||||
landmark_list = self._get_table_elements(self.landmark_label)
|
||||
self.overlay_image = self.overlay_image_object.update_new_location(
|
||||
latitude,
|
||||
longitude,
|
||||
self.last_heading,
|
||||
navigation_list,
|
||||
landmark_list)
|
||||
|
||||
def gps_position_updated_callback(self, data):
|
||||
self.latitude = data.latitude
|
||||
self.longitude = data.longitude
|
||||
|
||||
def on_imu_data_received(self, data):
|
||||
self.imu_data = data
|
||||
self.new_imu_data = True
|
||||
|
||||
def calculate_euler_from_imu(self):
|
||||
quat = (
|
||||
self.imu_data.orientation.x,
|
||||
self.imu_data.orientation.y,
|
||||
self.imu_data.orientation.z,
|
||||
self.imu_data.orientation.w,
|
||||
)
|
||||
self.roll, self.pitch, self.yaw = transformations.euler_from_quaternion(quat)
|
||||
self.last_heading = (self.euler_interpolator(self.yaw) + MappingSettings.DECLINATION_OFFSET) % 360
|
||||
|
||||
def __mouse_released_event(self, _):
|
||||
self.x_drag_start = -1
|
||||
self.y_drag_start = -1
|
||||
|
||||
self.x_drag_end += self.x_drag
|
||||
self.y_drag_end += self.y_drag
|
||||
|
||||
self.x_drag = 0
|
||||
self.x_drag = 0
|
||||
|
||||
def __mouse_wheel_event(self, event):
|
||||
self.zoom_subtraction += event.angleDelta().y() / 12
|
||||
|
||||
def __mouse_move_event(self, event):
|
||||
if self.x_drag_start != -1 and self.x_drag_start != -1:
|
||||
buttons = event.buttons()
|
||||
|
||||
if buttons == QtCore.Qt.LeftButton:
|
||||
x_pos = event.pos().x()
|
||||
y_pos = event.pos().y()
|
||||
dx = x_pos - self.x_drag_start
|
||||
dy = y_pos - self.y_drag_start
|
||||
|
||||
self.x_drag = dx
|
||||
self.y_drag = dy
|
||||
else:
|
||||
self.x_drag_start = event.pos().x()
|
||||
self.y_drag_start = event.pos().y()
|
||||
|
||||
def update_zoom_combo_box_items(self, zooms, selection):
|
||||
self.map_zoom_level_combo_box.clear()
|
||||
self.map_zoom_level_combo_box.addItems([str(item) for item in zooms])
|
||||
self.map_zoom_level_combo_box.setCurrentText(str(selection))
|
||||
|
||||
def zoom_selection_changed__slot(self, zoom):
|
||||
|
||||
if zoom:
|
||||
MappingSettings.LAST_ZOOM_LEVEL = int(zoom)
|
||||
self.map_selection_zoom = int(zoom)
|
||||
self.setup_map_flag = True
|
||||
|
||||
def map_selection_changed__slot(self, selection):
|
||||
MappingSettings.LAST_SELECTION = selection
|
||||
MappingSettings.LAST_ZOOM_LEVEL = MappingSettings.MAPPING_LOCATIONS[selection]["default_zoom"]
|
||||
|
||||
self.update_settings_from_selection()
|
||||
self.setup_map_flag = True
|
||||
|
||||
self.map_selection_name = selection
|
||||
|
||||
@staticmethod
|
||||
def constrain(val, min_val, max_val):
|
||||
return min(max_val, max(min_val, val))
|
||||
@@ -0,0 +1,46 @@
|
||||
import PIL.Image
|
||||
import math
|
||||
|
||||
|
||||
class MapHelper(object):
|
||||
|
||||
@staticmethod
|
||||
def new_image(width, height, alpha=False):
|
||||
"""
|
||||
Generates a new image using PIL.Image module
|
||||
|
||||
returns PIL.IMAGE OBJECT
|
||||
"""
|
||||
if alpha is True:
|
||||
return PIL.Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
||||
else:
|
||||
return PIL.Image.new('RGBA', (width, height))
|
||||
|
||||
@staticmethod
|
||||
def fast_round(value, precision):
|
||||
"""
|
||||
Function to round values instead of using python's
|
||||
|
||||
return INT
|
||||
"""
|
||||
return int(value * 10 ** precision) / 10. ** precision
|
||||
|
||||
@staticmethod
|
||||
def pixels_to_degrees(pixels, zoom):
|
||||
"""
|
||||
Generates pixels to be expected at zoom levels
|
||||
|
||||
returns INT
|
||||
"""
|
||||
return pixels * 2 ** (21-zoom)
|
||||
|
||||
@staticmethod
|
||||
def pixels_to_meters(latitude, zoom):
|
||||
"""
|
||||
Function generates how many pixels per meter it
|
||||
should be from the projecction
|
||||
|
||||
returns FLOAT
|
||||
"""
|
||||
# https://groups.google.com/forum/#!topic/google-maps-js-api-v3/hDRO4oHVSeM
|
||||
return 2 ** zoom / (156543.03392 * math.cos(math.radians(latitude)))
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Signs a URL using a URL signing secret """
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import urlparse
|
||||
|
||||
def sign_url(input_url=None, secret=None):
|
||||
""" Sign a request URL with a URL signing secret.
|
||||
|
||||
Usage:
|
||||
from urlsigner import sign_url
|
||||
|
||||
signed_url = sign_url(input_url=my_url, secret=SECRET)
|
||||
|
||||
Args:
|
||||
input_url - The URL to sign
|
||||
secret - Your URL signing secret
|
||||
|
||||
Returns:
|
||||
The signed request URL
|
||||
"""
|
||||
|
||||
if not input_url or not secret:
|
||||
raise Exception("Both input_url and secret are required")
|
||||
|
||||
url = urlparse.urlparse(input_url)
|
||||
|
||||
# We only need to sign the path+query part of the string
|
||||
url_to_sign = url.path + "?" + url.query
|
||||
|
||||
# Decode the private key into its binary format
|
||||
# We need to decode the URL-encoded private key
|
||||
decoded_key = base64.urlsafe_b64decode(secret)
|
||||
|
||||
# Create a signature using the private key and the URL-encoded
|
||||
# string using HMAC SHA1. This signature will be binary.
|
||||
signature = hmac.new(decoded_key, url_to_sign, hashlib.sha1)
|
||||
|
||||
# Encode the binary signature into base64 for use within a URL
|
||||
encoded_signature = base64.urlsafe_b64encode(signature.digest())
|
||||
|
||||
original_url = url.scheme + "://" + url.netloc + url.path + "?" + url.query
|
||||
|
||||
# Return signed URL
|
||||
return original_url + "&signature=" + encoded_signature
|
||||
|
||||
if __name__ == "__main__":
|
||||
input_url = raw_input("URL to Sign: ")
|
||||
secret = raw_input("URL signing secret: ")
|
||||
print "Signed URL: " + sign_url(input_url, secret)
|
||||
@@ -0,0 +1,201 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
from time import time
|
||||
import paramiko
|
||||
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
THREAD_HERTZ = 5
|
||||
|
||||
IP = "192.168.1.10"
|
||||
USER = "nvidia"
|
||||
PASS = "nvidia"
|
||||
|
||||
|
||||
#####################################
|
||||
# BashConsole Class Definition
|
||||
#####################################
|
||||
class BashConsole(QtCore.QThread):
|
||||
|
||||
text_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(BashConsole, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
self.console_text_edit = self.left_screen.console_line_edit # type: QtWidgets.QTextEdit
|
||||
self.ssh_console_command_line_edit = self.left_screen.ssh_console_command_line_edit # type:QtWidgets.QLineEdit
|
||||
|
||||
self.ssh_scan_for_hosts_button = self.left_screen.ssh_scan_for_hosts_button # type: QtWidgets.QPushButton
|
||||
self.ssh_host_line_edit = self.left_screen.ssh_host_line_edit # type: QtWidgets.QLineEdit
|
||||
|
||||
self.ssh_list_wifi_button = self.left_screen.ssh_list_wifi_button # type: QtWidgets.QPushButton
|
||||
self.ssh_equipment_login_button = self.left_screen.ssh_equipment_login_button # type: QtWidgets.QPushButton
|
||||
self.ssh_equipment_logout_button = self.left_screen.ssh_equipment_logout_button # type: QtWidgets.QPushButton
|
||||
self.ssh_equipment_status_button = self.left_screen.ssh_equipment_status_button # type: QtWidgets.QPushButton
|
||||
self.ssh_equipment_start_button = self.left_screen.ssh_equipment_start_button # type: QtWidgets.QPushButton
|
||||
self.ssh_equipment_stop_button = self.left_screen.ssh_equipment_stop_button # type: QtWidgets.QPushButton
|
||||
|
||||
self.ssh_ssid_line_edit = self.left_screen.ssh_ssid_line_edit # type:QtWidgets.QLineEdit
|
||||
self.ssh_connect_ssid_push_button = self.left_screen.ssh_ssid_push_button # type: QtWidgets.QPushButton
|
||||
self.ssh_disconnect_wifi_button = self.left_screen.ssh_disconnect_wifi_button # type: QtWidgets.QPushButton
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.bash_process = None
|
||||
self.new_widget = None
|
||||
self.window = None
|
||||
|
||||
self.wait_time = 1.0 / THREAD_HERTZ
|
||||
|
||||
self.ssh_client = None
|
||||
|
||||
self.set_text_contents = ""
|
||||
|
||||
self.new_command_text = ""
|
||||
self.new_command = False
|
||||
|
||||
def run(self):
|
||||
while not self.ssh_client:
|
||||
try:
|
||||
self.ssh_client = paramiko.SSHClient()
|
||||
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.ssh_client.connect(IP, username=USER, password=PASS, compress=True)
|
||||
except:
|
||||
print "No connection"
|
||||
if not self.run_thread_flag:
|
||||
return
|
||||
self.ssh_client = None
|
||||
self.msleep(1000)
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
if self.new_command:
|
||||
_, ssh_stdout, ssh_stderr = self.ssh_client.exec_command(self.new_command_text)
|
||||
|
||||
stdout_read = ssh_stdout.read()
|
||||
stderr_read = ssh_stderr.read()
|
||||
|
||||
output = ""
|
||||
output += "\n%s@%s:$" % (USER, IP)
|
||||
output += self.new_command_text + "\n"
|
||||
output += stdout_read.decode("utf-8") if stdout_read else ""
|
||||
output += stderr_read.decode("utf-8") if stderr_read else ""
|
||||
|
||||
self.set_text_contents += output
|
||||
self.text_update_ready__signal.emit(self.set_text_contents)
|
||||
self.new_command = False
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int(self.wait_time - time_diff), 0))
|
||||
|
||||
del self.bash_process
|
||||
|
||||
def on_text_readout_updated__slot(self):
|
||||
self.console_text_edit.moveCursor(QtGui.QTextCursor.End)
|
||||
|
||||
def on_text_editing_finished__slot(self):
|
||||
self.new_command_text = self.ssh_console_command_line_edit.text()
|
||||
self.new_command = True
|
||||
|
||||
def on_list_wifi_button_pressed__slot(self):
|
||||
self.new_command_text = "nmcli dev wifi list"
|
||||
self.new_command = True
|
||||
|
||||
def on_login_button_pressed__slot(self):
|
||||
current_ip = self.ssh_host_line_edit.text()
|
||||
self.new_command_text = "python equipment_servicing_interface.py '%s' 'LOGIN MTECH GITRDONE' HELP" % current_ip
|
||||
print self.new_command_text
|
||||
self.new_command = True
|
||||
|
||||
def on_logout_button_pressed__slot(self):
|
||||
current_ip = self.ssh_host_line_edit.text()
|
||||
self.new_command_text = "python equipment_servicing_interface.py '%s' LOGOUT" % current_ip
|
||||
self.new_command = True
|
||||
|
||||
def on_status_button_pressed__slot(self):
|
||||
current_ip = self.ssh_host_line_edit.text()
|
||||
self.new_command_text = "python equipment_servicing_interface.py '%s' STATUS" % current_ip
|
||||
self.new_command = True
|
||||
|
||||
def on_start_button_pressed__slot(self):
|
||||
current_ip = self.ssh_host_line_edit.text()
|
||||
self.new_command_text = "python equipment_servicing_interface.py '%s' START" % current_ip
|
||||
self.new_command = True
|
||||
|
||||
def on_stop_button_pressed__slot(self):
|
||||
current_ip = self.ssh_host_line_edit.text()
|
||||
self.new_command_text = "python equipment_servicing_interface.py '%s' STOP" % current_ip
|
||||
self.new_command = True
|
||||
|
||||
def on_ssh_scan_for_hosts_pressed__slot(self):
|
||||
current_ip = self.ssh_host_line_edit.text()
|
||||
|
||||
find_dot = current_ip.rfind(".")
|
||||
|
||||
if find_dot > 0:
|
||||
current_ip = current_ip[:find_dot + 1] + "0"
|
||||
self.new_command_text = "nmap -sP %s/24 -oG - | awk '/Up$/{print $2}'" % current_ip
|
||||
self.new_command = True
|
||||
else:
|
||||
self.set_text_contents += "IP address for range search not valid. Try again."
|
||||
self.text_update_ready__signal.emit(self.set_text_contents)
|
||||
|
||||
def on_connect_ssid_button_pressed__slot(self):
|
||||
ssid_text = self.ssh_ssid_line_edit.text()
|
||||
|
||||
self.new_command_text = "sudo nmcli dev wifi connect %s" % ssid_text
|
||||
|
||||
self.new_command = True
|
||||
|
||||
def on_disconnect_ssid_button_pressed__slot(self):
|
||||
ssid_text = self.ssh_ssid_line_edit.text()
|
||||
|
||||
self.new_command_text = "sudo nmcli con down id %s ; sudo nmcli connection delete %s" % (ssid_text, ssid_text)
|
||||
|
||||
self.new_command = True
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.text_update_ready__signal.connect(self.console_text_edit.setText)
|
||||
self.ssh_console_command_line_edit.editingFinished.connect(self.on_text_editing_finished__slot)
|
||||
self.console_text_edit.textChanged.connect(self.on_text_readout_updated__slot)
|
||||
|
||||
self.ssh_scan_for_hosts_button.clicked.connect(self.on_ssh_scan_for_hosts_pressed__slot)
|
||||
|
||||
self.ssh_equipment_login_button.clicked.connect(self.on_login_button_pressed__slot)
|
||||
self.ssh_equipment_logout_button.clicked.connect(self.on_logout_button_pressed__slot)
|
||||
self.ssh_equipment_status_button.clicked.connect(self.on_status_button_pressed__slot)
|
||||
self.ssh_equipment_start_button.clicked.connect(self.on_start_button_pressed__slot)
|
||||
self.ssh_equipment_stop_button.clicked.connect(self.on_stop_button_pressed__slot)
|
||||
|
||||
self.ssh_list_wifi_button.clicked.connect(self.on_list_wifi_button_pressed__slot)
|
||||
self.ssh_connect_ssid_push_button.clicked.connect(self.on_connect_ssid_button_pressed__slot)
|
||||
self.ssh_disconnect_wifi_button.clicked.connect(self.on_disconnect_ssid_button_pressed__slot)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,257 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
import rospy
|
||||
|
||||
from rover_control.msg import MiningStatusMessage, MiningControlMessage, CameraControlMessage
|
||||
from rover_science.msg import SoilSensorStatusMessage
|
||||
from std_msgs.msg import Float64
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
MINING_STATUS_TOPIC = "/rover_control/mining/status"
|
||||
MINING_CONTROL_TOPIC = "/rover_control/mining/control"
|
||||
|
||||
SCALE_MEASUREMENT_TOPIC = "/rover_control/scale/measurement"
|
||||
|
||||
SOIL_PROBE_TOPIC = "/rover_science/soil_probe/data"
|
||||
|
||||
CAMERA_CONTROL_TOPIC = "/rover_control/camera/control"
|
||||
|
||||
TRAVEL_POSITION_LIFT = 110
|
||||
TRAVEL_POSITION_TILT = 1023
|
||||
|
||||
MEASURE_POSITION_LIFT = 350
|
||||
MEASURE_POSITION_TILT = 1023
|
||||
|
||||
SCOOP_POSITION_LIFT = 228
|
||||
SCOOP_POSITION_TILT = 215
|
||||
|
||||
PANORAMA_POSITION_LIFT = 535
|
||||
PANORAMA_POSITION_TILT = 995
|
||||
|
||||
SAMPLE_POSITION_LIFT = 220
|
||||
SAMPLE_POSITION_TILT = 240
|
||||
|
||||
PROBE_POSITION_LIFT = 950
|
||||
PROBE_POSITION_TILT = 440
|
||||
|
||||
|
||||
#####################################
|
||||
# UbiquitiRadioSettings Class Definition
|
||||
#####################################
|
||||
class Mining(QtCore.QObject):
|
||||
|
||||
lift_position_update_ready__signal = QtCore.pyqtSignal(int)
|
||||
tilt_position_update_ready__signal = QtCore.pyqtSignal(int)
|
||||
|
||||
weight_measurement_update_ready__signal = QtCore.pyqtSignal(int)
|
||||
|
||||
temp_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
moisture_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
loss_tangent_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
electrical_conductivity_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
real_dielectric_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
imaginary_dielectric_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(Mining, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
self.mining_qlcdnumber = self.left_screen.mining_qlcdnumber # type:QtWidgets.QLCDNumber
|
||||
self.mining_zero_button = self.left_screen.mining_zero_button # type:QtWidgets.QPushButton
|
||||
self.lift_position_progress_bar = self.left_screen.lift_position_progress_bar # type:QtWidgets.QProgressBar
|
||||
self.tilt_position_progress_bar = self.left_screen.tilt_position_progress_bar # type:QtWidgets.QProgressBar
|
||||
|
||||
self.mining_measure_move_button = self.left_screen.mining_measure_move_button # type:QtWidgets.QPushButton
|
||||
self.mining_transport_move_button = self.left_screen.mining_transport_move_button # type:QtWidgets.QPushButton
|
||||
self.mining_scoop_move_button = self.left_screen.mining_scoop_move_button # type:QtWidgets.QPushButton
|
||||
|
||||
self.mining_panorama_button = self.left_screen.mining_panorama_button # type:QtWidgets.QPushButton
|
||||
self.mining_sample_button = self.left_screen.mining_sample_button # type:QtWidgets.QPushButton
|
||||
self.mining_probe_button = self.left_screen.mining_probe_button # type:QtWidgets.QPushButton
|
||||
|
||||
self.science_temp_lcd_number = self.left_screen.science_temp_lcd_number # type:QtWidgets.QLCDNumber
|
||||
self.science_moisture_lcd_number = self.left_screen.science_moisture_lcd_number # type:QtWidgets.QLCDNumber
|
||||
self.science_loss_tangent_lcd_number = self.left_screen.science_loss_tangent_lcd_number # type:QtWidgets.QLCDNumber
|
||||
self.science_electrical_conductivity_lcd_number = self.left_screen.science_electrical_conductivity_lcd_number # type:QtWidgets.QLCDNumber
|
||||
self.science_real_dielectric_lcd_number = self.left_screen.science_real_dielectric_lcd_number # type:QtWidgets.QLCDNumber
|
||||
self.science_imaginary_dielectric_lcd_number = self.left_screen.science_imaginary_dielectric_lcd_number # type:QtWidgets.QLCDNumber
|
||||
|
||||
self.cam_lcd_output_button = self.left_screen.cam_lcd_output_button # type:QtWidgets.QPushButton
|
||||
self.cam_network_output_button = self.left_screen.cam_network_output_button # type:QtWidgets.QPushButton
|
||||
|
||||
self.cam_zoom_in_button = self.left_screen.cam_zoom_in_button # type:QtWidgets.QPushButton
|
||||
self.cam_zoom_out_button = self.left_screen.cam_zoom_out_button # type:QtWidgets.QPushButton
|
||||
self.cam_full_zoom_in_button = self.left_screen.cam_full_zoom_in_button # type:QtWidgets.QPushButton
|
||||
self.cam_full_zoom_out_button = self.left_screen.cam_full_zoom_out_button # type:QtWidgets.QPushButton
|
||||
|
||||
self.cam_shoot_button = self.left_screen.cam_shoot_button # type:QtWidgets.QPushButton
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.mining_status_subscriber = rospy.Subscriber(MINING_STATUS_TOPIC, MiningStatusMessage,
|
||||
self.mining_status_message_received__callback)
|
||||
|
||||
self.soil_probe_subscriber = rospy.Subscriber(SOIL_PROBE_TOPIC, SoilSensorStatusMessage, self.on_soil_probe_message_received__callback)
|
||||
self.scale_measurement_subscriber = rospy.Subscriber(SCALE_MEASUREMENT_TOPIC, Float64, self.on_scale_measurement_received__callback)
|
||||
|
||||
self.mining_control_publisher = rospy.Publisher(MINING_CONTROL_TOPIC, MiningControlMessage, queue_size=1)
|
||||
self.camera_control_publisher = rospy.Publisher(CAMERA_CONTROL_TOPIC, CameraControlMessage, queue_size=1)
|
||||
|
||||
self.current_scale_measurement = 0
|
||||
self.scale_zero_offset = 0
|
||||
|
||||
self.connect_signals_and_slots()
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.mining_zero_button.clicked.connect(self.on_mining_zero_clicked__slot)
|
||||
|
||||
self.mining_measure_move_button.clicked.connect(self.on_mining_move_measure_clicked__slot)
|
||||
self.mining_transport_move_button.clicked.connect(self.on_mining_move_transport_clicked__slot)
|
||||
self.mining_scoop_move_button.clicked.connect(self.on_mining_move_scoop_clicked__slot)
|
||||
|
||||
self.mining_panorama_button.clicked.connect(self.on_mining_move_panorama_clicked__slot)
|
||||
self.mining_sample_button.clicked.connect(self.on_mining_move_sample_clicked__slot)
|
||||
self.mining_probe_button.clicked.connect(self.on_mining_move_probe_clicked__slot)
|
||||
|
||||
self.tilt_position_update_ready__signal.connect(self.tilt_position_progress_bar.setValue)
|
||||
self.lift_position_update_ready__signal.connect(self.lift_position_progress_bar.setValue)
|
||||
|
||||
self.weight_measurement_update_ready__signal.connect(self.mining_qlcdnumber.display)
|
||||
|
||||
self.temp_update_ready__signal.connect(self.science_temp_lcd_number.display)
|
||||
self.moisture_update_ready__signal.connect(self.science_moisture_lcd_number.display)
|
||||
self.loss_tangent_update_ready__signal.connect(self.science_loss_tangent_lcd_number.display)
|
||||
self.electrical_conductivity_update_ready__signal.connect(self.science_electrical_conductivity_lcd_number.display)
|
||||
self.real_dielectric_update_ready__signal.connect(self.science_real_dielectric_lcd_number.display)
|
||||
self.imaginary_dielectric_update_ready__signal.connect(self.science_imaginary_dielectric_lcd_number.display)
|
||||
|
||||
self.cam_lcd_output_button.clicked.connect(self.on_cam_lcd_button_clicked__slot)
|
||||
self.cam_network_output_button.clicked.connect(self.on_cam_network_button_clicked__slot)
|
||||
|
||||
self.cam_zoom_in_button.clicked.connect(self.on_cam_zoom_in_button_clicked__slot)
|
||||
self.cam_zoom_out_button.clicked.connect(self.on_cam_zoom_out_button_clicked__slot)
|
||||
self.cam_full_zoom_in_button.clicked.connect(self.on_cam_full_zoom_in_button_clicked__slot)
|
||||
self.cam_full_zoom_out_button.clicked.connect(self.on_cam_full_zoom_out_button_clicked__slot)
|
||||
self.cam_shoot_button.clicked.connect(self.on_cam_shoot_button_clicked__slot)
|
||||
|
||||
def on_mining_zero_clicked__slot(self):
|
||||
self.scale_zero_offset = -self.current_scale_measurement
|
||||
|
||||
def on_mining_move_transport_clicked__slot(self):
|
||||
message = MiningControlMessage()
|
||||
message.tilt_set_absolute = TRAVEL_POSITION_TILT
|
||||
message.lift_set_absolute = TRAVEL_POSITION_LIFT
|
||||
message.cal_factor = -1
|
||||
|
||||
self.mining_control_publisher.publish(message)
|
||||
|
||||
def on_mining_move_measure_clicked__slot(self):
|
||||
message = MiningControlMessage()
|
||||
message.tilt_set_absolute = MEASURE_POSITION_TILT
|
||||
message.lift_set_absolute = MEASURE_POSITION_LIFT
|
||||
message.cal_factor = -1
|
||||
|
||||
self.mining_control_publisher.publish(message)
|
||||
|
||||
def on_mining_move_scoop_clicked__slot(self):
|
||||
message = MiningControlMessage()
|
||||
message.tilt_set_absolute = SCOOP_POSITION_TILT
|
||||
message.lift_set_absolute = SCOOP_POSITION_LIFT
|
||||
message.cal_factor = -1
|
||||
|
||||
self.mining_control_publisher.publish(message)
|
||||
|
||||
def on_mining_move_panorama_clicked__slot(self):
|
||||
message = MiningControlMessage()
|
||||
message.tilt_set_absolute = PANORAMA_POSITION_TILT
|
||||
message.lift_set_absolute = PANORAMA_POSITION_LIFT
|
||||
message.cal_factor = -1
|
||||
|
||||
self.mining_control_publisher.publish(message)
|
||||
|
||||
def on_mining_move_sample_clicked__slot(self):
|
||||
message = MiningControlMessage()
|
||||
message.tilt_set_absolute = SAMPLE_POSITION_TILT
|
||||
message.lift_set_absolute = SAMPLE_POSITION_LIFT
|
||||
message.cal_factor = -1
|
||||
|
||||
self.mining_control_publisher.publish(message)
|
||||
|
||||
def on_mining_move_probe_clicked__slot(self):
|
||||
message = MiningControlMessage()
|
||||
message.tilt_set_absolute = PROBE_POSITION_TILT
|
||||
message.lift_set_absolute = PROBE_POSITION_LIFT
|
||||
message.cal_factor = -1
|
||||
|
||||
self.mining_control_publisher.publish(message)
|
||||
|
||||
def on_cam_lcd_button_clicked__slot(self):
|
||||
message = CameraControlMessage()
|
||||
message.camera_mode = 1
|
||||
self.camera_control_publisher.publish(message)
|
||||
|
||||
def on_cam_network_button_clicked__slot(self):
|
||||
message = CameraControlMessage()
|
||||
message.camera_mode = 2
|
||||
self.camera_control_publisher.publish(message)
|
||||
|
||||
def on_cam_zoom_in_button_clicked__slot(self):
|
||||
message = CameraControlMessage()
|
||||
message.zoom_in = 1
|
||||
self.camera_control_publisher.publish(message)
|
||||
|
||||
def on_cam_zoom_out_button_clicked__slot(self):
|
||||
message = CameraControlMessage()
|
||||
message.zoom_out = 1
|
||||
self.camera_control_publisher.publish(message)
|
||||
|
||||
def on_cam_full_zoom_in_button_clicked__slot(self):
|
||||
message = CameraControlMessage()
|
||||
message.full_zoom_in = 1
|
||||
self.camera_control_publisher.publish(message)
|
||||
|
||||
def on_cam_full_zoom_out_button_clicked__slot(self):
|
||||
message = CameraControlMessage()
|
||||
message.full_zoom_out = 1
|
||||
self.camera_control_publisher.publish(message)
|
||||
|
||||
def on_cam_shoot_button_clicked__slot(self):
|
||||
message = CameraControlMessage()
|
||||
message.shoot = 1
|
||||
self.camera_control_publisher.publish(message)
|
||||
|
||||
def mining_status_message_received__callback(self, status):
|
||||
status = status # type:MiningStatusMessage
|
||||
self.tilt_position_update_ready__signal.emit(status.tilt_position)
|
||||
self.lift_position_update_ready__signal.emit(status.lift_position)
|
||||
|
||||
def on_soil_probe_message_received__callback(self, data):
|
||||
self.temp_update_ready__signal.emit(data.temp_c)
|
||||
self.moisture_update_ready__signal.emit(data.moisture)
|
||||
self.loss_tangent_update_ready__signal.emit(data.loss_tangent)
|
||||
self.electrical_conductivity_update_ready__signal.emit(data.soil_electrical_conductivity)
|
||||
self.real_dielectric_update_ready__signal.emit(data.real_dielectric_permittivity)
|
||||
self.imaginary_dielectric_update_ready__signal.emit(data.imaginary_dielectric_permittivity)
|
||||
|
||||
def on_scale_measurement_received__callback(self, data):
|
||||
grams = data.data * 1000
|
||||
self.current_scale_measurement = grams
|
||||
self.weight_measurement_update_ready__signal.emit(grams + self.scale_zero_offset)
|
||||
@@ -0,0 +1,312 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
import rospy
|
||||
from time import time
|
||||
|
||||
from rover_arm.msg import ArmControlMessage, ArmStatusMessage
|
||||
from rover_control.msg import GripperControlMessage
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
ARM_RELATIVE_CONTROL_TOPIC = "/rover_arm/control/relative"
|
||||
ARM_ABSOLUTE_CONTROL_TOPIC = "/rover_arm/control/absolute"
|
||||
ARM_STATUS_TOPIC = "/rover_arm/status"
|
||||
|
||||
GRIPPER_CONTROL_TOPIC = "/rover_control/gripper/control"
|
||||
|
||||
THREAD_HERTZ = 5
|
||||
|
||||
POSITIONAL_TOLERANCE = 0.02
|
||||
|
||||
# Order is [base, shoulder, elbow, roll, wrist_pitch, wrist_roll]
|
||||
ARM_STOW_PROCEDURE = [
|
||||
[0.0, -0.035, -0.28, 0.0, 0.0, 0.0], # Out in front of rover
|
||||
[0.0, -0.035, -0.28, -0.25, 0.25, 0.0],
|
||||
[0.0, -0.035, -0.5, -0.25, 0.25, 0.0],
|
||||
[0.0, -0.25, -0.5, -0.25, 0.25, -0.25]
|
||||
]
|
||||
|
||||
ARM_UNSTOW_PROCEDURE = [
|
||||
[0.0, -0.25, -0.5, -0.25, 0.25, -0.25],
|
||||
[0.0, -0.035, -0.5, -0.25, 0.25, 0.0],
|
||||
[0.0, -0.035, -0.28, -0.25, 0.25, 0.0],
|
||||
[0.0, -0.035, -0.28, 0.0, 0.0, 0.0]
|
||||
]
|
||||
|
||||
APPROACH_O2 = [
|
||||
[0.0, -0.035, -0.28, 0.0, 0.0, 0.0], # Out in front of rover
|
||||
[0.0, 0.0485472094896, -0.273685193022, -0.00102834607876, -0.17200773156, 0], # Correct positioning out to left side of rover
|
||||
[-0.436250296246, 0.0485472094896, -0.273685193022, -0.00102834607876, -0.17200773156, 0] # Directly positioned, ready to grip
|
||||
]
|
||||
|
||||
APPROACH_BEACON = [
|
||||
[0.0, -0.035, -0.28, 0.0, 0.0, 0.0], # Out in front of rover
|
||||
[0.0, 0.0361371382336, -0.325145836608, -0.00731261537597, -0.129662333807, 0.0569339269566], # Correct positioning out to right side of rover
|
||||
[0.458505849045, 0.0361371382336, -0.325145633259, -0.00731200471916, -0.131140162948, 0.0920117742056]
|
||||
]
|
||||
|
||||
|
||||
#####################################
|
||||
# UbiquitiRadioSettings Class Definition
|
||||
#####################################
|
||||
class MiscArm(QtCore.QThread):
|
||||
def __init__(self, shared_objects):
|
||||
super(MiscArm, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
self.arm_control_upright_zeroed_button = self.left_screen.arm_control_upright_zeroed_button # type:QtWidgets.QPushButton
|
||||
self.arm_controls_stow_arm_button = self.left_screen.arm_controls_stow_arm_button # type:QtWidgets.QPushButton
|
||||
self.arm_controls_unstow_arm_button = self.left_screen.arm_controls_unstow_arm_button # type:QtWidgets.QPushButton
|
||||
|
||||
# ##### FIXME #####
|
||||
# Remove these once the arm is fixed
|
||||
self.arm_controls_stow_arm_button.setEnabled(False)
|
||||
self.arm_controls_unstow_arm_button.setEnabled(False)
|
||||
# #################
|
||||
|
||||
self.arm_controls_calibration_button = self.left_screen.arm_controls_calibration_button # type:QtWidgets.QPushButton
|
||||
self.arm_controls_clear_faults_button = self.left_screen.arm_controls_clear_faults_button # type:QtWidgets.QPushButton
|
||||
self.arm_controls_reset_motor_drivers_button = self.left_screen.arm_controls_reset_motor_drivers_button # type:QtWidgets.QPushButton
|
||||
|
||||
self.arm_controls_approach_o2_button = self.left_screen.arm_controls_approach_o2_button # type:QtWidgets.QPushButton
|
||||
self.arm_controls_depart_o2_button = self.left_screen.arm_controls_depart_o2_button # type:QtWidgets.QPushButton
|
||||
self.arm_controls_approach_beacon_button = self.left_screen.arm_controls_approach_beacon_button # type:QtWidgets.QPushButton
|
||||
self.arm_controls_depart_beacon_button = self.left_screen.arm_controls_depart_beacon_button # type:QtWidgets.QPushButton
|
||||
|
||||
self.gripper_home_button = self.left_screen.gripper_home_button # type:QtWidgets.QPushButton
|
||||
self.gripper_toggle_light_button = self.left_screen.gripper_toggle_light_button # type:QtWidgets.QPushButton
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.wait_time = 1.0 / THREAD_HERTZ
|
||||
|
||||
self.arm_status_subscriber = rospy.Subscriber(ARM_STATUS_TOPIC, ArmStatusMessage,
|
||||
self.new_arm_status_message_received__callback)
|
||||
|
||||
self.arm_relative_control_publisher = rospy.Publisher(ARM_RELATIVE_CONTROL_TOPIC, ArmControlMessage,
|
||||
queue_size=1)
|
||||
self.arm_absolute_control_publisher = rospy.Publisher(ARM_ABSOLUTE_CONTROL_TOPIC, ArmControlMessage,
|
||||
queue_size=1)
|
||||
|
||||
self.gripper_control_publisher = rospy.Publisher(GRIPPER_CONTROL_TOPIC, GripperControlMessage, queue_size=1)
|
||||
|
||||
self.base_position = 0
|
||||
self.shoulder_position = 0
|
||||
self.elbow_position = 0
|
||||
self.roll_position = 0
|
||||
self.wrist_pitch_position = 0
|
||||
self.wrist_roll_position = 0
|
||||
|
||||
self.should_stow_arm = False
|
||||
self.should_unstow_arm = False
|
||||
|
||||
self.should_approach_o2 = False
|
||||
self.should_depart_o2 = False
|
||||
self.should_approach_beacon = False
|
||||
self.should_depart_beacon = False
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting MiscArm Thread")
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
if self.should_stow_arm:
|
||||
self.stow_rover_arm()
|
||||
self.should_stow_arm = False
|
||||
elif self.should_unstow_arm:
|
||||
self.unstow_rover_arm()
|
||||
self.should_unstow_arm = False
|
||||
|
||||
elif self.should_approach_o2:
|
||||
self.approach_o2()
|
||||
self.should_approach_o2 = False
|
||||
|
||||
elif self.should_depart_o2:
|
||||
self.depart_o2()
|
||||
self.should_depart_o2 = False
|
||||
|
||||
elif self.should_approach_beacon:
|
||||
self.approach_beacon()
|
||||
self.should_approach_beacon = False
|
||||
|
||||
elif self.should_depart_beacon:
|
||||
self.depart_beacon()
|
||||
self.should_depart_beacon = False
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int(self.wait_time - time_diff), 0))
|
||||
|
||||
self.logger.debug("Stopping MiscArm Thread")
|
||||
|
||||
def stow_rover_arm(self):
|
||||
for movement in ARM_STOW_PROCEDURE:
|
||||
self.process_absolute_move_command(movement)
|
||||
|
||||
def unstow_rover_arm(self):
|
||||
for movement in ARM_UNSTOW_PROCEDURE:
|
||||
self.process_absolute_move_command(movement)
|
||||
|
||||
def approach_o2(self):
|
||||
for movement in APPROACH_O2:
|
||||
self.process_absolute_move_command(movement)
|
||||
|
||||
def depart_o2(self):
|
||||
for movement in reversed(APPROACH_O2):
|
||||
self.process_absolute_move_command(movement)
|
||||
|
||||
def approach_beacon(self):
|
||||
for movement in APPROACH_BEACON:
|
||||
self.process_absolute_move_command(movement)
|
||||
|
||||
def depart_beacon(self):
|
||||
for movement in reversed(APPROACH_BEACON):
|
||||
self.process_absolute_move_command(movement)
|
||||
|
||||
def process_absolute_move_command(self, movement):
|
||||
message = ArmControlMessage()
|
||||
|
||||
message.base = movement[0]
|
||||
message.shoulder = movement[1]
|
||||
message.elbow = movement[2]
|
||||
message.roll = movement[3]
|
||||
message.wrist_pitch = movement[4]
|
||||
message.wrist_roll = movement[5]
|
||||
|
||||
self.arm_absolute_control_publisher.publish(message)
|
||||
|
||||
self.wait_for_targets_reached(movement)
|
||||
self.msleep(250)
|
||||
|
||||
def wait_for_targets_reached(self, movement):
|
||||
base_set = movement[0]
|
||||
shoulder_set = movement[1]
|
||||
elbow_set = movement[2]
|
||||
roll_set = movement[3]
|
||||
wrist_pitch_set = movement[4]
|
||||
wrist_roll_set = movement[5] - (wrist_pitch_set / 2.0)
|
||||
|
||||
while abs(self.base_position - base_set) > POSITIONAL_TOLERANCE:
|
||||
# self.logger.debug("Waiting for base| %f\t%f" % (self.base_position, base_set))
|
||||
self.msleep(10)
|
||||
|
||||
while abs(self.shoulder_position - shoulder_set) > POSITIONAL_TOLERANCE:
|
||||
# self.logger.debug("Waiting for shoulder| %f\t%f" % (self.shoulder_position, shoulder_set))
|
||||
self.msleep(10)
|
||||
|
||||
while abs(self.elbow_position - elbow_set) > POSITIONAL_TOLERANCE:
|
||||
# self.logger.debug("Waiting for elbow| %f\t%f" % (self.elbow_position, elbow_set))
|
||||
self.msleep(10)
|
||||
|
||||
while abs(self.roll_position - roll_set) > POSITIONAL_TOLERANCE:
|
||||
# self.logger.debug("Waiting for roll| %f\t%f" % (self.roll_position, roll_set))
|
||||
self.msleep(10)
|
||||
|
||||
while abs(self.wrist_pitch_position - wrist_pitch_set) > POSITIONAL_TOLERANCE:
|
||||
# self.logger.debug("Waiting for wrist_pitch| %f\t%f" % (self.wrist_pitch_position, wrist_pitch_set))
|
||||
self.msleep(10)
|
||||
|
||||
while abs(self.wrist_roll_position - wrist_roll_set) > POSITIONAL_TOLERANCE:
|
||||
# self.logger.debug("Waiting for wrist_roll| %f\t%f" % (self.wrist_roll_position, wrist_roll_set))
|
||||
self.msleep(10)
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.arm_controls_stow_arm_button.clicked.connect(self.on_stow_arm_button_pressed__slot)
|
||||
self.arm_controls_unstow_arm_button.clicked.connect(self.on_unstow_arm_button_pressed__slot)
|
||||
self.arm_control_upright_zeroed_button.clicked.connect(self.on_upright_zeroed_button_pressed__slot)
|
||||
|
||||
self.arm_controls_calibration_button.clicked.connect(self.on_set_calibration_button_pressed__slot)
|
||||
self.arm_controls_clear_faults_button.clicked.connect(self.on_clear_faults_button_pressed__slot)
|
||||
self.arm_controls_reset_motor_drivers_button.clicked.connect(self.on_reset_drivers_button_pressed__slot)
|
||||
|
||||
self.arm_controls_approach_o2_button.clicked.connect(self.on_approach_o2_button_pressed__slot)
|
||||
self.arm_controls_approach_beacon_button.clicked.connect(self.on_approach_beacon_button_pressed__slot)
|
||||
|
||||
self.gripper_home_button.clicked.connect(self.on_gripper_home_pressed)
|
||||
self.gripper_toggle_light_button.clicked.connect(self.on_gripper_toggle_light_pressed)
|
||||
|
||||
def on_upright_zeroed_button_pressed__slot(self):
|
||||
self.process_absolute_move_command([0 for _ in range(6)])
|
||||
|
||||
def on_set_calibration_button_pressed__slot(self):
|
||||
message = ArmControlMessage()
|
||||
message.calibrate = True
|
||||
self.arm_relative_control_publisher.publish(message)
|
||||
|
||||
def on_clear_faults_button_pressed__slot(self):
|
||||
message = ArmControlMessage()
|
||||
message.clear_faults = True
|
||||
self.arm_relative_control_publisher.publish(message)
|
||||
|
||||
def on_reset_drivers_button_pressed__slot(self):
|
||||
message = ArmControlMessage()
|
||||
message.reset_controllers = True
|
||||
self.arm_relative_control_publisher.publish(message)
|
||||
|
||||
def on_stow_arm_button_pressed__slot(self):
|
||||
self.should_stow_arm = True
|
||||
|
||||
def on_unstow_arm_button_pressed__slot(self):
|
||||
self.should_unstow_arm = True
|
||||
|
||||
def on_approach_o2_button_pressed__slot(self):
|
||||
self.should_approach_o2 = True
|
||||
|
||||
def on_depart_o2_button_pressed__slot(self):
|
||||
self.should_depart_o2 = True
|
||||
|
||||
def on_approach_beacon_button_pressed__slot(self):
|
||||
self.should_approach_beacon = True
|
||||
|
||||
def on_depart_beacon_button_pressed__slot(self):
|
||||
self.should_depart_beacon = True
|
||||
|
||||
def on_gripper_home_pressed(self):
|
||||
message = GripperControlMessage()
|
||||
message.gripper_mode = 2
|
||||
message.gripper_position_absolute = -1
|
||||
message.should_home = True
|
||||
|
||||
self.gripper_control_publisher.publish(message)
|
||||
|
||||
def on_gripper_toggle_light_pressed(self):
|
||||
message = GripperControlMessage()
|
||||
message.gripper_mode = 2
|
||||
message.toggle_light = True
|
||||
message.gripper_position_absolute = -1
|
||||
|
||||
self.gripper_control_publisher.publish(message)
|
||||
|
||||
def new_arm_status_message_received__callback(self, data):
|
||||
self.base_position = data.base
|
||||
self.shoulder_position = data.shoulder
|
||||
self.elbow_position = data.elbow
|
||||
self.roll_position = data.roll
|
||||
self.wrist_pitch_position = data.wrist_pitch
|
||||
self.wrist_roll_position = data.wrist_roll
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,166 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
import rospy
|
||||
from time import time
|
||||
import scipy.fftpack
|
||||
import numpy
|
||||
|
||||
from std_msgs.msg import Float64MultiArray
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
RDF_DATA_TOPIC = "/rover_science/rdf/data"
|
||||
|
||||
THREAD_HERTZ = 5
|
||||
|
||||
|
||||
COLOR_GREEN = "background-color:darkgreen;"
|
||||
COLOR_RED = "background-color:darkred;"
|
||||
|
||||
|
||||
ALLOWED_RDF_VARIANCE = 0.15
|
||||
|
||||
|
||||
#####################################
|
||||
# UbiquitiRadioSettings Class Definition
|
||||
#####################################
|
||||
class RDF(QtCore.QThread):
|
||||
|
||||
rssi_lcd_number_update_ready__signal = QtCore.pyqtSignal(int)
|
||||
beacon_lcd_number_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
|
||||
beacon_valid_text_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
beacon_valid_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(RDF, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
self.rssi_lcdnumber = self.left_screen.rssi_lcdnumber # type:QtWidgets.QLCDNumber
|
||||
self.beacon_frequency_lcd_number = self.left_screen.beacon_frequency_lcd_number # type:QtWidgets.QLCDNumber
|
||||
self.beacon_frequency_valid_label = self.left_screen.beacon_frequency_valid_label # type:QtWidgets.QLCDNumber
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.wait_time = 1.0 / THREAD_HERTZ
|
||||
|
||||
self.rdf_subscriber = rospy.Subscriber(RDF_DATA_TOPIC, Float64MultiArray, self.new_rdf_message_received__callback)
|
||||
|
||||
self.moving_average_raw_data = []
|
||||
|
||||
self.raw_data = numpy.array([])
|
||||
self.raw_data_timestamps = numpy.array([])
|
||||
self.data_window_size = 200
|
||||
|
||||
self.previous_frequencies = []
|
||||
self.num_previous_frequencies = 3
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting RDF Thread")
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
temp = list(self.moving_average_raw_data)
|
||||
if temp:
|
||||
average = sum(temp) / len(temp)
|
||||
self.rssi_lcd_number_update_ready__signal.emit(average)
|
||||
|
||||
if self.raw_data.size >= self.data_window_size and self.raw_data_timestamps.size >= self.data_window_size:
|
||||
|
||||
try:
|
||||
time_step_array = numpy.array([])
|
||||
for n in range(0, self.data_window_size - 1):
|
||||
time_step_array = numpy.append(time_step_array, self.raw_data_timestamps[n + 1] - self.raw_data_timestamps[n])
|
||||
|
||||
T = numpy.average(time_step_array)
|
||||
|
||||
yf = scipy.fftpack.fft(self.raw_data)
|
||||
xf = numpy.linspace(0.0, 1.0 / (2.0 * T), self.data_window_size / 2)
|
||||
|
||||
valid_range = []
|
||||
|
||||
for n in range(0, len(xf)):
|
||||
if (xf[n] > 0.5) and (xf[n] <= 5.0):
|
||||
valid_range.append(n)
|
||||
|
||||
yf = numpy.take(yf, valid_range)
|
||||
xf = numpy.take(xf, valid_range)
|
||||
|
||||
max_index = numpy.argmax(numpy.abs(yf))
|
||||
freq = xf[max_index]
|
||||
|
||||
if len(self.previous_frequencies) == self.num_previous_frequencies:
|
||||
del self.previous_frequencies[0]
|
||||
|
||||
self.previous_frequencies.append(freq)
|
||||
|
||||
if len(self.previous_frequencies) == self.num_previous_frequencies:
|
||||
variance_too_large = False
|
||||
|
||||
if abs(self.previous_frequencies[0] - self.previous_frequencies[1]) > ALLOWED_RDF_VARIANCE:
|
||||
variance_too_large = True
|
||||
|
||||
if abs(self.previous_frequencies[0] - self.previous_frequencies[2]) > ALLOWED_RDF_VARIANCE:
|
||||
variance_too_large = True
|
||||
|
||||
if abs(self.previous_frequencies[1] - self.previous_frequencies[2]) > ALLOWED_RDF_VARIANCE:
|
||||
variance_too_large = True
|
||||
|
||||
self.beacon_valid_stylesheet_change_ready__signal.emit(COLOR_GREEN if not variance_too_large else COLOR_RED)
|
||||
self.beacon_valid_text_change_ready__signal.emit("Yes" if not variance_too_large else "No")
|
||||
|
||||
self.beacon_lcd_number_update_ready__signal.emit(freq)
|
||||
except Exception, e:
|
||||
print e
|
||||
|
||||
self.raw_data = numpy.array([])
|
||||
self.raw_data_timestamps = numpy.array([])
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int(self.wait_time - time_diff), 0))
|
||||
|
||||
self.logger.debug("Stopping RDF Thread")
|
||||
|
||||
def new_rdf_message_received__callback(self, data):
|
||||
if len(self.moving_average_raw_data) >= self.data_window_size:
|
||||
del self.moving_average_raw_data[0]
|
||||
|
||||
self.moving_average_raw_data.append(data.data[0])
|
||||
|
||||
if self.raw_data.size != self.data_window_size and self.raw_data_timestamps.size != self.data_window_size:
|
||||
self.raw_data = numpy.append(self.raw_data, data.data[0])
|
||||
self.raw_data_timestamps = numpy.append(self.raw_data_timestamps, data.data[1])
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.rssi_lcd_number_update_ready__signal.connect(self.rssi_lcdnumber.display)
|
||||
self.beacon_lcd_number_update_ready__signal.connect(self.beacon_frequency_lcd_number.display)
|
||||
|
||||
self.beacon_valid_text_change_ready__signal.connect(self.beacon_frequency_valid_label.setText)
|
||||
self.beacon_valid_stylesheet_change_ready__signal.connect(self.beacon_frequency_valid_label.setStyleSheet)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,182 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
from time import time
|
||||
import PIL.Image
|
||||
from PIL.ImageQt import ImageQt
|
||||
from random import random
|
||||
import rospy
|
||||
from tf import transformations
|
||||
from scipy.interpolate import interp1d
|
||||
import math
|
||||
from sensor_msgs.msg import Imu
|
||||
from Resources.Settings import MappingSettings as MappingSettings
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
THREAD_HERTZ = 20
|
||||
|
||||
ROTATION_SPEED_MODIFIER = 2.5
|
||||
|
||||
IMU_DATA_TOPIC = "/rover_odometry/imu/data"
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class SpeedAndHeadingIndication(QtCore.QThread):
|
||||
show_compass_image__signal = QtCore.pyqtSignal()
|
||||
heading_text_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
new_speed_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
pitch_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
roll_update_ready__signal = QtCore.pyqtSignal(float)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(SpeedAndHeadingIndication, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.right_screen = self.shared_objects["screens"]["right_screen"]
|
||||
|
||||
self.heading_compass_label = self.right_screen.heading_compass_label # type: QtWidgets.QLabel
|
||||
self.heading_text_label = self.right_screen.current_heading_label # type: QtWidgets.QLabel
|
||||
self.next_goal_label = self.right_screen.next_goal_label # type: QtWidgets.QLabel
|
||||
self.current_speed_label = self.right_screen.current_speed_label # type: QtWidgets.QLabel
|
||||
|
||||
self.imu_pitch_lcd_number = self.right_screen.imu_pitch_lcd_number # type: QtWidgets.QLCDNumber
|
||||
self.imu_roll_lcd_number = self.right_screen.imu_roll_lcd_number # type: QtWidgets.QLCDNumber
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.wait_time = 1.0 / THREAD_HERTZ
|
||||
|
||||
self.main_compass_image = PIL.Image.open("Resources/Images/compass.png").resize((300, 300), PIL.Image.ANTIALIAS)
|
||||
self.compass_pixmap = None
|
||||
|
||||
self.current_heading = 0
|
||||
self.current_heading_changed = True
|
||||
|
||||
self.shown_heading = (self.current_heading + (1.5 * ROTATION_SPEED_MODIFIER)) % 360
|
||||
self.current_heading_shown_rotation_angle = 0
|
||||
self.last_current_heading_shown = -1000
|
||||
self.rotation_direction = 1
|
||||
|
||||
self.imu_data = None
|
||||
self.new_imu_data = False
|
||||
|
||||
self.yaw = None
|
||||
self.pitch = None
|
||||
self.roll = None
|
||||
|
||||
self.euler_interpolator = interp1d([math.pi, -math.pi], [-180, 180])
|
||||
|
||||
self.imu_data_subscriber = rospy.Subscriber(IMU_DATA_TOPIC, Imu, self.on_imu_data_received)
|
||||
|
||||
def run(self):
|
||||
self.on_heading_changed__slot(self.current_heading)
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
if self.new_imu_data:
|
||||
self.calculate_euler_from_imu()
|
||||
self.new_imu_data = False
|
||||
|
||||
if self.current_heading_changed:
|
||||
self.update_heading_movement()
|
||||
self.current_heading_changed = False
|
||||
|
||||
self.rotate_compass_if_needed()
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int(self.wait_time - time_diff), 0))
|
||||
|
||||
def calculate_euler_from_imu(self):
|
||||
quat = (
|
||||
self.imu_data.orientation.x,
|
||||
self.imu_data.orientation.y,
|
||||
self.imu_data.orientation.z,
|
||||
self.imu_data.orientation.w,
|
||||
)
|
||||
self.roll, self.pitch, self.yaw = transformations.euler_from_quaternion(quat)
|
||||
self.current_heading = (self.euler_interpolator(self.yaw) + MappingSettings.DECLINATION_OFFSET) % 360
|
||||
|
||||
self.pitch_update_ready__signal.emit(self.pitch)
|
||||
self.roll_update_ready__signal.emit(self.roll)
|
||||
|
||||
def rotate_compass_if_needed(self):
|
||||
|
||||
self.current_heading_shown_rotation_angle = int(self.current_heading)
|
||||
|
||||
if self.current_heading_shown_rotation_angle != self.last_current_heading_shown:
|
||||
new_compass_image = self.main_compass_image.rotate(self.current_heading_shown_rotation_angle, resample=PIL.Image.BICUBIC)
|
||||
self.last_current_heading_shown = self.current_heading_shown_rotation_angle
|
||||
|
||||
self.compass_pixmap = QtGui.QPixmap.fromImage(ImageQt(new_compass_image))
|
||||
self.show_compass_image__signal.emit()
|
||||
self.heading_text_update_ready__signal.emit(str(self.current_heading_shown_rotation_angle) + "°")
|
||||
|
||||
def update_heading_movement(self):
|
||||
current_minus_shown = (self.current_heading - self.shown_heading) % 360
|
||||
|
||||
if current_minus_shown >= 180:
|
||||
self.rotation_direction = -1
|
||||
else:
|
||||
self.rotation_direction = 1
|
||||
|
||||
def on_heading_changed__slot(self, new_heading):
|
||||
self.current_heading = new_heading
|
||||
self.heading_text_update_ready__signal.emit(str(new_heading) + "°")
|
||||
self.current_heading_changed = True
|
||||
|
||||
def __on_heading_clicked__slot(self, event):
|
||||
new_heading = self.current_heading
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
new_heading = (self.current_heading + 5) % 360
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
new_heading = (self.current_heading - 5) % 360
|
||||
|
||||
self.on_heading_changed__slot(new_heading)
|
||||
self.new_speed_update_ready__signal.emit("%.2f" % (random() * 2.5))
|
||||
self.heading_text_update_ready__signal.emit(str(new_heading) + "°")
|
||||
|
||||
def on_new_compass_image_ready__slot(self):
|
||||
self.heading_compass_label.setPixmap(self.compass_pixmap)
|
||||
|
||||
def on_imu_data_received(self, data):
|
||||
self.imu_data = data
|
||||
self.new_imu_data = True
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.show_compass_image__signal.connect(self.on_new_compass_image_ready__slot)
|
||||
self.heading_text_update_ready__signal.connect(self.heading_text_label.setText)
|
||||
self.heading_text_update_ready__signal.connect(self.next_goal_label.setText)
|
||||
self.new_speed_update_ready__signal.connect(self.current_speed_label.setText)
|
||||
|
||||
self.heading_compass_label.mousePressEvent = self.__on_heading_clicked__slot
|
||||
|
||||
self.pitch_update_ready__signal.connect(self.imu_pitch_lcd_number.display)
|
||||
self.roll_update_ready__signal.connect(self.imu_roll_lcd_number.display)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,324 @@
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
import rospy
|
||||
|
||||
from sensor_msgs.msg import NavSatFix
|
||||
|
||||
GPS_POSITION_TOPIC = "/rover_odometry/fix"
|
||||
|
||||
|
||||
class WaypointsCoordinator(QtCore.QThread):
|
||||
new_manual_waypoint_entry = QtCore.pyqtSignal(str, float, float, int)
|
||||
update_waypoint_entry = QtCore.pyqtSignal(str, str, str, int)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(WaypointsCoordinator, self).__init__()
|
||||
self.run_thread_flag = True
|
||||
self.navigation_table_cur_click = None
|
||||
self.landmark_table_cur_click = None
|
||||
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
self.navigation_label = (self.left_screen.
|
||||
navigation_waypoints_table_widget)
|
||||
self.landmark_label = self.left_screen.landmark_waypoints_table_widget
|
||||
|
||||
self.name_edit_label = self.left_screen.manual_waypoint_landmark_name_line_edit
|
||||
self.latitude_label = self.left_screen.manual_waypoint_decimal_lattitude_spin_box
|
||||
self.longitude_label = self.left_screen.manual_waypoint_decimal_longitude_spin_box
|
||||
|
||||
self.latitude_degree_label = self.left_screen.manual_waypoint_degrees_lattitude_spin_box
|
||||
|
||||
self.longitude_degree_label = self.left_screen.manual_waypoint_degrees_longitude_spin_box
|
||||
|
||||
self.latitude_minute_label = self.left_screen.manual_waypoint_minutes_lattitude_spin_box
|
||||
|
||||
self.longitude_minute_label = self.left_screen.manual_waypoint_minutes_longitude_spin_box
|
||||
|
||||
self.latitude_second_label = self.left_screen.manual_waypoint_seconds_lattitude_spin_box
|
||||
|
||||
self.longitude_second_label = self.left_screen.manual_waypoint_seconds_longitude_spin_box
|
||||
|
||||
self.latitude_card_label = self.left_screen.manual_waypoint_cardinal_lattitude_combo_box
|
||||
|
||||
self.longitude_card_label = self.left_screen.manual_waypoint_cardinal_longitude_combo_box
|
||||
|
||||
# Color Labels and Buttons
|
||||
self.nav_color_label = self.left_screen.manual_waypoint_navigation_color_label
|
||||
self.nav_color_set_button = self.left_screen.manual_waypoint_navigation_color_set_button
|
||||
|
||||
self.landmark_color_label = self.left_screen.manual_waypoint_landmark_color_label
|
||||
self.landmark_color_set_button = self.left_screen.manual_waypoint_landmark_color_set_button
|
||||
|
||||
# Nav Table Buttons
|
||||
self.nav_set_button_label = (self.left_screen.
|
||||
navigation_waypoints_set_button)
|
||||
self.nav_add_manual_button_label = (
|
||||
self.left_screen.navigation_waypoints_add_manual_button)
|
||||
self.nav_add_gps_button_label = (self.left_screen.
|
||||
navigation_waypoints_add_gps_button)
|
||||
self.nav_delete_button_label = (self.left_screen.
|
||||
navigation_waypoints_delete_button)
|
||||
|
||||
# Land Table Buttons
|
||||
self.land_set_button_label = (self.left_screen.
|
||||
landmark_waypoints_set_button)
|
||||
|
||||
self.land_add_manual_button_label = (
|
||||
self.left_screen.landmark_waypoints_add_manual_button)
|
||||
|
||||
self.land_add_gps_button_label = (self.left_screen.
|
||||
landmark_waypoints_add_gps_button)
|
||||
|
||||
self.land_delete_button_label = (self.left_screen.
|
||||
landmark_waypoints_delete_button)
|
||||
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
self.gps_position_subscriber = rospy.Subscriber(GPS_POSITION_TOPIC, NavSatFix,
|
||||
self.gps_position_updated_callback)
|
||||
|
||||
self.longitude = None
|
||||
self.latitude = None
|
||||
|
||||
self.nav_color = QtCore.Qt.yellow
|
||||
self.landmark_color = QtCore.Qt.blue
|
||||
|
||||
self.nav_color_dialog = QtWidgets.QColorDialog()
|
||||
self.nav_color_dialog.setWindowFlags(
|
||||
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.X11BypassWindowManagerHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
|
||||
|
||||
self.landmark_color_dialog = QtWidgets.QColorDialog()
|
||||
self.landmark_color_dialog.setWindowFlags(
|
||||
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.X11BypassWindowManagerHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
|
||||
|
||||
def run(self):
|
||||
while self.run_thread_flag:
|
||||
self.msleep(3)
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.new_manual_waypoint_entry.connect(self.update_manual_entry)
|
||||
|
||||
# setting up signals for Navigation Table
|
||||
self.nav_add_gps_button_label.clicked.connect(self._nav_add_gps)
|
||||
self.nav_delete_button_label.clicked.connect(self._nav_del)
|
||||
self.nav_add_manual_button_label.clicked.connect(self._nav_add_manual)
|
||||
self.nav_set_button_label.clicked.connect(self._nav_save)
|
||||
|
||||
# setting up signals for Landmark Table
|
||||
self.land_add_gps_button_label.clicked.connect(self._land_add_gps)
|
||||
self.land_delete_button_label.clicked.connect(self._land_del)
|
||||
self.land_add_manual_button_label.clicked.connect(self.
|
||||
_land_add_manual)
|
||||
self.land_set_button_label.clicked.connect(self._land_save)
|
||||
|
||||
self.navigation_label.cellClicked.connect(self._on_nav_clicked)
|
||||
self.landmark_label.cellClicked.connect(self._on_land_clicked)
|
||||
|
||||
self.nav_color_set_button.clicked.connect(self.nav_color_dialog.show)
|
||||
self.landmark_color_set_button.clicked.connect(self.landmark_color_dialog.show)
|
||||
|
||||
self.nav_color_dialog.currentColorChanged.connect(self.__on_new_nav_color_selected)
|
||||
self.landmark_color_dialog.currentColorChanged.connect(self.__on_new_landmark_color_selected)
|
||||
|
||||
def _add_to_table(self, name, lat, lng, color, table):
|
||||
color_table_item = QtWidgets.QTableWidgetItem()
|
||||
color_table_item.setBackground(color)
|
||||
|
||||
count = table.rowCount()
|
||||
table.insertRow(count)
|
||||
table.setItem(count, 0, QtWidgets.QTableWidgetItem(name))
|
||||
table.setItem(count, 1, QtWidgets.QTableWidgetItem(lat))
|
||||
table.setItem(count, 2, QtWidgets.QTableWidgetItem(lng))
|
||||
table.setItem(count, 3, color_table_item)
|
||||
|
||||
def _clear_inputs(self):
|
||||
self.name_edit_label.clear()
|
||||
self.latitude_label.clear()
|
||||
self.longitude_label.clear()
|
||||
|
||||
def _is_empty_inputs(self):
|
||||
if not self.name_edit_label.text():
|
||||
return True
|
||||
if not self.latitude_label.text():
|
||||
return True
|
||||
if not self.longitude_label.text():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _nav_add_gps(self):
|
||||
if self.longitude and self.latitude:
|
||||
name = self.navigation_label.rowCount()
|
||||
self._add_to_table(str(name + 1), str(self.latitude),
|
||||
str(self.longitude), self.nav_color,
|
||||
self.navigation_label)
|
||||
self._clear_inputs()
|
||||
|
||||
def _nav_save(self):
|
||||
if not self._is_empty_inputs():
|
||||
lat = self.latitude_label.text()
|
||||
lng = self.longitude_label.text()
|
||||
self.navigation_label.setItem(
|
||||
self.navigation_table_cur_click,
|
||||
1,
|
||||
QtWidgets.QTableWidgetItem(lat))
|
||||
self.navigation_label.setItem(
|
||||
self.navigation_table_cur_click,
|
||||
2,
|
||||
QtWidgets.QTableWidgetItem(lng))
|
||||
|
||||
color_table_item = QtWidgets.QTableWidgetItem()
|
||||
color_table_item.setBackground(self.nav_color)
|
||||
|
||||
self.navigation_label.setItem(
|
||||
self.navigation_table_cur_click,
|
||||
3,
|
||||
color_table_item)
|
||||
self._clear_inputs()
|
||||
|
||||
def _nav_add_manual(self):
|
||||
# request GPS data
|
||||
if not self._is_empty_inputs():
|
||||
name = self.navigation_label.rowCount()
|
||||
lat = self.latitude_label.text()
|
||||
lng = self.longitude_label.text()
|
||||
self._add_to_table(str(name + 1), lat,
|
||||
lng, self.nav_color,
|
||||
self.navigation_label)
|
||||
self._clear_inputs()
|
||||
|
||||
def _nav_del(self):
|
||||
if self.navigation_table_cur_click is not None:
|
||||
self.navigation_label.removeRow(self.navigation_table_cur_click)
|
||||
count = self.navigation_label.rowCount()
|
||||
for x in range(self.navigation_table_cur_click, count):
|
||||
self.navigation_label.setItem(x,
|
||||
0,
|
||||
QtWidgets.
|
||||
QTableWidgetItem(str(x + 1)))
|
||||
self._clear_inputs()
|
||||
|
||||
def _land_add_gps(self):
|
||||
if self.longitude and self.latitude:
|
||||
name = self.name_edit_label.text()
|
||||
self._add_to_table(name, str(self.latitude), str(self.longitude), self.landmark_color, self.landmark_label)
|
||||
self._clear_inputs()
|
||||
|
||||
def _land_add_manual(self):
|
||||
if not self._is_empty_inputs():
|
||||
name = self.name_edit_label.text()
|
||||
lat = self.latitude_label.text()
|
||||
lng = self.longitude_label.text()
|
||||
self._add_to_table(name, lat,
|
||||
lng, self.landmark_color,
|
||||
self.landmark_label)
|
||||
self._clear_inputs()
|
||||
|
||||
def _land_del(self):
|
||||
if self.landmark_table_cur_click is not None:
|
||||
self.landmark_label.removeRow(self.landmark_table_cur_click)
|
||||
count = self.landmark_label.rowCount()
|
||||
for x in range(self.landmark_table_cur_click, count):
|
||||
self.navigation_label.setItem(x,
|
||||
0,
|
||||
QtWidgets.
|
||||
QTableWidgetItem(str(x + 1)))
|
||||
self._clear_inputs()
|
||||
|
||||
def _land_save(self):
|
||||
if not self._is_empty_inputs():
|
||||
name = self.name_edit_label.text()
|
||||
lat = self.latitude_label.text()
|
||||
lng = self.longitude_label.text()
|
||||
self.landmark_label.setItem(self.landmark_table_cur_click, 0,
|
||||
QtWidgets.QTableWidgetItem(name))
|
||||
|
||||
self.landmark_label.setItem(self.landmark_table_cur_click, 1,
|
||||
QtWidgets.QTableWidgetItem(lat))
|
||||
|
||||
self.landmark_label.setItem(self.landmark_table_cur_click, 2,
|
||||
QtWidgets.QTableWidgetItem(lng))
|
||||
|
||||
color_table_item = QtWidgets.QTableWidgetItem()
|
||||
color_table_item.setBackground(self.landmark_color)
|
||||
|
||||
self.landmark_label.setItem(self.landmark_table_cur_click, 3,
|
||||
color_table_item)
|
||||
|
||||
self._clear_inputs()
|
||||
|
||||
def setup_signals(self, start_signal,
|
||||
signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested_slot)
|
||||
|
||||
def on_kill_threads_requested_slot(self):
|
||||
self.run_thread_flag = False
|
||||
|
||||
def update_manual_entry(self, name, lat, lng, table):
|
||||
self.name_edit_label.setEnabled(bool(table))
|
||||
self.name_edit_label.setText(name)
|
||||
self.latitude_label.setValue(lat)
|
||||
self.longitude_label.setValue(lng)
|
||||
|
||||
lat_d = float(abs(int(lat)))
|
||||
lat_m = float(int((abs(lat) - lat_d) * 60))
|
||||
lat_s = ((abs(lat) - lat_d - (lat_m / 60.0)) * 3600.)
|
||||
if lat > 0.:
|
||||
self.latitude_card_label.setCurrentText("N")
|
||||
else:
|
||||
self.latitude_card_label.setCurrentText("S")
|
||||
self.latitude_degree_label.setValue(lat_d)
|
||||
self.latitude_minute_label.setValue(lat_m)
|
||||
self.latitude_second_label.setValue(lat_s)
|
||||
|
||||
lng_d = float(abs(int(lng)))
|
||||
lng_m = float(int((abs(lng) - lng_d) * 60))
|
||||
lng_s = ((abs(lng) - lng_d - (lng_m / 60.0)) * 3600.)
|
||||
if lng > 0.:
|
||||
self.longitude_card_label.setCurrentText("E")
|
||||
else:
|
||||
self.longitude_card_label.setCurrentText("W")
|
||||
self.longitude_degree_label.setValue(lng_d)
|
||||
self.longitude_minute_label.setValue(lng_m)
|
||||
self.longitude_second_label.setValue(lng_s)
|
||||
|
||||
def _on_nav_clicked(self, row, col):
|
||||
self.navigation_table_cur_click = row
|
||||
self.landmark_table_cur_click = None
|
||||
self.new_manual_waypoint_entry.emit(
|
||||
self.navigation_label.item(row, 0).text(),
|
||||
float(self.navigation_label.item(row, 1).text()),
|
||||
float(self.navigation_label.item(row, 2).text()),
|
||||
0
|
||||
)
|
||||
|
||||
def _on_land_clicked(self, row, col):
|
||||
self.landmark_table_cur_click = row
|
||||
self.navigation_table_cur_click = None
|
||||
self.new_manual_waypoint_entry.emit(
|
||||
self.landmark_label.item(row, 0).text(),
|
||||
float(self.landmark_label.item(row, 1).text()),
|
||||
float(self.landmark_label.item(row, 2).text()),
|
||||
1
|
||||
)
|
||||
|
||||
def __on_new_nav_color_selected(self, color):
|
||||
self.nav_color_label.setStyleSheet(
|
||||
"background-color: rgb(%s, %s, %s)" % (color.red(), color.green(), color.blue()))
|
||||
|
||||
self.nav_color = color
|
||||
|
||||
def __on_new_landmark_color_selected(self, color):
|
||||
self.landmark_color_label.setStyleSheet(
|
||||
"background-color: rgb(%s, %s, %s)" % (color.red(), color.green(), color.blue()))
|
||||
|
||||
self.landmark_color = color
|
||||
|
||||
def gps_position_updated_callback(self, data):
|
||||
self.latitude = data.latitude
|
||||
self.longitude = data.longitude
|
||||
@@ -0,0 +1,125 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
from time import time
|
||||
import paramiko
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
THREAD_HERTZ = 5
|
||||
|
||||
ACCESS_POINT_IP = "192.168.1.20" # The channel only has to be set on the access point. The staion will adjust.
|
||||
ACCESS_POINT_USER = "ubnt"
|
||||
ACCESS_POINT_PASSWORD = "rover4lyfe^" # We don't care about this password, don't freak out. Wifi is open anyways...
|
||||
|
||||
GET_CURRENT_CHANNEL_COMMAND = "iwlist ath0 channel"
|
||||
SET_CHANNEL_COMMAND = "iwconfig ath0 channel"
|
||||
|
||||
|
||||
#####################################
|
||||
# UbiquitiRadioSettings Class Definition
|
||||
#####################################
|
||||
class UbiquitiRadioSettings(QtCore.QThread):
|
||||
|
||||
show_channel__signal = QtCore.pyqtSignal(int)
|
||||
set_gui_elements_enabled__signal = QtCore.pyqtSignal(bool)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(UbiquitiRadioSettings, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
self.ubiquiti_channel_spin_box = self.left_screen.ubiquiti_channel_spin_box # type: QtWidgets.QSpinBox
|
||||
self.ubiquiti_channel_apply_button = self.left_screen.ubiquiti_channel_apply_button # type: QtWidgets.QPushButton
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.wait_time = 1.0 / THREAD_HERTZ
|
||||
|
||||
self.channel_change_needed = False
|
||||
self.new_channel = 0
|
||||
|
||||
self.ssh_client = None
|
||||
|
||||
def run(self):
|
||||
self.set_gui_elements_enabled__signal.emit(False)
|
||||
|
||||
try:
|
||||
self.setup_and_connect_ssh_client()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
self.get_and_show_current_channel()
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
self.apply_channel_if_needed()
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int(self.wait_time - time_diff), 0))
|
||||
|
||||
def setup_and_connect_ssh_client(self):
|
||||
self.ssh_client = paramiko.SSHClient()
|
||||
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.ssh_client.connect(ACCESS_POINT_IP, username=ACCESS_POINT_USER, password=ACCESS_POINT_PASSWORD,
|
||||
compress=True)
|
||||
|
||||
def apply_channel_if_needed(self):
|
||||
if self.channel_change_needed:
|
||||
self.show_channel__signal.emit(0)
|
||||
self.set_gui_elements_enabled__signal.emit(False)
|
||||
self.ssh_client.exec_command(SET_CHANNEL_COMMAND + " %02d" % self.new_channel)
|
||||
self.get_and_show_current_channel()
|
||||
self.channel_change_needed = False
|
||||
|
||||
def get_and_show_current_channel(self):
|
||||
channel = 0
|
||||
|
||||
ssh_stdin, ssh_stdout, ssh_stderr = self.ssh_client.exec_command(GET_CURRENT_CHANNEL_COMMAND)
|
||||
output = ssh_stdout.read()
|
||||
|
||||
for line in output.split("\n"):
|
||||
if "Current Frequency:" in line:
|
||||
channel = line.strip("()").split("Channel ")[1]
|
||||
break
|
||||
|
||||
self.msleep(500) # From the gui, this helps show something is actually happening
|
||||
|
||||
self.show_channel__signal.emit(int(channel))
|
||||
self.set_gui_elements_enabled__signal.emit(True)
|
||||
|
||||
def on_ubiquiti_channel_apply_pressed__slot(self):
|
||||
self.new_channel = self.ubiquiti_channel_spin_box.value()
|
||||
self.channel_change_needed = True
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.ubiquiti_channel_apply_button.clicked.connect(self.on_ubiquiti_channel_apply_pressed__slot)
|
||||
self.show_channel__signal.connect(self.ubiquiti_channel_spin_box.setValue)
|
||||
|
||||
self.set_gui_elements_enabled__signal.connect(self.ubiquiti_channel_spin_box.setEnabled)
|
||||
self.set_gui_elements_enabled__signal.connect(self.ubiquiti_channel_apply_button.setEnabled)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,45 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
import time
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import rospy
|
||||
|
||||
# Custom Imports
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
|
||||
|
||||
#####################################
|
||||
# RoverVideoReceiver Class Definition
|
||||
#####################################
|
||||
class ROSMasterChecker(QtCore.QThread):
|
||||
def __init__(self):
|
||||
super(ROSMasterChecker, self).__init__()
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.ros_master_present = False
|
||||
|
||||
self.start_time = time.time()
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
master = rospy.get_master()
|
||||
master.getPid()
|
||||
self.ros_master_present = True
|
||||
except socket.error:
|
||||
return
|
||||
|
||||
def master_present(self, timeout):
|
||||
while self.isRunning() and (time.time() - self.start_time) < timeout:
|
||||
self.msleep(100)
|
||||
|
||||
return self.ros_master_present
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
import rospy
|
||||
from rover_status.msg import *
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui, uic
|
||||
from std_msgs.msg import Empty
|
||||
import PIL.Image
|
||||
from PIL.ImageQt import ImageQt
|
||||
import time
|
||||
|
||||
from std_msgs.msg import UInt16
|
||||
|
||||
# import Timer
|
||||
|
||||
REQUEST_UPDATE_TOPIC = "/rover_status/update_requested"
|
||||
|
||||
CAMERA_TOPIC_NAME = "/rover_status/camera_status"
|
||||
BOGIE_TOPIC_NAME = "/rover_status/bogie_status"
|
||||
FRSKY_TOPIC_NAME = "/rover_status/frsky_status"
|
||||
GPS_TOPIC_NAME = "/rover_status/gps_status"
|
||||
JETSON_TOPIC_NAME = "/rover_status/jetson_status"
|
||||
MISC_TOPIC_NAME = "/rover_status/misc_status"
|
||||
BATTERY_TOPIC_NAME = "/rover_status/battery_status"
|
||||
CO2_TOPIC_NAME = "/rover_control/tower/status/co2"
|
||||
|
||||
COLOR_GREEN = "background-color: darkgreen; border: 1px solid black;"
|
||||
COLOR_ORANGE = "background-color: orange; border: 1px solid black;"
|
||||
COLOR_YELLOW = "background-color: rgb(204,204,0); border: 1px solid black; color: black;"
|
||||
COLOR_RED = "background-color: darkred; border: 1px solid black;"
|
||||
|
||||
GPS_BEST_CASE_ACCURACY = 3
|
||||
|
||||
LOW_BATTERY_DIALOG_TIMEOUT = 120
|
||||
CRITICAL_BATTERY_DIALOG_TIMEOUT = 30
|
||||
|
||||
|
||||
class SensorCore(QtCore.QThread):
|
||||
# ########## create signals for slots ##########
|
||||
jetson_cpu_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
jetson_cpu_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
jetson_emmc_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
jetson_emmc_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
jetson_gpu_temp_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
jetson_gpu_temp_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
jetson_ram_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
jetson_ram_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
bogie_connection_1_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
bogie_connection_2_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
bogie_connection_3_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
gps_fix_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
gps_heading_valid_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
gps_num_satellites_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
gps_accuracy_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
co2_levels_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
camera_zed_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
camera_under_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
camera_chassis_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
camera_main_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
gps_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
frsky_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
battery_voltage_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
battery_voltage_stylesheet_change_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
show_battery_low__signal = QtCore.pyqtSignal()
|
||||
show_battery_critical__signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(SensorCore, self).__init__()
|
||||
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.screen_main_window = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
# self.cpu_read = self.screen_main_window.lineEdit # type: QtWidgets.QLabel
|
||||
# self.ram_read = self.screen_main_window.lineEdit_2 # type: QtWidgets.QLabel
|
||||
|
||||
# ########## set vars to gui elements
|
||||
self.osurc_logo_label = self.screen_main_window.osurc_logo_label # type: QtWidgets.QLabel
|
||||
self.rover_conn = self.screen_main_window.rover # type: QtWidgets.QLabel
|
||||
self.frsky = self.screen_main_window.frsky # type: QtWidgets.QLabel
|
||||
self.nav_mouse = self.screen_main_window.nav_mouse # type: QtWidgets.QLabel
|
||||
self.joystick = self.screen_main_window.joystick # type: QtWidgets.QLabel
|
||||
self.gps_fix_label = self.screen_main_window.gps_fix_label # type: QtWidgets.QLabel
|
||||
self.gps_heading_valid_label = self.screen_main_window.gps_heading_valid_label # type: QtWidgets.QLabel
|
||||
self.gps_num_satellites_label = self.screen_main_window.gps_num_satellites_label # type: QtWidgets.QLabel
|
||||
self.gps_accuracy_label = self.screen_main_window.gps_accuracy_label # type: QtWidgets.QLabel
|
||||
self.zed = self.screen_main_window.zed # type: QtWidgets.QLabel
|
||||
self.main_cam = self.screen_main_window.main_cam # type: QtWidgets.QLabel
|
||||
self.chassis_cam = self.screen_main_window.chassis_cam # type: QtWidgets.QLabel
|
||||
self.under_cam = self.screen_main_window.under_cam # type: QtWidgets.QLabel
|
||||
self.clock = self.screen_main_window.clock_qlcdnumber # type: QtWidgets.QLCDNumber
|
||||
self.cpu = self.screen_main_window.cpu # type: QtWidgets.QLabel
|
||||
self.ram = self.screen_main_window.ram # type: QtWidgets.QLabel
|
||||
self.gpu_temp = self.screen_main_window.gpu_temp # type: QtWidgets.QLabel
|
||||
self.emmc = self.screen_main_window.emmc # type: QtWidgets.QLabel
|
||||
self.battery = self.screen_main_window.battery_voltage_status_label # type: QtWidgets.QLabel
|
||||
self.co2_levels_label = self.screen_main_window.co2_levels_label # type: QtWidgets.QLabel
|
||||
|
||||
# ########## subscriptions pulling data from system_statuses_node.py ##########
|
||||
self.camera_status = rospy.Subscriber(CAMERA_TOPIC_NAME, CameraStatuses, self.__camera_callback)
|
||||
self.frsky_status = rospy.Subscriber(FRSKY_TOPIC_NAME, FrSkyStatus, self.__frsky_callback)
|
||||
self.gps_status = rospy.Subscriber(GPS_TOPIC_NAME, GPSInfo, self.__gps_callback)
|
||||
self.jetson_status = rospy.Subscriber(JETSON_TOPIC_NAME, JetsonInfo, self.__jetson_callback)
|
||||
self.misc_status = rospy.Subscriber(MISC_TOPIC_NAME, MiscStatuses, self.__misc_callback)
|
||||
self.battery_status = rospy.Subscriber(BATTERY_TOPIC_NAME, BatteryStatusMessage, self.__battery_callback)
|
||||
self.co2_status = rospy.Subscriber(CO2_TOPIC_NAME, UInt16, self.__co2_callback)
|
||||
|
||||
self.camera_msg = CameraStatuses()
|
||||
self.bogie_msg = None # BogieStatuses()
|
||||
self.FrSky_msg = FrSkyStatus()
|
||||
self.GPS_msg = GPSInfo()
|
||||
self.jetson_msg = JetsonInfo()
|
||||
self.misc_msg = MiscStatuses()
|
||||
self.battery_msg = BatteryStatusMessage()
|
||||
|
||||
self.update_requester = rospy.Publisher(REQUEST_UPDATE_TOPIC, Empty, queue_size=10)
|
||||
|
||||
# Apply OSURC Logo
|
||||
self.osurc_logo_pil = PIL.Image.open("Resources/Images/osurclogo.png").resize((210, 75), PIL.Image.BICUBIC)
|
||||
self.osurc_logo_pixmap = QtGui.QPixmap.fromImage(ImageQt(self.osurc_logo_pil))
|
||||
self.osurc_logo_label.setPixmap(self.osurc_logo_pixmap) # Init should be in main thread, should be fine
|
||||
|
||||
self.low_battery_warning_dialog = QtWidgets.QMessageBox()
|
||||
self.low_battery_warning_dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
self.low_battery_warning_dialog.setText("\n\n\n\nRover battery low!\nReturn and charge soon to avoid battery damage!\n\n\n\n")
|
||||
self.low_battery_warning_dialog.setWindowTitle("Low Battery")
|
||||
self.low_battery_warning_dialog.setStandardButtons(QtWidgets.QMessageBox.Ok)
|
||||
self.low_battery_warning_dialog.setWindowFlags(
|
||||
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.X11BypassWindowManagerHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
|
||||
self.low_battery_warning_dialog.setStyleSheet(COLOR_YELLOW)
|
||||
|
||||
self.critical_battery_warning_dialog = QtWidgets.QMessageBox()
|
||||
self.critical_battery_warning_dialog.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
self.critical_battery_warning_dialog.setText(
|
||||
"\n\n\n\nRover battery critical!\nPower down immediately or battery damage will occur!\n\n\n\n")
|
||||
self.critical_battery_warning_dialog.setWindowTitle("Critical Battery")
|
||||
self.critical_battery_warning_dialog.setStandardButtons(QtWidgets.QMessageBox.Ok)
|
||||
self.critical_battery_warning_dialog.setWindowFlags(
|
||||
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.X11BypassWindowManagerHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
|
||||
self.critical_battery_warning_dialog.setStyleSheet(COLOR_RED)
|
||||
|
||||
self.low_battery_warning_last_shown = 0
|
||||
self.critical_battery_warning_last_shown = 0
|
||||
|
||||
def __camera_callback(self, data):
|
||||
self.camera_msg.camera_zed = data.camera_zed
|
||||
self.camera_msg.camera_undercarriage = data.camera_undercarriage
|
||||
self.camera_msg.camera_chassis = data.camera_chassis
|
||||
self.camera_msg.camera_main_navigation = data.camera_main_navigation
|
||||
|
||||
if data.camera_zed is False:
|
||||
# self.zed.setStyleSheet("background-color: red;")
|
||||
self.camera_zed_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
# self.zed.setStyleSheet(COLOR_GREEN)
|
||||
self.camera_zed_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
if data.camera_undercarriage is False:
|
||||
# self.under_cam.setStyleSheet(COLOR_RED)
|
||||
self.camera_under_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
# self.under_cam.setStyleSheet(COLOR_GREEN)
|
||||
self.camera_under_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
if data.camera_chassis is False:
|
||||
# self.chassis_cam.setStyleSheet(COLOR_RED)
|
||||
self.camera_chassis_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
# self.chassis_cam.setStyleSheet(COLOR_GREEN)
|
||||
self.camera_chassis_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
if data.camera_main_navigation is False:
|
||||
# self.main_cam.setStyleSheet(COLOR_RED)
|
||||
self.camera_main_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
# self.main_cam.setStyleSheet(COLOR_GREEN)
|
||||
self.camera_main_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
def __frsky_callback(self, data):
|
||||
self.FrSky_msg.FrSky_controller_connection_status = data.FrSky_controller_connection_status
|
||||
|
||||
if self.FrSky_msg.FrSky_controller_connection_status is False:
|
||||
self.frsky_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
self.frsky_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
def __jetson_callback(self, data):
|
||||
self.jetson_cpu_update_ready__signal.emit("TX2 CPU\n" + str(data.jetson_CPU) + " %")
|
||||
|
||||
if data.jetson_CPU > 85:
|
||||
self.jetson_cpu_stylesheet_change_ready__signal.emit(COLOR_ORANGE)
|
||||
elif data.jetson_CPU > 95:
|
||||
self.jetson_cpu_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
self.jetson_cpu_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
self.jetson_ram_update_ready__signal.emit("TX2 RAM\n" + str(data.jetson_RAM) + " %")
|
||||
|
||||
if data.jetson_RAM > 79:
|
||||
self.jetson_ram_stylesheet_change_ready__signal.emit(COLOR_ORANGE)
|
||||
elif data.jetson_RAM > 89:
|
||||
self.jetson_ram_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
self.jetson_ram_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
self.jetson_gpu_temp_update_ready__signal.emit("TX2 TEMP\n" + str(data.jetson_GPU_temp) + " °C")
|
||||
|
||||
if data.jetson_GPU_temp > 64:
|
||||
self.jetson_gpu_temp_stylesheet_change_ready__signal.emit(COLOR_ORANGE)
|
||||
elif data.jetson_GPU_temp > 79:
|
||||
self.jetson_gpu_temp_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
self.jetson_gpu_temp_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
self.jetson_emmc_update_ready__signal.emit("TX2 EMMC\n" + str(data.jetson_EMMC) + " %")
|
||||
|
||||
if data.jetson_EMMC > 79:
|
||||
self.jetson_emmc_stylesheet_change_ready__signal.emit(COLOR_ORANGE)
|
||||
elif data.jetson_EMMC > 89:
|
||||
self.jetson_emmc_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
else:
|
||||
self.jetson_emmc_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
def __gps_callback(self, data):
|
||||
|
||||
if data.gps_connected:
|
||||
self.gps_fix_update_ready__signal.emit("GPS Fix\nTrue")
|
||||
self.gps_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
|
||||
else:
|
||||
self.gps_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
self.gps_fix_update_ready__signal.emit("GPS Fix\nFalse")
|
||||
|
||||
if data.gps_heading != -1:
|
||||
self.gps_heading_valid_update_ready__signal.emit("GPS Heading Valid\nTrue")
|
||||
else:
|
||||
self.gps_heading_valid_update_ready__signal.emit("GPS Heading Valid\nFalse")
|
||||
|
||||
self.gps_num_satellites_update_ready__signal.emit("GPS Satellites\n%s" % data.num_satellites)
|
||||
self.gps_accuracy_update_ready__signal.emit(
|
||||
"GPS Accuracy\n%2.2f m" % (data.horizontal_dilution * GPS_BEST_CASE_ACCURACY))
|
||||
|
||||
def __misc_callback(self, data):
|
||||
self.misc_msg.arm_connection_status = data.arm_connection_status
|
||||
self.misc_msg.arm_end_effector_connection_statuses = data.arm_end_effector_connection_statuses
|
||||
self.misc_msg.sample_containment_connection_status = data.sample_containment_connection_status
|
||||
self.misc_msg.tower_connection_status = data.tower_connection_status
|
||||
self.misc_msg.chassis_pan_tilt_connection_status = data.chassis_pan_tilt_connection_status
|
||||
|
||||
def __battery_callback(self, data):
|
||||
voltage = data.battery_voltage / 1000.0
|
||||
|
||||
if voltage >= 21:
|
||||
self.battery_voltage_stylesheet_change_ready__signal.emit(COLOR_GREEN)
|
||||
elif voltage >= 19:
|
||||
self.battery_voltage_stylesheet_change_ready__signal.emit(COLOR_YELLOW)
|
||||
|
||||
if (time.time() - self.low_battery_warning_last_shown) > LOW_BATTERY_DIALOG_TIMEOUT:
|
||||
self.show_battery_low__signal.emit()
|
||||
self.low_battery_warning_last_shown = time.time()
|
||||
|
||||
else:
|
||||
self.battery_voltage_stylesheet_change_ready__signal.emit(COLOR_RED)
|
||||
|
||||
if (time.time() - self.critical_battery_warning_last_shown) > CRITICAL_BATTERY_DIALOG_TIMEOUT:
|
||||
self.show_battery_critical__signal.emit()
|
||||
self.critical_battery_warning_last_shown = time.time()
|
||||
|
||||
self.battery_voltage_update_ready__signal.emit("Battery Voltage\n" + str(voltage) + " V")
|
||||
|
||||
def __co2_callback(self, data):
|
||||
if data.data != 9999:
|
||||
self.co2_levels_update_ready__signal.emit("CO2 Levels\n%d ppm" % data.data)
|
||||
else:
|
||||
self.co2_levels_update_ready__signal.emit("CO2 Levels\n--- ppm")
|
||||
|
||||
def __display_time(self):
|
||||
time = QtCore.QTime.currentTime()
|
||||
temp = time.toString('hh:mm')
|
||||
self.clock.display(temp)
|
||||
|
||||
def run(self):
|
||||
while self.run_thread_flag:
|
||||
# self.update_requester.publish(Empty())
|
||||
self.__display_time()
|
||||
self.msleep(1000)
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.jetson_cpu_update_ready__signal.connect(self.cpu.setText)
|
||||
self.jetson_cpu_stylesheet_change_ready__signal.connect(self.cpu.setStyleSheet)
|
||||
self.jetson_ram_update_ready__signal.connect(self.ram.setText)
|
||||
self.jetson_ram_stylesheet_change_ready__signal.connect(self.ram.setStyleSheet)
|
||||
self.jetson_emmc_update_ready__signal.connect(self.emmc.setText)
|
||||
self.jetson_emmc_stylesheet_change_ready__signal.connect(self.emmc.setStyleSheet)
|
||||
self.jetson_gpu_temp_update_ready__signal.connect(self.gpu_temp.setText)
|
||||
self.jetson_gpu_temp_stylesheet_change_ready__signal.connect(self.gpu_temp.setStyleSheet)
|
||||
self.camera_zed_stylesheet_change_ready__signal.connect(self.zed.setStyleSheet)
|
||||
self.camera_under_stylesheet_change_ready__signal.connect(self.under_cam.setStyleSheet)
|
||||
self.camera_chassis_stylesheet_change_ready__signal.connect(self.chassis_cam.setStyleSheet)
|
||||
self.camera_main_stylesheet_change_ready__signal.connect(self.main_cam.setStyleSheet)
|
||||
self.gps_stylesheet_change_ready__signal.connect(self.gps_fix_label.setStyleSheet)
|
||||
self.frsky_stylesheet_change_ready__signal.connect(self.frsky.setStyleSheet)
|
||||
|
||||
self.gps_fix_update_ready__signal.connect(self.gps_fix_label.setText)
|
||||
self.gps_heading_valid_update_ready__signal.connect(self.gps_heading_valid_label.setText)
|
||||
self.gps_num_satellites_update_ready__signal.connect(self.gps_num_satellites_label.setText)
|
||||
self.gps_accuracy_update_ready__signal.connect(self.gps_accuracy_label.setText)
|
||||
|
||||
self.co2_levels_update_ready__signal.connect(self.co2_levels_label.setText)
|
||||
|
||||
self.battery_voltage_update_ready__signal.connect(self.battery.setText)
|
||||
self.battery_voltage_stylesheet_change_ready__signal.connect(self.battery.setStyleSheet)
|
||||
|
||||
self.show_battery_low__signal.connect(self.low_battery_warning_dialog.show)
|
||||
self.show_battery_critical__signal.connect(self.critical_battery_warning_dialog.show)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
rover_statuses = SensorCore()
|
||||
rover_statuses.run()
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python2
|
||||
|
||||
import sys
|
||||
|
||||
from PyQt5 import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.uic import loadUi
|
||||
|
||||
# [ms]
|
||||
TICK_TIME = 2**6
|
||||
|
||||
|
||||
class Timer(Qt.QMainWindow):
|
||||
def __init__(self):
|
||||
super(Timer, self).__init__()
|
||||
|
||||
# self.reset.clicked.connect(self.do_reset)
|
||||
# self.start.clicked.connect(self.do_start)
|
||||
|
||||
self.timer = Qt.QTimer()
|
||||
self.timer.setInterval(TICK_TIME)
|
||||
self.timer.timeout.connect(self.tick)
|
||||
|
||||
self.running = False;
|
||||
|
||||
self.do_reset()
|
||||
|
||||
def __start_stop_reset_event(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
if self.running is False:
|
||||
self.do_start
|
||||
elif self.running is True:
|
||||
self.do_pause
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
self.do_reset
|
||||
|
||||
def display(self):
|
||||
self.timer.display("%d:%05.2f" % (self.time // 60, self.time % 60))
|
||||
|
||||
@Qt.pyqtSlot()
|
||||
def tick(self):
|
||||
self.time += TICK_TIME/1000
|
||||
self.display()
|
||||
|
||||
@Qt.pyqtSlot()
|
||||
def do_start(self):
|
||||
self.timer.start()
|
||||
# self.start.setText("Pause")
|
||||
# self.start.clicked.disconnect()
|
||||
# self.start.clicked.connect(self.do_pause)
|
||||
|
||||
@Qt.pyqtSlot()
|
||||
def do_pause(self):
|
||||
self.timer.stop()
|
||||
# self.start.setText("Start")
|
||||
# self.start.clicked.disconnect()
|
||||
# self.start.clicked.connect(self.do_start)
|
||||
|
||||
@Qt.pyqtSlot()
|
||||
def do_reset(self):
|
||||
self.time = 0
|
||||
self.display()
|
||||
|
||||
|
||||
app = Qt.QApplication(sys.argv)
|
||||
|
||||
timer = Timer()
|
||||
timer.show()
|
||||
|
||||
app.exec_()
|
||||
@@ -0,0 +1,126 @@
|
||||
# coding=utf-8
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
from time import time
|
||||
import paramiko
|
||||
from pprint import pprint
|
||||
import json
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
THREAD_HERTZ = 5
|
||||
|
||||
ACCESS_POINT_IP = "192.168.1.20" # The channel only has to be set on the access point. The staion will adjust.
|
||||
ACCESS_POINT_USER = "ubnt"
|
||||
ACCESS_POINT_PASSWORD = "rover4lyfe^" # We don't care about this password, don't freak out. Wifi is open anyways...
|
||||
|
||||
GENERAL_WIRELESS_INFO_COMMAND = "wstalist"
|
||||
|
||||
|
||||
#####################################
|
||||
# UbiquitiRadioSettings Class Definition
|
||||
#####################################
|
||||
class UbiquitiStatus(QtCore.QThread):
|
||||
connection_quality_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
successful_transmission_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
radio_rates_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
radio_latency_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(UbiquitiStatus, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.left_screen = self.shared_objects["screens"]["left_screen"]
|
||||
|
||||
self.connection_quality_label = self.left_screen.connection_quality_label
|
||||
self.successful_transmit_label = self.left_screen.successful_transmit_label
|
||||
self.radio_rates_label = self.left_screen.radio_rates_label
|
||||
self.radio_latency_label = self.left_screen.radio_latency_label
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.wait_time = 1.0 / THREAD_HERTZ
|
||||
|
||||
self.ssh_client = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.setup_and_connect_ssh_client()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
while self.run_thread_flag:
|
||||
start_time = time()
|
||||
|
||||
try:
|
||||
self.get_and_show_ubiquiti_status()
|
||||
except Exception, e:
|
||||
print e
|
||||
|
||||
time_diff = time() - start_time
|
||||
|
||||
self.msleep(max(int((self.wait_time - time_diff) * 1000), 0))
|
||||
|
||||
def setup_and_connect_ssh_client(self):
|
||||
self.ssh_client = paramiko.SSHClient()
|
||||
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.ssh_client.connect(ACCESS_POINT_IP, username=ACCESS_POINT_USER, password=ACCESS_POINT_PASSWORD,
|
||||
compress=True)
|
||||
|
||||
def get_and_show_ubiquiti_status(self):
|
||||
ssh_stdin, ssh_stdout, ssh_stderr = self.ssh_client.exec_command(GENERAL_WIRELESS_INFO_COMMAND)
|
||||
|
||||
try:
|
||||
output_json = json.loads(ssh_stdout.read())[0]
|
||||
|
||||
transmit_percent = output_json["ccq"]
|
||||
quality = output_json["airmax"]["quality"]
|
||||
# capacity = output_json["airmax"]["capacity"]
|
||||
rx_rate = output_json["rx"]
|
||||
tx_rate = output_json["tx"]
|
||||
ground_tx_latency = output_json["tx_latency"]
|
||||
rover_tx_latency = output_json["remote"]["tx_latency"]
|
||||
|
||||
except IndexError:
|
||||
transmit_percent = 0
|
||||
quality = 0
|
||||
# capacity = output_json["airmax"]["capacity"]
|
||||
rx_rate = 0
|
||||
tx_rate = 0
|
||||
ground_tx_latency = "----"
|
||||
rover_tx_latency = "----"
|
||||
|
||||
|
||||
self.connection_quality_update_ready__signal.emit("Connection Quality\n%s %%" % quality)
|
||||
self.successful_transmission_update_ready__signal.emit("Successful Transmit\n%s %%" % transmit_percent)
|
||||
self.radio_rates_update_ready__signal.emit("TX Rate: %s Mbps\nRX Rate: %s Mbps" % (tx_rate, rx_rate))
|
||||
self.radio_latency_update_ready__signal.emit(
|
||||
"TX Latency: %s ms\nRX Latency: %s ms" % (ground_tx_latency, rover_tx_latency))
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
self.connection_quality_update_ready__signal.connect(self.connection_quality_label.setText)
|
||||
self.successful_transmission_update_ready__signal.connect(self.successful_transmit_label.setText)
|
||||
self.radio_rates_update_ready__signal.connect(self.radio_rates_label.setText)
|
||||
self.radio_latency_update_ready__signal.connect(self.radio_latency_label.setText)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,391 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
from time import time
|
||||
|
||||
import rospy
|
||||
|
||||
# Custom Imports
|
||||
import RoverVideoReceiver
|
||||
from rover_camera.msg import CameraControlMessage
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
CAMERA_TOPIC_PATH = "/cameras/"
|
||||
EXCLUDED_CAMERAS = ["zed"]
|
||||
|
||||
PRIMARY_LABEL_MAX = (640, 360)
|
||||
SECONDARY_LABEL_MAX = (640, 360)
|
||||
TERTIARY_LABEL_MAX = (640, 360)
|
||||
|
||||
LOW_RES = (256, 144)
|
||||
|
||||
GUI_SELECTION_CHANGE_TIMEOUT = 3 # Seconds
|
||||
|
||||
STYLESHEET_SELECTED = "border: 2px solid orange; background-color:black;"
|
||||
STYLESHEET_UNSELECTED = "background-color:black;"
|
||||
|
||||
COLOR_GREEN = "background-color: darkgreen;"
|
||||
COLOR_RED = "background-color: darkred;"
|
||||
|
||||
|
||||
#####################################
|
||||
# RoverVideoCoordinator Class Definition
|
||||
#####################################
|
||||
class RoverVideoCoordinator(QtCore.QThread):
|
||||
pixmap_ready_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
update_element_stylesheet__signal = QtCore.pyqtSignal()
|
||||
|
||||
pan_tilt_selection_changed__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
low_res_button_text_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
low_res_button_stylesheet_update_ready__signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, shared_objects):
|
||||
super(RoverVideoCoordinator, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.shared_objects = shared_objects
|
||||
self.right_screen = self.shared_objects["screens"]["right_screen"]
|
||||
self.primary_video_display_label = self.right_screen.primary_video_label # type:QtWidgets.QLabel
|
||||
self.secondary_video_display_label = self.right_screen.secondary_video_label # type:QtWidgets.QLabel
|
||||
self.tertiary_video_display_label = self.right_screen.tertiary_video_label # type:QtWidgets.QLabel
|
||||
|
||||
self.low_res_mode_button = self.right_screen.low_res_mode_button # type: QtWidgets.QLabel
|
||||
|
||||
self.index_to_label_element = {
|
||||
0: self.primary_video_display_label,
|
||||
1: self.secondary_video_display_label,
|
||||
2: self.tertiary_video_display_label
|
||||
}
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
self.setup_cameras_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
# Camera variables
|
||||
self.camera_threads = {}
|
||||
self.valid_cameras = []
|
||||
self.disabled_cameras = []
|
||||
|
||||
reset_camera_message = CameraControlMessage()
|
||||
reset_camera_message.enable_small_broadcast = True
|
||||
|
||||
# Reset default cameras
|
||||
rospy.Publisher("/cameras/chassis/camera_control", CameraControlMessage, queue_size=1).publish(reset_camera_message)
|
||||
rospy.Publisher("/cameras/undercarriage/camera_control", CameraControlMessage, queue_size=1).publish(reset_camera_message)
|
||||
rospy.Publisher("/cameras/main_navigation/camera_control", CameraControlMessage, queue_size=1).publish(reset_camera_message)
|
||||
rospy.Publisher("/cameras/end_effector/camera_control", CameraControlMessage, queue_size=1).publish(reset_camera_message)
|
||||
|
||||
self.msleep(3000)
|
||||
|
||||
# Setup cameras
|
||||
self.main_nav_index = -1
|
||||
self.chassis_index = -1
|
||||
|
||||
self.__get_cameras()
|
||||
self.__setup_video_threads()
|
||||
|
||||
self.primary_label_current_setting = 0
|
||||
self.secondary_label_current_setting = min(self.primary_label_current_setting + 1, len(self.valid_cameras))
|
||||
self.tertiary_label_current_setting = min(self.secondary_label_current_setting + 1, len(self.valid_cameras))
|
||||
|
||||
self.index_to_label_current_setting = {
|
||||
0: self.primary_label_current_setting,
|
||||
1: self.secondary_label_current_setting,
|
||||
2: self.tertiary_label_current_setting
|
||||
}
|
||||
|
||||
self.current_label_for_joystick_adjust = 0
|
||||
self.gui_selection_changed = True
|
||||
self.last_gui_selection_changed_time = time()
|
||||
|
||||
self.set_max_resolutions_flag = True
|
||||
|
||||
self.first_image_received = False
|
||||
|
||||
self.in_low_res_mode = False
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting Video Coordinator Thread")
|
||||
|
||||
self.__broadcast_current_pan_tilt_selection()
|
||||
|
||||
while self.run_thread_flag:
|
||||
self.__set_max_resolutions()
|
||||
self.__toggle_background_cameras_if_needed()
|
||||
self.__update_gui_element_selection()
|
||||
self.msleep(10)
|
||||
|
||||
self.__wait_for_camera_threads()
|
||||
|
||||
self.logger.debug("Stopping Video Coordinator Thread")
|
||||
|
||||
def __broadcast_current_pan_tilt_selection(self):
|
||||
setting = None
|
||||
if self.current_label_for_joystick_adjust == 0: # primary
|
||||
setting = self.primary_label_current_setting
|
||||
elif self.current_label_for_joystick_adjust == 1: # secondary
|
||||
setting = self.secondary_label_current_setting
|
||||
elif self.current_label_for_joystick_adjust == 2: # tertiary
|
||||
setting = self.tertiary_label_current_setting
|
||||
|
||||
if setting == self.main_nav_index:
|
||||
self.pan_tilt_selection_changed__signal.emit("tower_pan_tilt")
|
||||
elif setting == self.chassis_index:
|
||||
self.pan_tilt_selection_changed__signal.emit("chassis_pan_tilt")
|
||||
else:
|
||||
self.pan_tilt_selection_changed__signal.emit("no_pan_tilt")
|
||||
|
||||
def __set_max_resolutions(self):
|
||||
if self.set_max_resolutions_flag:
|
||||
if self.in_low_res_mode:
|
||||
for camera in self.camera_threads:
|
||||
self.camera_threads[camera].set_hard_max_resolution(LOW_RES)
|
||||
else:
|
||||
self.camera_threads[self.valid_cameras[self.primary_label_current_setting]].set_hard_max_resolution(PRIMARY_LABEL_MAX)
|
||||
|
||||
if self.secondary_label_current_setting != self.primary_label_current_setting:
|
||||
self.camera_threads[self.valid_cameras[self.secondary_label_current_setting]].set_hard_max_resolution(SECONDARY_LABEL_MAX)
|
||||
|
||||
if self.tertiary_label_current_setting != self.primary_label_current_setting and self.tertiary_label_current_setting != self.secondary_label_current_setting:
|
||||
self.camera_threads[self.valid_cameras[self.tertiary_label_current_setting]].set_hard_max_resolution(SECONDARY_LABEL_MAX)
|
||||
|
||||
self.set_max_resolutions_flag = False
|
||||
|
||||
def __toggle_background_cameras_if_needed(self):
|
||||
enabled = list({self.primary_label_current_setting, self.secondary_label_current_setting,
|
||||
self.tertiary_label_current_setting})
|
||||
|
||||
for camera_index, camera_name in enumerate(self.valid_cameras):
|
||||
if camera_index not in enabled and camera_index not in self.disabled_cameras and self.camera_threads[camera_name].video_enabled:
|
||||
self.camera_threads[camera_name].toggle_video_display()
|
||||
elif camera_index in enabled and camera_index not in self.disabled_cameras and not self.camera_threads[camera_name].video_enabled:
|
||||
self.camera_threads[camera_name].toggle_video_display()
|
||||
|
||||
def __update_gui_element_selection(self):
|
||||
if (time() - self.last_gui_selection_changed_time) > GUI_SELECTION_CHANGE_TIMEOUT \
|
||||
and self.gui_selection_changed:
|
||||
|
||||
elements_to_reset = range(len(self.index_to_label_element))
|
||||
|
||||
for index in elements_to_reset:
|
||||
self.index_to_label_element[index].setStyleSheet(STYLESHEET_UNSELECTED)
|
||||
|
||||
self.gui_selection_changed = False
|
||||
|
||||
def __on_gui_element_stylesheet_update__slot(self):
|
||||
elements_to_reset = range(len(self.index_to_label_element))
|
||||
elements_to_reset.remove(self.current_label_for_joystick_adjust)
|
||||
|
||||
for index in elements_to_reset:
|
||||
self.index_to_label_element[index].setStyleSheet(STYLESHEET_UNSELECTED)
|
||||
|
||||
self.index_to_label_element[self.current_label_for_joystick_adjust].setStyleSheet(STYLESHEET_SELECTED)
|
||||
|
||||
self.gui_selection_changed = True
|
||||
self.last_gui_selection_changed_time = time()
|
||||
|
||||
def __get_cameras(self):
|
||||
topics = rospy.get_published_topics(CAMERA_TOPIC_PATH)
|
||||
|
||||
names = []
|
||||
|
||||
for topics_group in topics:
|
||||
main_topic = topics_group[0]
|
||||
if "heartbeat" in main_topic:
|
||||
continue
|
||||
camera_name = main_topic.split("/")[2]
|
||||
names.append(camera_name)
|
||||
|
||||
names = set(names)
|
||||
|
||||
for camera in EXCLUDED_CAMERAS:
|
||||
if camera in names:
|
||||
names.remove(camera)
|
||||
|
||||
self.valid_cameras = []
|
||||
current_count = 0
|
||||
|
||||
if "main_navigation" in names:
|
||||
self.valid_cameras.append("main_navigation")
|
||||
self.main_nav_index = current_count
|
||||
current_count += 1
|
||||
|
||||
if "chassis" in names:
|
||||
self.valid_cameras.append("chassis")
|
||||
self.chassis_index = current_count
|
||||
|
||||
if "undercarriage" in names:
|
||||
self.valid_cameras.append("undercarriage")
|
||||
|
||||
if "end_effector" in names:
|
||||
self.valid_cameras.append("end_effector")
|
||||
|
||||
def __setup_video_threads(self):
|
||||
for camera in self.valid_cameras:
|
||||
self.camera_threads[camera] = RoverVideoReceiver.RoverVideoReceiver(camera)
|
||||
|
||||
def __wait_for_camera_threads(self):
|
||||
for camera in self.camera_threads:
|
||||
self.camera_threads[camera].wait()
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
for thread in self.camera_threads:
|
||||
self.camera_threads[thread].image_ready_signal.connect(self.pixmap_ready__slot)
|
||||
|
||||
self.primary_video_display_label.mousePressEvent = self.__change_display_source_primary_mouse_press_event
|
||||
self.secondary_video_display_label.mousePressEvent = self.__change_display_source_secondary_mouse_press_event
|
||||
self.tertiary_video_display_label.mousePressEvent = self.__change_display_source_tertiary_mouse_press_event
|
||||
|
||||
self.shared_objects["threaded_classes"]["Joystick Sender"].change_gui_element_selection__signal.connect(
|
||||
self.on_camera_gui_element_selection_changed)
|
||||
self.shared_objects["threaded_classes"]["Joystick Sender"].change_camera_selection__signal.connect(
|
||||
self.on_camera_selection_for_current_gui_element_changed)
|
||||
self.shared_objects["threaded_classes"]["Joystick Sender"].toggle_selected_gui_camera__signal.connect(
|
||||
self.on_gui_selected_camera_toggled)
|
||||
|
||||
self.low_res_mode_button.clicked.connect(self.on_low_res_button_clicked__slot)
|
||||
self.low_res_button_text_update_ready__signal.connect(self.low_res_mode_button.setText)
|
||||
self.low_res_button_stylesheet_update_ready__signal.connect(self.low_res_mode_button.setStyleSheet)
|
||||
|
||||
self.update_element_stylesheet__signal.connect(self.__on_gui_element_stylesheet_update__slot)
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
for camera in self.camera_threads:
|
||||
self.camera_threads[camera].setup_signals(start_signal, signals_and_slots_signal, kill_signal)
|
||||
|
||||
def __change_display_source_primary_mouse_press_event(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.primary_label_current_setting = (self.primary_label_current_setting + 1) % len(self.valid_cameras)
|
||||
self.set_max_resolutions_flag = True
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
if self.primary_label_current_setting in self.disabled_cameras:
|
||||
self.disabled_cameras.remove(self.primary_label_current_setting)
|
||||
else:
|
||||
self.disabled_cameras.append(self.primary_label_current_setting)
|
||||
self.camera_threads[self.valid_cameras[self.primary_label_current_setting]].toggle_video_display()
|
||||
|
||||
def __change_display_source_secondary_mouse_press_event(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.secondary_label_current_setting = (self.secondary_label_current_setting + 1) % len(self.valid_cameras)
|
||||
self.set_max_resolutions_flag = True
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
if self.secondary_label_current_setting in self.disabled_cameras:
|
||||
self.disabled_cameras.remove(self.secondary_label_current_setting)
|
||||
else:
|
||||
self.disabled_cameras.append(self.secondary_label_current_setting)
|
||||
self.camera_threads[self.valid_cameras[self.secondary_label_current_setting]].toggle_video_display()
|
||||
|
||||
def __change_display_source_tertiary_mouse_press_event(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.tertiary_label_current_setting = (self.tertiary_label_current_setting + 1) % len(self.valid_cameras)
|
||||
self.set_max_resolutions_flag = True
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
if self.tertiary_label_current_setting in self.disabled_cameras:
|
||||
self.disabled_cameras.remove(self.tertiary_label_current_setting)
|
||||
else:
|
||||
self.disabled_cameras.append(self.tertiary_label_current_setting)
|
||||
self.camera_threads[self.valid_cameras[self.tertiary_label_current_setting]].toggle_video_display()
|
||||
|
||||
def pixmap_ready__slot(self, camera):
|
||||
if self.valid_cameras[self.primary_label_current_setting] == camera:
|
||||
try:
|
||||
self.primary_video_display_label.setPixmap(self.camera_threads[camera].pixmap_1280x720_image)
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.valid_cameras[self.secondary_label_current_setting] == camera:
|
||||
try:
|
||||
self.secondary_video_display_label.setPixmap(self.camera_threads[camera].pixmap_640x360_image)
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.valid_cameras[self.tertiary_label_current_setting] == camera:
|
||||
try:
|
||||
self.tertiary_video_display_label.setPixmap(self.camera_threads[camera].pixmap_640x360_image)
|
||||
except:
|
||||
pass
|
||||
|
||||
def on_camera_gui_element_selection_changed(self, direction):
|
||||
new_selection = self.current_label_for_joystick_adjust - direction
|
||||
|
||||
if new_selection < 0:
|
||||
self.current_label_for_joystick_adjust = len(self.index_to_label_element) - 1
|
||||
elif new_selection == len(self.index_to_label_element):
|
||||
self.current_label_for_joystick_adjust = 0
|
||||
else:
|
||||
self.current_label_for_joystick_adjust = new_selection
|
||||
|
||||
self.__broadcast_current_pan_tilt_selection()
|
||||
|
||||
self.update_element_stylesheet__signal.emit()
|
||||
|
||||
def on_camera_selection_for_current_gui_element_changed(self, direction):
|
||||
if self.current_label_for_joystick_adjust == 0: # primary
|
||||
self.primary_label_current_setting = (self.primary_label_current_setting + direction) % len(self.valid_cameras)
|
||||
elif self.current_label_for_joystick_adjust == 1: # secondary
|
||||
self.secondary_label_current_setting = (self.secondary_label_current_setting + direction) % len(self.valid_cameras)
|
||||
elif self.current_label_for_joystick_adjust == 2: # tertiary
|
||||
self.tertiary_label_current_setting = (self.tertiary_label_current_setting + direction) % len(self.valid_cameras)
|
||||
|
||||
self.__broadcast_current_pan_tilt_selection()
|
||||
|
||||
self.set_max_resolutions_flag = True
|
||||
self.update_element_stylesheet__signal.emit()
|
||||
|
||||
def on_gui_selected_camera_toggled(self):
|
||||
if self.current_label_for_joystick_adjust == 0: # primary
|
||||
if self.primary_label_current_setting in self.disabled_cameras:
|
||||
self.disabled_cameras.remove(self.primary_label_current_setting)
|
||||
else:
|
||||
self.disabled_cameras.append(self.primary_label_current_setting)
|
||||
self.camera_threads[self.valid_cameras[self.primary_label_current_setting]].toggle_video_display()
|
||||
elif self.current_label_for_joystick_adjust == 1: # secondary
|
||||
if self.secondary_label_current_setting in self.disabled_cameras:
|
||||
self.disabled_cameras.remove(self.secondary_label_current_setting)
|
||||
else:
|
||||
self.disabled_cameras.append(self.secondary_label_current_setting)
|
||||
self.camera_threads[self.valid_cameras[self.secondary_label_current_setting]].toggle_video_display()
|
||||
elif self.current_label_for_joystick_adjust == 2: # tertiary
|
||||
if self.tertiary_label_current_setting in self.disabled_cameras:
|
||||
self.disabled_cameras.remove(self.tertiary_label_current_setting)
|
||||
else:
|
||||
self.disabled_cameras.append(self.tertiary_label_current_setting)
|
||||
self.camera_threads[self.valid_cameras[self.tertiary_label_current_setting]].toggle_video_display()
|
||||
|
||||
self.update_element_stylesheet__signal.emit()
|
||||
|
||||
def on_low_res_button_clicked__slot(self):
|
||||
if self.low_res_mode_button.text() == "ENABLED":
|
||||
self.in_low_res_mode = False
|
||||
self.set_max_resolutions_flag = True
|
||||
|
||||
self.low_res_button_text_update_ready__signal.emit("DISABLED")
|
||||
self.low_res_button_stylesheet_update_ready__signal.emit(COLOR_GREEN)
|
||||
else:
|
||||
self.in_low_res_mode = True
|
||||
self.set_max_resolutions_flag = True
|
||||
|
||||
self.low_res_button_text_update_ready__signal.emit("ENABLED")
|
||||
self.low_res_button_stylesheet_update_ready__signal.emit(COLOR_RED)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,268 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
import logging
|
||||
import cv2
|
||||
import numpy as np
|
||||
import qimage2ndarray
|
||||
from time import time
|
||||
|
||||
import rospy
|
||||
import dynamic_reconfigure.client
|
||||
from cv_bridge import CvBridge
|
||||
from sensor_msgs.msg import CompressedImage
|
||||
|
||||
# Custom Imports
|
||||
from rover_camera.msg import CameraControlMessage
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
CAMERA_TOPIC_PATH = "/cameras/"
|
||||
|
||||
QUALITY_MAX = 80
|
||||
QUALITY_MIN = 15
|
||||
|
||||
FRAMERATE_AVERAGING_TIME = 10 # seconds
|
||||
|
||||
MIN_FRAMERATE_BEFORE_ADJUST = 23
|
||||
MAX_FRAMERATE_BEFORE_ADJUST = 28
|
||||
|
||||
|
||||
#####################################
|
||||
# RoverVideoReceiver Class Definition
|
||||
#####################################
|
||||
class RoverVideoReceiver(QtCore.QThread):
|
||||
image_ready_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
RESOLUTION_OPTIONS = [(256, 144), (640, 360), (1280, 720)]
|
||||
|
||||
RESOLUTION_MAPPINGS = {
|
||||
(1280, 720): None,
|
||||
(640, 360): None,
|
||||
(256, 144): None
|
||||
}
|
||||
|
||||
def __init__(self, camera_name):
|
||||
super(RoverVideoReceiver, self).__init__()
|
||||
|
||||
# ########## Reference to class init variables ##########
|
||||
self.camera_name = camera_name
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.camera_title_name = self.camera_name.replace("_", " ").title()
|
||||
|
||||
self.topic_base_path = CAMERA_TOPIC_PATH + self.camera_name
|
||||
self.control_topic_path = self.topic_base_path + "/camera_control"
|
||||
|
||||
# Subscription variables
|
||||
self.video_subscribers = []
|
||||
|
||||
# Publisher variables
|
||||
self.camera_control_publisher = rospy.Publisher(self.control_topic_path, CameraControlMessage, queue_size=1)
|
||||
|
||||
# Set up resolution mappings
|
||||
self.RESOLUTION_MAPPINGS[(1280, 720)] = CameraControlMessage()
|
||||
self.RESOLUTION_MAPPINGS[(640, 360)] = CameraControlMessage()
|
||||
self.RESOLUTION_MAPPINGS[(256, 144)] = CameraControlMessage()
|
||||
|
||||
self.RESOLUTION_MAPPINGS[(1280, 720)].enable_large_broadcast = True
|
||||
self.RESOLUTION_MAPPINGS[(640, 360)].enable_medium_broadcast = True
|
||||
self.RESOLUTION_MAPPINGS[(256, 144)].enable_small_broadcast = True
|
||||
|
||||
# Auto resolution adjustment variables
|
||||
self.current_resolution_index = 1
|
||||
self.last_resolution_index = self.current_resolution_index
|
||||
self.max_resolution_index = len(self.RESOLUTION_OPTIONS)
|
||||
|
||||
self.frame_count = 0
|
||||
self.last_framerate_time = time()
|
||||
self.resolution_just_adjusted = False
|
||||
|
||||
# Image variables
|
||||
self.raw_image = None
|
||||
self.opencv_1280x720_image = None
|
||||
self.opencv_640x360_image = None
|
||||
|
||||
self.pixmap_1280x720_image = None
|
||||
self.pixmap_640x360_image = None
|
||||
|
||||
# Processing variables
|
||||
self.bridge = CvBridge() # OpenCV ROS Video Data Processor
|
||||
self.video_enabled = True
|
||||
self.new_frame = False
|
||||
|
||||
# Text Drawing Variables
|
||||
self.font = cv2.FONT_HERSHEY_TRIPLEX
|
||||
self.font_thickness = 1
|
||||
self.font_baseline = 0
|
||||
|
||||
self.camera_name_opencv_1280x720_image = None
|
||||
self.camera_name_opencv_640x360_image = None
|
||||
|
||||
# Initial class setup to make text images and get camera resolutions
|
||||
self.__create_camera_name_opencv_images()
|
||||
|
||||
# Attach subscribers now that everything is set up
|
||||
self.video_subscribers.append(rospy.Subscriber(self.topic_base_path + "/image_1280x720/compressed", CompressedImage, self.__image_data_received_callback))
|
||||
self.video_subscribers.append(rospy.Subscriber(self.topic_base_path + "/image_640x360/compressed", CompressedImage, self.__image_data_received_callback))
|
||||
self.video_subscribers.append(rospy.Subscriber(self.topic_base_path + "/image_256x144/compressed", CompressedImage, self.__image_data_received_callback))
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Starting \"%s\" Camera Thread" % self.camera_title_name)
|
||||
|
||||
self.__enable_camera_resolution(self.RESOLUTION_OPTIONS[self.current_resolution_index])
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.video_enabled:
|
||||
self.__show_video_enabled()
|
||||
else:
|
||||
self.__show_video_disabled()
|
||||
|
||||
self.msleep(10)
|
||||
|
||||
self.logger.debug("Stopping \"%s\" Camera Thread" % self.camera_title_name)
|
||||
|
||||
def __enable_camera_resolution(self, resolution):
|
||||
self.camera_control_publisher.publish(self.RESOLUTION_MAPPINGS[resolution])
|
||||
|
||||
def __check_framerate_and_adjust_resolution(self):
|
||||
time_diff = time() - self.last_framerate_time
|
||||
if time_diff > FRAMERATE_AVERAGING_TIME:
|
||||
current_fps = self.frame_count / time_diff
|
||||
|
||||
if current_fps >= MAX_FRAMERATE_BEFORE_ADJUST:
|
||||
self.current_resolution_index = min(self.current_resolution_index + 1, self.max_resolution_index)
|
||||
elif current_fps <= MIN_FRAMERATE_BEFORE_ADJUST:
|
||||
self.current_resolution_index = max(self.current_resolution_index - 1, 0)
|
||||
else:
|
||||
self.current_resolution_index = min(self.current_resolution_index, self.max_resolution_index)
|
||||
|
||||
if self.last_resolution_index != self.current_resolution_index:
|
||||
self.camera_control_publisher.publish(
|
||||
self.RESOLUTION_MAPPINGS[self.RESOLUTION_OPTIONS[self.current_resolution_index]])
|
||||
print "Setting %s to %s" % (self.camera_title_name, self.RESOLUTION_OPTIONS[self.current_resolution_index])
|
||||
self.last_resolution_index = self.current_resolution_index
|
||||
self.resolution_just_adjusted = True
|
||||
|
||||
# print "%s: %s FPS" % (self.camera_title_name, current_fps)
|
||||
self.last_framerate_time = time()
|
||||
self.frame_count = 0
|
||||
|
||||
def __show_video_enabled(self):
|
||||
if self.new_frame:
|
||||
self.__check_framerate_and_adjust_resolution()
|
||||
|
||||
try:
|
||||
opencv_image = self.bridge.compressed_imgmsg_to_cv2(self.raw_image, "rgb8")
|
||||
|
||||
self.__create_final_pixmaps(opencv_image)
|
||||
|
||||
self.image_ready_signal.emit(self.camera_name)
|
||||
except Exception, error:
|
||||
print "Failed with error:" + str(error)
|
||||
|
||||
self.new_frame = False
|
||||
|
||||
def __show_video_disabled(self):
|
||||
blank_frame = np.zeros((720, 1280, 3), np.uint8)
|
||||
|
||||
self.__create_final_pixmaps(blank_frame)
|
||||
|
||||
self.image_ready_signal.emit(self.camera_name)
|
||||
|
||||
def __create_final_pixmaps(self, opencv_image):
|
||||
height, width, _ = opencv_image.shape
|
||||
|
||||
if width != 1280 and height != 720:
|
||||
self.opencv_1280x720_image = cv2.resize(opencv_image, (1280, 720))
|
||||
else:
|
||||
self.opencv_1280x720_image = opencv_image
|
||||
|
||||
if width != 640 and height != 360:
|
||||
self.opencv_640x360_image = cv2.resize(opencv_image, (640, 360))
|
||||
else:
|
||||
self.opencv_640x360_image = opencv_image
|
||||
|
||||
self.__apply_camera_name(self.opencv_1280x720_image, self.camera_name_opencv_1280x720_image)
|
||||
self.__apply_camera_name(self.opencv_640x360_image, self.camera_name_opencv_640x360_image)
|
||||
|
||||
self.pixmap_1280x720_image = QtGui.QPixmap.fromImage(qimage2ndarray.array2qimage(
|
||||
self.opencv_1280x720_image))
|
||||
self.pixmap_640x360_image = QtGui.QPixmap.fromImage(qimage2ndarray.array2qimage(
|
||||
self.opencv_640x360_image))
|
||||
|
||||
def __image_data_received_callback(self, raw_image):
|
||||
self.raw_image = raw_image
|
||||
self.frame_count += 1
|
||||
self.new_frame = True
|
||||
|
||||
if self.resolution_just_adjusted:
|
||||
self.frame_count = 0
|
||||
self.last_framerate_time = time()
|
||||
self.resolution_just_adjusted = False
|
||||
|
||||
def __create_camera_name_opencv_images(self):
|
||||
camera_name_text_width, camera_name_text_height = \
|
||||
cv2.getTextSize(self.camera_title_name, self.font, self.font_thickness, self.font_baseline)[0]
|
||||
|
||||
camera_name_width_buffered = camera_name_text_width + 10
|
||||
camera_name_height_buffered = camera_name_text_height + 20
|
||||
|
||||
camera_name_opencv_image = np.zeros(
|
||||
(camera_name_height_buffered, camera_name_width_buffered, 3), np.uint8)
|
||||
|
||||
cv2.putText(
|
||||
camera_name_opencv_image,
|
||||
self.camera_title_name,
|
||||
((camera_name_width_buffered - camera_name_text_width) / 2, int((camera_name_height_buffered * 2) / 3)),
|
||||
self.font,
|
||||
1,
|
||||
(255, 255, 255),
|
||||
1,
|
||||
cv2.LINE_AA)
|
||||
|
||||
self.camera_name_opencv_1280x720_image = \
|
||||
cv2.resize(camera_name_opencv_image, (camera_name_width_buffered, camera_name_height_buffered))
|
||||
|
||||
self.camera_name_opencv_640x360_image = \
|
||||
cv2.resize(camera_name_opencv_image, (camera_name_width_buffered / 2, camera_name_height_buffered / 2))
|
||||
|
||||
def set_hard_max_resolution(self, resolution):
|
||||
self.max_resolution_index = self.RESOLUTION_OPTIONS.index(resolution)
|
||||
|
||||
def toggle_video_display(self):
|
||||
if not self.video_enabled:
|
||||
self.camera_control_publisher.publish(self.RESOLUTION_MAPPINGS[self.RESOLUTION_OPTIONS[self.current_resolution_index]])
|
||||
self.video_enabled = True
|
||||
self.new_frame = True
|
||||
else:
|
||||
self.camera_control_publisher.publish(CameraControlMessage())
|
||||
self.video_enabled = False
|
||||
|
||||
def connect_signals_and_slots(self):
|
||||
pass
|
||||
|
||||
def setup_signals(self, start_signal, signals_and_slots_signal, kill_signal):
|
||||
start_signal.connect(self.start)
|
||||
signals_and_slots_signal.connect(self.connect_signals_and_slots)
|
||||
kill_signal.connect(self.on_kill_threads_requested__slot)
|
||||
|
||||
def on_kill_threads_requested__slot(self):
|
||||
self.run_thread_flag = False
|
||||
|
||||
@staticmethod
|
||||
def __apply_camera_name(opencv_image, font_opencv_image):
|
||||
opencv_image[0:0 + font_opencv_image.shape[0], 0:0 + font_opencv_image.shape[1]] = \
|
||||
font_opencv_image
|
||||
@@ -0,0 +1,14 @@
|
||||
# Ground Station Software
|
||||
This code is for the ground station side of the Mars Rover project.
|
||||
|
||||
## Requirements
|
||||
* Python 2.X
|
||||
* PyQt5
|
||||
* ROS Kinetic
|
||||
|
||||
Python Libraries
|
||||
* inputs
|
||||
|
||||
## How to Launch
|
||||
Navigate to this directory on a linux system with the requirements listed above installed.
|
||||
Run "python RoverGroundStation.py" or ./RoverGroundStation.py
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
@@ -0,0 +1,66 @@
|
||||
# Note that the lat and lon positions below correspond to the center point of the maps you want to download
|
||||
# Proper zoom level selection determines total viewable area
|
||||
|
||||
MAPPING_LOCATIONS = {
|
||||
"Graf Hall": {
|
||||
"latitude": 44.5675721667,
|
||||
"longitude": -123.2750535,
|
||||
"default_zoom": 18,
|
||||
"valid_zoom_options": [16, 17, 18, 19, 20],
|
||||
"pre_cache_map_zoom_levels": [18, 19, 20]
|
||||
},
|
||||
|
||||
"Crystal Lake": {
|
||||
"latitude": 44.547155,
|
||||
"longitude": -123.251438,
|
||||
"default_zoom": 18,
|
||||
"valid_zoom_options": [16, 17, 18, 19, 20],
|
||||
"pre_cache_map_zoom_levels": [18, 19, 20]
|
||||
},
|
||||
|
||||
"Bend's \"The Pit\"": {
|
||||
"latitude": 43.9941317,
|
||||
"longitude": -121.4150066,
|
||||
"default_zoom": 17,
|
||||
"valid_zoom_options": [16, 17, 18, 19, 20],
|
||||
"pre_cache_map_zoom_levels": [18, 19, 20]
|
||||
},
|
||||
|
||||
"McMullen": {
|
||||
"latitude": 51.470326,
|
||||
"longitude": -112.773995,
|
||||
"default_zoom": 17,
|
||||
"valid_zoom_options": [16, 17, 18, 19, 20],
|
||||
"pre_cache_map_zoom_levels": [18, 19, 20]
|
||||
},
|
||||
|
||||
"Compound": {
|
||||
"latitude": 51.470941,
|
||||
"longitude": -112.752322,
|
||||
"default_zoom": 17,
|
||||
"valid_zoom_options": [16, 17, 18, 19, 20],
|
||||
"pre_cache_map_zoom_levels": [18, 19, 20]
|
||||
},
|
||||
|
||||
"1st Avenue": {
|
||||
"latitude": 51.453744,
|
||||
"longitude": -112.715879,
|
||||
"default_zoom": 17,
|
||||
"valid_zoom_options": [16, 17, 18, 19, 20],
|
||||
"pre_cache_map_zoom_levels": [18, 19, 20]
|
||||
},
|
||||
|
||||
"Rosedale": {
|
||||
"latitude": 51.421368,
|
||||
"longitude": -112.641666,
|
||||
"default_zoom": 17,
|
||||
"valid_zoom_options": [16, 17, 18, 19, 20],
|
||||
"pre_cache_map_zoom_levels": [18, 19, 20]
|
||||
},
|
||||
}
|
||||
|
||||
LAST_SELECTION = "Graf Hall"
|
||||
LAST_ZOOM_LEVEL = MAPPING_LOCATIONS[LAST_SELECTION]["default_zoom"]
|
||||
|
||||
# ##### This is the offset from magnetic north to true north
|
||||
DECLINATION_OFFSET = 0 # We set this to 0 so we can use a phone to calibrate
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
import sys
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui, uic
|
||||
import signal
|
||||
import rospy
|
||||
import logging
|
||||
import qdarkstyle
|
||||
|
||||
|
||||
# Custom Imports
|
||||
import Framework.StartupSystems.ROSMasterChecker as ROSMasterChecker
|
||||
import Framework.LoggingSystems.Logger as Logger
|
||||
import Framework.VideoSystems.RoverVideoCoordinator as RoverVideoCoordinator
|
||||
import Framework.MapSystems.RoverMapCoordinator as RoverMapCoordinator
|
||||
import Framework.ControlSystems.DriveAndCameraControlSender as JoystickControlSender
|
||||
import Framework.ControlSystems.EffectorsAndArmControlSender as ControllerControlSender
|
||||
import Framework.NavigationSystems.SpeedAndHeadingIndication as SpeedAndHeading
|
||||
import Framework.NavigationSystems.WaypointsCoordinator as WaypointsCoordinator
|
||||
import Framework.ArmSystems.ArmIndication as ArmIndication
|
||||
import Framework.StatusSystems.StatusCore as StatusCore
|
||||
import Framework.StatusSystems.UbiquitiStatusCore as UbiquitiStatusCore
|
||||
import Framework.SettingsSystems.UbiquitiRadioSettings as UbiquitiRadioSettings
|
||||
import Framework.MiscSystems.MiningCore as MiningCore
|
||||
import Framework.MiscSystems.BashConsoleCore as BashConsoleCore
|
||||
import Framework.MiscSystems.MiscArmCore as MiscArmCore
|
||||
import Framework.MiscSystems.RDFCore as RDFCore
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
UI_FILE_LEFT = "Resources/Ui/left_screen.ui"
|
||||
UI_FILE_RIGHT = "Resources/Ui/right_screen.ui"
|
||||
|
||||
#####################################
|
||||
# Class Organization
|
||||
#####################################
|
||||
# Class Name:
|
||||
# "init"
|
||||
# "run (if there)" - personal pref
|
||||
# "private methods"
|
||||
# "public methods, minus slots"
|
||||
# "slot methods"
|
||||
# "static methods"
|
||||
# "run (if there)" - personal pref
|
||||
|
||||
|
||||
#####################################
|
||||
# ApplicationWindow Class Definition
|
||||
#####################################
|
||||
class ApplicationWindow(QtWidgets.QMainWindow):
|
||||
exit_requested_signal = QtCore.pyqtSignal()
|
||||
|
||||
kill_threads_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None, ui_file_path=None):
|
||||
super(ApplicationWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ui_file_path, self)
|
||||
|
||||
QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+Q"), self, self.exit_requested_signal.emit)
|
||||
|
||||
|
||||
#####################################
|
||||
# GroundStation Class Definition
|
||||
#####################################
|
||||
class GroundStation(QtCore.QObject):
|
||||
LEFT_SCREEN_ID = 1
|
||||
RIGHT_SCREEN_ID = 0
|
||||
|
||||
exit_requested_signal = QtCore.pyqtSignal()
|
||||
|
||||
start_threads_signal = QtCore.pyqtSignal()
|
||||
connect_signals_and_slots_signal = QtCore.pyqtSignal()
|
||||
kill_threads_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None,):
|
||||
# noinspection PyArgumentList
|
||||
super(GroundStation, self).__init__(parent)
|
||||
|
||||
# ##### Setup the Logger Immediately #####
|
||||
self.logger_setup_class = Logger.Logger(console_output=True) # Doesn't need to be shared
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("groundstation")
|
||||
|
||||
self.shared_objects = {
|
||||
"screens": {},
|
||||
"regular_classes": {},
|
||||
"threaded_classes": {}
|
||||
}
|
||||
|
||||
# ###### Instantiate Left And Right Screens ######
|
||||
self.shared_objects["screens"]["left_screen"] = \
|
||||
self.create_application_window(UI_FILE_LEFT, "Rover Ground Station Left Screen",
|
||||
self.LEFT_SCREEN_ID) # type: ApplicationWindow
|
||||
|
||||
self.shared_objects["screens"]["right_screen"] = \
|
||||
self.create_application_window(UI_FILE_RIGHT, "Rover Ground Station Right Screen",
|
||||
self.RIGHT_SCREEN_ID) # type: ApplicationWindow
|
||||
|
||||
# ###### Initialize the Ground Station Node ######
|
||||
rospy.init_node("ground_station")
|
||||
|
||||
# ##### Instantiate Regular Classes ######
|
||||
self.__add_non_thread("Mining System", MiningCore.Mining(self.shared_objects))
|
||||
self.__add_non_thread("Arm Indication", ArmIndication.ArmIndication(self.shared_objects))
|
||||
|
||||
# ##### Instantiate Threaded Classes ######
|
||||
self.__add_thread("Video Coordinator", RoverVideoCoordinator.RoverVideoCoordinator(self.shared_objects))
|
||||
self.__add_thread("Map Coordinator", RoverMapCoordinator.RoverMapCoordinator(self.shared_objects))
|
||||
self.__add_thread("Joystick Sender", JoystickControlSender.DriveAndCameraControlSender(self.shared_objects))
|
||||
self.__add_thread("Controller Sender", ControllerControlSender.EffectorsAndArmControlSender(self.shared_objects))
|
||||
self.__add_thread("Speed and Heading", SpeedAndHeading.SpeedAndHeadingIndication(self.shared_objects))
|
||||
self.__add_thread("Rover Status", StatusCore.SensorCore(self.shared_objects))
|
||||
self.__add_thread("Ubiquiti Status", UbiquitiStatusCore.UbiquitiStatus(self.shared_objects))
|
||||
self.__add_thread("Ubiquiti Radio Settings", UbiquitiRadioSettings.UbiquitiRadioSettings(self.shared_objects))
|
||||
self.__add_thread("Waypoints Coordinator", WaypointsCoordinator.WaypointsCoordinator(self.shared_objects))
|
||||
self.__add_thread("Bash Console", BashConsoleCore.BashConsole(self.shared_objects))
|
||||
self.__add_thread("Misc Arm", MiscArmCore.MiscArm(self.shared_objects))
|
||||
self.__add_thread("RDF", RDFCore.RDF(self.shared_objects))
|
||||
|
||||
self.connect_signals_and_slots_signal.emit()
|
||||
self.__connect_signals_to_slots()
|
||||
self.start_threads_signal.emit()
|
||||
|
||||
def ___ros_master_running(self):
|
||||
checker = ROSMasterChecker.ROSMasterChecker()
|
||||
|
||||
if not checker.master_present(5):
|
||||
self.logger.debug("ROS Master Not Found!!!! Exiting!!!")
|
||||
QtGui.QGuiApplication.exit()
|
||||
return False
|
||||
return True
|
||||
|
||||
def __add_thread(self, thread_name, instance):
|
||||
self.shared_objects["threaded_classes"][thread_name] = instance
|
||||
instance.setup_signals(self.start_threads_signal, self.connect_signals_and_slots_signal,
|
||||
self.kill_threads_signal)
|
||||
|
||||
def __add_non_thread(self, name, instance):
|
||||
self.shared_objects["regular_classes"][name] = instance
|
||||
|
||||
def __connect_signals_to_slots(self):
|
||||
self.shared_objects["screens"]["left_screen"].exit_requested_signal.connect(self.on_exit_requested__slot)
|
||||
self.shared_objects["screens"]["right_screen"].exit_requested_signal.connect(self.on_exit_requested__slot)
|
||||
|
||||
def on_exit_requested__slot(self):
|
||||
self.kill_threads_signal.emit()
|
||||
|
||||
# Wait for Threads
|
||||
for thread in self.shared_objects["threaded_classes"]:
|
||||
self.shared_objects["threaded_classes"][thread].wait()
|
||||
|
||||
QtGui.QGuiApplication.exit()
|
||||
|
||||
@staticmethod
|
||||
def create_application_window(ui_file_path, title, display_screen):
|
||||
system_desktop = QtWidgets.QDesktopWidget() # This gets us access to the desktop geometry
|
||||
|
||||
app_window = ApplicationWindow(parent=None, ui_file_path=ui_file_path) # Make a window in this application
|
||||
app_window.setWindowTitle(title) # Sets the window title
|
||||
|
||||
app_window.setWindowFlags(app_window.windowFlags() | # Sets the windows flags to:
|
||||
QtCore.Qt.FramelessWindowHint | # remove the border and frame on the application,
|
||||
QtCore.Qt.WindowStaysOnTopHint | # and makes the window stay on top of all others
|
||||
QtCore.Qt.X11BypassWindowManagerHint) # This is needed to show fullscreen in gnome
|
||||
|
||||
app_window.setGeometry(
|
||||
system_desktop.screenGeometry(display_screen)) # Sets the window to be on the first screen
|
||||
|
||||
app_window.showFullScreen() # Shows the window in full screen mode
|
||||
|
||||
return app_window
|
||||
|
||||
|
||||
#####################################
|
||||
# Main Definition
|
||||
#####################################
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # This allows the keyboard interrupt kill to work properly
|
||||
|
||||
# ########## Start the QApplication Framework ##########
|
||||
application = QtWidgets.QApplication(sys.argv) # Create the ase qt gui application
|
||||
application.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
|
||||
|
||||
# ########## Set Organization Details for QSettings ##########
|
||||
QtCore.QCoreApplication.setOrganizationName("OSURC")
|
||||
QtCore.QCoreApplication.setOrganizationDomain("http://osurobotics.club/")
|
||||
QtCore.QCoreApplication.setApplicationName("groundstation")
|
||||
|
||||
# ########## Check ROS Master Status ##########
|
||||
master_checker = ROSMasterChecker.ROSMasterChecker()
|
||||
|
||||
if not master_checker.master_present(5):
|
||||
message_box = QtWidgets.QMessageBox()
|
||||
message_box.setWindowTitle("Rover Ground Station")
|
||||
message_box.setText("Connection to ROS Master Failed!!!\n" +
|
||||
"Ensure ROS master is running or check for network issues.")
|
||||
message_box.exec_()
|
||||
exit()
|
||||
|
||||
# ########## Start Ground Station If Ready ##########
|
||||
ground_station = GroundStation()
|
||||
application.exec_() # Execute launching of the application
|
||||
Reference in New Issue
Block a user