mirror of
https://github.com/caperren/school_archives.git
synced 2025-11-09 21:51:15 +00:00
Added 2016-2017 rover code, and a ROB assignment
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user