import time
import sys
import threading
import pdb
import os
from PySide2 import QtWidgets, QtCore, QtGui
from vent.common.message import ControlSetting
from vent.alarm import AlarmSeverity, Alarm
from vent.common.values import ValueName
from vent import gui
from vent.gui import widgets, set_gui_instance, get_gui_instance, styles, PLOTS
from vent.gui.alarm_manager import AlarmManager
from vent.common import values
[docs]class Vent_Gui(QtWidgets.QMainWindow):
gui_closing = QtCore.Signal()
"""
:class:`PySide2.QtCore.Signal` emitted when the GUI is closing.
"""
alarms_updated = QtCore.Signal(dict)
"""
:class:`PySide2.QtCore.Signal` emitted whenever alarms are updated.
Returns the result of ``self.coordinator.get_active_alarms``, so will emit an
empty dict if there are no active alarms.
"""
MONITOR = values.SENSOR
"""
see :data:`.gui.defaults.SENSOR`
"""
CONTROL = values.CONTROL
"""
see :data:`.gui.defaults.CONTROL`
"""
PLOTS = PLOTS
"""
see :data:`.gui.defaults.PLOTS`
"""
monitor_width = 2
plot_width = 4
control_width = 2
total_width = monitor_width + plot_width + control_width
"""
computed from ``monitor_width+plot_width+control_width``
"""
status_height = 1
main_height = 5
total_height = status_height+main_height
"""
computed from ``status_height+main_height``
"""
[docs] def __init__(self, coordinator, update_period = 0.1):
"""
The Main GUI window.
Only one instance can be created at a time. Uses :func:`set_gui_instance` to
store a reference to itself. after initialization, use `get_gui_instance` to
retrieve a reference.
.. todo:
Jonny add a screenshot here when final version.
Attributes:
monitor (dict): Dictionary mapping :data:`.values.SENSOR` keys to :class:`.widgets.Monitor_Value` objects
plots (dict): Dictionary mapping :data:`.gui.PLOT` keys to :class:`.widgets.Plot` objects
controls (dict): Dictionary mapping :data:`.values.CONTROL` keys to :class:`.widgets.Control` objects
coordinator (:class:`vent.coordinator.coordinator.CoordinatorBase`): Some coordinator object that we use to communicate with the controller
control_module (:class:`vent.controller.control_module.ControlModuleBase`): Reference to the control module, retrieved from coordinator
start_time (float): Start time as returned by :func:`time.time`
update_period (float): The global delay between redraws of the GUI (seconds)
alarm_manager (:class:`~.AlarmManager`)
Arguments:
update_period (float): The global delay between redraws of the GUI (seconds)
test (bool): Whether the monitored values and plots should be fed sine waves for visual testing.
"""
if get_gui_instance() is not None and gui.limit_gui():
raise Exception('Instance of gui already running!') # pragma: no cover
else:
set_gui_instance(self)
super(Vent_Gui, self).__init__()
self.alarm_manager = AlarmManager()
self._alarm_state = AlarmSeverity.OFF
self.monitor = {}
self.plots = {}
self.controls = {}
self.coordinator = coordinator
try:
self.control_module = self.coordinator.control_module
except AttributeError:
self.control_module = None
# start QTimer to update values
self.timer = QtCore.QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.update_gui)
# stop QTimer when program closing
self.gui_closing.connect(self.timer.stop)
# set update period (after timer is created!!)
self._update_period = None
self.update_period = update_period
# initialize controls to starting values
self.init_controls()
self.init_ui()
self.start_time = time.time()
self.update_gui()
@property
def update_period(self):
return self._update_period
@update_period.setter
def update_period(self, update_period):
assert(isinstance(update_period, float) or isinstance(update_period, int))
if update_period != self._update_period:
# if the timer is active, stop it and restart with new period
# if self.timer.isActive():
self.timer.setInterval(update_period*1000)
# store new value
self._update_period = update_period
[docs] def init_controls(self):
"""
on startup, set controls in coordinator to ensure init state is synchronized
"""
for control_name, control_params in self.CONTROL.items():
self.set_value(control_params.default, control_name)
[docs] def set_value(self, new_value, value_name=None):
"""
Set a control value with the ``coordinator``
Args:
new_value (float): Som
"""
# get sender ID
if value_name is None:
value_name = self.sender().objectName()
elif not isinstance(value_name, str):
# TODO: More explicitly check for enum
value_name = value_name.name
control_object = ControlSetting(name=getattr(ValueName, value_name),
value=new_value,
min_value = self.CONTROL[getattr(ValueName, value_name)]['safe_range'][0],
max_value = self.CONTROL[getattr(ValueName, value_name)]['safe_range'][1],
timestamp = time.time())
self.coordinator.set_control(control_object)
[docs] def update_gui(self):
try:
# get alarms
#active_alarms = self.coordinator.get_active_alarms()
#self.alarms_updated.emit(active_alarms)
vals = self.coordinator.get_sensors()
for plot_key, plot_obj in self.plots.items():
if hasattr(vals, plot_key):
plot_obj.update_value((time.time(), getattr(vals, plot_key)))
for monitor_key, monitor_obj in self.monitor.items():
if hasattr(vals, monitor_key):
monitor_obj.update_value(getattr(vals, monitor_key))
#
finally:
self.timer.start()
[docs] def update_value(self, value_name, new_value):
"""
Arguments:
value_name (str): Name of key in :attr:`.Vent_Gui.monitor` and :attr:`.Vent_Gui.plots` to update
new_value (int, float): New value to display/plot
"""
if value_name in self.monitor.keys():
self.monitor[value_name].update_value(new_value)
elif value_name in self.plots.keys():
self.plots[value_name].update_value(new_value)
[docs] def init_ui(self):
"""
Create the UI components for the ventilator screen
"""
# basic initialization
self.main_widget = QtWidgets.QWidget()
self.main_widget.setContentsMargins(0,0,0,0)
#
self.setCentralWidget(self.main_widget)
# layout
# - two rows (status, main)
# - three columns
# left: monitor values
# center: plotted values
# right: controls
self.layout = QtWidgets.QVBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.main_widget.setLayout(self.layout)
# layout that includes the display and controls
self.main_layout = QtWidgets.QHBoxLayout()
self.main_layout.setContentsMargins(0,0,0,0)
# call sub-create functions to make ui sections
# top status bar
self.init_ui_status_bar()
# left monitored values
self.init_ui_monitor()
# plots
self.init_ui_plots()
# controls
self.init_ui_controls()
# add main ui area to layout
self.layout.addLayout(self.main_layout, self.main_height)
# connect signals and slots
self.init_ui_signals()
self.show()
[docs] def init_ui_status_bar(self):
############
# Status Bar
status_box = QtWidgets.QGroupBox('System Status')
status_box.setStyleSheet(styles.STATUS_BOX)
status_layout = QtWidgets.QHBoxLayout()
self.status_bar = widgets.Status_Bar()
status_layout.addWidget(self.status_bar)
status_layout.setContentsMargins(0,0,0,0)
status_box.setLayout(status_layout)
self.layout.addWidget(status_box, self.status_height)
[docs] def init_ui_monitor(self):
#########
# display values
# box that contains both the monitors and the plots
self.monitor_box = QtWidgets.QGroupBox("Sensor Monitor")
self.monitor_layout = QtWidgets.QHBoxLayout()
self.monitor_layout.setContentsMargins(0, 0, 0, 0)
self.monitor_box.setLayout(self.monitor_layout)
# box that just displays the monitor widgets
self.display_layout = QtWidgets.QVBoxLayout()
self.display_layout.setContentsMargins(0,0,0,0)
for display_key, display_params in self.MONITOR.items():
self.monitor[display_key.name] = widgets.Monitor(display_params, enum_name=display_key)
self.display_layout.addWidget(self.monitor[display_key.name])
self.display_layout.addWidget(widgets.components.QHLine())
self.display_layout.addStretch(10)
self.monitor_layout.addLayout(self.display_layout, self.monitor_width)
self.main_layout.addWidget(self.monitor_box, self.plot_width + self.monitor_width)
[docs] def init_ui_plots(self):
###########
# plots
self.plot_layout = QtWidgets.QVBoxLayout()
self.plot_layout.setContentsMargins(0, 0, 0, 0)
# button to set plot history
button_box = QtWidgets.QGroupBox("Plot History")
# button_group = QtWidgets.QButtonGroup()
# button_group.exclusive()
times = (("5s", 5),
("10s", 10),
("30s", 30),
("1m", 60),
("5m", 60 * 5),
("15m", 60 * 15),
("60m", 60 * 60))
self.time_buttons = {}
button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch()
for a_time in times:
self.time_buttons[a_time[0]] = QtWidgets.QRadioButton(a_time[0])
self.time_buttons[a_time[0]].setObjectName(str(a_time[1]))
self.time_buttons[a_time[0]].clicked.connect(self.set_plot_duration)
button_layout.addWidget(self.time_buttons[a_time[0]])
button_box.setLayout(button_layout)
self.plot_layout.addWidget(button_box)
# the plot widgets themselves
for plot_key, plot_params in self.PLOTS.items():
self.plots[plot_key.name] = widgets.Plot(**plot_params)
self.plot_layout.addWidget(self.plots[plot_key.name])
# self.main_layout.addLayout(self.plot_layout,5)
self.monitor_layout.addLayout(self.plot_layout, self.plot_width)
######################
# set the default view as 30s
self.time_buttons[times[2][0]].click()
[docs] def init_ui_controls(self):
####################
# Controls
self.controls_box = QtWidgets.QGroupBox("Ventilator Controls")
# set name so it catches the stylesheet
self.controls_box.setObjectName('CONTROLBOX')
# controls_box.setStyleSheet(styles.CONTROL_BOX)
self.controls_box.setContentsMargins(0, 0, 0, 0)
self.controls_layout = QtWidgets.QVBoxLayout()
self.controls_layout.setContentsMargins(0, 0, 0, 0)
for control_name, control_params in self.CONTROL.items():
self.controls[control_name.name] = widgets.Control(control_params)
self.controls[control_name.name].setObjectName(control_name.name)
self.controls[control_name.name].value_changed.connect(self.set_value)
self.controls_layout.addWidget(self.controls[control_name.name])
self.controls_layout.addWidget(widgets.components.QHLine(color=styles.DIVIDER_COLOR_DARK))
self.controls_layout.addStretch(10)
self.controls_box.setLayout(self.controls_layout)
self.main_layout.addWidget(self.controls_box, self.control_width)
[docs] def init_ui_signals(self):
"""
Connect Qt signals and slots
"""
# hook up monitors and plots
for value in ValueName:
if value.name in self.plots.keys():
self.monitor[value.name].limits_changed.connect(
self.plots[value.name].set_safe_limits)
self.plots[value.name].limits_changed.connect(
self.monitor[value.name].update_limits)
# connect monitors to alarm_manager
for monitor in self.monitor.values():
monitor.alarm.connect(self.alarm_manager.monitor_alarm)
# connect alarms to alarm manager, and then back to us
self.alarms_updated.connect(self.alarm_manager.update_alarms)
self.alarm_manager.new_alarm.connect(self.handle_alarm)
# FIXME: THis should be handled by alarm manager, that's what it's there for!
#self.status_bar.status_message.level_changed.connect(self.alarm_state_changed)
self.status_bar.status_message.message_cleared.connect(self.handle_cleared_alarm)
# connect start button to coordinator start
self.status_bar.start_button.clicked.connect(self.coordinator.start)
[docs] @QtCore.Slot(Alarm)
def handle_alarm(self, alarm):
self.status_bar.status_message.update_message(alarm)
try:
self.monitor[alarm.alarm_name.name].alarm_state = True
except:
# FIXME: will be fixed when values are displayed next to controls
pass
if alarm.severity.value > self.alarm_state.value:
self.alarm_state = alarm.severity
[docs] @QtCore.Slot(Alarm)
def handle_cleared_alarm(self, alarm):
try:
self.monitor[alarm.alarm_name.name].alarm_state = False
except:
# FIXME: will be fixed when values are displayed next to controls
pass
@property
def alarm_state(self):
return self._alarm_state
@alarm_state.setter
def alarm_state(self, state):
if state == AlarmSeverity.HIGH:
pass
[docs] @QtCore.Slot(AlarmSeverity)
def alarm_state_changed(self, state):
self.alarm_state = state
[docs] def set_plot_duration(self, dur):
dur = int(self.sender().objectName())
for plot in self.plots.values():
plot.set_duration(dur)
[docs] def closeEvent(self, event):
"""
Emit :attr:`.gui_closing` and close!
"""
#globals()['_GUI_INSTANCE'] = None
set_gui_instance(None)
self.gui_closing.emit()
if self.coordinator:
try:
self.coordinator.stop()
except:
raise Warning('had a thread object, but the thread object couldnt be stopped')
event.accept()
[docs] def start(self):
"""
Click the :meth:`~.gui.widgets.status_bar.Status_Bar.start` button
Returns:
"""
self.status_bar.start_button.click()
[docs]def launch_gui(coordinator):
# just for testing, should be run from main
app = QtWidgets.QApplication(sys.argv)
app.setStyle('Fusion')
app.setStyleSheet(styles.DARK_THEME)
app = styles.set_dark_palette(app)
gui = Vent_Gui(coordinator)
return app, gui