class SENSOR_TEST(QWidget): bars = None series = None chart = None labelWidgets = [] comms = None commsStatus = False deviceID = "HARP" def __init__(self): QWidget.__init__(self) # ADD GUI WIDGETS self.setupLayout() # LAUNCH GUI self.show() self.resize(800,600) def setupLayout(self): parentLayout = QVBoxLayout() parentLayout.setContentsMargins(20,20,20,20) buttonLayout = QHBoxLayout() # CREATE SERIAL COMMUNICATION CONNECT BUTTON self.connectButton = QPushButton("CONNECT") self.connectButton.setFixedSize(150, 40) self.connectButton.setCheckable(True) self.connectButton.setStyleSheet("QPushButton {background-color: #66BB6A; color: black; border-radius: 20px}" "QPushButton:hover {background-color: #4CAF50; color: black; border-radius: 20px}" "QPushButton:checked {background-color: #EF5350; color: white; border-radius: 20px}" "QPushButton:checked:hover {background-color: #F44336; color: white; border-radius: 20px}") # CREATE SENSORS CALIBRATION BUTTON self.calibrateButton = QPushButton("CALIBRATE") self.calibrateButton.setFixedSize(150, 40) self.calibrateButton.setStyleSheet("QPushButton {background-color: #E0E0E0; color: black; border-radius: 20px}" "QPushButton:hover {background-color: #BDBDBD; color: black; border-radius: 20px}" "QPushButton:pressed {background-color: #D5D5D5; color: black; border-radius: 20px}") # ADD BUTTONS TO HORIZONTAL LAYOUT buttonLayout.addWidget(self.connectButton, alignment = Qt.AlignLeft) buttonLayout.addWidget(self.calibrateButton, alignment = Qt.AlignRight) # CREATE BAR CHART TO DISPLAY SENSOR READINGS self.bars = QBarSet('Sensor Readings') self.bars.append([0 for i in range(5)]) self.chart = QChart() self.chart.setBackgroundRoundness(20) self.chart.layout().setContentsMargins(0, 0, 0, 0) self.series = QBarSeries() self.series.append(self.bars) self.chart.addSeries(self.series) self.chart.setTitle('Sensor Readings') # CREATE X-AXIS AS SENSOR LABELS xAxis = QBarCategoryAxis() labels = ["Sensor {}".format(i+1) for i in range(5)] xAxis.append(labels) # CREATE Y-AXIS AND SENSOR READINGS yAxis = QValueAxis() yAxis.setRange(-100, 100) yAxis.setTickCount(11) yAxis.setTitleText("Pressure (%)") # ADD AXES TO CHART self.chart.addAxis(xAxis, Qt.AlignBottom) self.chart.addAxis(yAxis, Qt.AlignLeft) self.chart.legend().setVisible(False) # ATTACH AXES TO SERIES self.series.attachAxis(xAxis) self.series.attachAxis(yAxis) # ADD CHART TO A VIEW chartView = QChartView(self.chart) labelLayout = QFrame() labelLayout.setStyleSheet("background-color: white; border-radius: 20px") layout1 = QVBoxLayout() labelLayout.setLayout(layout1) layout2 = QHBoxLayout() layout3 = QHBoxLayout() for i in range(5): label = QLabel("Sensor {}".format(i+1)) label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) label.setStyleSheet("font-weight: bold;") layout2.addWidget(label) value = QLabel("0 mV") value.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.labelWidgets.append(value) layout3.addWidget(value) layout1.addLayout(layout2, 1) layout1.addLayout(layout3, 1) parentLayout.addLayout(buttonLayout) parentLayout.addWidget(chartView, 5) parentLayout.addWidget(labelLayout, 1) # LINK WIDGETS self.connectButton.clicked.connect(self.serialToggle) self.calibrateButton.clicked.connect(self.sensorCalibrate) self.setLayout(parentLayout) def getSensorReadings(self): """ PURPOSE Requests sensor readings from ROV and updates GUI. INPUT NONE RETURNS NONE """ # SENSOR POLLING RATE (HZ) refreshRate = 100 # START QTIMER TO REPEATEDLY UPDATE SENSORS AT THE DESIRED POLLING RATE self.timer = QTimer() self.timer.setTimerType(Qt.PreciseTimer) self.timer.timeout.connect(self.getSensorReadings) self.timer.start(int(1000*1/refreshRate)) # STOP REQUESTING SENSOR VALUES IF ROV IS DISCONNECTED if self.commsStatus == False: self.timer.stop() else: # REQEST SINGLE READING sensorReadings = self.getSensors() # UPDATE VOLTAGE READINGS self.updateVoltageReadings(sensorReadings) # SCALE SENSOR READINGS scaledReadings = self.mapPressureReadings(sensorReadings) try: # UPDATE GUI self.series.remove(self.bars) self.bars = QBarSet("") self.bars.append([float(item) for item in scaledReadings]) self.series.append(self.bars) except: self.teensyDisconnect() def mapPressureReadings(self, values): mappedValues = [] for value in values: try: mappedValues.append((200/1023)*float(value) - 100) except: pass return mappedValues def updateVoltageReadings(self, values): voltageValues = [round((3300/1023)*float(i)) for i in values] for i, label in enumerate(self.labelWidgets): label.setText(str(voltageValues[i]) + " mV") def serialToggle(self, buttonState): """ PURPOSE Determines whether to connect or disconnect from the ROV serial interface. INPUT - buttonState = the state of the button (checked or unchecked). RETURNS NONE """ # CONNECT if buttonState: self.teensyConnect() # DISCONNECT else: self.teensyDisconnect() def sensorCalibrate(self): """ PURPOSE INPUT RETURNS """ self.calibrateSensors() ######################## ### SERIAL FUNCTIONS ### ######################## def teensyConnect(self): """ PURPOSE Attempts to connect to the ROV using the comms library. Changes the appearance of the connect buttons. If connection is successful, the ROV startup procedure is initiated. INPUT NONE RETURNS NONE """ # DISABLE BUTTONS TO AVOID DOUBLE CLICKS self.connectButton.setEnabled(False) # FIND ALL AVAILABLE COM PORTS availableComPorts, comPort, identity = self.findComPorts(115200, self.deviceID) print(availableComPorts, comPort, identity) # ATTEMPT CONNECTION TO ROV COM PORT status, message = self.serialConnect(comPort, 115200) print(status, message) # IF CONNECTION IS SUCCESSFUL if status == True: # MODIFY BUTTON STYLE self.connectButton.setText('DISCONNECT') # START READING SENSOR VALUES self.getSensorReadings() # IF CONNECTION IS UNSUCCESSFUL else: self.teensyDisconnect() # RE-ENABLE CONNECT BUTTONS self.connectButton.setEnabled(True) def teensyDisconnect(self): """ PURPOSE Disconnects from the ROV using the comms library. Changes the appearance of the connect buttons INPUT NONE RETURNS NONE """ # MODIFY BUTTON STYLE self.connectButton.setText('CONNECT') self.connectButton.setChecked(False) # CLOSE COM PORT if self.commsStatus: self.comms.close() self.commsStatus = False def findComPorts(self, baudRate, identity): """ PURPOSE Find all available COM ports and requests the devices identity. INPUT - menuObject = pointer to the drop down menu to display the available COM ports. - baudRate = baud rate of the serial interface. - identity = string containing the required device identity to connect to. RETURNS - availableComPorts = list of all the available COM ports. - rovComPort = the COM port that belongs to the device. - identity = the devices response from an identity request. """ # CREATE LIST OF ALL POSSIBLE COM PORTS ports = ['COM%s' % (i + 1) for i in range(256)] deviceIdentity = "" comPort = None availableComPorts = [] # CHECK WHICH COM PORTS ARE AVAILABLE for port in ports: try: comms = serial.Serial(port, baudRate, timeout = 1) availableComPorts.append(port) # REQUEST IDENTITY FROM COM PORT self.commsStatus = True deviceIdentity = self.getIdentity(comms, identity) comms.close() self.commsStatus = False # FIND WHICH COM PORT IS THE ROV if deviceIdentity == identity: comPort = port break # SKIP COM PORT IF UNAVAILABLE except (OSError, serial.SerialException): pass return availableComPorts, comPort, deviceIdentity def getIdentity(self, serialInterface, identity): """ PURPOSE Request identity from a defined COM port. INPUT - serialInterface = pointer to the serial interface object. - identity = the desired identity response from the device connected to the COM port. RETURNS - identity = the devices response. """ identity = "" startTime = datetime.now() elapsedTime = 0 # REPEATIDELY REQUEST IDENTIFICATION FROM DEVICE FOR UP TO 3 SECONDS while (identity == "") and (elapsedTime < 3): self.serialSend("?I", serialInterface) identity = self.serialReceive(serialInterface) elapsedTime = (datetime.now() - startTime).total_seconds() return identity def serialConnect(self, comPort, baudRate): """ PURPOSE Attempts to initialise a serial communication interface with a desired COM port. INPUT - rovComPort = the COM port of the ROV. - baudRate = the baud rate of the serial interface. RETURNS NONE """ self.commsStatus = False if comPort != None: try: self.comms = serial.Serial(comPort, baudRate, timeout = 1) message = "Connection to ROV successful." self.commsStatus = True except: message = "Failed to connect to {}.".format(comPort) else: message = "Failed to recognise device identity." return self.commsStatus, message def serialSend(self, command, serialInterface): """ PURPOSE Sends a string down the serial interface to the ROV. INPUT - command = the command to send. - serialInterface = pointer to the serial interface object. RETURNS NONE """ if self.commsStatus: try: serialInterface.write((command + '\n').encode('ascii')) except: self.teensyDisconnect() print("Failed to send command.") def serialReceive(self, serialInterface): """ PURPOSE Waits for data until a newline character is received. INPUT - serialInterface = pointer to the serial interface object. RETURNS NONE """ received = "" try: received = serialInterface.readline().decode('ascii').strip() except: self.teensyDisconnect() print("Failed to receive data.") return(received) def getSensors(self): """ PURPOSE Send request to device to return sensor readings. INPUT NONE RETURNS - results = array containing the sensor readings. """ results = [] # REQUEST SENSOR READINGS command = "?S" self.serialSend(command, self.comms) # READ RESPONSE INTO AN ARRAY results = self.serialReceive(self.comms).split(",") print(results) return results def calibrateSensors(self): """ """ # REQUEST SENSOR READINGS command = "?C" self.serialSend(command, self.comms)
class RowingMonitorMainWindow(QtWidgets.QMainWindow): COLOR_RED = QColor('#E03A3E') COLOR_BLUE = QColor('#009DDC') DISABLE_LOGGING = False PLOT_VISIBLE_SAMPLES = 200 PLOT_MIN_Y = -1 PLOT_MAX_Y = 55 PLOT_TIME_WINDOW_SECONDS = 7 PLOT_WIDTH_INCHES = 2 PLOT_HEIGHT_INCHES = 1 PLOT_DPI = 300 PLOT_FAST_DRAWING = False WORK_PLOT_VISIBLE_STROKES = 64 WORK_PLOT_MIN_Y = 0 WORK_PLOT_MAX_Y = 350 BOAT_SPEED_PLOT_VISIBLE_STROKES = 64 BOAT_SPEED_PLOT_MIN_Y = 0 BOAT_SPEED_PLOT_MAX_Y = 10 GUI_FONT = QtGui.QFont('Nunito SemiBold', 12) GUI_FONT_LARGE = QtGui.QFont('Nunito', 24) GUI_FONT_MEDIUM = QtGui.QFont('Nunito', 16) def __init__(self, config, data_source, *args, **kwargs): super(RowingMonitorMainWindow, self).__init__(*args, **kwargs) self.setWindowTitle('Rowing Monitor') self.config = config self.log_folder_path = config.log_folder_path self.workout = wo.WorkoutMetricsTracker( config=config, data_source=data_source ) # Connect workut emitter to UI update self.workout_qt_emitter = SignalEmitter() self.workout_qt_emitter.updated.connect(self.ui_callback) # Setup main window layout self.main_widget = QtWidgets.QWidget() self.main_widget.setFocus() self.setCentralWidget(self.main_widget) self.app_layout = QtWidgets.QVBoxLayout(self.main_widget) self.app_layout.setContentsMargins(0, 0, 0, 0) #(left, top, right, bottom) # Build button bar self.button_bar_background_widget = QtWidgets.QWidget(self.main_widget) self.button_bar_background_widget.setObjectName('ButtonBarBackground') self.button_bar_background_widget.setStyleSheet('QWidget#ButtonBarBackground {background-color: #F1F1F1;}') self.button_bar_layout = QtWidgets.QHBoxLayout(self.button_bar_background_widget) self.start_button = QtWidgets.QPushButton('Start') self.start_button.setFlat(True) # Start button style palette = self.start_button.palette() palette.setColor(palette.Button, QColor('#E03A3E')) palette.setColor(palette.ButtonText, QColor('white')) self.start_button.setAutoFillBackground(True) self.start_button.setPalette(palette) self.start_button.update() self.start_button.setFont(self.GUI_FONT) self.start_button.setMinimumSize(97, 60) self.start_button.setMaximumSize(97, 60) # Add to main window self.button_bar_layout.addWidget(self.start_button) #self.button_bar_layout.addWidget(self.button_bar_background_widget) self.button_bar_layout.setAlignment(QtCore.Qt.AlignLeft) self.button_bar_layout.setContentsMargins(0, 0, 0, 0) #(left, top, right, bottom) self.app_layout.addWidget(self.button_bar_background_widget)#.addLayout(self.button_bar_layout) self.stats_layout = QtWidgets.QHBoxLayout() self.stats_layout.setContentsMargins(0, 0, 0, 0) self.stats_layout.setSpacing(0) self.app_layout.addLayout(self.stats_layout) # Build workout stats bar self.metrics_panel_layout = QtWidgets.QVBoxLayout() self.charts_panel_layout = QtWidgets.QVBoxLayout() self.workout_totals_layout = QtWidgets.QVBoxLayout() self.time_label = QtWidgets.QLabel(self._format_total_workout_time(0)) self.distance_label = QtWidgets.QLabel(self._format_total_workout_distance(0)) self.time_label.setAlignment(QtCore.Qt.AlignCenter) self.distance_label.setAlignment(QtCore.Qt.AlignCenter) self.time_label.setFixedHeight(40) self.distance_label.setFixedHeight(30) self.workout_totals_layout.addWidget(self.time_label) self.workout_totals_layout.addWidget(self.distance_label) #self.workout_totals_layout.setSpacing(0) self.workout_totals_layout.setContentsMargins(0, 0, 0, 30) self.metrics_panel_layout.addLayout(self.workout_totals_layout) self.stroke_stats_layout = QtWidgets.QVBoxLayout() self.spm_label = QtWidgets.QLabel(self._format_strokes_per_minute(99)) self.stroke_ratio_label = QtWidgets.QLabel(self._format_stroke_ratio(1)) self.spm_label.setAlignment(QtCore.Qt.AlignCenter) self.stroke_ratio_label.setAlignment(QtCore.Qt.AlignCenter) self.spm_label.setFixedHeight(40) self.stroke_ratio_label.setFixedHeight(30) self.stroke_stats_layout.addWidget(self.spm_label) self.stroke_stats_layout.addWidget(self.stroke_ratio_label) #self.stroke_stats_layout.setSpacing(0) self.stroke_stats_layout.setContentsMargins(0, 30, 0, 30) self.metrics_panel_layout.addLayout(self.stroke_stats_layout) self.boat_stats_layout = QtWidgets.QVBoxLayout() self.boat_speed_label = QtWidgets.QLabel(self._format_boat_speed(0)) self.split_time_label = QtWidgets.QLabel(self._format_boat_pace(0)) self.boat_speed_label.setAlignment(QtCore.Qt.AlignCenter) self.split_time_label.setAlignment(QtCore.Qt.AlignCenter) self.boat_speed_label.setFixedHeight(40) self.split_time_label.setFixedHeight(30) self.boat_stats_layout.addWidget(self.boat_speed_label) self.boat_stats_layout.addWidget(self.split_time_label) #self.boat_stats_layout.setSpacing(0) self.boat_stats_layout.setContentsMargins(0, 30, 0, 0) self.metrics_panel_layout.addLayout(self.boat_stats_layout) # Appearance self.time_label.setFont(self.GUI_FONT_LARGE) self.distance_label.setFont(self.GUI_FONT_MEDIUM) self.spm_label.setFont(self.GUI_FONT_LARGE) self.stroke_ratio_label.setFont(self.GUI_FONT_MEDIUM) self.boat_speed_label.setFont(self.GUI_FONT_LARGE) self.split_time_label.setFont(self.GUI_FONT_MEDIUM) # Add to main window self.metrics_panel_layout.setSpacing(0) self.metrics_panel_layout.setContentsMargins(60, 30, 30, 30) #(left, top, right, bottom) self.charts_panel_layout.setSpacing(30) self.charts_panel_layout.setContentsMargins(30, 30, 60, 60)#(30, 30, 60, 60) #(left, top, right, bottom) self.stats_layout.addLayout(self.metrics_panel_layout) self.stats_layout.addLayout(self.charts_panel_layout) self.xdata = [0.0 for i in range(self.PLOT_VISIBLE_SAMPLES)] self.ydata = [0.0 for i in range(self.PLOT_VISIBLE_SAMPLES)] self.work_per_stroke_data = [0.0 for i in range(self.WORK_PLOT_VISIBLE_STROKES)] self.boat_speed_data = [0.0 for i in range(self.WORK_PLOT_VISIBLE_STROKES)] self.seen_strokes = 0 ############################################ # Add torque chart self.torque_plot = QChart() self.torque_plot.setContentsMargins(-26, -26, -26, -26) #self.torque_plot.setAnimationOptions(QChart.GridAxisAnimations) self.torque_plot.legend().setVisible(False) self.torque_plot_horizontal_axis = QValueAxis() self.torque_plot_vertical_axis = QValueAxis() self.torque_plot.addAxis(self.torque_plot_vertical_axis, QtCore.Qt.AlignLeft) self.torque_plot.addAxis(self.torque_plot_horizontal_axis, QtCore.Qt.AlignBottom) # Line series self.torque_plot_series = QLineSeries(self) for i in range(self.PLOT_VISIBLE_SAMPLES): self.torque_plot_series.append(0, 0) #self.torque_plot_series.setColor(QColor('#009DDC')) pen = self.torque_plot_series.pen() pen.setWidth(3) pen.setColor(self.COLOR_BLUE) pen.setJoinStyle(QtCore.Qt.RoundJoin) pen.setCapStyle(QtCore.Qt.RoundCap) self.torque_plot_series.setPen(pen) # Area series self.torque_plot_area_series = QAreaSeries() self.torque_plot_area_series.setUpperSeries(self.torque_plot_series) self.torque_plot_area_series.setLowerSeries(QLineSeries(self)) for i in range(self.PLOT_VISIBLE_SAMPLES): self.torque_plot_area_series.lowerSeries().append(0, 0) self.torque_plot_area_series.setColor(self.COLOR_BLUE) # Compose plot # self.torque_plot.addSeries(self.torque_plot_area_series) # self.torque_plot_area_series.attachAxis(self.torque_plot_horizontal_axis) # self.torque_plot_area_series.attachAxis(self.torque_plot_vertical_axis) self.torque_plot.addSeries(self.torque_plot_series) self.torque_plot_series.attachAxis(self.torque_plot_horizontal_axis) self.torque_plot_series.attachAxis(self.torque_plot_vertical_axis) # Set axes range self.torque_plot_vertical_axis.setRange(self.PLOT_MIN_Y, self.PLOT_MAX_Y) #self.torque_plot_vertical_axis.setTickCount(10) self.torque_plot_vertical_axis.setVisible(False) self.torque_plot_horizontal_axis.setRange(-self.PLOT_TIME_WINDOW_SECONDS, 0) self.torque_plot_horizontal_axis.setVisible(False) # Add plot view to GUI self.torque_plot_chartview = QChartView(self.torque_plot) self.torque_plot_chartview.setRenderHint(QPainter.Antialiasing) #self.torque_plot_chartview.setMinimumHeight(250) #self.torque_plot_chartview.resize(250, 250) self.torque_plot_box = QtWidgets.QGroupBox("Force") self.torque_plot_box.setFont(self.GUI_FONT) self.torque_plot_box.setAlignment(QtCore.Qt.AlignLeft) self.torque_plot_box_layout = QtWidgets.QVBoxLayout() self.torque_plot_box_layout.addWidget(self.torque_plot_chartview) self.torque_plot_box.setLayout(self.torque_plot_box_layout) self.charts_panel_layout.addWidget(self.torque_plot_box) ############################################ ############################################ # Add work chart self.work_plot = QChart() self.work_plot.setContentsMargins(-26, -26, -26, -26) self.work_plot.legend().setVisible(False) self.work_plot_horizontal_axis = QBarCategoryAxis() self.work_plot_vertical_axis = QValueAxis() self.work_plot.addAxis(self.work_plot_vertical_axis, QtCore.Qt.AlignLeft) self.work_plot.addAxis(self.work_plot_horizontal_axis, QtCore.Qt.AlignBottom) # Define series self.work_plot_series = QBarSeries() self.work_plot_bar_set_list = [QBarSet(str(i)) for i in range(self.WORK_PLOT_VISIBLE_STROKES)] self.work_plot_series.append(self.work_plot_bar_set_list) for bar_set in self.work_plot_bar_set_list: bar_set.append(0) self.work_plot_series.setBarWidth(1.0) # Compose plot self.work_plot.addSeries(self.work_plot_series) self.work_plot_series.attachAxis(self.work_plot_horizontal_axis) self.work_plot_series.attachAxis(self.work_plot_vertical_axis) # Set axes range self.work_plot_vertical_axis.setRange(self.WORK_PLOT_MIN_Y, self.WORK_PLOT_MAX_Y) self.work_plot_vertical_axis.setTickCount(8) self.work_plot_vertical_axis.setVisible(False) self.work_plot_horizontal_axis.append("1") self.work_plot_horizontal_axis.setVisible(False) # Add plot view to GUI self.work_plot_chartview = QChartView(self.work_plot) self.work_plot_chartview.setRenderHint(QPainter.Antialiasing) #self.work_plot_chartview.setMinimumHeight(250) #self.work_plot_chartview.resize(250, 250) self.work_plot_box = QtWidgets.QGroupBox("Work per stroke") self.work_plot_box.setFont(self.GUI_FONT) self.work_plot_box.setAlignment(QtCore.Qt.AlignLeft) self.work_plot_box_layout = QtWidgets.QVBoxLayout() self.work_plot_box_layout.addWidget(self.work_plot_chartview) self.work_plot_box.setLayout(self.work_plot_box_layout) self.charts_panel_layout.addWidget(self.work_plot_box) ############################################ ############################################ # Add boat speed chart self.boat_speed_plot = QChart() self.boat_speed_plot.setContentsMargins(-26, -26, -26, -26) #self.boat_speed_plot.setBackgroundRoundness(0) self.boat_speed_plot.legend().setVisible(False) self.boat_speed_plot_horizontal_axis = QBarCategoryAxis() self.boat_speed_plot_vertical_axis = QValueAxis() self.boat_speed_plot.addAxis(self.boat_speed_plot_vertical_axis, QtCore.Qt.AlignLeft) self.boat_speed_plot.addAxis(self.boat_speed_plot_horizontal_axis, QtCore.Qt.AlignBottom) # Define series self.boat_speed_plot_series = QBarSeries() self.boat_speed_plot_bar_set_list = [QBarSet(str(i)) for i in range(self.BOAT_SPEED_PLOT_VISIBLE_STROKES)] self.boat_speed_plot_series.append(self.boat_speed_plot_bar_set_list) for bar_set in self.boat_speed_plot_bar_set_list: bar_set.append(0) self.boat_speed_plot_series.setBarWidth(1.0) # Compose plot self.boat_speed_plot.addSeries(self.boat_speed_plot_series) self.boat_speed_plot_series.attachAxis(self.boat_speed_plot_horizontal_axis) self.boat_speed_plot_series.attachAxis(self.boat_speed_plot_vertical_axis) # Set axes range self.boat_speed_plot_vertical_axis.setRange(self.BOAT_SPEED_PLOT_MIN_Y, self.BOAT_SPEED_PLOT_MAX_Y) self.boat_speed_plot_vertical_axis.setTickCount(8) self.boat_speed_plot_vertical_axis.setVisible(False) self.boat_speed_plot_horizontal_axis.append("1") self.boat_speed_plot_horizontal_axis.setVisible(False) # Add plot view to GUI self.boat_speed_plot_chartview = QChartView(self.boat_speed_plot) self.boat_speed_plot_chartview.setRenderHint(QPainter.Antialiasing) #self.boat_speed_plot_chartview.setContentsMargins(0, 0, 0, 0) self.boat_speed_plot_box = QtWidgets.QGroupBox("Boat speed") self.boat_speed_plot_box.setFont(self.GUI_FONT) #self.boat_speed_plot_box.setFlat(True) #self.boat_speed_plot_box.setContentsMargins(0, 0, 0, 0) #self.boat_speed_plot_box.setObjectName("BoatSpeedGB") # Changed here... #self.boat_speed_plot_box.setStyleSheet('QGroupBox {background-color: white;}') #self.main_widget.setStyleSheet('QGroupBox::title { background-color: blue }') self.boat_speed_plot_box.setAlignment(QtCore.Qt.AlignLeft) self.boat_speed_plot_box_layout = QtWidgets.QVBoxLayout() self.boat_speed_plot_box_layout.addWidget(self.boat_speed_plot_chartview) self.boat_speed_plot_box.setLayout(self.boat_speed_plot_box_layout) self.charts_panel_layout.addWidget(self.boat_speed_plot_box) ############################################ # Set interaction behavior self.start_button.clicked.connect(self.start) # Update workout duration every second self.timer = QtCore.QTimer() self.timer.setInterval(1000) self.timer.timeout.connect(self.timer_tick) self.start_timestamp = None self.started = False self.show() def update_torque_plot(self): self.torque_plot_series.append(self.xdata[-1], self.ydata[-1]) self.torque_plot_series.remove(0) self.torque_plot_area_series.lowerSeries().append(self.xdata[-1], 0) self.torque_plot_area_series.lowerSeries().remove(0) self.torque_plot_horizontal_axis.setRange(self.xdata[-1] - self.PLOT_TIME_WINDOW_SECONDS, self.xdata[-1]) def update_work_plot(self): # Create new bar set new_bar_set = QBarSet(str(self.seen_strokes)) value = self.work_per_stroke_data[-1] value_rel = int(value * 255 / self.WORK_PLOT_MAX_Y) new_bar_set.append(value) new_bar_set.setColor(self.COLOR_BLUE) # QColor(value_rel, value_rel, value_rel)) # Append new set, and remove oldest self.work_plot_series.append(new_bar_set) self.work_plot_series.remove(self.work_plot_series.barSets()[0]) def update_boat_speed_plot(self): # Create new bar set new_bar_set = QBarSet(str(self.seen_strokes)) value = self.boat_speed_data[-1] value_rel = int(value * 255 / self.BOAT_SPEED_PLOT_MAX_Y) new_bar_set.append(value) new_bar_set.setColor(self.COLOR_BLUE) # QColor(value_rel, value_rel, value_rel)) # Append new set, and remove oldest self.boat_speed_plot_series.append(new_bar_set) self.boat_speed_plot_series.remove(self.boat_speed_plot_series.barSets()[0]) def start(self): if not self.started: self.start_workout() self.start_button.setText('Stop') self.started = True else: self.stop_workout() self.start_button.setText('Start') self.started = False def start_workout(self): self.timer.start() self.workout.start(qt_signal_emitter=self.workout_qt_emitter) def stop_workout(self): self.timer.stop() self.workout.stop() if not self.DISABLE_LOGGING and not DEV_MODE: self.workout.save(output_folder_path=self.log_folder_path) def _format_total_workout_time(self, value_seconds): minutes = value_seconds // 60 seconds = value_seconds % 60 return '%d:%02d' % (minutes, seconds) def _format_total_workout_distance(self, value): return f'{int(value):,} m' def _format_strokes_per_minute(self, value): return '%.1f spm' % value def _format_stroke_ratio(self, value): return '1:%.1f ratio' % value def _format_boat_speed(self, value): return '%0.2f m/s' % value def _format_boat_pace(self, value_seconds): return '%s /500m' % (self._format_total_workout_time(value_seconds)) def ui_callback(self): # If this is the first pulse, capture the current time if self.start_timestamp is None: self.start_timestamp = QtCore.QTime.currentTime() # Update distance distance = self.workout.boat.position.values[-1] self.distance_label.setText(self._format_total_workout_distance(distance)) if len(self.workout.person.torque) > 0: self.ydata = self.ydata[1:] + [self.workout.person.torque.values[-1]] self.xdata = self.xdata[1:] + [self.workout.person.torque.timestamps[-1]] self.update_torque_plot() # Update SPM new_stroke_info_available = len(self.workout.person.strokes) > self.seen_strokes if new_stroke_info_available: # SPM indicator spm = 60 / self.workout.person.strokes.values[-1].duration ratio = self.workout.person.strokes.values[-1].drive_to_recovery_ratio self.spm_label.setText(self._format_strokes_per_minute(spm)) self.stroke_ratio_label.setText(self._format_stroke_ratio(ratio)) # Work plot self.work_per_stroke_data = self.work_per_stroke_data[1:] + \ [self.workout.person.strokes.values[-1].work_done_by_person] self.update_work_plot() self.seen_strokes += 1 # Boat speed plot average_boat_speed = self.workout.boat.speed.get_average_value( start_time=self.workout.person.strokes.values[-1].start_time, end_time=self.workout.person.strokes.values[-1].end_time ) self.boat_speed_data = self.boat_speed_data[1:] + [average_boat_speed] self.boat_speed_label.setText(self._format_boat_speed(average_boat_speed)) split_time_seconds = 500.0 / average_boat_speed self.split_time_label.setText(self._format_boat_pace(split_time_seconds)) self.update_boat_speed_plot() def timer_tick(self): # Do nothing if we haven't received an encoder pulse yet. if self.start_timestamp is None: return # Update workout time label time_since_start = self.start_timestamp.secsTo(QtCore.QTime.currentTime()) self.time_label.setText(self._format_total_workout_time(time_since_start))