Skip to content

Commit

Permalink
Merge pull request #108 from cwruRobotics/heartbeat-widget
Browse files Browse the repository at this point in the history
I LOVE CIRCLES (heartbeat widget)
  • Loading branch information
InvincibleRMC authored Feb 10, 2024
2 parents 3b29496 + 7956260 commit c3a4c8f
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 40 deletions.
2 changes: 1 addition & 1 deletion src/surface/gui/gui/pilot_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(self) -> None:
layout = QHBoxLayout()
self.setLayout(layout)

# Look into QStackedLayout for possibly switching between
# TODO Look into QStackedLayout for possibly switching between
# 1 big camera feed and 2 smaller ones
video_area = SwitchableVideoWidget(["front_cam/image_raw",
"bottom_cam/image_raw",
Expand Down
44 changes: 34 additions & 10 deletions src/surface/gui/gui/styles/custom_styles.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
class Style():
"""Represents a single class that can be applied to gui objects to change their appearance."""
from PyQt6.QtWidgets import QWidget, QPushButton

PROPERTY_NAME: str

class IndicatorMixin(QWidget):

class WidgetState(Style):
"""Represents the state of a widget that can be alternately active or inactive."""

PROPERTY_NAME = "widgetState"
_PROPERTY_NAME = "widgetState"

# A component is running, enabled, or armed
ON = "on"
_ON = "on"

# A component is disabled, not running, or disarmed, but could be enabled through this widget
OFF = "off"
_OFF = "off"

# A component is disabled, not expected to have any effect or perform its function because of
# some external factor, either another widget or something external to the gui
# For example, a the arm button when the pi is not connected
INACTIVE = "inactive"
_INACTIVE = "inactive"

# Removes any state
_NO_STATE = ""

def set_on(self) -> None:
self.setProperty(IndicatorMixin._PROPERTY_NAME, IndicatorMixin._ON)
self._update_style()

def set_off(self) -> None:
self.setProperty(IndicatorMixin._PROPERTY_NAME, IndicatorMixin._OFF)
self._update_style()

def set_inactive(self) -> None:
self.setProperty(IndicatorMixin._PROPERTY_NAME, IndicatorMixin._INACTIVE)
self._update_style()

def remove_state(self) -> None:
self.setProperty(IndicatorMixin._PROPERTY_NAME, IndicatorMixin._NO_STATE)
self._update_style()

def _update_style(self) -> None:
style = self.style()
if style is not None:
style.polish(self)


class ButtonIndicator(QPushButton, IndicatorMixin):
pass
34 changes: 14 additions & 20 deletions src/surface/gui/gui/widgets/arm.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from gui.event_nodes.client import GUIEventClient
from gui.event_nodes.subscriber import GUIEventSubscriber
from gui.styles.custom_styles import WidgetState
from gui.styles.custom_styles import ButtonIndicator
from mavros_msgs.srv import CommandBool
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QHBoxLayout, QPushButton, QWidget
from PyQt6.QtWidgets import QHBoxLayout, QWidget

from rov_msgs.msg import VehicleState


Expand All @@ -16,7 +17,7 @@ class Arm(QWidget):
BUTTON_HEIGHT = 60
BUTTON_STYLESHEET = 'QPushButton { font-size: 20px; }'

command_response_signal: pyqtSignal = pyqtSignal(CommandBool.Response)
command_response_signal = pyqtSignal(CommandBool.Response)
vehicle_state_signal = pyqtSignal(VehicleState)

def __init__(self) -> None:
Expand All @@ -26,8 +27,8 @@ def __init__(self) -> None:
layout = QHBoxLayout()
self.setLayout(layout)

self.arm_button = QPushButton()
self.disarm_button = QPushButton()
self.arm_button = ButtonIndicator()
self.disarm_button = ButtonIndicator()

self.arm_button.setText("Arm")
self.disarm_button.setText("Disarm")
Expand All @@ -40,8 +41,8 @@ def __init__(self) -> None:

self.arm_button.setStyleSheet(self.BUTTON_STYLESHEET)
self.disarm_button.setStyleSheet(self.BUTTON_STYLESHEET)
self.arm_button.setProperty(WidgetState.PROPERTY_NAME, WidgetState.INACTIVE)
self.disarm_button.setProperty(WidgetState.PROPERTY_NAME, WidgetState.INACTIVE)
self.arm_button.set_inactive()
self.disarm_button.set_inactive()

self.arm_button.clicked.connect(self.arm_clicked)
self.disarm_button.clicked.connect(self.disarm_clicked)
Expand Down Expand Up @@ -81,18 +82,11 @@ def arm_status(self, res: CommandBool.Response) -> None:
def vehicle_state_callback(self, msg: VehicleState) -> None:
if msg.pixhawk_connected:
if msg.armed:
self.arm_button.setProperty(WidgetState.PROPERTY_NAME, WidgetState.ON)
self.disarm_button.setProperty(WidgetState.PROPERTY_NAME, "")
self.arm_button.set_on()
self.disarm_button.remove_state()
else:
self.arm_button.setProperty(WidgetState.PROPERTY_NAME, "")
self.disarm_button.setProperty(WidgetState.PROPERTY_NAME, WidgetState.OFF)
self.arm_button.remove_state()
self.disarm_button.set_off()
else:
self.arm_button.setProperty(WidgetState.PROPERTY_NAME, WidgetState.INACTIVE)
self.disarm_button.setProperty(WidgetState.PROPERTY_NAME, WidgetState.INACTIVE)

for button in (self.arm_button, self.disarm_button):
style = button.style()
if style is not None:
style.polish(button)
else:
self.arm_client.get_logger().error("Gui button missing a style")
self.arm_button.set_inactive()
self.disarm_button.set_inactive()
31 changes: 31 additions & 0 deletions src/surface/gui/gui/widgets/circle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Optional
from gui.styles.custom_styles import IndicatorMixin
from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QWidget, QLabel


class Circle(QLabel):
def __init__(self, parent: Optional[QWidget] = None,
radius: int = 50,
color: Optional[QColor | Qt.GlobalColor] = None) -> None:
super().__init__(parent)
self.setFixedSize(QSize(2 * radius, 2 * radius))
stylesheet = self.styleSheet()
self.setStyleSheet(f"{stylesheet}border-radius: {radius}px;")

if color:
self.set_color(color)

def set_color(self, color: QColor | Qt.GlobalColor) -> None:
if isinstance(color, Qt.GlobalColor):
color = QColor(color)
style = f"background-color: rgb({color.red()}, {color.green()}, {color.blue()});"
self.setStyleSheet(f"{self.styleSheet()}{style}")


class CircleIndicator(Circle, IndicatorMixin):
def __init__(self, parent: Optional[QWidget] = None,
radius: int = 50) -> None:
super().__init__(parent, radius)
self.set_inactive()
6 changes: 5 additions & 1 deletion src/surface/gui/gui/widgets/debug_tab.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from gui.widgets.arm import Arm
from gui.widgets.heartbeat import HeartbeatWidget
from gui.widgets.ip_widget import IPWidget
from gui.widgets.logger import Logger
from gui.widgets.thruster_tester import ThrusterTester
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget


class DebugWidget(QWidget):
Expand All @@ -14,6 +15,9 @@ def __init__(self) -> None:
top_bar.addWidget(IPWidget(), alignment=Qt.AlignmentFlag.AlignTop |
Qt.AlignmentFlag.AlignLeft)

top_bar.addWidget(HeartbeatWidget(), alignment=Qt.AlignmentFlag.AlignTop |
Qt.AlignmentFlag.AlignLeft)

right_bar = QVBoxLayout()
right_bar.addWidget(ThrusterTester())
right_bar.addWidget(Arm())
Expand Down
25 changes: 17 additions & 8 deletions src/surface/gui/gui/widgets/flood_warning.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from gui.event_nodes.subscriber import GUIEventSubscriber
from gui.widgets.circle import CircleIndicator
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget

from rov_msgs.msg import Flooding


class FloodWarning(QWidget):

signal: pyqtSignal = pyqtSignal(Flooding)
signal = pyqtSignal(Flooding)

def __init__(self) -> None:
super().__init__()
Expand All @@ -18,26 +19,34 @@ def __init__(self) -> None:
# Create a latch variable
self.warning_msg_latch: bool = False
# Create basic 2 vertical stacked boxes layout
self.flood_layout = QVBoxLayout()
flood_layout = QVBoxLayout()
# Create the label that tells us what this is
self.label = QLabel('Flooding Indicator')

header_layout = QHBoxLayout()
label = QLabel('Flooding Status')
font = QFont("Arial", 14)
self.label.setFont(font)
self.flood_layout.addWidget(self.label)
label.setFont(font)
header_layout.addWidget(label)
self.indicator_circle = CircleIndicator(radius=10)
header_layout.addWidget(self.indicator_circle)

flood_layout.addLayout(header_layout)

self.indicator = QLabel('No Water present')
self.indicator.setFont(font)
self.flood_layout.addWidget(self.indicator)
self.setLayout(self.flood_layout)
flood_layout.addWidget(self.indicator)
self.setLayout(flood_layout)

@pyqtSlot(Flooding)
def refresh(self, msg: Flooding) -> None:
if msg.flooding:
self.indicator.setText('FLOODING')
self.subscription.get_logger().error("Robot is actively flooding, do something!")
self.warning_msg_latch = True
self.indicator_circle.set_off()
else:
self.indicator.setText('No Water present')
self.indicator_circle.set_on()
if self.warning_msg_latch:
self.subscription.get_logger().warning("Robot flooding has reset itself.")
self.warning_msg_latch = False
58 changes: 58 additions & 0 deletions src/surface/gui/gui/widgets/heartbeat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from gui.event_nodes.subscriber import GUIEventSubscriber
from gui.widgets.circle import CircleIndicator
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget

from rov_msgs.msg import VehicleState


class HeartbeatWidget(QWidget):

signal = pyqtSignal(VehicleState)

def __init__(self) -> None:
super().__init__()

self.signal.connect(self.refresh)
self.subscription = GUIEventSubscriber(VehicleState, 'vehicle_state_event', self.signal)
# Create a latch variable
self.warning_msg_latch: bool = False

heartbeat_layout = QVBoxLayout()

font = QFont("Arial", 14)

pi_status_layout = QHBoxLayout()
self.pi_indicator = QLabel('No Pi Status')
self.pi_indicator.setFont(font)
pi_status_layout.addWidget(self.pi_indicator)
self.pi_indicator_circle = CircleIndicator(radius=10)
pi_status_layout.addWidget(self.pi_indicator_circle)
heartbeat_layout.addLayout(pi_status_layout)

pixhawk_status_layout = QHBoxLayout()
self.pixhawk_indicator = QLabel('No Pixhawk Status')
self.pixhawk_indicator.setFont(font)
pixhawk_status_layout.addWidget(self.pixhawk_indicator)
self.pixhawk_indicator_circle = CircleIndicator(radius=10)
pixhawk_status_layout.addWidget(self.pixhawk_indicator_circle)
heartbeat_layout.addLayout(pixhawk_status_layout)

self.setLayout(heartbeat_layout)

@pyqtSlot(VehicleState)
def refresh(self, msg: VehicleState) -> None:
if msg.pi_connected:
self.pi_indicator.setText('Pi Connected')
self.pi_indicator_circle.set_on()
else:
self.pi_indicator.setText('Pi Disconnected')
self.pi_indicator_circle.set_off()

if msg.pixhawk_connected:
self.pixhawk_indicator.setText('Pixhawk Connected')
self.pixhawk_indicator_circle.set_on()
else:
self.pixhawk_indicator.setText('Pixhawk Disconnected')
self.pixhawk_indicator_circle.set_off()

0 comments on commit c3a4c8f

Please sign in to comment.