/*
Copyright © 2015 Hasan Yavuz Özderya
This file is part of serialplot.
serialplot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
serialplot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with serialplot. If not, see .
*/
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "utils.h"
#include "version.h"
#if defined(Q_OS_WIN) && defined(QT_STATIC)
#include
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin)
#endif
struct Range
{
double rmin;
double rmax;
};
Q_DECLARE_METATYPE(Range);
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
aboutDialog(this),
portControl(&serialPort),
commandPanel(&serialPort),
dataFormatPanel(&serialPort, &channelBuffers),
snapshotMan(this, &channelBuffers)
{
ui->setupUi(this);
ui->tabWidget->insertTab(0, &portControl, "Port");
ui->tabWidget->insertTab(1, &dataFormatPanel, "Data Format");
ui->tabWidget->insertTab(3, &commandPanel, "Commands");
ui->tabWidget->setCurrentIndex(0);
addToolBar(portControl.toolBar());
ui->mainToolBar->addAction(snapshotMan.takeSnapshotAction());
ui->menuBar->insertMenu(ui->menuHelp->menuAction(), snapshotMan.menu());
setupAboutDialog();
// init view menu
for (auto a : ui->plot->menuActions())
{
ui->menuView->addAction(a);
}
// init UI signals
// menu signals
QObject::connect(ui->actionHelpAbout, &QAction::triggered,
&aboutDialog, &QWidget::show);
QObject::connect(ui->actionExportCsv, &QAction::triggered,
this, &MainWindow::onExportCsv);
ui->actionQuit->setShortcutContext(Qt::ApplicationShortcut);
QObject::connect(ui->actionQuit, &QAction::triggered,
this, &MainWindow::close);
// port control signals
QObject::connect(&portControl, &PortControl::portToggled,
this, &MainWindow::onPortToggled);
QObject::connect(&portControl, &PortControl::skipByteRequested,
&dataFormatPanel, &DataFormatPanel::requestSkipByte);
QObject::connect(ui->spNumOfSamples, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged),
this, &MainWindow::onNumOfSamplesChanged);
QObject::connect(ui->cbAutoScale, &QCheckBox::toggled,
this, &MainWindow::onAutoScaleChecked);
QObject::connect(ui->spYmin, SIGNAL(valueChanged(double)),
this, SLOT(onYScaleChanged()));
QObject::connect(ui->spYmax, SIGNAL(valueChanged(double)),
this, SLOT(onYScaleChanged()));
QObject::connect(ui->actionClear, SIGNAL(triggered(bool)),
this, SLOT(clearPlot()));
QObject::connect(snapshotMan.takeSnapshotAction(), &QAction::triggered,
ui->plot, &Plot::flashSnapshotOverlay);
// init port signals
QObject::connect(&(this->serialPort), SIGNAL(error(QSerialPort::SerialPortError)),
this, SLOT(onPortError(QSerialPort::SerialPortError)));
// set limits for axis limit boxes
ui->spYmin->setRange((-1) * std::numeric_limits::max(),
std::numeric_limits::max());
ui->spYmax->setRange((-1) * std::numeric_limits::max(),
std::numeric_limits::max());
// init data format and reader
QObject::connect(&dataFormatPanel, &DataFormatPanel::dataAdded,
ui->plot, &QwtPlot::replot);
QObject::connect(ui->actionPause, &QAction::triggered,
&dataFormatPanel, &DataFormatPanel::pause);
// init data arrays and plot
numOfSamples = ui->spNumOfSamples->value();
unsigned numOfChannels = dataFormatPanel.numOfChannels();
QObject::connect(&dataFormatPanel,
&DataFormatPanel::numOfChannelsChanged,
this,
&MainWindow::onNumOfChannelsChanged);
// init channel data and curve list
for (unsigned int i = 0; i < numOfChannels; i++)
{
channelBuffers.append(new FrameBuffer(numOfSamples));
curves.append(new QwtPlotCurve());
curves[i]->setSamples(channelBuffers[i]);
curves[i]->setPen(Plot::makeColor(i));
curves[i]->attach(ui->plot);
}
// init auto scale
ui->plot->setAxis(ui->cbAutoScale->isChecked(),
ui->spYmin->value(), ui->spYmax->value());
// init scale range preset list
for (int nbits = 8; nbits <= 24; nbits++) // signed binary formats
{
int rmax = pow(2, nbits-1)-1;
int rmin = -rmax-1;
Range r = {double(rmin), double(rmax)};
ui->cbRangePresets->addItem(
QString().sprintf("Signed %d bits %d to +%d", nbits, rmin, rmax),
QVariant::fromValue(r));
}
for (int nbits = 8; nbits <= 24; nbits++) // unsigned binary formats
{
int rmax = pow(2, nbits)-1;
ui->cbRangePresets->addItem(
QString().sprintf("Unsigned %d bits %d to +%d", nbits, 0, rmax),
QVariant::fromValue(Range{0, double(rmax)}));
}
ui->cbRangePresets->addItem("-1 to +1", QVariant::fromValue(Range{-1, +1}));
ui->cbRangePresets->addItem("0 to +1", QVariant::fromValue(Range{0, +1}));
ui->cbRangePresets->addItem("-100 to +100", QVariant::fromValue(Range{-100, +100}));
ui->cbRangePresets->addItem("0 to +100", QVariant::fromValue(Range{0, +100}));
QObject::connect(ui->cbRangePresets, SIGNAL(activated(int)),
this, SLOT(onRangeSelected()));
// Init sps (sample per second) counter
spsLabel.setText("0sps");
spsLabel.setToolTip("samples per second (total of all channels)");
ui->statusBar->addPermanentWidget(&spsLabel);
QObject::connect(&dataFormatPanel,
&DataFormatPanel::samplesPerSecondChanged,
this, &MainWindow::onSpsChanged);
// init demo
QObject::connect(ui->actionDemoMode, &QAction::toggled,
this, &MainWindow::enableDemo);
{ // init demo indicator
QwtText demoText(" DEMO RUNNING "); // looks better with spaces
demoText.setColor(QColor("white"));
demoText.setBackgroundBrush(Qt::darkRed);
demoText.setBorderRadius(4);
demoText.setRenderFlags(Qt::AlignLeft | Qt::AlignTop);
demoIndicator.setText(demoText);
demoIndicator.hide();
demoIndicator.attach(ui->plot);
}
}
MainWindow::~MainWindow()
{
for (auto curve : curves)
{
// also deletes respective FrameBuffer
delete curve;
}
if (serialPort.isOpen())
{
serialPort.close();
}
delete ui;
ui = NULL; // we check if ui is deleted in messageHandler
}
void MainWindow::setupAboutDialog()
{
Ui_AboutDialog uiAboutDialog;
uiAboutDialog.setupUi(&aboutDialog);
QObject::connect(uiAboutDialog.pbAboutQt, &QPushButton::clicked,
[](){ QApplication::aboutQt();});
QString aboutText = uiAboutDialog.lbAbout->text();
aboutText.replace("$VERSION_STRING$", VERSION_STRING);
aboutText.replace("$VERSION_REVISION$", VERSION_REVISION);
uiAboutDialog.lbAbout->setText(aboutText);
}
void MainWindow::onPortToggled(bool open)
{
// make sure demo mode is disabled
if (open && isDemoRunning()) enableDemo(false);
ui->actionDemoMode->setEnabled(!open);
}
void MainWindow::onPortError(QSerialPort::SerialPortError error)
{
switch(error)
{
case QSerialPort::NoError :
break;
case QSerialPort::ResourceError :
qWarning() << "Port error: resource unavaliable; most likely device removed.";
if (serialPort.isOpen())
{
qWarning() << "Closing port on resource error: " << serialPort.portName();
portControl.togglePort();
}
portControl.loadPortList();
break;
case QSerialPort::DeviceNotFoundError:
qCritical() << "Device doesn't exists: " << serialPort.portName();
break;
case QSerialPort::PermissionError:
qCritical() << "Permission denied. Either you don't have \
required privileges or device is already opened by another process.";
break;
case QSerialPort::OpenError:
qWarning() << "Device is already opened!";
break;
case QSerialPort::NotOpenError:
qCritical() << "Device is not open!";
break;
case QSerialPort::ParityError:
qCritical() << "Parity error detected.";
break;
case QSerialPort::FramingError:
qCritical() << "Framing error detected.";
break;
case QSerialPort::BreakConditionError:
qCritical() << "Break condition is detected.";
break;
case QSerialPort::WriteError:
qCritical() << "An error occurred while writing data.";
break;
case QSerialPort::ReadError:
qCritical() << "An error occurred while reading data.";
break;
case QSerialPort::UnsupportedOperationError:
qCritical() << "Operation is not supported.";
break;
case QSerialPort::TimeoutError:
qCritical() << "A timeout error occurred.";
break;
case QSerialPort::UnknownError:
qCritical() << "Unknown error!";
break;
default:
qCritical() << "Unhandled port error: " << error;
break;
}
}
void MainWindow::clearPlot()
{
for (int ci = 0; ci < channelBuffers.size(); ci++)
{
channelBuffers[ci]->clear();
}
ui->plot->replot();
}
void MainWindow::onNumOfSamplesChanged(int value)
{
numOfSamples = value;
for (int ci = 0; ci < channelBuffers.size(); ci++)
{
channelBuffers[ci]->resize(numOfSamples);
}
ui->plot->replot();
}
void MainWindow::onNumOfChannelsChanged(unsigned value)
{
unsigned int oldNum = channelBuffers.size();
unsigned numOfChannels = value;
if (numOfChannels > oldNum)
{
// add new channels
for (unsigned int i = 0; i < numOfChannels - oldNum; i++)
{
channelBuffers.append(new FrameBuffer(numOfSamples));
curves.append(new QwtPlotCurve());
curves.last()->setSamples(channelBuffers.last());
curves.last()->setPen(Plot::makeColor(curves.length()-1));
curves.last()->attach(ui->plot);
}
}
else if(numOfChannels < oldNum)
{
// remove channels
for (unsigned int i = 0; i < oldNum - numOfChannels; i++)
{
// also deletes owned FrameBuffer
delete curves.takeLast();
channelBuffers.removeLast();
}
}
}
void MainWindow::onAutoScaleChecked(bool checked)
{
if (checked)
{
ui->plot->setAxis(true);
ui->lYmin->setEnabled(false);
ui->lYmax->setEnabled(false);
ui->spYmin->setEnabled(false);
ui->spYmax->setEnabled(false);
}
else
{
ui->lYmin->setEnabled(true);
ui->lYmax->setEnabled(true);
ui->spYmin->setEnabled(true);
ui->spYmax->setEnabled(true);
ui->plot->setAxis(false, ui->spYmin->value(), ui->spYmax->value());
}
}
void MainWindow::onYScaleChanged()
{
ui->plot->setAxis(false, ui->spYmin->value(), ui->spYmax->value());
}
void MainWindow::onRangeSelected()
{
Range r = ui->cbRangePresets->currentData().value();
ui->spYmin->setValue(r.rmin);
ui->spYmax->setValue(r.rmax);
ui->cbAutoScale->setChecked(false);
}
void MainWindow::onSpsChanged(unsigned sps)
{
spsLabel.setText(QString::number(sps) + "sps");
}
bool MainWindow::isDemoRunning()
{
return ui->actionDemoMode->isChecked();
}
void MainWindow::enableDemo(bool enabled)
{
if (enabled)
{
if (!serialPort.isOpen())
{
dataFormatPanel.enableDemo(true);
ui->actionDemoMode->setChecked(true);
demoIndicator.show();
ui->plot->replot();
}
else
{
ui->actionDemoMode->setChecked(false);
}
}
else
{
dataFormatPanel.enableDemo(false);
ui->actionDemoMode->setChecked(false);
demoIndicator.hide();
ui->plot->replot();
}
}
void MainWindow::onExportCsv()
{
bool wasPaused = ui->actionPause->isChecked();
ui->actionPause->setChecked(true); // pause plotting
QString fileName = QFileDialog::getSaveFileName(this, tr("Export CSV File"));
if (fileName.isNull()) // user canceled export
{
ui->actionPause->setChecked(wasPaused);
}
else
{
QFile file(fileName);
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream fileStream(&file);
unsigned numOfChannels = channelBuffers.size();
for (unsigned int ci = 0; ci < numOfChannels; ci++)
{
fileStream << "Channel " << ci;
if (ci != numOfChannels-1) fileStream << ",";
}
fileStream << '\n';
for (unsigned int i = 0; i < numOfSamples; i++)
{
for (unsigned int ci = 0; ci < numOfChannels; ci++)
{
fileStream << channelBuffers[ci]->sample(i).y();
if (ci != numOfChannels-1) fileStream << ",";
}
fileStream << '\n';
}
}
else
{
qCritical() << "File open error during export: " << file.error();
}
}
}
void MainWindow::messageHandler(QtMsgType type,
const QMessageLogContext &context,
const QString &msg)
{
QString logString;
switch (type)
{
case QtDebugMsg:
logString = "[Debug] " + msg;
break;
case QtWarningMsg:
logString = "[Warning] " + msg;
break;
case QtCriticalMsg:
logString = "[Error] " + msg;
break;
case QtFatalMsg:
logString = "[Fatal] " + msg;
break;
}
if (ui != NULL) ui->ptLog->appendPlainText(logString);
std::cerr << logString.toStdString() << std::endl;
if (type != QtDebugMsg && ui != NULL)
{
ui->statusBar->showMessage(msg, 5000);
}
}