Skip to content

Commit

Permalink
Merge pull request #188 from cwruRobotics/horizontal-pilot-gui
Browse files Browse the repository at this point in the history
Use livesteam gui as pilot gui
  • Loading branch information
NoahMollerstuen authored Jun 9, 2024
2 parents 75e58c4 + 54337af commit 1d8046e
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 64 deletions.
38 changes: 17 additions & 21 deletions src/surface/gui/gui/operator_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from gui.widgets.float_comm import FloatComm
from gui.widgets.timer import InteractiveTimer
from gui.widgets.task_selector import TaskSelector
from gui.widgets.flood_warning import FloodWarning
from gui.widgets.temperature import TemperatureSensor
from gui.widgets.heartbeat import HeartbeatWidget
from gui.widgets.ip_widget import IPWidget
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QGridLayout, QTabWidget, QWidget, QVBoxLayout
from PyQt6.QtWidgets import QTabWidget, QWidget, QVBoxLayout, QHBoxLayout


class OperatorApp(App):
Expand All @@ -19,32 +19,28 @@ def __init__(self) -> None:

# Main tab
main_tab = QWidget()
main_layout = QGridLayout()
main_layout = QHBoxLayout()
main_tab.setLayout(main_layout)

right_bar = QVBoxLayout()
main_layout.addLayout(right_bar, 0, 1)
left_pane = QVBoxLayout()
right_pane = QVBoxLayout()

timer = InteractiveTimer()
right_bar.addWidget(timer)

temp_sensor = TemperatureSensor()
right_bar.addWidget(temp_sensor)

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

right_bar.addWidget(IPWidget(), alignment=Qt.AlignmentFlag.AlignTop |
Qt.AlignmentFlag.AlignLeft)

task_selector = TaskSelector()
main_layout.addWidget(task_selector, 1, 1)
main_layout.addLayout(left_pane)
main_layout.addLayout(right_pane)

self.float_comm: FloatComm = FloatComm()
main_layout.addWidget(self.float_comm, 0, 0)
left_pane.addWidget(self.float_comm)

logger = Logger()
main_layout.addWidget(logger, 1, 0)
left_pane.addWidget(logger)

right_pane.addWidget(InteractiveTimer())
right_pane.addWidget(HeartbeatWidget())
right_pane.addWidget(FloodWarning())
right_pane.addWidget(TemperatureSensor())
right_pane.addWidget(IPWidget())
right_pane.addStretch()
right_pane.addWidget(TaskSelector())

# Add tabs to root
root_layout = QVBoxLayout()
Expand Down
97 changes: 69 additions & 28 deletions src/surface/gui/gui/pilot_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,21 @@
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout
from PyQt6.QtGui import QScreen

import enum


FRONT_CAM_TOPIC = 'front_cam/image_raw'
BOTTOM_CAM_TOPIC = 'bottom_cam/image_raw'


def make_bottom_bar() -> QHBoxLayout:
bottom_screen_layout = QHBoxLayout()

timer = TimerDisplay()
bottom_screen_layout.addWidget(timer)

flood_widget = FloodWarning()
bottom_screen_layout.addWidget(flood_widget, alignment=Qt.AlignmentFlag.AlignHCenter |
Qt.AlignmentFlag.AlignBottom)
class GuiType(enum.Enum):
PILOT = "pilot"
LIVESTREAM = "livestream"
DEBUG = "debug"

arm = Arm()
bottom_screen_layout.addWidget(arm, alignment=Qt.AlignmentFlag.AlignRight |
Qt.AlignmentFlag.AlignBottom)

return bottom_screen_layout
TWO_MONITOR_CONFIG: dict[GuiType, int | None] = {GuiType.PILOT: None, GuiType.LIVESTREAM: 1}
THREE_MONITOR_CONFIG: dict[GuiType, int | None] = {GuiType.PILOT: 2, GuiType.LIVESTREAM: 1}


class PilotApp(App):
Expand All @@ -49,7 +44,9 @@ def __init__(self) -> None:
front_cam_type = CameraType.ETHERNET
bottom_cam_type = CameraType.ETHERNET

if gui_param.value == 'pilot':
gui_type = GuiType(gui_param.value)

if gui_type == GuiType.PILOT:
self.setWindowTitle('Pilot GUI - CWRUbotix ROV 2024')

front_cam_description = CameraDescription(
Expand All @@ -72,11 +69,9 @@ def __init__(self) -> None:
alignment=Qt.AlignmentFlag.AlignHCenter
)

main_layout.addLayout(make_bottom_bar())

self.show_on_monitor(1)
main_layout.addLayout(self.make_bottom_bar())

elif gui_param.value == 'livestream':
elif gui_type == GuiType.LIVESTREAM:
top_bar = QHBoxLayout()
top_bar.addWidget(LivestreamHeader())
top_bar.addWidget(TimerDisplay(), 2)
Expand All @@ -92,13 +87,13 @@ def __init__(self) -> None:
front_cam_type,
FRONT_CAM_TOPIC,
"Forward Camera",
1280, 720
920, 690
)
bottom_cam_description = CameraDescription(
bottom_cam_type,
BOTTOM_CAM_TOPIC,
"Down Camera",
1280, 720
920, 690
)

video_layout = QHBoxLayout()
Expand All @@ -107,12 +102,11 @@ def __init__(self) -> None:
alignment=Qt.AlignmentFlag.AlignHCenter)
video_layout.addWidget(VideoWidget(bottom_cam_description),
alignment=Qt.AlignmentFlag.AlignHCenter)
video_layout.setSpacing(0)

main_layout.addLayout(video_layout)
main_layout.addStretch()

self.show_on_monitor(2)

else:
self.setWindowTitle('Debug GUI - CWRUbotix ROV 2024')

Expand All @@ -139,18 +133,65 @@ def __init__(self) -> None:
)

main_layout.addLayout(video_layout)
main_layout.addLayout(make_bottom_bar())
main_layout.addLayout(self.make_bottom_bar())

self.apply_monitor_config(gui_type)

def make_bottom_bar(self) -> QHBoxLayout:
"""Generate a bottom pane used by multiple gui types.
Returns
-------
QHBoxLayout
The layout containing the bottom bar widgets
"""
bottom_screen_layout = QHBoxLayout()

timer = TimerDisplay()
bottom_screen_layout.addWidget(timer)

def show_on_monitor(self, monitor_id: int) -> None:
flood_widget = FloodWarning()
bottom_screen_layout.addWidget(flood_widget, alignment=Qt.AlignmentFlag.AlignHCenter |
Qt.AlignmentFlag.AlignBottom)

arm = Arm()
bottom_screen_layout.addWidget(arm, alignment=Qt.AlignmentFlag.AlignRight |
Qt.AlignmentFlag.AlignBottom)

return bottom_screen_layout

def apply_monitor_config(self, gui_type: GuiType) -> None:
"""Fullscreen the app to a specific monitor, depending on gui_type and the monitor config.
Either fullscreens the app to a monitor specified by TWO_MONITOR_CONFIG or
THREE_MONITOR_CONFIG (depending on the number of monitors present), or does nothing if no
config exists for the number of monitors and gui type
Parameters
----------
gui_type : GuiType
The type of gui that is being initialized
"""
screen = self.screen()
if screen is None:
return

monitors = QScreen.virtualSiblings(screen)
if len(monitors) > monitor_id:
monitor = monitors[monitor_id].availableGeometry()
self.move(monitor.left(), monitor.top())
self.showFullScreen()

monitor_id: int | None
if len(monitors) == 2:
monitor_id = TWO_MONITOR_CONFIG[gui_type]
elif len(monitors) >= 3:
monitor_id = THREE_MONITOR_CONFIG[gui_type]
else:
return

if monitor_id is None:
return

monitor = monitors[monitor_id].availableGeometry()
self.move(monitor.left(), monitor.top())
self.showFullScreen()


def run_gui_pilot() -> None:
Expand Down
Binary file added src/surface/gui/gui/sounds/alarm.wav
Binary file not shown.
22 changes: 21 additions & 1 deletion src/surface/gui/gui/widgets/flood_warning.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from gui.gui_nodes.event_nodes.subscriber import GUIEventSubscriber
from gui.widgets.circle import CircleIndicator
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QUrl
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from PyQt6.QtMultimedia import QSoundEffect

from rov_msgs.msg import Flooding

from ament_index_python.packages import get_package_share_directory
import os


# The 'Loop' enum has int values, not 'Loop', unbeknownst to mypy
Q_SOUND_EFFECT_LOOP_FOREVER: int = QSoundEffect.Loop.Infinite.value # type: ignore


class FloodWarning(QWidget):

Expand Down Expand Up @@ -37,16 +45,28 @@ def __init__(self) -> None:
flood_layout.addWidget(self.indicator)
self.setLayout(flood_layout)

alarm_sound_path = os.path.join(
get_package_share_directory("gui"), "sounds", "alarm.wav"
)
self.alarm_sound = QSoundEffect()
self.alarm_sound.setSource(QUrl.fromLocalFile(alarm_sound_path))
self.alarm_sound.setLoopCount(Q_SOUND_EFFECT_LOOP_FOREVER)

@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()

if not self.alarm_sound.isPlaying():
self.alarm_sound.setLoopCount(Q_SOUND_EFFECT_LOOP_FOREVER)
self.alarm_sound.play()
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
self.alarm_sound.setLoopCount(0)
3 changes: 2 additions & 1 deletion src/surface/gui/launch/operator_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def generate_launch_description() -> LaunchDescription:
("/surface/gui/temperature", "/tether/temperature"),
("/surface/gui/vehicle_state_event", "/surface/vehicle_state_event"),
("/surface/gui/mavros/cmd/arming", "/tether/mavros/cmd/arming"),
("/surface/gui/ip_address", "/tether/ip_address")],
("/surface/gui/ip_address", "/tether/ip_address"),
("/surface/gui/flooding", "/tether/flooding")],
emulate_tty=True,
output='screen'
)
Expand Down
3 changes: 1 addition & 2 deletions src/surface/gui/launch/pilot_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ def generate_launch_description() -> LaunchDescription:
("/surface/gui/bottom_cam/image_raw", "/surface/bottom_cam/image_raw"),
("/surface/gui/front_cam/image_raw", "/surface/front_cam/image_raw"),
("/surface/gui/depth_cam/image_raw", "/tether/depth_cam/image_raw"),
("/surface/gui/vehicle_state_event", "/surface/vehicle_state_event"),
("/surface/gui/flooding", "/tether/flooding")],
("/surface/gui/vehicle_state_event", "/surface/vehicle_state_event")],
emulate_tty=True,
output='screen'
)
Expand Down
3 changes: 3 additions & 0 deletions src/surface/gui/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
# Include all images.
(os.path.join('share', PACKAGE_NAME, 'images'),
glob('gui/images/*')),
# Include all sounds.
(os.path.join('share', PACKAGE_NAME, 'sounds'),
glob('gui/sounds/*')),
],
install_requires=['setuptools'],
zip_safe=True,
Expand Down
11 changes: 0 additions & 11 deletions src/surface/surface_main/launch/competition_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,16 @@
def generate_launch_description() -> LaunchDescription:

surface_path = get_package_share_directory('surface_main')
gui_path = get_package_share_directory('gui')

all_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(
surface_path, 'launch', 'surface_all_nodes_launch.py'
)
]),
)

# Launches livestream gui
gui_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(
gui_path, 'launch', 'pilot_launch.py'
)
]),
launch_arguments=[('gui', 'livestream')]
)

return LaunchDescription([
all_launch,
gui_launch
])

0 comments on commit 1d8046e

Please sign in to comment.