diff --git a/CPP/HriPcController/mainwindow.cpp b/CPP/HriPcController/mainwindow.cpp index 0af4c92..d02c441 100644 --- a/CPP/HriPcController/mainwindow.cpp +++ b/CPP/HriPcController/mainwindow.cpp @@ -1,474 +1,486 @@ /* * Copyright (C) 2017 EPFL-LSRO (Laboratoire de Systemes Robotiques). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "mainwindow.h" #include "ui_mainwindow.h" #include #include #include #include #include #include #include #define GRAPH_UPDATE_PERIOD 50 ///< Plot window refresh period [ms]. #define LOG_CHECKBOX_BASE_LABEL QString("Log to CSV file") #define SETTING_WINDOW_SIZE "window_size" +#define SETTING_LIST_FRAME_HEIGHT "list_frame_height" +#define SETTING_PLOT_FRAME_HEIGHT "plot_frame_height" #define SETTING_LOGS_DIR "logs_dir" /** * @brief Constructor * Setups the GUI, and try to connect to the HRI board. * @param parent parent of this widget. */ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { // ui->setupUi(this); ui->clearButton->setIcon(QIcon(qApp->style()->standardPixmap(QStyle::SP_DialogResetButton))); ui->pausePlotButton->setIcon(QIcon(qApp->style()->standardPixmap(QStyle::SP_MediaPause))); ui->setLogLocationButton->setIcon(qApp->style()->standardPixmap(QStyle::SP_DirOpenIcon)); variablesListLayout = new QGridLayout(); variablesListLayout->setAlignment(Qt::AlignTop); delete ui->scrollWidget->layout(); ui->scrollWidget->setLayout(variablesListLayout); chart = new QtCharts::QChart(); ui->graphicsView->setChart(chart); ui->graphicsView->setRenderHint(QPainter::Antialiasing); graphUpdateTimer.setInterval(GRAPH_UPDATE_PERIOD); graphUpdateTimer.setSingleShot(false); connect(&graphUpdateTimer, SIGNAL(timeout()), this, SLOT(updateGraph())); // syncVars = nullptr; // Recover the previous logs save location, or define if this is the first // time the app is started. if(settings.contains(SETTING_LOGS_DIR)) logsDirectory = settings.value(SETTING_LOGS_DIR).toString(); else { logsDirectory = qApp->applicationDirPath(); #if defined(Q_OS_MACX) || defined(Q_OS_MAC64) // For MacOSX, go to out of the app bundle. logsDirectory.remove(QRegExp("/[^/]*.app/Contents/MacOS/?$")); #endif } ui->logToFileCheckbox->setText(LOG_CHECKBOX_BASE_LABEL + " (in " + logsDirectory + ")"); - // Resize the window to the previous size. + // Resize the window and the panes to the previous size. if(settings.contains(SETTING_WINDOW_SIZE)) resize(settings.value(SETTING_WINDOW_SIZE).toSize()); + if(settings.contains(SETTING_LIST_FRAME_HEIGHT) && + settings.contains(SETTING_PLOT_FRAME_HEIGHT)) + { + int plotGroupWidth = settings.value(SETTING_LIST_FRAME_HEIGHT).toInt(); + int plotGroupHeight = settings.value(SETTING_PLOT_FRAME_HEIGHT).toInt(); + ui->splitter->setSizes({ plotGroupWidth, plotGroupHeight }); + } + // Ask for the COM port. QString comPortName; QStringList ports = HriBoard::getComPorts(); if(ports.size() == 0) { QMessageBox::critical(this, qApp->applicationName(), "No COM port."); exit(0); } else if(ports.size() == 1) comPortName = ports.first(); else { bool ok; QString portChoice = QInputDialog::getItem(this, qApp->applicationName(), "Serial port:", ports, 0, false, &ok); int portIndex = ports.indexOf(portChoice); if(portIndex == -1 || !ok) exit(0); comPortName = ports[portIndex]; } // connect(ui->clearButton, SIGNAL(clicked(bool)), this, SLOT(clearPlot())); connect(ui->logToFileCheckbox, SIGNAL(toggled(bool)), this, SLOT(onLogToFileCheckboxToggled())); connect(ui->setLogLocationButton, SIGNAL(clicked(bool)), this, SLOT(setLogfilesDirectory())); // Establish the link with the HRI board. try { hriBoard.openLink(comPortName); } catch(std::runtime_error) { QMessageBox::critical(nullptr, qApp->applicationName(), "Could not open the serial port " + comPortName); exit(0); } connect(&hriBoard, SIGNAL(syncVarsListReceived(const QList&)), this, SLOT(onVarsListReceived(const QList&))); connect(&hriBoard, SIGNAL(syncVarUpdated(SyncVarBase*)), this, SLOT(onVarUpdated(SyncVarBase*))); } /** * @brief Destructor. */ MainWindow::~MainWindow() { // Store the settings. settings.setValue(SETTING_WINDOW_SIZE, size()); + settings.setValue(SETTING_LIST_FRAME_HEIGHT, ui->splitter->sizes()[0]); + settings.setValue(SETTING_PLOT_FRAME_HEIGHT, ui->splitter->sizes()[1]); settings.setValue(SETTING_LOGS_DIR, logsDirectory); // delete ui; } /** * @brief Displays the SyncVars list, and starts the streaming. * @param syncVars SyncVars list. * @remark This slot function is called automatically by the HriBoard object, as * soon as the list is received from the board. */ void MainWindow::onVarsListReceived(const QList &syncVars) { this->syncVars = &syncVars; // Reset the plot frame. clearPlot(); chart->removeAllSeries(); linesSeries.clear(); // Create the widgets to display the SyncVars list. QLayoutItem *li; while((li = variablesListLayout->takeAt(0)) != nullptr) li->widget()->deleteLater(); syncVarsWidgets.clear(); for(int i=0; igetName()); variablesListLayout->addWidget(varsWidgets.name, i, 0); // Get button. if(syncVars[i]->getAccess() != WRITEONLY) { varsWidgets.getButton = new QPushButton("Get"); variablesListLayout->addWidget(varsWidgets.getButton, i, 1); connect(varsWidgets.getButton, SIGNAL(clicked(bool)), this, SLOT(onGetButtonPressed())); } // Value field. varsWidgets.valueSpinBox = new QDoubleSpinBox(); varsWidgets.valueSpinBox->setDecimals(4); varsWidgets.valueSpinBox->setMinimum(std::numeric_limits::lowest()); varsWidgets.valueSpinBox->setMaximum(std::numeric_limits::max()); variablesListLayout->addWidget(varsWidgets.valueSpinBox, i, 2); if(syncVars[i]->getAccess() == READONLY) { varsWidgets.valueSpinBox->setReadOnly(true); varsWidgets.valueSpinBox->setButtonSymbols(QAbstractSpinBox::NoButtons); } else { connect(varsWidgets.valueSpinBox, SIGNAL(editingFinished()), this, SLOT(onSetButtonPressed())); } // Set button. if(syncVars[i]->getAccess() != READONLY) { varsWidgets.setButton = new QPushButton("Set"); variablesListLayout->addWidget(varsWidgets.setButton, i, 3); connect(varsWidgets.setButton, SIGNAL(clicked(bool)), this, SLOT(onSetButtonPressed())); } // Stream checkbox. if(syncVars[i]->getAccess() != WRITEONLY) { varsWidgets.streamCheckbox = new QCheckBox("Stream"); variablesListLayout->addWidget(varsWidgets.streamCheckbox, i, 4); connect(varsWidgets.streamCheckbox, SIGNAL(toggled(bool)), this, SLOT(onStreamCheckboxToggled())); } if(syncVars[i]->getAccess() != WRITEONLY) { varsWidgets.scaleSpinbox = new QDoubleSpinBox(); variablesListLayout->addWidget(varsWidgets.scaleSpinbox, i, 5); varsWidgets.scaleSpinbox->setPrefix("x"); varsWidgets.scaleSpinbox->setValue(1.0); varsWidgets.scaleSpinbox->setRange(-1000000.0, 1000000.0); } syncVarsWidgets.append(varsWidgets); } // Acquire the value of all the readable SyncVars. for(SyncVarBase *sv : syncVars) { if(sv->getAccess() != WRITEONLY) hriBoard.readRemoteVar(sv); } } /** * @brief Updates the "value" field of a SyncVar. * @param var the SyncVar corresponding to the "value" field to update. */ void MainWindow::onVarUpdated(SyncVarBase *var) { int varIndex = syncVars->indexOf(var); if(varIndex >= 0 && varIndex < syncVarsWidgets.size()) syncVarsWidgets[varIndex].valueSpinBox->setValue(var->toDouble()); } /** * @brief Gets the value of a SyncVar whose the "Get" button was pressed. * @warning This function should only be called by the "Get" button signal. */ void MainWindow::onGetButtonPressed() { int varIndex = getVarIndexFromWidget((QWidget*)sender()); hriBoard.readRemoteVar(syncVars->at(varIndex)); } /** * @brief Sets the value of a SyncVar whose the "Set" button was pressed. * @warning This function should only be called by the "Set" button signal. */ void MainWindow::onSetButtonPressed() { int varIndex = getVarIndexFromWidget((QWidget*)sender()); SyncVarBase *sv = syncVars->at(varIndex); sv->fromDouble(syncVarsWidgets[varIndex].valueSpinBox->value()); hriBoard.writeRemoteVar(sv); } /** * @brief Updates the list of SyncVars to stream from the checkboxes' states. */ void MainWindow::onStreamCheckboxToggled() { // Setup the streaming. QList varsToStream; for(int i=0; iisChecked()) varsToStream.append(syncVars->at(i)); } hriBoard.setStreamedVars(varsToStream, &streamedVarsValuesBuffer); // Setup the graph. streamedVarsValuesBuffer.clear(); chart->removeAllSeries(); linesSeries.clear(); for(SyncVarBase *sv : varsToStream) { QtCharts::QLineSeries *series = new QtCharts::QLineSeries(); series->setName(sv->getName()); chart->addSeries(series); linesSeries.append(series); } chart->createDefaultAxes(); chart->axisX()->setTitleText("Time [s]"); // Setup the update timer. if(varsToStream.isEmpty()) graphUpdateTimer.stop(); else graphUpdateTimer.start(); } /** * @brief Clears the plot window. */ void MainWindow::clearPlot() { for(QtCharts::QLineSeries *ls : linesSeries) ls->clear(); ui->pausePlotButton->setChecked(false); } /** * @brief Starts or stops the streamed variables logging to file. */ void MainWindow::onLogToFileCheckboxToggled() { if(ui->logToFileCheckbox->isChecked()) { if(!hriBoard.startLoggingToFile(logsDirectory)) { // If the logging cannot start, uncheck the box. QSignalBlocker signalBlocker(ui->logToFileCheckbox); ui->logToFileCheckbox->setChecked(false); } } else hriBoard.stopLoggingToFile(); } /** * @brief Sets the directory to save the logfiles. */ void MainWindow::setLogfilesDirectory() { QString newDir = QFileDialog::getExistingDirectory(this, "Select the directory to save the logfiles", logsDirectory); if(!newDir.isEmpty()) { logsDirectory = newDir; ui->logToFileCheckbox->setText(LOG_CHECKBOX_BASE_LABEL + " (in " + logsDirectory + ")"); } } /** * @brief Refreshes the plot frame. */ void MainWindow::updateGraph() { // Do not refresh the graph in "pause" mode. if(ui->pausePlotButton->isChecked()) return; // Get the variables scales. const QList &streamedVars = hriBoard.getStreamedVars(); QList plotScales; for(SyncVarBase* sv : streamedVars) { int varIndex = syncVars->indexOf(sv); if(varIndex >= 0) plotScales.append(syncVarsWidgets[varIndex].scaleSpinbox->value()); else return; } // Add the new points to the graph series. int decimationCounter = 0; while(!streamedVarsValuesBuffer.isEmpty()) { if(decimationCounter <= 0) { decimationCounter = ui->plotDecimSpinbox->value(); QList &values = streamedVarsValuesBuffer.first(); for(int i=0; iappend(values[0], values[i+1] * plotScales[i]); } else decimationCounter--; streamedVarsValuesBuffer.removeFirst(); } // Remove the oldest points, to limit the number of points displayed. for(QtCharts::QLineSeries *ls : linesSeries) { if(ls->count() > ui->nPointsSpinbox->value()) ls->removePoints(0, ls->count() - ui->nPointsSpinbox->value()); } // Compute the display range. if(linesSeries.size() > 0 && linesSeries.first()->count() > 0) { // Compute the horizontal (time) range. double firstTime = linesSeries.first()->points().first().x(); double lastTime = linesSeries.first()->points().last().x(); chart->axisX()->setRange(firstTime, lastTime); // Compute the vertical range. double min = std::numeric_limits::max(); double max = std::numeric_limits::lowest(); for(QtCharts::QLineSeries *ls : linesSeries) { QVector points = ls->pointsVector(); for(QPointF p : points) { if(p.y() < min) min = p.y(); if(p.y() > max) max = p.y(); } } if(min == max) { min -= 1.0; max += 1.0; } else { min -= (max-min) * 0.05; // Add a 5% margin at the top and the max += (max-min) * 0.05; // bottom. } chart->axisY()->setRange(min, max); } } /** * @brief Gets the variable index from a widget of the SyncVars list. * @param widget the widget belonging to the variables list. * @return The index of the SyncVar corresponding to the given widget. */ int MainWindow::getVarIndexFromWidget(QWidget *widget) { int index = variablesListLayout->indexOf(widget); int row, column, rowSpan, columnSpan; variablesListLayout->getItemPosition(index, &row, &column, &rowSpan, &columnSpan); return row; } diff --git a/CPP/HriPcController/mainwindow.ui b/CPP/HriPcController/mainwindow.ui index fc06d21..9589e3d 100644 --- a/CPP/HriPcController/mainwindow.ui +++ b/CPP/HriPcController/mainwindow.ui @@ -1,173 +1,180 @@ MainWindow 0 0 648 649 0 0 HRI PC Controller - - - - - - 0 - 0 - + + + + + Qt::Vertical - - Log to CSV file - - - - - - - Live graph - - - - - - 10 - - - 1000000 - - - 1000 - - - - - - - Number of points to show: - - - - - - - Decimation: - - - - - - - - - - 1 - - - 100 - - - 10 - - - - - - - Clear plot - - - - - - - Pause plot - - - true - - - false - - - - - - - - - - - 0 - 0 - - - - Change location... - - - - - - - Variables list - - - - - - - 0 - 0 - - - - Qt::ScrollBarAlwaysOff - - - QAbstractScrollArea::AdjustIgnored - - - true - - - - - 0 - 0 - 616 - 274 - + + + Variables list + + + + + + + 0 + 0 + + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustIgnored + + true + + + + + 0 + 0 + 616 + 101 + + + - - - + + + + + + Live graph + + + + + + 10 + + + 1000000 + + + 1000 + + + + + + + Number of points to show: + + + + + + + Decimation: + + + + + + + + + + 1 + + + 100 + + + 10 + + + + + + + Clear plot + + + + + + + Pause plot + + + true + + + false + + + + + + + + + + + + 0 + 0 + + + + Log to CSV file + + + + + + + + 0 + 0 + + + + Change location... + + + + + QtCharts::QChartView QGraphicsView
QtCharts/QChartView