diff --git a/CPP/HriBoardLib/hriboard.cpp b/CPP/HriBoardLib/hriboard.cpp index 4720143..84eb8a2 100644 --- a/CPP/HriBoardLib/hriboard.cpp +++ b/CPP/HriBoardLib/hriboard.cpp @@ -1,414 +1,415 @@ /* * Copyright (C) 2017 EPFL-LSRO (Laboratoire de Systèmes 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 "hriboard.h" #include #include #include #include #include #include const int SYNCVAR_LIST_ITEM_SIZE = SYNCVAR_NAME_SIZE + 3; /** * @brief Constructor. */ HriBoard::HriBoard() { } /** * @brief Establish the link with the board. * Establish the link with the board, then stops the streaming and requests the * variables list. * @param comPortName serial port name, in the format "COM1" on Windows, or * "/dev/ttyO1" on UNIX. * @throws A runtime_error is thrown if the serial port could not be opened. */ void HriBoard::openLink(QString comPortName) { streamID = 0; // Setup the serial port. serial.setPortName(comPortName); serial.setBaudRate(UART_BAUDRATE); serial.setDataBits(QSerialPort::Data8); serial.setFlowControl(QSerialPort::NoFlowControl); serial.setParity(QSerialPort::NoParity); connect(&serial, SIGNAL(readyRead()), this, SLOT(onReceivedData())); qDebug() << "Opening the serial COM port..."; if(serial.open(QIODevice::ReadWrite)) qDebug() << "COM port opened successfully."; else throw std::runtime_error("Can't open the COM port."); // Stop the streaming, in case it was enabled. QByteArray ba; ba.append((char)0); sendPacket(PC_MESSAGE_SET_STREAMED_VAR, ba); // Request the variables list. sendPacket(PC_MESSAGE_GET_VARS_LIST); } /** * @brief Sets the SyncVars to stream. * @param varsToStream list of the SyncVars to stream. * @param streamedVarsValues pointer to array where the streaming values will be * appended. This parameter can be nullptr to not use this feature. */ void HriBoard::setStreamedVars(QList varsToStream, QLinkedList > *streamedVarsValues) { this->streamedVarsValues = streamedVarsValues; streamedVarsValues->clear(); // Copy the list of variables to stream, and compute the size of a streaming // packet. streamedVars.clear(); streamPacketSize = sizeof(quint8) + sizeof(quint32); // Stream ID + timestamp. for(SyncVarBase* sv : varsToStream) { streamedVars.append(syncVars[sv->getIndex()]); streamPacketSize += sv->getSize(); } // streamID++; QByteArray ba; ba.append((quint8)varsToStream.size()); ba.append(streamID); for(SyncVarBase* sv : varsToStream) { int varIndex = sv->getIndex(); ba.append((quint8)varIndex); } sendPacket(PC_MESSAGE_SET_STREAMED_VAR, ba); } /** * @brief Updates a SyncVar on the board with the value of the local one. * @param var the SyncVar to synchronize. */ void HriBoard::writeRemoteVar(SyncVarBase *var) { QByteArray ba; ba.append(var->getIndex()); ba.append(var->getData()); sendPacket(PC_MESSAGE_SET_VAR, ba); } /** * @brief Updates a local SyncVar with the value of the SyncVar on the board. * @param var the SyncVar to synchronize. */ void HriBoard::readRemoteVar(SyncVarBase *var) { QByteArray ba; ba.append(var->getIndex()); sendPacket(PC_MESSAGE_GET_VAR, ba); var->setOutOfDate(); } /** * @brief Makes a list of all the candidate serial ports. * @return a list of all the serial port that use the right USB-to-UART chip. */ QStringList HriBoard::getComPorts() { QList ports = QSerialPortInfo::availablePorts(); for(int i=ports.size()-1; i >= 0; i--) { // Remove from the list all the serial COM ports that are not the CP210x // USB-to-serial chip. if(!ports[i].description().contains("CP210")) ports.removeAt(i); } // Make a list of the COM ports names. QStringList portsNames; for(QSerialPortInfo comInfo : ports) portsNames << comInfo.portName(); return portsNames; } /** * @brief Start logging the streamed variables to a CSV file. * @return true if the logfile could be created, false otherwise. */ bool HriBoard::startLoggingToFile() { stopLoggingToFile(); QDateTime now = QDateTime::currentDateTime(); logFile.setFileName("logs/log_" + now.toString("yyyy-MM-dd_hh-mm-ss") + ".csv"); if(QDir().mkpath("logs/") && !logFile.open(QFile::WriteOnly | QFile::Truncate)) { QMessageBox::warning(nullptr, qApp->applicationName(), "Could not create the logfile."); return false; } logStream.setDevice(&logFile); // Write the file header. - logStream << "Timestamp [s];"; + logStream << "timestamp [s];"; + logStream.setRealNumberPrecision(10); for(int i=0; igetName(); if(i < syncVars.size()-1) logStream << ";"; } logStream << endl; return true; } /** * @brief Stop logging the streamed variables to a file. * @remark This function does nothing if the logging was not enabled. */ void HriBoard::stopLoggingToFile() { if(logFile.isOpen()) { logFile.close(); QFileInfo fileInfo(logFile.fileName()); QMessageBox::information(nullptr, qApp->applicationName(), "Logfile saved as " + fileInfo.absoluteFilePath()); } } /** * @brief Return the list of the streamed variables. * @return A const reference to the list of the streamed variables. */ const QList &HriBoard::getStreamedVars() { return streamedVars; } /** * @brief Interprets the received bytes, and reacts accordingly. */ void HriBoard::onReceivedData() { QByteArray rxData = serial.readAll(); //qDebug() << "RX:" << rxData; for(int i=0; i 0) rxDataBytesBuffer[dataBytesReady-1] = (firstHalfByte<<4) + (rxByte & 0xf); switch(rxCurrentMessageType) { case STM_MESSAGE_START_INFO: if(dataBytesReady == 0) { // Request variables list. sendPacket(PC_MESSAGE_GET_VARS_LIST); } break; case STM_MESSAGE_VAR: if(dataBytesReady >= 1) { quint8 varIndex = rxDataBytesBuffer[0]; if(dataBytesReady == 1 + syncVars[varIndex]->getSize()) { QByteArray ba((char*)&rxDataBytesBuffer[1], syncVars[varIndex]->getSize()); syncVars[varIndex]->setData(ba); emit syncVarUpdated(syncVars[varIndex]); } } break; case STM_MESSAGE_VARS_LIST: if(dataBytesReady >= 1) { quint8 nVars = rxDataBytesBuffer[0]; if(dataBytesReady == 1 + nVars * SYNCVAR_LIST_ITEM_SIZE) { // Clear the SyncVar array. for(SyncVarBase *sv : syncVars) delete sv; syncVars.clear(); // quint8* p = &rxDataBytesBuffer[1]; for(int i=0; i values; // Decode the timestamp. quint32 timestamp; memcpy(×tamp, &rxDataBytesBuffer[1], sizeof(timestamp)); double time = ((double)timestamp) / 1000000.0; values.append(time); // Decode the variables values. quint8 const* p = &rxDataBytesBuffer[5]; for(int i=0; igetSize()); streamedVars[i]->setData(value); p += streamedVars[i]->getSize(); values.append(streamedVars[i]->toDouble()); } if(streamedVarsValues != nullptr) streamedVarsValues->append(values); emit streamedSyncVarsUpdated(time, streamedVars); // Log to file, if enabled. if(logFile.isOpen()) { logStream << time << ";"; for(int i=0; iisUpToDate()) logStream << syncVars[i]->toDouble(); else logStream << 0; if(i < syncVars.size()-1) logStream << ";"; } logStream << endl; } } } break; case STM_MESSAGE_DEBUG_TEXT: if(dataBytesReady > 0 && rxDataBytesBuffer[dataBytesReady-1] == '\0') { qDebug() << QString((const char*)rxDataBytesBuffer); rxCurrentMessageType = 0; } break; default: // Ignore. break; } } } } /** * @brief Sends a communication packet to the board. * @param messageType the type of the message. * @param dataBytes the data that will be part of the packet, which is * type-specific. If omitted, there will be no data in the message, so it will * contain only the type. */ void HriBoard::sendPacket(comm_PcMessage messageType, QByteArray dataBytes) { QByteArray txBuffer; txBuffer.append((1<<7) + (uint8_t)messageType); for(int i=0; i> 4); // MSB. txBuffer.append(((quint8)dataBytes[i]) & 0xf); // LSB. } serial.write(txBuffer); serial.waitForBytesWritten(-1); } diff --git a/CPP/HriPcController/mainwindow.cpp b/CPP/HriPcController/mainwindow.cpp index 3f58d04..c63d415 100644 --- a/CPP/HriPcController/mainwindow.cpp +++ b/CPP/HriPcController/mainwindow.cpp @@ -1,389 +1,402 @@ /* * Copyright (C) 2017 EPFL-LSRO (Laboratoire de Systèmes 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 #define GRAPH_UPDATE_PERIOD 50 ///< Plot window refresh period [ms]. #define SETTING_WINDOW_SIZE "window_size" /** * @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); 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; // Resize the window to the previous size. if(settings.contains(SETTING_WINDOW_SIZE)) resize(settings.value(SETTING_WINDOW_SIZE).toSize()); // 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())); // Establish the link with the HRI board. hriBoard.openLink(comPortName); 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 window size. settings.setValue(SETTING_WINDOW_SIZE, size()); // 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; 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); } + + // + clearPlot(); } /** * @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(); +} + /** * @brief Starts or stops the streamed variables logging to file. */ void MainWindow::onLogToFileCheckboxToggled() { if(ui->logToFileCheckbox->isChecked()) { if(!hriBoard.startLoggingToFile()) { QSignalBlocker signalBlocker(ui->logToFileCheckbox); ui->logToFileCheckbox->setChecked(false); } } else hriBoard.stopLoggingToFile(); } /** * @brief Refreshes the plot frame. */ void MainWindow::updateGraph() { // Get the variables scales. const QList &streamedVars = hriBoard.getStreamedVars(); QList plotScales; for(SyncVarBase* sv : streamedVars) { int varIndex = syncVars->indexOf(sv); if(varIndex != -1) plotScales.append(syncVarsWidgets[varIndex].scaleSpinbox->value()); } // 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.h b/CPP/HriPcController/mainwindow.h index c740eb1..5d2329a 100644 --- a/CPP/HriPcController/mainwindow.h +++ b/CPP/HriPcController/mainwindow.h @@ -1,97 +1,98 @@ /* * Copyright (C) 2017 EPFL-LSRO (Laboratoire de Systèmes 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. */ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include #include #include #include #include #include #include #include #include #include #include "../HriBoardLib/hriboard.h" #include "../HriBoardLib/syncvar.h" namespace Ui { class MainWindow; } /** * @addtogroup HriPcController * @{ */ struct SyncVarWidgetsLine { QLabel *name; QPushButton *getButton, *setButton; QDoubleSpinBox *valueSpinBox; QCheckBox *streamCheckbox; QDoubleSpinBox *scaleSpinbox; }; /** * @brief Qt MainWindow with widgets to interact with SyncVars. */ class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); public slots: void onVarsListReceived(const QList &syncVars); void onVarUpdated(SyncVarBase *var); void onGetButtonPressed(); void onSetButtonPressed(); void onStreamCheckboxToggled(); + void clearPlot(); void onLogToFileCheckboxToggled(); void updateGraph(); private: int getVarIndexFromWidget(QWidget* widget); Ui::MainWindow *ui; ///< Graphical user interface from the ui file. HriBoard hriBoard; ///< HRI board interface. const QList *syncVars; QList syncVarsWidgets; QSettings settings; QGridLayout *variablesListLayout; QtCharts::QChart *chart; QList linesSeries; QTimer graphUpdateTimer; QLinkedList> streamedVarsValuesBuffer; }; /** * @} */ #endif diff --git a/CPP/HriPcController/mainwindow.ui b/CPP/HriPcController/mainwindow.ui index 804c338..9358dba 100644 --- a/CPP/HriPcController/mainwindow.ui +++ b/CPP/HriPcController/mainwindow.ui @@ -1,134 +1,141 @@ MainWindow 0 0 648 649 0 0 HRI PC Controller Log to CSV file Live graph + + + + 10 + + + 1000000 + + + 1000 + + + Number of points to show: Decimation: - - - - 10 - - - 1000000 - - - 1000 - - + + 1 100 10 - - + + + + Clear plot + + Variables list 0 0 Qt::ScrollBarAlwaysOff QAbstractScrollArea::AdjustIgnored true 0 0 612 274 QtCharts::QChartView QGraphicsView
QtCharts/QChartView