2 ******************************************************************************
4 * @file scopegadgetwidget.cpp
5 * @author The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
6 * @addtogroup GCSPlugins GCS Plugins
8 * @addtogroup ScopePlugin Scope Gadget Plugin
10 * @brief The scope Gadget, graphically plots the states of UAVObjects
11 *****************************************************************************/
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 3 of the License, or
16 * (at your option) any later version.
18 * This program is distributed in the hope that it will be useful, but
19 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 * You should have received a copy of the GNU General Public License along
24 * with this program; if not, write to the Free Software Foundation, Inc.,
25 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
28 #include "scopegadgetwidget.h"
29 #include "utils/stylehelper.h"
31 #include "extensionsystem/pluginmanager.h"
32 #include "uavobjectmanager.h"
33 #include "uavobject.h"
34 #include "coreplugin/icore.h"
35 #include "coreplugin/connectionmanager.h"
36 #include <coreplugin/icore.h>
38 #include "qwt/src/qwt_plot_curve.h"
39 #include "qwt/src/qwt_plot_grid.h"
46 #include <QStringList>
48 #include <QVBoxLayout>
49 #include <QPushButton>
50 #include <QMutexLocker>
51 #include <QWheelEvent>
55 #include <QApplication>
57 #include <qwt/src/qwt_legend_label.h>
58 #include <qwt/src/qwt_picker_machine.h>
59 #include <qwt/src/qwt_plot_canvas.h>
60 #include <qwt/src/qwt_plot_layout.h>
62 ScopeGadgetWidget::ScopeGadgetWidget(QWidget
*parent
) : QwtPlot(parent
),
63 m_csvLoggingStarted(false), m_csvLoggingEnabled(false),
64 m_csvLoggingHeaderSaved(false), m_csvLoggingDataSaved(false),
65 m_csvLoggingNameSet(false), m_csvLoggingDataValid(false),
66 m_csvLoggingDataUpdated(false), m_csvLoggingConnected(false),
67 m_csvLoggingNewFileOnConnect(false),
68 m_csvLoggingStartTime(QDateTime::currentDateTime()),
69 m_csvLoggingPath("./csvlogging/"),
70 m_plotLegend(NULL
), m_picker(NULL
)
72 setMouseTracking(true);
74 QwtPlotCanvas
*plotCanvas
= dynamic_cast<QwtPlotCanvas
*>(canvas());
76 plotCanvas
->setFrameStyle(QFrame::StyledPanel
| QFrame::Sunken
);
77 plotCanvas
->setBorderRadius(8);
80 axisWidget(QwtPlot::yLeft
)->setMargin(2);
81 axisWidget(QwtPlot::xBottom
)->setMargin(2);
83 m_picker
= new QwtPlotPicker(QwtPlot::xBottom
,
85 QwtPlotPicker::HLineRubberBand
,
86 QwtPicker::ActiveOnly
,
88 m_picker
->setStateMachine(new QwtPickerDragPointMachine());
89 m_picker
->setRubberBandPen(QColor(Qt::darkMagenta
));
90 m_picker
->setTrackerPen(QColor(Qt::green
));
92 // Setup the timer that replots data
93 replotTimer
= new QTimer(this);
94 connect(replotTimer
, SIGNAL(timeout()), this, SLOT(replotNewData()));
96 // Listen to telemetry connection/disconnection events, no point in
97 // running the scopes if we are not connected and not replaying logs.
98 // Also listen to disconnect actions from the user
99 Core::ConnectionManager
*cm
= Core::ICore::instance()->connectionManager();
100 connect(cm
, SIGNAL(deviceAboutToDisconnect()), this, SLOT(stopPlotting()));
101 connect(cm
, SIGNAL(deviceConnected(QIODevice
*)), this, SLOT(startPlotting()));
103 // Listen to autopilot connection events
104 connect(cm
, SIGNAL(deviceAboutToDisconnect()), this, SLOT(csvLoggingDisconnect()));
105 connect(cm
, SIGNAL(deviceConnected(QIODevice
*)), this, SLOT(csvLoggingConnect()));
107 setContextMenuPolicy(Qt::CustomContextMenu
);
108 connect(this, SIGNAL(customContextMenuRequested(const QPoint
&)), this, SLOT(popUpMenu(const QPoint
&)));
111 ScopeGadgetWidget::~ScopeGadgetWidget()
124 // Get the object to de-monitor
125 ExtensionSystem::PluginManager
*pm
= ExtensionSystem::PluginManager::instance();
126 UAVObjectManager
*objManager
= pm
->getObject
<UAVObjectManager
>();
127 foreach(QString uavObjName
, m_connectedUAVObjects
) {
128 UAVDataObject
*obj
= dynamic_cast<UAVDataObject
*>(objManager
->getObject(uavObjName
));
130 disconnect(obj
, SIGNAL(objectUpdated(UAVObject
*)), this, SLOT(uavObjectReceived(UAVObject
*)));
136 void ScopeGadgetWidget::mousePressEvent(QMouseEvent
*e
)
138 QwtPlot::mousePressEvent(e
);
141 void ScopeGadgetWidget::mouseReleaseEvent(QMouseEvent
*e
)
143 QwtPlot::mouseReleaseEvent(e
);
146 void ScopeGadgetWidget::mouseDoubleClickEvent(QMouseEvent
*e
)
148 // On double-click, toggle legend
157 // On double-click, reset plot zoom
158 setAxisAutoScale(QwtPlot::yLeft
, true);
162 QwtPlot::mouseDoubleClickEvent(e
);
165 void ScopeGadgetWidget::mouseMoveEvent(QMouseEvent
*e
)
167 QwtPlot::mouseMoveEvent(e
);
170 void ScopeGadgetWidget::wheelEvent(QWheelEvent
*e
)
172 // Change zoom on scroll wheel event
173 QwtInterval yInterval
= axisInterval(QwtPlot::yLeft
);
175 if (yInterval
.minValue() != yInterval
.maxValue()) { // Make sure that the two values are never the same. Sometimes axisInterval returns (0,0)
176 // Determine what y value to zoom about. NOTE, this approach has a bug that the in that
177 // the value returned by Qt includes the legend, whereas the value transformed by Qwt
178 // does *not*. Thus, when zooming with a legend, there will always be a small bias error.
179 // In practice, this seems not to be a UI problem.
180 QPoint mouse_pos
= e
->pos(); // Get the mouse coordinate in the frame
181 double zoomLine
= invTransform(QwtPlot::yLeft
, mouse_pos
.y()); // Transform the y mouse coordinate into a frame value.
183 double zoomScale
= 1.1; // THIS IS AN ARBITRARY CONSTANT, AND PERHAPS SHOULD BE IN A DEFINE INSTEAD OF BURIED HERE
185 m_mutex
.lock(); // DOES THIS mutex.lock NEED TO BE HERE? I DON'T KNOW, I JUST COPIED IT FROM THE ABOVE CODE
187 if (e
->delta() < 0) {
188 setAxisScale(QwtPlot::yLeft
,
189 (yInterval
.minValue() - zoomLine
) * zoomScale
+ zoomLine
,
190 (yInterval
.maxValue() - zoomLine
) * zoomScale
+ zoomLine
);
192 setAxisScale(QwtPlot::yLeft
,
193 (yInterval
.minValue() - zoomLine
) / zoomScale
+ zoomLine
,
194 (yInterval
.maxValue() - zoomLine
) / zoomScale
+ zoomLine
);
198 QwtPlot::wheelEvent(e
);
201 void ScopeGadgetWidget::showEvent(QShowEvent
*e
)
204 QwtPlot::showEvent(e
);
208 * Starts/stops telemetry
210 void ScopeGadgetWidget::startPlotting()
212 if (replotTimer
&& !replotTimer
->isActive()) {
213 foreach(PlotData
* plot
, m_curvesData
.values()) {
214 if (plot
->wantsInitialData()) {
218 replotTimer
->start(m_refreshInterval
);
222 void ScopeGadgetWidget::stopPlotting()
229 void ScopeGadgetWidget::deleteLegend()
232 insertLegend(NULL
, QwtPlot::TopLegend
);
237 void ScopeGadgetWidget::addLegend()
243 // Show a legend at the top
244 m_plotLegend
= new QwtLegend(this);
245 m_plotLegend
->setDefaultItemMode(QwtLegendData::Checkable
);
246 m_plotLegend
->setFrameStyle(QFrame::NoFrame
);
247 m_plotLegend
->setToolTip(tr("Click legend to show/hide scope trace.\nDouble click legend or plot to show/hide legend."));
250 QPalette pal
= m_plotLegend
->palette();
251 pal
.setColor(m_plotLegend
->backgroundRole(), QColor(100, 100, 100));
252 pal
.setColor(QPalette::Text
, QColor(0, 0, 0));
253 m_plotLegend
->setPalette(pal
);
255 insertLegend(m_plotLegend
, QwtPlot::TopLegend
);
257 // Update the checked/unchecked state of the legend items
258 // -> this is necessary when hiding a legend where some plots are
259 // not visible, and then un-hiding it.
260 foreach(QwtPlotItem
* plotItem
, itemList()) {
261 QWidget
*legendWidget
= m_plotLegend
->legendWidget(QwtPlot::itemToInfo(plotItem
));
263 if (legendWidget
&& legendWidget
->inherits("QwtLegendLabel")) {
264 ((QwtLegendLabel
*)legendWidget
)->setChecked(!plotItem
->isVisible());
268 connect(m_plotLegend
, SIGNAL(checked(QVariant
, bool, int)), this, SLOT(showCurve(QVariant
, bool, int)));
271 void ScopeGadgetWidget::preparePlot(PlotType plotType
)
273 m_plotType
= plotType
;
277 setMinimumSize(64, 64);
278 setSizePolicy(QSizePolicy::MinimumExpanding
, QSizePolicy::MinimumExpanding
);
280 setCanvasBackground(QColor(64, 64, 64));
283 QwtPlotGrid
*grid
= new QwtPlotGrid
;
284 grid
->setPen(Qt::darkGray
, 1, Qt::DotLine
);
287 // Only start the timer if we are already connected
288 Core::ConnectionManager
*cm
= Core::ICore::instance()->connectionManager();
289 if (cm
->isConnected() && replotTimer
) {
290 if (!replotTimer
->isActive()) {
291 replotTimer
->start(m_refreshInterval
);
293 replotTimer
->setInterval(m_refreshInterval
);
298 void ScopeGadgetWidget::showCurve(QVariant itemInfo
, bool visible
, int index
)
301 QwtPlotItem
*item
= infoToItem(itemInfo
);
302 item
->setVisible(!visible
);
303 emit
visibilityChanged(item
);
305 QWidget
*legendItem
= legend()->find((WId
)item
);
306 if (legendItem
&& legendItem
->inherits("QwtLegendLabel")) {
307 ((QwtLegendLabel
*)legendItem
)->setChecked(visible
);
316 void ScopeGadgetWidget::setupSequentialPlot()
318 preparePlot(SequentialPlot
);
320 setAxisScaleDraw(QwtPlot::xBottom
, new QwtScaleDraw());
321 setAxisScale(QwtPlot::xBottom
, 0, m_plotDataSize
);
322 setAxisLabelRotation(QwtPlot::xBottom
, 0.0);
323 setAxisLabelAlignment(QwtPlot::xBottom
, Qt::AlignLeft
| Qt::AlignBottom
);
325 // reduce the axis font size
326 QFont
fnt(axisFont(QwtPlot::xBottom
));
328 setAxisFont(QwtPlot::xBottom
, fnt
); // x-axis
329 setAxisFont(QwtPlot::yLeft
, fnt
); // y-axis
332 void ScopeGadgetWidget::setupChronoPlot()
334 preparePlot(ChronoPlot
);
336 setAxisScaleDraw(QwtPlot::xBottom
, new TimeScaleDraw());
337 uint NOW
= QDateTime::currentDateTime().toTime_t();
338 setAxisScale(QwtPlot::xBottom
, NOW
- m_plotDataSize
/ 1000, NOW
);
339 setAxisLabelRotation(QwtPlot::xBottom
, 0.0);
340 setAxisLabelAlignment(QwtPlot::xBottom
, Qt::AlignLeft
| Qt::AlignBottom
);
342 // reduce the axis font size
343 QFont
fnt(axisFont(QwtPlot::xBottom
));
345 setAxisFont(QwtPlot::xBottom
, fnt
); // x-axis
346 setAxisFont(QwtPlot::yLeft
, fnt
); // y-axis
349 void ScopeGadgetWidget::addCurvePlot(QString objectName
, QString fieldPlusSubField
, int scaleFactor
,
350 int meanSamples
, QString mathFunction
, QPen pen
, bool antialiased
)
352 QString fieldName
= fieldPlusSubField
;
356 if (fieldPlusSubField
.contains("-")) {
357 QStringList fieldSubfield
= fieldName
.split("-", QString::SkipEmptyParts
);
358 fieldName
= fieldSubfield
.at(0);
359 elementName
= fieldSubfield
.at(1);
362 // Get the uav object
363 ExtensionSystem::PluginManager
*pm
= ExtensionSystem::PluginManager::instance();
364 UAVObjectManager
*objManager
= pm
->getObject
<UAVObjectManager
>();
365 UAVDataObject
*object
= dynamic_cast<UAVDataObject
*>(objManager
->getObject(objectName
));
367 qDebug() << "Object" << objectName
<< "is missing";
371 UAVObjectField
*field
= object
->getField(fieldName
);
373 qDebug() << "In scope gadget, in fields loaded from GCS config file, field" <<
374 fieldName
<< "of object" << objectName
<< "is missing";
378 if (!elementName
.isEmpty()) {
379 element
= field
->getElementNames().indexOf(QRegExp(elementName
, Qt::CaseSensitive
, QRegExp::FixedString
));
381 qDebug() << "In scope gadget, in fields loaded from GCS config file, field" <<
382 fieldName
<< "of object" << objectName
<< "element name" << elementName
<< "is missing";
389 if (m_plotType
== SequentialPlot
) {
390 plotData
= new SequentialPlotData(object
, field
, element
, scaleFactor
,
391 meanSamples
, mathFunction
, m_plotDataSize
,
393 } else if (m_plotType
== ChronoPlot
) {
394 plotData
= new ChronoPlotData(object
, field
, element
, scaleFactor
,
395 meanSamples
, mathFunction
, m_plotDataSize
,
398 connect(this, SIGNAL(visibilityChanged(QwtPlotItem
*)), plotData
, SLOT(visibilityChanged(QwtPlotItem
*)));
399 plotData
->attach(this);
401 // Keep the curve details for later
402 m_curvesData
.insert(plotData
->plotName(), plotData
);
404 // Link to the new signal data only if this UAVObject has not been connected yet
405 if (!m_connectedUAVObjects
.contains(object
->getName())) {
406 m_connectedUAVObjects
.append(object
->getName());
407 connect(object
, SIGNAL(objectUpdated(UAVObject
*)), this, SLOT(uavObjectReceived(UAVObject
*)));
415 void ScopeGadgetWidget::uavObjectReceived(UAVObject
*obj
)
417 foreach(PlotData
* plotData
, m_curvesData
.values()) {
418 if (plotData
->append(obj
)) {
419 m_csvLoggingDataUpdated
= 1;
425 void ScopeGadgetWidget::replotNewData()
431 QMutexLocker
locker(&m_mutex
);
432 foreach(PlotData
* plotData
, m_curvesData
.values()) {
433 plotData
->removeStaleData();
434 plotData
->updatePlotData();
437 QDateTime NOW
= QDateTime::currentDateTime();
438 double toTime
= NOW
.toTime_t();
439 toTime
+= NOW
.time().msec() / 1000.0;
440 if (m_plotType
== ChronoPlot
) {
441 setAxisScale(QwtPlot::xBottom
, toTime
- m_plotDataSize
, toTime
);
444 csvLoggingInsertData();
449 void ScopeGadgetWidget::clearCurvePlots()
451 foreach(PlotData
* plotData
, m_curvesData
.values()) {
455 m_curvesData
.clear();
458 void ScopeGadgetWidget::saveState(QSettings
*qSettings
)
463 foreach(PlotData
* plotData
, m_curvesData
.values()) {
464 bool plotVisible
= plotData
->isVisible();
467 qSettings
->setValue(QString("plot%1").arg(i
), plotVisible
);
472 qSettings
->setValue("legendVisible", legend() != NULL
);
475 void ScopeGadgetWidget::restoreState(QSettings
*qSettings
)
480 foreach(PlotData
* plotData
, m_curvesData
.values()) {
481 plotData
->setVisible(qSettings
->value(QString("plot%1").arg(i
), true).toBool());
485 bool legendVisible
= qSettings
->value("legendVisible", true).toBool();
494 int csvLoggingEnable;
495 int csvLoggingHeaderSaved;
496 int csvLoggingDataSaved;
497 QString csvLoggingPath;
498 QFile csvLoggingFile;
500 int ScopeGadgetWidget::csvLoggingStart()
502 if (!m_csvLoggingStarted
) {
503 if (m_csvLoggingEnabled
) {
504 if ((!m_csvLoggingNewFileOnConnect
) || (m_csvLoggingNewFileOnConnect
&& m_csvLoggingConnected
)) {
505 QDateTime NOW
= QDateTime::currentDateTime();
506 m_csvLoggingStartTime
= NOW
;
507 m_csvLoggingHeaderSaved
= 0;
508 m_csvLoggingDataSaved
= 0;
509 m_csvLoggingBuffer
.clear();
510 QDir
PathCheck(m_csvLoggingPath
);
511 if (!PathCheck
.exists()) {
512 PathCheck
.mkpath("./");
515 if (m_csvLoggingNameSet
) {
516 m_csvLoggingFile
.setFileName(QString("%1/%2_%3_%4.csv").arg(m_csvLoggingPath
).arg(m_csvLoggingName
).arg(NOW
.toString("yyyy-MM-dd")).arg(NOW
.toString("hh-mm-ss")));
518 m_csvLoggingFile
.setFileName(QString("%1/Log_%2_%3.csv").arg(m_csvLoggingPath
).arg(NOW
.toString("yyyy-MM-dd")).arg(NOW
.toString("hh-mm-ss")));
520 QDir
FileCheck(m_csvLoggingFile
.fileName());
521 if (FileCheck
.exists()) {
522 m_csvLoggingFile
.setFileName("");
524 m_csvLoggingStarted
= 1;
525 csvLoggingInsertHeader();
534 int ScopeGadgetWidget::csvLoggingStop()
536 m_csvLoggingStarted
= 0;
541 int ScopeGadgetWidget::csvLoggingInsertHeader()
543 if (!m_csvLoggingStarted
) {
546 if (m_csvLoggingHeaderSaved
) {
549 if (m_csvLoggingDataSaved
) {
553 m_csvLoggingHeaderSaved
= 1;
554 if (m_csvLoggingFile
.open(QIODevice::WriteOnly
| QIODevice::Append
) == false) {
555 qDebug() << "Unable to open " << m_csvLoggingFile
.fileName() << " for csv logging Header";
557 QTextStream
ts(&m_csvLoggingFile
);
558 ts
<< "date" << ", " << "Time" << ", " << "Sec since start" << ", " << "Connected" << ", " << "Data changed";
560 foreach(PlotData
* plotData2
, m_curvesData
.values()) {
562 ts
<< plotData2
->objectName();
563 ts
<< "." << plotData2
->field()->getName();
564 if (!plotData2
->elementName().isEmpty()) {
565 ts
<< "." << plotData2
->elementName();
569 m_csvLoggingFile
.close();
574 int ScopeGadgetWidget::csvLoggingAddData()
576 if (!m_csvLoggingStarted
) {
579 m_csvLoggingDataValid
= false;
580 QDateTime NOW
= QDateTime::currentDateTime();
583 QTextStream
ss(&tempString
);
584 ss
<< NOW
.toString("yyyy-MM-dd") << ", " << NOW
.toString("hh:mm:ss.z") << ", ";
586 #if QT_VERSION >= 0x040700
587 ss
<< (NOW
.toMSecsSinceEpoch() - m_csvLoggingStartTime
.toMSecsSinceEpoch()) / 1000.00;
589 ss
<< (NOW
.toTime_t() - m_csvLoggingStartTime
.toTime_t());
591 ss
<< ", " << m_csvLoggingConnected
<< ", " << m_csvLoggingDataUpdated
;
592 m_csvLoggingDataUpdated
= false;
594 foreach(PlotData
* plotData2
, m_curvesData
.values()) {
596 if (plotData2
->hasData()) {
597 ss
<< plotData2
->lastDataAsString();
598 m_csvLoggingDataValid
= true;
602 if (m_csvLoggingDataValid
) {
603 QTextStream
ts(&m_csvLoggingBuffer
);
610 int ScopeGadgetWidget::csvLoggingInsertData()
612 if (!m_csvLoggingStarted
) {
615 m_csvLoggingDataSaved
= 1;
617 if (m_csvLoggingFile
.open(QIODevice::WriteOnly
| QIODevice::Append
) == false) {
618 qDebug() << "Unable to open " << m_csvLoggingFile
.fileName() << " for csv logging Data";
620 QTextStream
ts(&m_csvLoggingFile
);
621 ts
<< m_csvLoggingBuffer
;
622 m_csvLoggingFile
.close();
624 m_csvLoggingBuffer
.clear();
629 void ScopeGadgetWidget::csvLoggingSetName(QString newName
)
631 m_csvLoggingName
= newName
;
632 m_csvLoggingNameSet
= 1;
635 void ScopeGadgetWidget::csvLoggingConnect()
637 m_csvLoggingConnected
= 1;
638 if (m_csvLoggingNewFileOnConnect
) {
643 void ScopeGadgetWidget::csvLoggingDisconnect()
645 m_csvLoggingHeaderSaved
= 0;
646 m_csvLoggingConnected
= 0;
647 if (m_csvLoggingNewFileOnConnect
) {
652 void ScopeGadgetWidget::popUpMenu(const QPoint
&mousePosition
)
654 Q_UNUSED(mousePosition
);
657 QAction
*action
= menu
.addAction(tr("Clear"));
658 connect(action
, &QAction::triggered
, this, &ScopeGadgetWidget::clearPlot
);
659 action
= menu
.addAction(tr("Copy to Clipboard"));
660 connect(action
, &QAction::triggered
, this, &ScopeGadgetWidget::copyToClipboardAsImage
);
662 action
= menu
.addAction(tr("Options..."));
663 connect(action
, &QAction::triggered
, this, &ScopeGadgetWidget::showOptionDialog
);
664 menu
.exec(QCursor::pos());
667 void ScopeGadgetWidget::clearPlot()
670 foreach(PlotData
* plot
, m_curvesData
.values()) {
677 void ScopeGadgetWidget::copyToClipboardAsImage()
679 QPixmap pixmap
= QWidget::grab();
681 if (pixmap
.isNull()) {
682 qDebug("Failed to capture the plot");
685 QClipboard
*clipboard
= QApplication::clipboard();
686 clipboard
->setPixmap(pixmap
);
689 void ScopeGadgetWidget::showOptionDialog()
691 Core::ICore::instance()->showOptionsDialog("ScopeGadget", objectName());