Added 2016-2017 rover code, and a ROB assignment
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
This file contains the live logs page sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
from inputs import devices, GamePad
|
||||
import time
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
GAME_CONTROLLER_NAME = "FrSky FrSky Taranis Joystick"
|
||||
CONTROLLER_DATA_UPDATE_FREQUENCY = 50 # Times per second
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class FreeSkyController(QtCore.QThread):
|
||||
|
||||
# ########## Signals ##########
|
||||
controller_connection_aquired = QtCore.pyqtSignal(bool)
|
||||
controller_update_ready_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, main_window):
|
||||
super(FreeSkyController, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
self.setup_controller_flag = True
|
||||
self.data_acquisition_and_broadcast_flag = True
|
||||
self.controller_aquired = False
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.gamepad = None # type: GamePad
|
||||
|
||||
self.controller_states = {
|
||||
"left_stick_x_axis": 0,
|
||||
"left_stick_y_axis": 0,
|
||||
|
||||
"right_stick_x_axis": 0,
|
||||
"right_stick_y_axis": 0,
|
||||
|
||||
"sa_state": 0,
|
||||
"sb_state": 0,
|
||||
"sc_state": 0,
|
||||
"sd_state": 0,
|
||||
"se_state": 0,
|
||||
"sf_state": 0,
|
||||
"sg_state": 0,
|
||||
"sh_state": 0,
|
||||
|
||||
"ls_axis": 0,
|
||||
"rs_axis": 0,
|
||||
|
||||
"s1_axis": 0,
|
||||
"s2_axis": 0
|
||||
}
|
||||
|
||||
self.raw_mapping_to_class_mapping = {
|
||||
"ABS_RX": "left_stick_x_axis",
|
||||
"ABS_X": "left_stick_y_axis",
|
||||
|
||||
|
||||
"ABS_Z": "right_stick_x_axis",
|
||||
"ABS_Y": "right_stick_y_axis",
|
||||
|
||||
"BTN_SOUTH": "sa_state",
|
||||
"BTN_EAST": "sb_state",
|
||||
"BTN_C": "sc_state",
|
||||
"BTN_NORTH": "sd_state",
|
||||
"BTN_WEST": "se_state",
|
||||
"BTN_Z": "sf_state",
|
||||
"BTN_TL": "sg_state",
|
||||
"BTN_TR": "sh_state",
|
||||
|
||||
"ABS_RY": "ls_axis",
|
||||
"ABS_RUDDER": "rs_axis",
|
||||
|
||||
"ABS_RZ": "s1_axis",
|
||||
"ABS_THROTTLE" : "s2_axis"
|
||||
}
|
||||
|
||||
self.last_time = time.time()
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("FreeSky Thread Starting...")
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.setup_controller_flag:
|
||||
self.controller_aquired = self.__setup_controller()
|
||||
self.setup_controller_flag = False
|
||||
if self.data_acquisition_and_broadcast_flag:
|
||||
self.__get_controller_data()
|
||||
self.__broadcast_if_ready()
|
||||
|
||||
self.logger.debug("FreeSky Thread Stopping...")
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def connect_signals_to_slots__slot(self):
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
|
||||
def __setup_controller(self):
|
||||
for device in devices.gamepads:
|
||||
if device.name == GAME_CONTROLLER_NAME:
|
||||
self.gamepad = device
|
||||
self.controller_connection_aquired.emit(True)
|
||||
return True
|
||||
|
||||
self.logger.info("FrySky Failed to Connect")
|
||||
self.controller_connection_aquired.emit(False)
|
||||
return False
|
||||
|
||||
def __get_controller_data(self):
|
||||
if self.controller_aquired:
|
||||
events = self.gamepad.read()
|
||||
# self.logger.debug(events[0].manager)
|
||||
|
||||
for event in events:
|
||||
if event.code in self.raw_mapping_to_class_mapping:
|
||||
self.controller_states[self.raw_mapping_to_class_mapping[event.code]] = event.state
|
||||
# self.logger.debug(event.state)
|
||||
# if event.code not in self.raw_mapping_to_class_mapping and event.code != "SYN_REPORT":
|
||||
# self.logger.debug(str(event.code) + " : " + str(event.state))
|
||||
#
|
||||
# if event.code == "ABS_RUDDER":
|
||||
# self.logger.debug(str(event.code) + " : " + str(event.state))
|
||||
# self.logger.debug("Frksy: " + str(self.controller_states))
|
||||
|
||||
def __broadcast_if_ready(self):
|
||||
current_time = time.time()
|
||||
|
||||
if (current_time - self.last_time) > (1/CONTROLLER_DATA_UPDATE_FREQUENCY):
|
||||
self.controller_update_ready_signal.emit()
|
||||
self.last_time = current_time
|
||||
# self.logger.debug(self.controller_states)
|
||||
|
||||
def on_kill_threads__slot(self):
|
||||
self.terminate() # DON'T normally do this, but fine in this instance
|
||||
self.run_thread_flag = False
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
This file contains the logging core sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore
|
||||
from os import makedirs, rename, walk, unlink
|
||||
from os.path import exists, getmtime
|
||||
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("RoverBaseStation")
|
||||
|
||||
# ########## Set variables with useful paths ##########
|
||||
self.appdata_base_directory = self.settings.value("appdata_directory", type=str)
|
||||
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,247 @@
|
||||
"""
|
||||
This file contains the live logs page sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
from Resources.Libraries.goompy import GooMPy
|
||||
from PIL.ImageQt import ImageQt
|
||||
from PIL import ImageDraw
|
||||
import logging
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
INITIAL_LATITUDE = 38.4064262
|
||||
INITIAL_LONGITUDE = -110.794115
|
||||
INITIAL_ZOOM = 18 # 15,16, and 18 18 seems best
|
||||
MAPTYPE = 'satellite'
|
||||
|
||||
POSSIBLE_ZOOM_LEVELS = [18]
|
||||
|
||||
WIDTH = 2 * 640
|
||||
HEIGHT = 640
|
||||
|
||||
HAB_X_OFFSET = 82
|
||||
HAB_Y_OFFSET = -9
|
||||
|
||||
|
||||
#####################################
|
||||
# Live Logs Class Definition
|
||||
#####################################
|
||||
class MapProcessor(QtCore.QThread):
|
||||
update_map_label_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, main_window):
|
||||
super(MapProcessor, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
self.setup_goompy_flag = True
|
||||
|
||||
# ########## References to GUI Elements ##########
|
||||
self.map_label = self.main_window.map_label # type: QtWidgets.QLabel
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.goompy = None
|
||||
|
||||
self.pillow = None
|
||||
self.pixmap = QtGui.QPixmap("Resources/Images/waiting.png")
|
||||
|
||||
self.gps_valid = None
|
||||
self.lat = None
|
||||
self.lon = None
|
||||
self.alt = None # {'gps_pos_valid': 1, 'altitude': 21052, 'longitude': -668740150, 'latitude': 229797144}
|
||||
|
||||
self.gps_track_valid = None
|
||||
self.gps_speed = None
|
||||
self.gps_heading = None
|
||||
|
||||
self.current_zoom_index = POSSIBLE_ZOOM_LEVELS.index(INITIAL_ZOOM)
|
||||
self.last_zoom_level_index = self.current_zoom_index
|
||||
self.num_zoom_levels = len(POSSIBLE_ZOOM_LEVELS)
|
||||
|
||||
# ########## Gui Event Variables ##########
|
||||
self.x_drag = 0
|
||||
self.y_drag = 0
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Map Processor Thread Starting...")
|
||||
self.update_map_label_signal.emit()
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.setup_goompy_flag:
|
||||
self.__setup_goompy()
|
||||
self.setup_goompy_flag = False
|
||||
else:
|
||||
self.__get_goompy_pillow_image()
|
||||
#self.__add_map_point_from_gps(38.406404, -110.783481)
|
||||
self.__show_map_plus_overlays()
|
||||
self.msleep(1)
|
||||
|
||||
self.logger.debug("Map Processor Thread Stopping...")
|
||||
|
||||
def __setup_goompy(self):
|
||||
self.goompy = GooMPy(WIDTH, HEIGHT, INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ZOOM, MAPTYPE, 1500)
|
||||
self.goompy.move(HAB_X_OFFSET, HAB_Y_OFFSET)
|
||||
|
||||
def __get_goompy_pillow_image(self):
|
||||
if self.last_zoom_level_index != self.current_zoom_index:
|
||||
self.logger.debug("Changed to zoom level: " + str(POSSIBLE_ZOOM_LEVELS[self.current_zoom_index]))
|
||||
self.goompy.useZoom(POSSIBLE_ZOOM_LEVELS[self.current_zoom_index])
|
||||
self.last_zoom_level_index = self.current_zoom_index
|
||||
|
||||
self.pillow = self.goompy.getImage().convert("RGBA")
|
||||
|
||||
def __show_map_plus_overlays(self):
|
||||
|
||||
self.__draw_circle_at_point(self.pillow, self.goompy.width // 2, self.goompy.height // 2, 3)
|
||||
|
||||
qim = ImageQt(self.pillow)
|
||||
self.pixmap = QtGui.QPixmap.fromImage(qim)
|
||||
|
||||
self.update_map_label_signal.emit()
|
||||
|
||||
def __add_map_point_from_gps(self, lat, lon):
|
||||
self.logger.debug("Beginning")
|
||||
|
||||
if self.goompy:
|
||||
viewport_lat_nw, viewport_lon_nw = self.goompy.northwest
|
||||
viewport_lat_se, viewport_lon_se = self.goompy.southeast
|
||||
|
||||
viewport_lat_diff = viewport_lat_nw - viewport_lat_se
|
||||
viewport_lon_diff = viewport_lon_se - viewport_lon_nw
|
||||
|
||||
viewport_upper_y = self.goompy.uppery
|
||||
viewport_left_x = self.goompy.leftx
|
||||
viewport_width = self.goompy.width
|
||||
viewport_height = self.goompy.height
|
||||
|
||||
bigimage_width = self.goompy.bigimage.size[0]
|
||||
bigimage_height = self.goompy.bigimage.size[1]
|
||||
|
||||
pixel_per_lat = bigimage_height / viewport_lat_diff
|
||||
pixel_per_lon = bigimage_width / viewport_lon_diff
|
||||
|
||||
#
|
||||
# viewport_left_percentage = viewport_left_x / bigimage_width
|
||||
# viewport_right_percentage = (viewport_left_x + viewport_width) / bigimage_width
|
||||
# viewport_top_percentage = viewport_upper_y / bigimage_height
|
||||
# viewport_bottom_percentage = (viewport_upper_y + viewport_height) / bigimage_height
|
||||
#
|
||||
new_lat_gps_range_percentage = (viewport_lat_nw - lat)
|
||||
new_lon_gps_range_percentage = (lon - viewport_lon_nw)
|
||||
|
||||
point_x = new_lon_gps_range_percentage * pixel_per_lon
|
||||
point_y = new_lat_gps_range_percentage * pixel_per_lat
|
||||
self.__draw_circle_at_point(self.goompy.bigimage, int(point_x), int(point_y), 3)
|
||||
#
|
||||
# self.logger.debug("vlp: " + str(viewport_left_percentage) + " nlatp: " + str(new_lon_gps_range_percentage) + " vrp: " + str(viewport_right_percentage))
|
||||
# self.logger.debug("vtp: " + str(viewport_top_percentage) + " nlonp: " + str(new_lat_gps_range_percentage) + " vbp: " + str(viewport_bottom_percentage))
|
||||
#
|
||||
# x = new_lon_gps_range_percentage * bigimage_width
|
||||
# y = new_lat_gps_range_percentage * bigimage_height
|
||||
#
|
||||
# self.__draw_circle_at_point(self.goompy.bigimage, x, y, 3)
|
||||
#
|
||||
# if viewport_left_percentage < new_lon_gps_range_percentage < viewport_right_percentage and \
|
||||
# viewport_top_percentage < new_lat_gps_range_percentage < viewport_bottom_percentage:
|
||||
# self.logger.debug("valid point")
|
||||
# else:
|
||||
# self.logger.debug("invalid point")
|
||||
|
||||
self.logger.debug("Made it here")
|
||||
|
||||
def __draw_circle_at_point(self, image, x, y, r):
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.ellipse((x - r, y - r, x + r, y + r), fill=(255, 0, 0, 255))
|
||||
|
||||
def __on_map_label_update_requested__slot(self):
|
||||
self.map_label.setPixmap(self.pixmap)
|
||||
|
||||
def on_gps_coordinates_updated__slot(self, sdict):
|
||||
self.gps_valid = sdict["gps_pos_valid"]
|
||||
self.lat = (sdict["latitude"] * 10 ** -5) / 60
|
||||
self.lon = (sdict["longitude"] * 10 ** -5) / 60
|
||||
self.alt = (sdict["altitude"] / 10)
|
||||
|
||||
if self.gps_valid:
|
||||
pass
|
||||
#self.logger.debug("LAT: " + str(self.lat) + " | LON: " + str(self.lon) + " | ALT: " + str(self.alt))
|
||||
else:
|
||||
pass
|
||||
#self.logger.debug("Waiting for GPS lock")
|
||||
|
||||
def on_gps_track_updated__slot(self, sdict):
|
||||
self.gps_track_valid = sdict["gps_track_valid"]
|
||||
self.gps_speed = sdict["gps_speed"]
|
||||
self.gps_heading = sdict["gps_heading"]
|
||||
|
||||
if self.gps_track_valid:
|
||||
pass
|
||||
# self.logger.debug("SPEED: " + str(self.gps_speed) + " | HEADING: " + str(self.gps_heading))
|
||||
else:
|
||||
pass
|
||||
|
||||
def __mouse_released_on_map_event(self, _):
|
||||
self.x_drag = -1
|
||||
self.y_drag = -1
|
||||
|
||||
def __mouse_scrolled_on_map(self, event):
|
||||
if self.goompy:
|
||||
if event.angleDelta().y() > 0:
|
||||
self.current_zoom_index += 1
|
||||
else:
|
||||
self.current_zoom_index -= 1
|
||||
|
||||
self.current_zoom_index = self.clamp(self.current_zoom_index, 0, self.num_zoom_levels - 1)
|
||||
|
||||
def __mouse_moved_on_map_label(self, event):
|
||||
if self.x_drag != -1 and self.y_drag != -1 and self.goompy:
|
||||
buttons = event.buttons()
|
||||
if buttons == QtCore.Qt.LeftButton:
|
||||
x_pos = event.pos().x()
|
||||
y_pos = event.pos().y()
|
||||
dx = x_pos - self.x_drag
|
||||
dy = y_pos - self.y_drag
|
||||
if dy < 300 and dx < 300:
|
||||
# self.logger.debug(str(dx) + " : " + str(dy))
|
||||
self.goompy.move(-dx, -dy)
|
||||
self.x_drag = x_pos
|
||||
self.y_drag = y_pos
|
||||
else:
|
||||
self.x_drag = event.pos().x()
|
||||
self.y_drag = event.pos().y()
|
||||
else:
|
||||
self.x_drag = event.pos().x()
|
||||
self.y_drag = event.pos().y()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def connect_signals_to_slots__slot(self):
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
self.update_map_label_signal.connect(self.__on_map_label_update_requested__slot)
|
||||
self.main_window.miniboard_class.data_gps_position.connect(self.on_gps_coordinates_updated__slot)
|
||||
self.main_window.miniboard_class.data_gps_track.connect(self.on_gps_track_updated__slot)
|
||||
|
||||
self.map_label.mouseReleaseEvent = self.__mouse_released_on_map_event
|
||||
self.map_label.mouseMoveEvent = self.__mouse_moved_on_map_label
|
||||
self.map_label.wheelEvent = self.__mouse_scrolled_on_map
|
||||
|
||||
def on_kill_threads__slot(self):
|
||||
self.run_thread_flag = False
|
||||
|
||||
@staticmethod
|
||||
def clamp(n, minn, maxn):
|
||||
return max(min(maxn, n), minn)
|
||||
@@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
This file contains the rover communication code.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import serial
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
import struct
|
||||
import signal
|
||||
import time
|
||||
from serial.tools.list_ports import comports
|
||||
from io import StringIO, BytesIO
|
||||
import enum
|
||||
|
||||
# Import docparse
|
||||
docparsepath = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + "/common/"
|
||||
sys.path.append(docparsepath)
|
||||
import docparse
|
||||
|
||||
with open(docparsepath + "/SPECIFICATION.md") as p:
|
||||
RoverCmdTable = docparse.extract_table(p.read())
|
||||
RoverCmdDict = {cmd["code"]: cmd for cmd in RoverCmdTable}
|
||||
for k in RoverCmdDict:
|
||||
expected_size = 5
|
||||
for a in RoverCmdDict[k]["argument"]:
|
||||
if a[0] == "*":
|
||||
expected_size += 256
|
||||
else:
|
||||
expected_size += int(a[0][1:]) // 8
|
||||
RoverCmdDict[k]["expected_size"] = expected_size
|
||||
|
||||
|
||||
# Create read/write functions
|
||||
def create_funcs(module_vars, cmd_table):
|
||||
"""Create functions of the form
|
||||
read_<command_name>(signal)
|
||||
write_<command_name>(signal, *args)
|
||||
such as
|
||||
read_pause(signal)
|
||||
write_pause(signal, pause_state)
|
||||
in the current module."""
|
||||
log = logging.getLogger("Miniboard Command Functions")
|
||||
|
||||
def build_read_func(cmd):
|
||||
"""Return a function that takes a signal (connected to MiniboardIO's send())
|
||||
and emits a string of packet data."""
|
||||
|
||||
def f(signal):
|
||||
packet_data = [cmd["code"] | 0x80]
|
||||
signal.emit(packet_data)
|
||||
|
||||
return f
|
||||
|
||||
def build_write_func(cmd):
|
||||
"""Return a function that takes a signal (connected to MiniboardIO's __send())
|
||||
and emits a string of packet data."""
|
||||
fmtcodes = {"u8": "<B", "i8": "<b", "u16": "<H", "i16": "<h", "u32": "<I", "i32": "<i", "i64": "<Q",
|
||||
"i64": "<q"}
|
||||
|
||||
def f(signal, *args):
|
||||
if len(args) != len(cmd["argument"]):
|
||||
log.error("Error calling write func for command %s: expected %d arguments, got %d" % (
|
||||
cmd["name"], len(args), len(cmd["arguments"])))
|
||||
return
|
||||
packet_data = [cmd["code"]]
|
||||
for a, v in zip(cmd["argument"], args):
|
||||
if a[0] == "*":
|
||||
packet_data += list(v)
|
||||
else:
|
||||
packet_data += list(struct.pack(fmtcodes[a[0]], v))
|
||||
signal.emit(packet_data)
|
||||
|
||||
return f
|
||||
|
||||
for c in cmd_table:
|
||||
if "r" in c["rw"]: module_vars["read_" + docparse.cannon_name(c["name"])] = build_read_func(c)
|
||||
if "w" in c["rw"]: module_vars["write_" + docparse.cannon_name(c["name"])] = build_write_func(c)
|
||||
|
||||
|
||||
create_funcs(vars(), RoverCmdTable)
|
||||
|
||||
|
||||
def make_signals():
|
||||
"""Return a string that when eval'd in MiniboardIO
|
||||
creates signals for received data from the rover.
|
||||
Each signal has the name data_<canonical command name>,
|
||||
such as data_battery_voltage.
|
||||
Each signal emits a dictionary containing string keys of the
|
||||
command's argument names."""
|
||||
s = ""
|
||||
for c in RoverCmdTable:
|
||||
signame = "data_" + docparse.cannon_name(c["name"])
|
||||
s += "%s = QtCore.pyqtSignal([dict])\n" % signame
|
||||
signame = "ack_" + docparse.cannon_name(c["name"])
|
||||
s += "%s = QtCore.pyqtSignal()\n" % signame
|
||||
return s
|
||||
|
||||
|
||||
signal_eval_str = make_signals()
|
||||
|
||||
|
||||
class MiniboardIO(QtCore.QThread):
|
||||
"""Handles reading and writing from the miniboard."""
|
||||
path = "/dev/ttyUSB0"
|
||||
# path = "COM21"
|
||||
baud = 115200
|
||||
on_kill_threads__slot = QtCore.pyqtSignal()
|
||||
message_received_signal = QtCore.pyqtSignal()
|
||||
|
||||
exec(signal_eval_str)
|
||||
|
||||
def __init__(self, main_window):
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
os.system("stty -F %s -hupcl" % self.path)
|
||||
|
||||
com_name = comports()[0].device
|
||||
|
||||
if "COM" in com_name:
|
||||
com_name = "COM21"
|
||||
|
||||
self.logger.debug("Connecting to " + com_name)
|
||||
|
||||
# self.tty = serial.Serial(port=com_name,
|
||||
# baudrate=self.baud,
|
||||
# parity=serial.PARITY_NONE,
|
||||
# stopbits=serial.STOPBITS_ONE,
|
||||
# bytesize=serial.EIGHTBITS)
|
||||
self.run_thread_flag = True
|
||||
self.queue = []
|
||||
|
||||
def calc_crc(self, body):
|
||||
remainder = 0xFFFF
|
||||
for i in range(0, len(body)):
|
||||
remainder ^= body[i] << 8
|
||||
remainder &= 0xFFFF
|
||||
for bit in reversed(range(1, 9)):
|
||||
if remainder & 0x8000:
|
||||
remainder = (remainder << 1) ^ 0x1021
|
||||
remainder &= 0xFFFF
|
||||
else:
|
||||
remainder <<= 1
|
||||
remainder &= 0xFFFF
|
||||
return remainder
|
||||
|
||||
def append(self, body_list):
|
||||
"""Add a command to the queue."""
|
||||
self.queue.append(body_list)
|
||||
|
||||
def __send(self, body_list):
|
||||
"""Given a packet body list, turn it into a packet and send it to the rover."""
|
||||
packet_contents = body_list
|
||||
plen = len(packet_contents) + 2
|
||||
crc = self.calc_crc(packet_contents)
|
||||
packet_contents = [0x01] + [plen] + [crc & 0xFF] + [crc >> 8] + packet_contents
|
||||
self.tty.write(packet_contents)
|
||||
self.msleep((4 + len(body_list)) * .001)
|
||||
|
||||
def run(self):
|
||||
"""Read from the serial port, recognize the command, and emit a signal."""
|
||||
return
|
||||
self.logger.debug("MiniboardIO Thread Starting...")
|
||||
|
||||
self.reply = []
|
||||
fmtcodes = {"u8": "<B", "i8": "<b", "u16": "<H", "i16": "<h", "u32": "<I", "i32": "<i", "i64": "<Q",
|
||||
"i64": "<q"}
|
||||
|
||||
waiting_for_command_reply = False
|
||||
start_time = time.time()
|
||||
|
||||
while self.run_thread_flag:
|
||||
if len(self.queue) > 0 and not waiting_for_command_reply:
|
||||
body = self.queue[0]
|
||||
self.__send(body)
|
||||
self.queue = self.queue[1:]
|
||||
if len(body) < 1:
|
||||
expected_size = 1000000
|
||||
else:
|
||||
if body[0] & 0x80: # read
|
||||
expected_size = RoverCmdDict[body[0] & 0x7F]["expected_size"]
|
||||
else:
|
||||
expected_size = 5
|
||||
|
||||
start_time = time.time()
|
||||
waiting_for_command_reply = True
|
||||
|
||||
if waiting_for_command_reply:
|
||||
if self.tty.inWaiting() >= expected_size:
|
||||
waiting_for_command_reply = False
|
||||
self.reply = list(self.tty.read(size=expected_size))
|
||||
# n = 2
|
||||
# while n > 0 and len(self.reply) < expected_size:
|
||||
# self.reply += list(self.tty.read(size=1))
|
||||
# n -= 1
|
||||
|
||||
while len(self.reply) > 0:
|
||||
while len(self.reply) > 0:
|
||||
if self.reply[0] != 0x01:
|
||||
self.reply = self.reply[1:]
|
||||
else:
|
||||
break
|
||||
if len(self.reply) > 0: # Got start byte
|
||||
if len(self.reply) >= 5: # Got minimum complete packet
|
||||
if len(self.reply) >= (self.reply[1] + 2): # Got enough bytes for this packet
|
||||
if self.calc_crc(self.reply[4:]) == struct.unpack("<H", bytes(self.reply[2:4]))[0]: # CRC OK
|
||||
if self.reply[4] & 0x80:
|
||||
# self.logger.debug("read")
|
||||
code = self.reply[4] & 0x7F
|
||||
cmd = RoverCmdDict[code]
|
||||
adict = {}
|
||||
b = 5
|
||||
vs = []
|
||||
for a, i in zip(cmd["argument"], range(0, len(cmd["argument"]))):
|
||||
if a[0] != "*":
|
||||
s = struct.calcsize(fmtcodes[a[0]])
|
||||
value = struct.unpack(fmtcodes[a[0]], bytes(self.reply[b:b + s]))[0]
|
||||
adict[a[1]] = value
|
||||
vs.append(value)
|
||||
b += s
|
||||
else:
|
||||
s = vs[i - 1]
|
||||
value = self.reply[b:b + s]
|
||||
adict[a[1]] = value
|
||||
b += s
|
||||
getattr(self, "data_" + docparse.cannon_name(cmd["name"])).emit(adict)
|
||||
# self.logger.debug(docparse.cannon_name + ": " + adict.__str__())
|
||||
else:
|
||||
code = self.reply[4] & 0x7F
|
||||
cmd = RoverCmdDict[code]
|
||||
getattr(self, "ack_" + docparse.cannon_name(cmd["name"])).emit()
|
||||
self.reply = self.reply[(self.reply[1] + 2):]
|
||||
self.message_received_signal.emit()
|
||||
|
||||
#self.logger.debug("MiniIO Loop: " + str(time.time() - start_time))
|
||||
if time.time() - start_time > 0.3:
|
||||
self.logger.error("MINIIO: Lost ACK : " + str(time.time() - start_time))
|
||||
waiting_for_command_reply = False
|
||||
self.queue = []
|
||||
self.reply = []
|
||||
self.tty.flushOutput()
|
||||
self.tty.flushInput()
|
||||
self.msleep(1)
|
||||
|
||||
self.logger.debug("MiniboardIO Thread Stopping...")
|
||||
|
||||
def clear_buffers_and_queues(self):
|
||||
self.reply = ""
|
||||
self.queue = []
|
||||
self.tty.flushInput()
|
||||
self.tty.flushOutput()
|
||||
|
||||
def connect_signals_to_slots__slot(self):
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
|
||||
def on_kill_threads__slot(self):
|
||||
self.run_thread_flag = False
|
||||
|
||||
|
||||
class DemoThread(QtCore.QThread):
|
||||
on_kill_threads__slot = QtCore.pyqtSignal()
|
||||
send_packet = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, main_window):
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self.connect_signals_to_slots()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
write_swerve_drive_state(self.send_packet, 3)
|
||||
read_swerve_drive_state(self.send_packet)
|
||||
read_callsign(self.send_packet)
|
||||
self.msleep(1000)
|
||||
|
||||
def connect_signals_to_slots(self):
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
self.send_packet.connect(self.main_window.m.append)
|
||||
self.main_window.m.data_swerve_drive_state.connect(self.handle_swerve_data)
|
||||
|
||||
def handle_swerve_data(self, sdict):
|
||||
pass
|
||||
# self.logger.debug(sdict)
|
||||
|
||||
|
||||
class DemoWindow(QtWidgets.QMainWindow):
|
||||
kill_threads_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.m = MiniboardIO(self)
|
||||
self.d = DemoThread(self)
|
||||
self.m.start()
|
||||
self.d.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # This allows the keyboard interrupt kill to work properly
|
||||
application = QtWidgets.QApplication(sys.argv) # Create the base qt gui application
|
||||
app_window = DemoWindow() # Make a window in this application
|
||||
app_window.setWindowTitle("Demo Demo") # Sets the window title
|
||||
app_window.show() # Show the window in the application
|
||||
application.exec_() # Execute launching of the application
|
||||
@@ -0,0 +1,507 @@
|
||||
"""
|
||||
This file contains the live logs page sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
from Framework.MiniBoardIOCore import write_drive_motor_power, read_drive_motor_power, write_pause, \
|
||||
write_arm_motors, write_swerve_drive_state, write_joystick
|
||||
import time
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
JOY_MIN = -127
|
||||
JOY_MAX = 127
|
||||
|
||||
DRIVE_MIN = -127
|
||||
DRIVE_MAX = 127
|
||||
|
||||
ARMS_MIN = -127
|
||||
ARMS_MAX = 127
|
||||
|
||||
DEAD_BAND_FRSKY = 20
|
||||
DEAD_BAND_XBOX = 1500
|
||||
|
||||
DRIVE_TIMEOUT = 0.5 # Seconds
|
||||
ARM_TIMEOUT = 0.25 # Seconds
|
||||
PAN_TILT_TIMEOUT = 0.25 # Seconds
|
||||
DRIVE_SWERVE_TIMEOUT = 5 # Seconds
|
||||
|
||||
|
||||
#####################################
|
||||
# Notes
|
||||
#####################################
|
||||
# ##### Xbox State Names
|
||||
# "left_stick_x_axis"
|
||||
# "left_stick_y_axis"
|
||||
# "left_stick_center_pressed"
|
||||
#
|
||||
# "right_stick_x_axis"
|
||||
# "right_stick_y_axis"
|
||||
# "right_stick_center_pressed"
|
||||
#
|
||||
# "left_trigger_z_axis"
|
||||
# "left_bumper_pressed"
|
||||
#
|
||||
# "right_trigger_z_axis"
|
||||
# "right_bumper_pressed"
|
||||
#
|
||||
# "dpad_x"
|
||||
# "dpad_y"
|
||||
#
|
||||
# "select_pressed"
|
||||
# "start_pressed"
|
||||
# "home_pressed"
|
||||
#
|
||||
# "a_pressed"
|
||||
# "b_pressed"
|
||||
# "x_pressed"
|
||||
# "y_pressed"
|
||||
|
||||
# ##### Frsky State Names
|
||||
# "left_stick_x_axis"
|
||||
# "left_stick_y_axis"
|
||||
#
|
||||
# "right_stick_x_axis"
|
||||
# "right_stick_y_axis"
|
||||
#
|
||||
# "sc_state"
|
||||
# "sg_state"
|
||||
# "se_state"
|
||||
# "sa_state"
|
||||
#
|
||||
# "ls_axis"
|
||||
# "rs_axis"
|
||||
#
|
||||
# "s1_axis"
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class MotionProcessor(QtCore.QThread):
|
||||
send_miniboard_control_packet = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, main_window):
|
||||
super(MotionProcessor, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
self.wait_for_drive_response = False
|
||||
self.wait_for_pan_tilt_response = False
|
||||
self.wait_for_arm_response = False
|
||||
self.wait_for_swerve_state_response = False
|
||||
self.wait_for_passthrough_response = False
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.xbox_states = self.main_window.xbox_controller_class.controller_states
|
||||
self.frsky_states = self.main_window.frsky_controller_class.controller_states
|
||||
|
||||
self.last_pause_state = 0
|
||||
self.last_swerve_state = 0
|
||||
self.last_drive_state = 0
|
||||
|
||||
self.last_left_drive_value = 0
|
||||
self.last_right_drive_value = 0
|
||||
|
||||
self.pan_position = -1
|
||||
self.tilt_position = -1
|
||||
|
||||
self.frsky_locked = False
|
||||
self.xbox_locked = False
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Motion Processor Thread Starting...")
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.xbox_states and self.frsky_states:
|
||||
|
||||
start_time = time.time()
|
||||
self.__send_controller_passthrough()
|
||||
if not self.frsky_states["sa_state"]: # 0 is drive mode
|
||||
if not self.frsky_states["se_state"]: # 0 is drive mode
|
||||
# TODO: Change to send faster
|
||||
pass
|
||||
else: # 1 is auto mode
|
||||
# TODO: Change to send slower
|
||||
pass
|
||||
# self.logger.debug("Control time: " + str(time.time() - start_time))
|
||||
self.msleep(1)
|
||||
|
||||
self.logger.debug("Motion Processor Thread Stopping...")
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def connect_signals_to_slots__slot(self):
|
||||
self.main_window.xbox_controller_class.controller_update_ready_signal.connect(self.on_xbox_states_updated__slot)
|
||||
self.main_window.frsky_controller_class.controller_update_ready_signal.connect(
|
||||
self.on_frsky_states_updated__slot)
|
||||
|
||||
self.send_miniboard_control_packet.connect(self.main_window.miniboard_class.append)
|
||||
|
||||
self.main_window.miniboard_class.data_drive_motor_power.connect(
|
||||
self.on_drive_motor_power_response_received__slot)
|
||||
self.main_window.miniboard_class.ack_drive_motor_power.connect(self.on_drive_response_received__slot)
|
||||
|
||||
# self.main_window.miniboard_class.data_pan_tilt.connect(self.on_pan_tilt_primary_position_response__slot)
|
||||
# self.main_window.miniboard_class.data_pan_tilt_secondary.connect(
|
||||
# self.on_pan_tilt_secondary_position_response__slot)
|
||||
|
||||
# self.main_window.miniboard_class.ack_pan_tilt.connect(self.on_primary_pan_tilt_write_acknowledged__slot)
|
||||
|
||||
self.main_window.miniboard_class.ack_arm_motors.connect(self.on_arm_motors_write_acknowledged__slot)
|
||||
|
||||
self.main_window.miniboard_class.ack_swerve_drive_state.connect(self.on_swerve_state_response_received__slot)
|
||||
|
||||
self.main_window.miniboard_class.ack_joystick.connect(self.on_passthrough_response_received__slot)
|
||||
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
|
||||
def __send_controller_passthrough(self):
|
||||
# ##### Frsky #####
|
||||
fr_left_horiz = int(self.clamp(self.frsky_states["left_stick_x_axis"], JOY_MIN, JOY_MAX))
|
||||
fr_left_vert = int(self.clamp(self.frsky_states["left_stick_y_axis"], JOY_MIN, JOY_MAX))
|
||||
fr_right_horiz = int(self.clamp(self.frsky_states["right_stick_x_axis"], JOY_MIN, JOY_MAX))
|
||||
fr_right_vert = int(self.clamp(self.frsky_states["right_stick_y_axis"], JOY_MIN, JOY_MAX))
|
||||
fr_left_pot = int(self.clamp(self.frsky_states["s1_axis"], JOY_MIN, JOY_MAX))
|
||||
fr_right_pot = int(self.clamp(self.frsky_states["s2_axis"], JOY_MIN, JOY_MAX))
|
||||
fr_left_side_pot = int(self.clamp(self.frsky_states["ls_axis"], JOY_MIN, JOY_MAX))
|
||||
fr_right_side_pot = int(self.clamp(self.frsky_states["rs_axis"], JOY_MIN, JOY_MAX))
|
||||
|
||||
fr_button_a = self.frsky_states["sa_state"]
|
||||
fr_button_b = self.frsky_states["sb_state"]
|
||||
fr_button_c = self.frsky_states["sc_state"]
|
||||
fr_button_d = self.frsky_states["sd_state"]
|
||||
fr_button_e = self.frsky_states["se_state"]
|
||||
fr_button_f = self.frsky_states["sf_state"]
|
||||
fr_button_g = self.frsky_states["sg_state"]
|
||||
fr_button_h = self.frsky_states["sh_state"]
|
||||
|
||||
frsky_buttons_byte = 0
|
||||
|
||||
frsky_buttons_byte |= (fr_button_a << 0)
|
||||
frsky_buttons_byte |= (fr_button_b << 1)
|
||||
frsky_buttons_byte |= (fr_button_c << 2)
|
||||
frsky_buttons_byte |= (fr_button_d << 3)
|
||||
frsky_buttons_byte |= (fr_button_e << 4)
|
||||
frsky_buttons_byte |= (fr_button_f << 5)
|
||||
frsky_buttons_byte |= (fr_button_g << 6)
|
||||
frsky_buttons_byte |= (fr_button_h << 7)
|
||||
|
||||
# ##### XBOX #####
|
||||
xb_left_horiz = int(self.clamp(self.xbox_states["left_stick_x_axis"] // 256, JOY_MIN, JOY_MAX))
|
||||
xb_left_vert = int(self.clamp(self.xbox_states["left_stick_y_axis"] // 256, JOY_MIN, JOY_MAX))
|
||||
xb_right_horiz = int(self.clamp(self.xbox_states["right_stick_x_axis"] // 256, JOY_MIN, JOY_MAX))
|
||||
xb_right_vert = int(self.clamp(self.xbox_states["right_stick_y_axis"] // 256, JOY_MIN, JOY_MAX))
|
||||
xb_left_trig = int(self.clamp(self.xbox_states["left_trigger_z_axis"] - 127, JOY_MIN, JOY_MAX))
|
||||
xb_right_trig = int(self.clamp(self.xbox_states["right_trigger_z_axis"] - 127, JOY_MIN, JOY_MAX))
|
||||
|
||||
xb_button_a = self.xbox_states["a_pressed"]
|
||||
xb_button_b = self.xbox_states["b_pressed"]
|
||||
xb_button_x = self.xbox_states["x_pressed"]
|
||||
xb_button_y = self.xbox_states["y_pressed"]
|
||||
xb_button_lb = self.xbox_states["left_bumper_pressed"]
|
||||
xb_button_rb = self.xbox_states["right_bumper_pressed"]
|
||||
xb_button_lsc = self.xbox_states["left_stick_center_pressed"]
|
||||
xb_button_rsc = self.xbox_states["right_stick_center_pressed"]
|
||||
xb_button_sel = self.xbox_states["select_pressed"]
|
||||
xb_button_strt = self.xbox_states["start_pressed"]
|
||||
xb_button_home = self.xbox_states["home_pressed"]
|
||||
xb_button_dph = self.xbox_states["dpad_x"]
|
||||
xb_button_dpv = self.xbox_states["dpad_y"]
|
||||
|
||||
xbox_buttons_low_byte = 0
|
||||
xbox_buttons_high_byte = 0
|
||||
|
||||
xbox_buttons_low_byte |= (xb_button_a << 0)
|
||||
xbox_buttons_low_byte |= (xb_button_b << 1)
|
||||
xbox_buttons_low_byte |= (xb_button_x << 2)
|
||||
xbox_buttons_low_byte |= (xb_button_y << 3)
|
||||
xbox_buttons_low_byte |= (xb_button_lb << 4)
|
||||
xbox_buttons_low_byte |= (xb_button_rb << 5)
|
||||
xbox_buttons_low_byte |= (xb_button_lsc << 6)
|
||||
xbox_buttons_low_byte |= (xb_button_rsc << 7)
|
||||
|
||||
xbox_buttons_high_byte |= (xb_button_sel << 0)
|
||||
xbox_buttons_high_byte |= (xb_button_strt << 1)
|
||||
xbox_buttons_high_byte |= (xb_button_home << 2)
|
||||
|
||||
dpad_l_state = 0
|
||||
dpad_r_state = 0
|
||||
dpad_u_state = 0
|
||||
dpad_d_state = 0
|
||||
|
||||
if xb_button_dph == -1:
|
||||
dpad_l_state = 1
|
||||
elif xb_button_dph == 1:
|
||||
dpad_r_state = 1
|
||||
|
||||
if xb_button_dpv == -1:
|
||||
dpad_u_state = 1
|
||||
elif xb_button_dpv == 1:
|
||||
dpad_d_state = 1
|
||||
|
||||
xbox_buttons_high_byte |= (dpad_l_state << 3)
|
||||
xbox_buttons_high_byte |= (dpad_r_state << 4)
|
||||
xbox_buttons_high_byte |= (dpad_u_state << 5)
|
||||
xbox_buttons_high_byte |= (dpad_d_state << 6)
|
||||
xbox_buttons_high_byte |= (1 << 7)
|
||||
|
||||
# self.logger.debug('{0:08b}'.format(xbox_buttons_high_byte) + " : " + '{0:08b}'.format(xbox_buttons_low_byte))
|
||||
|
||||
|
||||
|
||||
# current_array = [fr_left_horiz, fr_left_vert, fr_right_horiz, fr_right_vert, fr_left_pot, fr_right_pot, fr_left_side_pot, fr_right_side_pot, frsky_buttons_byte, xb_left_horiz, xb_left_vert, xb_right_horiz, xb_right_vert, xb_left_trig, xb_right_trig, xbox_buttons_high_byte, xbox_buttons_low_byte]
|
||||
# desired_array = [str(number) for number in current_array]
|
||||
# joined = " : ".join(desired_array)
|
||||
# self.logger.debug(joined)
|
||||
|
||||
self.wait_for_passthrough_response = True
|
||||
write_joystick(self.send_miniboard_control_packet, fr_left_horiz, fr_left_vert, fr_right_horiz, fr_right_vert, fr_left_pot, fr_right_pot, fr_left_side_pot, fr_right_side_pot, frsky_buttons_byte, xb_left_horiz, xb_left_vert, xb_right_horiz, xb_right_vert, xb_left_trig, xb_right_trig, xbox_buttons_high_byte, xbox_buttons_low_byte)
|
||||
|
||||
# ##### Standard timeout block #####
|
||||
start_time = time.time()
|
||||
time_elapsed = 0.0
|
||||
|
||||
while self.wait_for_passthrough_response and time_elapsed < DRIVE_TIMEOUT: # I'm being explicit here
|
||||
time_elapsed = time.time() - start_time
|
||||
self.msleep(1)
|
||||
|
||||
if time_elapsed > DRIVE_TIMEOUT:
|
||||
self.logger.debug("MOTION: TOO SLOW: " + str(time_elapsed))
|
||||
|
||||
# ##### End standard timeout block #####
|
||||
|
||||
def __set_pause_on_state_change(self):
|
||||
current_state = self.frsky_states['sf_state']
|
||||
|
||||
if current_state != self.last_pause_state:
|
||||
write_pause(self.send_miniboard_control_packet, current_state)
|
||||
self.last_pause_state = current_state
|
||||
|
||||
def __all_stop_on_arm_control(self):
|
||||
current_state = self.frsky_states["sa_state"]
|
||||
|
||||
if (current_state == 1) and (self.last_drive_state == 0):
|
||||
self.wait_for_drive_response = True
|
||||
write_drive_motor_power(self.send_miniboard_control_packet, 0, 0, 0, 0,
|
||||
0, 0)
|
||||
|
||||
# ##### Standard timeout block #####
|
||||
start_time = time.time()
|
||||
time_elapsed = 0
|
||||
|
||||
while self.wait_for_drive_response and time_elapsed < DRIVE_TIMEOUT: # I'm being explicit here
|
||||
time_elapsed = time.time() - start_time
|
||||
self.msleep(1)
|
||||
# ##### End standard timeout block #####
|
||||
|
||||
while self.frsky_states["left_stick_y_axis"] > DEAD_BAND_FRSKY and self.frsky_states["right_stick_y_axis"] > DEAD_BAND_FRSKY:
|
||||
self.msleep(1)
|
||||
|
||||
self.last_drive_state = current_state
|
||||
elif (current_state == 0) and (self.last_drive_state == 1):
|
||||
self.last_drive_state = 0
|
||||
|
||||
def __set_swerve_state_on_state_change(self):
|
||||
current_state = self.frsky_states['sg_state']
|
||||
|
||||
self.logger.debug(str(current_state) + str(self.last_swerve_state))
|
||||
if current_state != self.last_swerve_state:
|
||||
self.logger.debug("State changed")
|
||||
# #######################################
|
||||
# ##### Then send the state change #####
|
||||
# #######################################
|
||||
if current_state: # 1 indicates turn to swerve, command 2
|
||||
command = 2
|
||||
else: # 0 indicates straight, command 1
|
||||
command = 1
|
||||
|
||||
self.wait_for_swerve_state_response = True
|
||||
write_swerve_drive_state(self.send_miniboard_control_packet, command)
|
||||
|
||||
# ##### Standard timeout block #####
|
||||
start_time = time.time()
|
||||
time_elapsed = 0
|
||||
|
||||
while self.wait_for_swerve_state_response and time_elapsed < DRIVE_SWERVE_TIMEOUT: # I'm being explicit here
|
||||
time_elapsed = time.time() - start_time
|
||||
self.msleep(1)
|
||||
# ##### End standard timeout block #####
|
||||
self.msleep(DRIVE_SWERVE_TIMEOUT)
|
||||
|
||||
self.last_swerve_state = current_state
|
||||
|
||||
def __arm_control_manual(self):
|
||||
OFFSET = 127
|
||||
|
||||
max_speed_scale_raw = self.frsky_states["s1_axis"] + OFFSET
|
||||
base_speed_raw = self.frsky_states["left_stick_x_axis"]
|
||||
bicep_speed_raw = self.frsky_states["left_stick_y_axis"]
|
||||
forearm_speed_raw = self.frsky_states["right_stick_y_axis"]
|
||||
pitch_speed_raw = self.frsky_states["rs_axis"]
|
||||
wrist_rotation_speed_raw = self.frsky_states["right_stick_x_axis"]
|
||||
|
||||
scale_percentage = max_speed_scale_raw / 255
|
||||
|
||||
base_speed_scaled = base_speed_raw * scale_percentage
|
||||
bicep_speed_scaled = bicep_speed_raw * scale_percentage
|
||||
forearm_speed_scaled = forearm_speed_raw * scale_percentage
|
||||
pitch_speed_scaled = pitch_speed_raw * scale_percentage
|
||||
wrist_rotation_speed_scaled = wrist_rotation_speed_raw * scale_percentage
|
||||
|
||||
base_speed_scaled = int(self.clamp(base_speed_scaled, ARMS_MIN, ARMS_MAX))
|
||||
bicep_speed_scaled = int(self.clamp(bicep_speed_scaled, ARMS_MIN, ARMS_MAX))
|
||||
forearm_speed_scaled = int(self.clamp(forearm_speed_scaled, ARMS_MIN, ARMS_MAX))
|
||||
pitch_speed_scaled = int(self.clamp(pitch_speed_scaled, ARMS_MIN, ARMS_MAX))
|
||||
wrist_rotation_speed_scaled = int(self.clamp(wrist_rotation_speed_scaled, ARMS_MIN, ARMS_MAX))
|
||||
|
||||
self.wait_for_arm_response = True
|
||||
|
||||
write_arm_motors(self.send_miniboard_control_packet,
|
||||
base_speed_scaled,
|
||||
bicep_speed_scaled,
|
||||
forearm_speed_scaled,
|
||||
pitch_speed_scaled,
|
||||
wrist_rotation_speed_scaled)
|
||||
|
||||
# ##### Standard timeout block #####
|
||||
start_time = time.time()
|
||||
time_elapsed = 0
|
||||
|
||||
while self.wait_for_arm_response and time_elapsed < ARM_TIMEOUT: # I'm being explicit here
|
||||
time_elapsed = time.time() - start_time
|
||||
self.msleep(1)
|
||||
# ##### End standard timeout block #####
|
||||
|
||||
# write_arm_motors(self.send_miniboard_control_packet, )
|
||||
|
||||
def __drive_manual(self):
|
||||
OFFSET = 127
|
||||
|
||||
max_speed_scale_raw = self.frsky_states["s1_axis"] + OFFSET # Range (min to max) 0 to 255
|
||||
left_stick_y_axis_raw = self.frsky_states["left_stick_y_axis"] # Range (min to max) -127 to 127
|
||||
right_stick_y_axis_raw = self.frsky_states["right_stick_y_axis"] # Range (min to max) -127 to 127
|
||||
|
||||
scale_percentage = max_speed_scale_raw / 255
|
||||
|
||||
left_scaled = left_stick_y_axis_raw * scale_percentage
|
||||
right_scaled = right_stick_y_axis_raw * scale_percentage
|
||||
|
||||
left_scaled = int(self.clamp(left_scaled, DRIVE_MIN, DRIVE_MAX))
|
||||
right_scaled = int(self.clamp(right_scaled, DRIVE_MIN, DRIVE_MAX))
|
||||
|
||||
# if left_scaled != self.last_left_drive_value or right_scaled != self.last_right_drive_value:
|
||||
self.wait_for_drive_response = True
|
||||
write_drive_motor_power(self.send_miniboard_control_packet, left_scaled, left_scaled, left_scaled, right_scaled,
|
||||
right_scaled, right_scaled)
|
||||
|
||||
self.logger.debug(str(left_scaled) + " : " + str(right_scaled))
|
||||
# ##### Standard timeout block #####
|
||||
start_time = time.time()
|
||||
time_elapsed = 0
|
||||
|
||||
while self.wait_for_drive_response and time_elapsed < DRIVE_TIMEOUT: # I'm being explicit here
|
||||
time_elapsed = time.time() - start_time
|
||||
self.msleep(1)
|
||||
|
||||
# ##### End standard timeout block #####
|
||||
|
||||
# self.last_left_drive_value = left_scaled
|
||||
# self.last_right_drive_value = right_scaled
|
||||
|
||||
# read_drive_motor_power(self.send_miniboard_control_packet)
|
||||
|
||||
def __drive_auto(self):
|
||||
self.msleep(1)
|
||||
|
||||
def __pan_tilt_manual(self):
|
||||
# X axis (min to max) -32768 to 32768
|
||||
# Y axis (min to max) 32768 to 32768
|
||||
controller_pan_raw = self.xbox_states["right_stick_x_axis"]
|
||||
controller_tilt_raw = self.xbox_states["right_stick_y_axis"]
|
||||
|
||||
if abs(controller_pan_raw) < DEAD_BAND_XBOX:
|
||||
controller_pan_raw = 0
|
||||
|
||||
if abs(controller_tilt_raw) < DEAD_BAND_XBOX:
|
||||
controller_tilt_raw = 0
|
||||
|
||||
controller_pan = -controller_pan_raw/2560
|
||||
controller_tilt = -controller_tilt_raw/2560
|
||||
|
||||
new_pan = self.clamp(int(self.pan_position+controller_pan), 0, 65535)
|
||||
new_tilt = self.clamp(int(self.tilt_position+controller_tilt), 0, 65535)
|
||||
|
||||
self.wait_for_pan_tilt_response = True
|
||||
write_pan_tilt(self.send_miniboard_control_packet, new_pan, new_tilt)
|
||||
|
||||
# ##### Standard timeout block #####
|
||||
start_time = time.time()
|
||||
time_elapsed = 0
|
||||
|
||||
while self.wait_for_pan_tilt_response and time_elapsed < PAN_TILT_TIMEOUT: # I'm being explicit here
|
||||
time_elapsed = time.time() - start_time
|
||||
self.msleep(1)
|
||||
# ##### End standard timeout block #####
|
||||
|
||||
self.pan_position = new_pan
|
||||
self.tilt_position = new_tilt
|
||||
|
||||
def on_xbox_states_updated__slot(self):
|
||||
return
|
||||
|
||||
def on_frsky_states_updated__slot(self):
|
||||
return
|
||||
|
||||
def on_primary_pan_tilt_write_acknowledged__slot(self):
|
||||
self.wait_for_pan_tilt_response = False
|
||||
|
||||
def on_secondary_pan_tilt_write_acknowledged__slot(self):
|
||||
self.wait_for_pan_tilt_response = False #TODO: make secondary
|
||||
|
||||
def on_pan_tilt_primary_position_response__slot(self, sdict):
|
||||
try:
|
||||
self.pan_position = sdict["pan"]
|
||||
self.tilt_position = sdict["tilt"]
|
||||
except:
|
||||
pass
|
||||
|
||||
def on_pan_tilt_secondary_position_response__slot(self, sdict):
|
||||
pass
|
||||
#self.logger.debug(sdict)
|
||||
|
||||
def on_drive_motor_power_response_received__slot(self, sdict):
|
||||
pass
|
||||
#self.logger.debug(sdict)
|
||||
|
||||
def on_passthrough_response_received__slot(self):
|
||||
self.wait_for_passthrough_response = False
|
||||
|
||||
def on_drive_response_received__slot(self):
|
||||
self.wait_for_drive_response = False
|
||||
|
||||
def on_arm_motors_write_acknowledged__slot(self):
|
||||
self.wait_for_arm_response = False
|
||||
|
||||
def on_swerve_state_response_received__slot(self):
|
||||
self.wait_for_swerve_state_response = False
|
||||
|
||||
def on_kill_threads__slot(self):
|
||||
self.run_thread_flag = False
|
||||
|
||||
@staticmethod
|
||||
def clamp(n, minn, maxn):
|
||||
return max(min(maxn, n), minn)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
This file calls read voltage, and read drive motor power
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import logging
|
||||
from Framework.MiniBoardIOCore import read_drive_motor_power, read_battery_voltage, read_arm_motors, read_gps_position, read_gps_track
|
||||
import time
|
||||
|
||||
|
||||
class ReadUpdater(QtCore.QThread):
|
||||
send_miniboard_control_packet = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, main_window):
|
||||
super(ReadUpdater, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## Some Flags ##########
|
||||
self.run_thread_flag = True
|
||||
|
||||
# ########## Class variables ##########
|
||||
self.data_last_seen = time.time()
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Read Updater Thread Starting...")
|
||||
|
||||
while self.run_thread_flag:
|
||||
read_drive_motor_power(self.send_miniboard_control_packet)
|
||||
#read_battery_voltage(self.send_miniboard_control_packet)
|
||||
# read_arm_motors(self.send_miniboard_control_packet)
|
||||
#read_gps_position(self.send_miniboard_control_packet)
|
||||
#read_gps_track(self.send_miniboard_control_packet)
|
||||
self.msleep(3000)
|
||||
|
||||
self.logger.debug("Read Updater Thread Stopping...")
|
||||
|
||||
|
||||
def connect_signals_to_slots__slot(self):
|
||||
self.send_miniboard_control_packet.connect(self.main_window.miniboard_class.append)
|
||||
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
|
||||
def on_kill_threads__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
This file contains the Settings sub-class as part of the Framework Class
|
||||
This class handles initialization of system settings and handling defaults when no settings are found
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import os
|
||||
|
||||
# Custom imports
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
|
||||
|
||||
#####################################
|
||||
# PickAndPlateLogger Definition
|
||||
#####################################
|
||||
class Settings(QtCore.QObject):
|
||||
def __init__(self, main_window):
|
||||
super(Settings, self).__init__()
|
||||
|
||||
# ########## Reference to highest level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Set up settings for program ##########
|
||||
self.__setup_settings()
|
||||
|
||||
# ########## Create Instance of settings ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Create Instance of settings ##########
|
||||
self.__set_hardcoded_settings()
|
||||
|
||||
@staticmethod
|
||||
def __setup_settings():
|
||||
# noinspection PyCallByClass,PyTypeChecker,PyArgumentList
|
||||
QtCore.QCoreApplication.setOrganizationName("OSURC")
|
||||
# noinspection PyCallByClass,PyTypeChecker,PyArgumentList
|
||||
QtCore.QCoreApplication.setOrganizationDomain("http://osurobotics.club")
|
||||
# noinspection PyCallByClass,PyTypeChecker,PyArgumentList
|
||||
QtCore.QCoreApplication.setApplicationName("RoverBaseStation")
|
||||
|
||||
def __set_hardcoded_settings(self):
|
||||
# Set the temporary directory used to store files while processing them
|
||||
try:
|
||||
app_data_dir = os.environ["HOME"]
|
||||
except:
|
||||
app_data_dir = os.environ["APPDATA"]
|
||||
folder_name = "basestation"
|
||||
full_path = app_data_dir + "/" + folder_name + "/settings"
|
||||
self.settings.setValue("appdata_directory", full_path)
|
||||
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
This file contains the live logs page sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
from inputs import devices, GamePad
|
||||
import time
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
GAME_CONTROLLER_NAME = "Afterglow Gamepad for Xbox 360"
|
||||
CONTROLLER_DATA_UPDATE_FREQUENCY = 50 # Times per second
|
||||
|
||||
|
||||
#####################################
|
||||
# Controller Class Definition
|
||||
#####################################
|
||||
class XBOXController(QtCore.QThread):
|
||||
|
||||
# ########## Signals ##########
|
||||
controller_connection_aquired = QtCore.pyqtSignal(bool)
|
||||
controller_update_ready_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, main_window):
|
||||
super(XBOXController, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
self.setup_controller_flag = True
|
||||
self.data_acquisition_and_broadcast_flag = True
|
||||
self.controller_aquired = False
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.gamepad = None # type: GamePad
|
||||
|
||||
|
||||
self.controller_states = {
|
||||
"left_stick_x_axis": 0,
|
||||
"left_stick_y_axis": 0,
|
||||
"left_stick_center_pressed": 0,
|
||||
|
||||
"right_stick_x_axis": 0,
|
||||
"right_stick_y_axis": 0,
|
||||
"right_stick_center_pressed": 0,
|
||||
|
||||
"left_trigger_z_axis": 0,
|
||||
"left_bumper_pressed": 0,
|
||||
|
||||
"right_trigger_z_axis": 0,
|
||||
"right_bumper_pressed": 0,
|
||||
|
||||
"dpad_x": 0,
|
||||
"dpad_y": 0,
|
||||
|
||||
"select_pressed": 0,
|
||||
"start_pressed": 0,
|
||||
"home_pressed": 0,
|
||||
|
||||
"a_pressed": 0,
|
||||
"b_pressed": 0,
|
||||
"x_pressed": 0,
|
||||
"y_pressed": 0
|
||||
}
|
||||
|
||||
self.raw_mapping_to_class_mapping = {
|
||||
"ABS_X": "left_stick_x_axis",
|
||||
"ABS_Y": "left_stick_y_axis",
|
||||
"BTN_THUMBL": "left_stick_center_pressed",
|
||||
|
||||
"ABS_RX": "right_stick_x_axis",
|
||||
"ABS_RY": "right_stick_y_axis",
|
||||
"BTN_THUMBR": "right_stick_center_pressed",
|
||||
|
||||
"ABS_Z": "left_trigger_z_axis",
|
||||
"BTN_TL": "left_bumper_pressed",
|
||||
|
||||
"ABS_RZ": "right_trigger_z_axis",
|
||||
"BTN_TR": "right_bumper_pressed",
|
||||
|
||||
"ABS_HAT0X": "dpad_x",
|
||||
"ABS_HAT0Y": "dpad_y",
|
||||
|
||||
"BTN_SELECT": "select_pressed",
|
||||
"BTN_START": "start_pressed",
|
||||
"BTN_MODE": "home_pressed",
|
||||
|
||||
"BTN_SOUTH": "a_pressed",
|
||||
"BTN_EAST": "b_pressed",
|
||||
"BTN_NORTH": "x_pressed",
|
||||
"BTN_WEST": "y_pressed"
|
||||
}
|
||||
|
||||
self.last_time = time.time()
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Xbox Thread Starting...")
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.setup_controller_flag:
|
||||
self.controller_aquired = self.__setup_controller()
|
||||
self.setup_controller_flag = False
|
||||
if self.data_acquisition_and_broadcast_flag:
|
||||
self.__get_controller_data()
|
||||
self.__broadcast_if_ready()
|
||||
|
||||
self.logger.debug("Xbox Thread Stopping...")
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def connect_signals_to_slots__slot(self):
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
|
||||
def __setup_controller(self):
|
||||
for device in devices.gamepads:
|
||||
if device.name == GAME_CONTROLLER_NAME:
|
||||
self.gamepad = device
|
||||
self.controller_connection_aquired.emit(True)
|
||||
return True
|
||||
self.logger.info("XBOX Controller Failed to Connect")
|
||||
self.controller_connection_aquired.emit(False)
|
||||
return False
|
||||
|
||||
def __get_controller_data(self):
|
||||
if (self.controller_aquired):
|
||||
events = self.gamepad.read()
|
||||
|
||||
for event in events:
|
||||
if event.code in self.raw_mapping_to_class_mapping:
|
||||
self.controller_states[self.raw_mapping_to_class_mapping[event.code]] = event.state
|
||||
# self.logger.debug("XBOX: " + str(self.controller_states))
|
||||
|
||||
def __broadcast_if_ready(self):
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
if (current_time - self.last_time) > (1/CONTROLLER_DATA_UPDATE_FREQUENCY):
|
||||
self.controller_update_ready_signal.emit()
|
||||
self.last_time = current_time
|
||||
|
||||
def on_kill_threads__slot(self):
|
||||
self.terminate() # DON'T normally do this!!!!!
|
||||
self.run_thread_flag = False
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
This file contains the data view page sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
from Framework.MiniBoardIOCore import write_drive_motor_power, read_drive_motor_power, write_pause, write_arm_motors, write_soil_measure, write_soil_sensor_recv, read_soil_measurements, read_gps_position, read_gps_track
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
|
||||
#####################################
|
||||
# Global Variables
|
||||
#####################################
|
||||
JOYSTICK_AXIS_MIN = -144
|
||||
JOYSTICK_AXIS_MAX = 144
|
||||
|
||||
|
||||
#####################################
|
||||
# About Class Definition
|
||||
#####################################
|
||||
class DataView(QtCore.QObject):
|
||||
send_miniboard_control_packet = QtCore.pyqtSignal(list)
|
||||
get_data_from_probe_signal = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, main_window):
|
||||
super(DataView, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
self.miniboard_class = main_window.miniboard_class
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## References to GUI Elements ##########
|
||||
self.left_y_lb = self.main_window.left_y_lb
|
||||
self.right_y_lb = self.main_window.right_y_lb
|
||||
|
||||
self.pause_mode = self.main_window.pause_mode
|
||||
self.drive_mode = self.main_window.drive_mode
|
||||
|
||||
self.left_motor_power = self.main_window.left_motor
|
||||
self.right_motor_power = self.main_window.right_motor
|
||||
|
||||
self.battery_voltage = self.main_window.battery_voltage
|
||||
|
||||
self.base_power = self.main_window.base
|
||||
self.bicep_power = self.main_window.bicep
|
||||
self.forearm_power = self.main_window.forearm
|
||||
self.pitch_power = self.main_window.pitch
|
||||
self.wrist_power = self.main_window.wrist
|
||||
|
||||
self.temp_label = self.main_window.temp_label
|
||||
self.moisture_label = self.main_window.moisture_label
|
||||
self.salinity_label = self.main_window.salinity_label
|
||||
|
||||
self.get_data_button = self.main_window.science_data_button
|
||||
self.get_gps_button = self.main_window.gps_data_button
|
||||
|
||||
self.last_contact_label = self.main_window.last_contact_label
|
||||
self.lat_label = self.main_window.gps_lat_label
|
||||
self.lon_label = self.main_window.gps_lon_label
|
||||
self.alt_label = self.main_window.gps_altitude_label
|
||||
self.gps_heading_label = self.main_window.gps_heading_label
|
||||
self.gps_speed_label = self.main_window.gps_speed_label
|
||||
|
||||
# self.time_label = self.main_window.time_label
|
||||
# self.voltage_label = self.main_window.battery_voltage_label
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.controller_states = None
|
||||
self.sensor_read_timer = QtCore.QTimer()
|
||||
self.sensor_read_timer.setSingleShot(True)
|
||||
|
||||
self.gps_valid = None
|
||||
self.lat = None
|
||||
self.lon = None
|
||||
self.alt = None
|
||||
self.gps_track_valid = None
|
||||
self.gps_speed = None
|
||||
self.gps_heading = None
|
||||
|
||||
# ########## Make signal/slot connections ##########
|
||||
self.__connect_signals_to_slots()
|
||||
|
||||
# ########## Set flags ##########
|
||||
self.xbox_connected = False
|
||||
self.frysky_connected = False
|
||||
|
||||
def __connect_signals_to_slots(self):
|
||||
# These are incorrect as they don't show the scaled versions
|
||||
# self.main_window.frsky_controller_class.controller_update_ready_signal.connect(self.on_controller_update_ready__slot)
|
||||
# self.main_window.frsky_controller_class.controller_connection_aquired.connect(self.frysky_connected__slot)
|
||||
# self.main_window.xbox_controller_class.controller_connection_aquired.connect(self.xbox_connected__slot)
|
||||
|
||||
# the data signals pass dictionaries to our slots after read requested
|
||||
self.main_window.miniboard_class.data_drive_motor_power.connect(self.__update_drive_power)
|
||||
self.main_window.miniboard_class.data_battery_voltage.connect(self.__update_battery_voltage)
|
||||
self.main_window.miniboard_class.data_arm_motors.connect(self.__update_arm_power)
|
||||
|
||||
self.get_data_button.clicked.connect(self.on_get_science_data_button_pressed__slot)
|
||||
self.get_data_from_probe_signal.connect(self.sensor_read_timer.start)
|
||||
self.sensor_read_timer.timeout.connect(self.on_science_measure_response_received__slot)
|
||||
self.main_window.miniboard_class.data_soil_measurements.connect(self.on_science_data_received__slot)
|
||||
|
||||
self.main_window.miniboard_class.data_gps_position.connect(self.on_gps_coordinates_updated__slot)
|
||||
self.main_window.miniboard_class.data_gps_track.connect(self.on_gps_track_updated__slot)
|
||||
|
||||
self.send_miniboard_control_packet.connect(self.main_window.miniboard_class.append)
|
||||
self.main_window.miniboard_class.message_received_signal.connect(self.on_message_received__slot)
|
||||
|
||||
self.get_gps_button.clicked.connect(self.on_get_gps_button_pressed__slot)
|
||||
|
||||
def __update_drive_percentages(self):
|
||||
if self.frysky_connected:
|
||||
left_percentage = round((self.controller_states["left_stick_y_axis"] / JOYSTICK_AXIS_MAX * 100), 2)
|
||||
right_percentage = round((self.controller_states["right_stick_y_axis"] / JOYSTICK_AXIS_MAX * 100), 2)
|
||||
|
||||
self.left_y_lb.setText(str(left_percentage) + "%")
|
||||
self.right_y_lb.setText(str(right_percentage) + "%")
|
||||
|
||||
def __update_modes(self):
|
||||
pause_mode = self.controller_states["sf_state"]
|
||||
drive_mode = self.controller_states["se_state"]
|
||||
|
||||
self.pause_mode.setText(str(pause_mode))
|
||||
self.drive_mode.setText(str(drive_mode))
|
||||
|
||||
def __update_drive_power(self, power_dict):
|
||||
self.left_motor_power.setText("F:%3d M:%3d B:%3d" % (
|
||||
power_dict["l_f_drive"],
|
||||
power_dict["l_m_drive"],
|
||||
power_dict["l_b_drive"],
|
||||
))
|
||||
self.right_motor_power.setText("F:%3d M:%3d B:%3d" % (
|
||||
power_dict["r_f_drive"],
|
||||
power_dict["r_m_drive"],
|
||||
power_dict["r_b_drive"],
|
||||
))
|
||||
|
||||
def __update_battery_voltage(self, battery_dict):
|
||||
battery_visually_accurate = int(battery_dict["battery_voltage"]) / 1000
|
||||
self.battery_voltage.setText("%0.2f V" % battery_visually_accurate)
|
||||
|
||||
def __update_arm_power(self, power_dict):
|
||||
self.base_power.setText("%d" % power_dict["arm_motor_1"])
|
||||
self.bicep_power.setText("%d" % power_dict["arm_motor_2"])
|
||||
self.forearm_power.setText("%d" % power_dict["arm_motor_3"])
|
||||
self.pitch_power.setText("%d" % power_dict["arm_motor_4"])
|
||||
self.wrist_power.setText("%d" % power_dict["arm_motor_5"])
|
||||
|
||||
def on_update_other_gui_elements__slot(self, voltage, time):
|
||||
# self.time_label.setText(time)
|
||||
# self.voltage_label.setText(voltage)
|
||||
pass
|
||||
|
||||
def on_controller_update_ready__slot(self, controller_states):
|
||||
self.controller_states = controller_states
|
||||
self.__update_drive_percentages()
|
||||
self.__update_modes()
|
||||
|
||||
def xbox_connected__slot(self, connected):
|
||||
self.xbox_connected = connected
|
||||
|
||||
def frysky_connected__slot(self, connected):
|
||||
self.frysky_connected = connected
|
||||
|
||||
def on_get_science_data_button_pressed__slot(self):
|
||||
self.temp_label.setText("Reading From Sensor")
|
||||
self.moisture_label.setText("Reading From Sensor")
|
||||
self.salinity_label.setText("Reading From Sensor")
|
||||
write_soil_measure(self.send_miniboard_control_packet, 1) # Request data
|
||||
self.get_data_from_probe_signal.emit(5000)
|
||||
|
||||
def on_science_measure_response_received__slot(self):
|
||||
read_soil_measurements(self.send_miniboard_control_packet)
|
||||
|
||||
def on_science_data_received__slot(self, sdict):
|
||||
self.temp_label.setText(str(sdict["temperature"]/1000) + " °C")
|
||||
self.moisture_label.setText(str(sdict["moisture"]/10) + " %")
|
||||
self.salinity_label.setText(str(sdict["salinity"]/1000) + " g/L")
|
||||
# self.logger.debug(sdict)
|
||||
|
||||
def on_gps_coordinates_updated__slot(self, sdict):
|
||||
self.gps_valid = sdict["gps_pos_valid"]
|
||||
self.lat = "{0:.6f}".format((sdict["latitude"] * 10 ** -5) / 60)
|
||||
self.lon = "{0:.6f}".format((sdict["longitude"] * 10 ** -5) / 60)
|
||||
self.alt = "{0:.6f}".format((sdict["altitude"] / 10))
|
||||
|
||||
if self.gps_valid:
|
||||
self.lat_label.setText(str(self.lat))
|
||||
self.lon_label.setText(str(self.lon))
|
||||
self.alt_label.setText(str(self.alt))
|
||||
else:
|
||||
self.lat_label.setText("No GPS Lock")
|
||||
self.lon_label.setText("No GPS Lock")
|
||||
self.alt_label.setText("No GPS Lock")
|
||||
|
||||
def on_gps_track_updated__slot(self, sdict):
|
||||
self.gps_track_valid = sdict["gps_track_valid"]
|
||||
self.gps_speed = sdict["gps_speed"]
|
||||
self.gps_heading = sdict["gps_heading"]
|
||||
|
||||
if self.gps_track_valid:
|
||||
self.gps_speed_label.setText(str(self.gps_speed))
|
||||
self.gps_heading_label(str(self.gps_heading))
|
||||
else:
|
||||
self.gps_speed_label.setText("No Movement")
|
||||
self.gps_heading_label.setText("No Movement")
|
||||
|
||||
def on_get_gps_button_pressed__slot(self):
|
||||
read_gps_position(self.send_miniboard_control_packet)
|
||||
read_gps_track(self.send_miniboard_control_packet)
|
||||
|
||||
def on_message_received__slot(self):
|
||||
|
||||
self.last_contact_label.setText(datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
This file contains the interface core sub-class
|
||||
This instantiates all the high level sub-classes for the interface
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtWebEngine
|
||||
|
||||
# Custom imports
|
||||
from Interface.LiveLogs.LiveLogsCore import LiveLogs
|
||||
from PyQt5.QtQuick import QQuickView, QQuickItem
|
||||
from PyQt5.QtCore import QUrl, QObject
|
||||
from Interface.Map.MapCore import Map
|
||||
from Interface.DataView.DataViewCore import DataView
|
||||
|
||||
|
||||
#####################################
|
||||
# Interface Class Definition
|
||||
#####################################
|
||||
class Interface(QtCore.QObject):
|
||||
def __init__(self, main_window):
|
||||
super(Interface, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
QtWebEngine.QtWebEngine.initialize()
|
||||
|
||||
# ########## Instantiations of sub-classes ##########
|
||||
self.live_logs_class = LiveLogs(self.main_window)
|
||||
# self.maps_class = Map(self.main_window)
|
||||
self.data_view_class = DataView(self.main_window)
|
||||
|
||||
|
||||
# ########## References to GUI Elements ##########
|
||||
self.tab_widget = self.main_window.tab_widget # type: QtWidgets.QTabWidget
|
||||
|
||||
# ########## Set default interface parameters ##########
|
||||
# Always open to first tab on launch
|
||||
self.tab_widget.setCurrentIndex(0)
|
||||
|
||||
def qml_clicked__slot(self):
|
||||
print("UI CLICKED")
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
This file contains the live logs page sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
import logging
|
||||
|
||||
|
||||
#####################################
|
||||
# Live Logs Class Definition
|
||||
#####################################
|
||||
class LiveLogs(QtCore.QThread):
|
||||
|
||||
text_ready_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
send_packet = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, main_window):
|
||||
super(LiveLogs, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## Thread Flags ##########
|
||||
self.run_thread_flag = True
|
||||
self.open_log_file_flag = True
|
||||
self.show_log_file_flag = True
|
||||
|
||||
# ########## References to GUI Elements ##########
|
||||
self.live_log_tb = self.main_window.live_log_text_browser # type: QtWidgets.QTextBrowser
|
||||
|
||||
# ########## Class Variables ##########
|
||||
self.log_file_path = None
|
||||
self.log_file_reader = None
|
||||
self.log_file_prev_mtime = 0
|
||||
|
||||
def run(self):
|
||||
self.logger.debug("Live Logs Thread Starting...")
|
||||
|
||||
while self.run_thread_flag:
|
||||
if self.open_log_file_flag:
|
||||
self.__open_log_file()
|
||||
self.open_log_file_flag = False
|
||||
elif self.show_log_file_flag:
|
||||
self.__show_updated_log_file()
|
||||
self.msleep(250)
|
||||
|
||||
self.logger.debug("Live Logs Thread Stopping...")
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def connect_signals_to_slots__slot(self):
|
||||
self.text_ready_signal.connect(self.live_log_tb.setText)
|
||||
self.live_log_tb.textChanged.connect(self.__on_move_cursor_needed__slot)
|
||||
|
||||
self.main_window.kill_threads_signal.connect(self.on_kill_threads__slot)
|
||||
|
||||
def __open_log_file(self):
|
||||
# Get the log file path
|
||||
appdata_base_directory = self.settings.value("appdata_directory", type=str)
|
||||
log_directory = appdata_base_directory + "/logs"
|
||||
self.log_file_path = log_directory + "/log.txt"
|
||||
|
||||
# Open the class' reader for the file
|
||||
self.log_file_reader = open(self.log_file_path, 'r')
|
||||
|
||||
def __show_updated_log_file(self):
|
||||
log_browser_string = ""
|
||||
|
||||
# Seek back to the beginning of the file and read in the lines
|
||||
self.log_file_reader.seek(0)
|
||||
log_lines = self.log_file_reader.readlines()
|
||||
|
||||
# Go through line by line and only add lines that are selected to be shown via the checkboxes
|
||||
for line in log_lines:
|
||||
log_browser_string += line
|
||||
|
||||
# Display the text
|
||||
self.text_ready_signal.emit(log_browser_string)
|
||||
|
||||
def __on_move_cursor_needed__slot(self):
|
||||
# Move the cursor to the end when the text browser text updates. This essentially scrolls constantly.
|
||||
self.live_log_tb.moveCursor(QtGui.QTextCursor.End)
|
||||
|
||||
def on_kill_threads__slot(self):
|
||||
self.run_thread_flag = False
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
This file contains the map page sub-class
|
||||
"""
|
||||
|
||||
#####################################
|
||||
# Imports
|
||||
#####################################
|
||||
# Python native imports
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui, QtWebEngine
|
||||
from PyQt5.QtCore import QUrl
|
||||
import logging
|
||||
|
||||
|
||||
#####################################
|
||||
# About Class Definition
|
||||
#####################################
|
||||
class Map(QtCore.QObject):
|
||||
def __init__(self, main_window):
|
||||
super(Map, self).__init__()
|
||||
|
||||
# ########## Reference to top level window ##########
|
||||
self.main_window = main_window # type: QtWidgets.QMainWindow
|
||||
|
||||
# ########## Get the settings instance ##########
|
||||
self.settings = QtCore.QSettings()
|
||||
|
||||
# ########## Get the Pick And Plate instance of the logger ##########
|
||||
self.logger = logging.getLogger("RoverBaseStation")
|
||||
|
||||
# ########## References to GUI Elements ##########
|
||||
self.maps_view = self.main_window.map_label # type: QtWidgets.QLabel
|
||||
|
||||
# ########## Class Variables ##########
|
||||
# self.map_view_pixmap = QtGui.QPixmap("Resources/Maps/mars_testing_site.png")
|
||||
#
|
||||
# self.maps_view.setPixmap(self.map_view_pixmap)
|
||||
QtWebEngine.QtWebEngine.initialize()
|
||||
self.map_view = self.main_window.leaflet_map # type: QtWidgets.QQuickView
|
||||
self.map_view.setSource(QUrl("Resources/UI/map_view.qml"))
|
||||
|
||||
# ########## Set defaults on GUI Elements ##########
|
||||
self.__load_settings()
|
||||
|
||||
|
||||
|
||||
def __load_settings(self):
|
||||
self.logger.info("Map Interface Configured")
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,202 @@
|
||||
'''
|
||||
GooMPy: Google Maps for Python
|
||||
|
||||
Copyright (C) 2015 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/>.
|
||||
'''
|
||||
|
||||
import math
|
||||
import PIL.Image
|
||||
from io import StringIO, BytesIO
|
||||
import urllib.request
|
||||
import os
|
||||
import time
|
||||
|
||||
_KEY = '&key=AIzaSyAFjAWMwQauf2Dy5KteOuT8KexfMjOCIS8' # This is Corwin's temp API key
|
||||
|
||||
_EARTHPIX = 268435456 # Number of pixels in half the earth's circumference at zoom = 21
|
||||
_DEGREE_PRECISION = 4 # Number of decimal places for rounding coordinates
|
||||
_TILESIZE = 640 # Larget tile we can grab without paying
|
||||
_GRABRATE = 4 # Fastest rate at which we can download tiles without paying
|
||||
|
||||
_pixrad = _EARTHPIX / math.pi
|
||||
|
||||
|
||||
def _new_image(width, height):
|
||||
return PIL.Image.new('RGB', (width, height))
|
||||
|
||||
|
||||
def _roundto(value, digits):
|
||||
return int(value * 10 ** digits) / 10. ** digits
|
||||
|
||||
|
||||
def _pixels_to_degrees(pixels, zoom):
|
||||
return pixels * 2 ** (21 - zoom)
|
||||
|
||||
|
||||
def _grab_tile(lat, lon, zoom, maptype, _TILESIZE, sleeptime):
|
||||
urlbase = 'https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=%d&maptype=%s&size=%dx%d&format=jpg'
|
||||
urlbase += _KEY
|
||||
|
||||
specs = lat, lon, zoom, maptype, _TILESIZE, _TILESIZE
|
||||
|
||||
filename = 'Resources/Maps/' + ('%f_%f_%d_%s_%d_%d' % specs) + '.jpg'
|
||||
|
||||
|
||||
|
||||
tile = None
|
||||
|
||||
if os.path.isfile(filename):
|
||||
tile = PIL.Image.open(filename)
|
||||
|
||||
else:
|
||||
url = urlbase % specs
|
||||
|
||||
result = urllib.request.urlopen(url).read()
|
||||
tile = PIL.Image.open(BytesIO(result))
|
||||
if not os.path.exists('Resources/Maps'):
|
||||
os.mkdir('Resources/Maps')
|
||||
tile.save(filename)
|
||||
time.sleep(sleeptime) # Choke back speed to avoid maxing out limit
|
||||
|
||||
return tile
|
||||
|
||||
|
||||
def _pix_to_lon(j, lonpix, ntiles, _TILESIZE, zoom):
|
||||
return math.degrees((lonpix + _pixels_to_degrees(((j) - ntiles / 2) * _TILESIZE, zoom) - _EARTHPIX) / _pixrad)
|
||||
|
||||
|
||||
def _pix_to_lat(k, latpix, ntiles, _TILESIZE, zoom):
|
||||
return math.degrees(math.pi / 2 - 2 * math.atan(
|
||||
math.exp(((latpix + _pixels_to_degrees((k - ntiles / 2) * _TILESIZE, zoom)) - _EARTHPIX) / _pixrad)))
|
||||
|
||||
|
||||
def fetchTiles(latitude, longitude, zoom, maptype, radius_meters=None, default_ntiles=4):
|
||||
'''
|
||||
Fetches tiles from GoogleMaps at the specified coordinates, zoom level (0-22), and map type ('roadmap',
|
||||
'terrain', 'satellite', or 'hybrid'). The value of radius_meters deteremines the number of tiles that will be
|
||||
fetched; if it is unspecified, the number defaults to default_ntiles. Tiles are stored as JPEG images
|
||||
in the mapscache folder.
|
||||
'''
|
||||
|
||||
latitude = _roundto(latitude, _DEGREE_PRECISION)
|
||||
longitude = _roundto(longitude, _DEGREE_PRECISION)
|
||||
|
||||
# https://groups.google.com/forum/#!topic/google-maps-js-api-v3/hDRO4oHVSeM
|
||||
pixels_per_meter = 2 ** zoom / (156543.03392 * math.cos(math.radians(latitude)))
|
||||
|
||||
# number of tiles required to go from center latitude to desired radius in meters
|
||||
ntiles = default_ntiles if radius_meters is None else int(
|
||||
round(2 * pixels_per_meter / (_TILESIZE / 2. / radius_meters)))
|
||||
|
||||
lonpix = _EARTHPIX + longitude * math.radians(_pixrad)
|
||||
|
||||
sinlat = math.sin(math.radians(latitude))
|
||||
latpix = _EARTHPIX - _pixrad * math.log((1 + sinlat) / (1 - sinlat)) / 2
|
||||
|
||||
bigsize = ntiles * _TILESIZE
|
||||
bigimage = _new_image(bigsize, bigsize)
|
||||
|
||||
for j in range(ntiles):
|
||||
lon = _pix_to_lon(j, lonpix, ntiles, _TILESIZE, zoom)
|
||||
for k in range(ntiles):
|
||||
lat = _pix_to_lat(k, latpix, ntiles, _TILESIZE, zoom)
|
||||
tile = _grab_tile(lat, lon, zoom, maptype, _TILESIZE, 1. / _GRABRATE)
|
||||
bigimage.paste(tile, (j * _TILESIZE, k * _TILESIZE))
|
||||
|
||||
west = _pix_to_lon(0, lonpix, ntiles, _TILESIZE, zoom)
|
||||
east = _pix_to_lon(ntiles - 1, lonpix, ntiles, _TILESIZE, zoom)
|
||||
|
||||
north = _pix_to_lat(0, latpix, ntiles, _TILESIZE, zoom)
|
||||
south = _pix_to_lat(ntiles - 1, latpix, ntiles, _TILESIZE, zoom)
|
||||
|
||||
return bigimage, (north, west), (south, east)
|
||||
|
||||
|
||||
class GooMPy(object):
|
||||
def __init__(self, width, height, latitude, longitude, zoom, maptype, radius_meters=None, default_ntiles=4):
|
||||
'''
|
||||
Creates a GooMPy object for specified display widthan and height at the specified coordinates,
|
||||
zoom level (0-22), and map type ('roadmap', 'terrain', 'satellite', or 'hybrid').
|
||||
The value of radius_meters deteremines the number of tiles that will be used to create
|
||||
the map image; if it is unspecified, the number defaults to default_ntiles.
|
||||
'''
|
||||
|
||||
self.lat = latitude
|
||||
self.lon = longitude
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
self.zoom = zoom
|
||||
self.maptype = maptype
|
||||
self.radius_meters = radius_meters
|
||||
|
||||
self.winimage = _new_image(self.width, self.height)
|
||||
|
||||
self._fetch()
|
||||
|
||||
halfsize = self.bigimage.size[0] // 2
|
||||
self.leftx = halfsize
|
||||
self.uppery = halfsize
|
||||
|
||||
self._update()
|
||||
|
||||
def getImage(self):
|
||||
'''
|
||||
Returns the current image as a PIL.Image object.
|
||||
'''
|
||||
|
||||
return self.winimage
|
||||
|
||||
def move(self, dx, dy):
|
||||
'''
|
||||
Moves the view by the specified pixels dx, dy.
|
||||
'''
|
||||
|
||||
self.leftx = self._constrain(self.leftx, dx, self.width)
|
||||
self.uppery = self._constrain(self.uppery, dy, self.height)
|
||||
self._update()
|
||||
|
||||
def useMaptype(self, maptype):
|
||||
'''
|
||||
Uses the specified map type 'roadmap', 'terrain', 'satellite', or 'hybrid'.
|
||||
Map tiles are fetched as needed.
|
||||
'''
|
||||
|
||||
self.maptype = maptype
|
||||
self._fetch_and_update()
|
||||
|
||||
def useZoom(self, zoom):
|
||||
'''
|
||||
Uses the specified zoom level 0 through 22.
|
||||
Map tiles are fetched as needed.
|
||||
'''
|
||||
|
||||
self.zoom = zoom
|
||||
self._fetch_and_update()
|
||||
|
||||
def _fetch_and_update(self):
|
||||
self._fetch()
|
||||
self._update()
|
||||
|
||||
def _fetch(self):
|
||||
self.bigimage, self.northwest, self.southeast = fetchTiles(self.lat, self.lon, self.zoom, self.maptype,
|
||||
self.radius_meters)
|
||||
|
||||
def _update(self):
|
||||
self.winimage.paste(self.bigimage, (-self.leftx, -self.uppery))
|
||||
|
||||
def _constrain(self, oldval, diff, dimsize):
|
||||
newval = oldval + diff
|
||||
return newval if newval > 0 and newval < self.bigimage.size[0] - dimsize else oldval
|
||||
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 56 KiB |