/*
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 "utils.h"
#include "version.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
setupAboutDialog();
// init UI signals
// menu signals
QObject::connect(ui->actionHelpAbout, &QAction::triggered,
&aboutDialog, &QWidget::show);
// port tab signals
QObject::connect(ui->pbReloadPorts, &QPushButton::clicked,
this, &MainWindow::loadPortList);
QObject::connect(ui->pbOpenPort, &QPushButton::clicked,
this, &MainWindow::togglePort);
QObject::connect(this, &MainWindow::portToggled,
this, &MainWindow::onPortToggled);
QObject::connect(ui->cbPortList,
SELECT::OVERLOAD_OF(&QComboBox::activated),
this, &MainWindow::selectPort);
QObject::connect(ui->cbBaudRate,
SELECT::OVERLOAD_OF(&QComboBox::activated),
this, &MainWindow::selectBaudRate);
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()));
// setup number of channels spinbox
QObject::connect(ui->spNumOfChannels,
SELECT::OVERLOAD_OF(&QSpinBox::valueChanged),
this, &MainWindow::onNumOfChannelsChanged);
// setup number format buttons
numberFormatButtons.addButton(ui->rbUint8, NumberFormat_uint8);
numberFormatButtons.addButton(ui->rbUint16, NumberFormat_uint16);
numberFormatButtons.addButton(ui->rbUint32, NumberFormat_uint32);
numberFormatButtons.addButton(ui->rbInt8, NumberFormat_int8);
numberFormatButtons.addButton(ui->rbInt16, NumberFormat_int16);
numberFormatButtons.addButton(ui->rbInt32, NumberFormat_int32);
QObject::connect(&numberFormatButtons, SIGNAL(buttonToggled(int, bool)),
this, SLOT(onNumberFormatButtonToggled(int, bool)));
// setup parity selection buttons
parityButtons.addButton(ui->rbNoParity, (int) QSerialPort::NoParity);
parityButtons.addButton(ui->rbEvenParity, (int) QSerialPort::EvenParity);
parityButtons.addButton(ui->rbOddParity, (int) QSerialPort::OddParity);
QObject::connect(&parityButtons,
SELECT::OVERLOAD_OF(&QButtonGroup::buttonClicked),
this, &MainWindow::selectParity);
// setup data bits selection buttons
dataBitsButtons.addButton(ui->rb8Bits, (int) QSerialPort::Data8);
dataBitsButtons.addButton(ui->rb7Bits, (int) QSerialPort::Data7);
dataBitsButtons.addButton(ui->rb6Bits, (int) QSerialPort::Data6);
dataBitsButtons.addButton(ui->rb5Bits, (int) QSerialPort::Data5);
QObject::connect(&dataBitsButtons,
SELECT::OVERLOAD_OF(&QButtonGroup::buttonClicked),
this, &MainWindow::selectDataBits);
// setup stop bits selection buttons
stopBitsButtons.addButton(ui->rb1StopBit, (int) QSerialPort::OneStop);
stopBitsButtons.addButton(ui->rb2StopBit, (int) QSerialPort::TwoStop);
QObject::connect(&stopBitsButtons,
SELECT::OVERLOAD_OF(&QButtonGroup::buttonClicked),
this, &MainWindow::selectStopBits);
// setup flow control selection buttons
flowControlButtons.addButton(ui->rbNoFlowControl,
(int) QSerialPort::NoFlowControl);
flowControlButtons.addButton(ui->rbHardwareControl,
(int) QSerialPort::HardwareControl);
flowControlButtons.addButton(ui->rbSoftwareControl,
(int) QSerialPort::SoftwareControl);
QObject::connect(&flowControlButtons,
SELECT::OVERLOAD_OF(&QButtonGroup::buttonClicked),
this, &MainWindow::selectFlowControl);
// init port signals
QObject::connect(&(this->serialPort), SIGNAL(error(QSerialPort::SerialPortError)),
this, SLOT(onPortError(QSerialPort::SerialPortError)));
QObject::connect(&(this->serialPort), &QSerialPort::readyRead,
this, &MainWindow::onDataReady);
// init skip byte button
skipByteRequested = false;
QObject::connect(ui->pbSkipByte, &QPushButton::clicked,
this, &MainWindow::skipByte);
loadPortList();
loadBaudRateList();
ui->cbBaudRate->setCurrentIndex(ui->cbBaudRate->findText("9600"));
// 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 arrays and plot
numOfSamples = ui->spNumOfSamples->value();
numOfChannels = 1;
dataX.resize(numOfSamples);
for (int i = 0; i < dataX.size(); i++)
{
dataX[i] = i;
}
// init channel data and curve list
for (int i = 0; i < numOfChannels; i++)
{
channelsData.append(DataArray(numOfSamples, 0.0));
curves.append(new QwtPlotCurve());
curves[i]->setSamples(dataX, channelsData[i]);
curves[i]->setPen(makeColor(i));
curves[i]->attach(ui->plot);
}
// init number format
if (numberFormatButtons.checkedId() >= 0)
{
selectNumberFormat((NumberFormat) numberFormatButtons.checkedId());
}
else
{
selectNumberFormat(NumberFormat_uint8);
}
// Init demo mode
demoCount = 0;
demoTimer.setInterval(100);
QObject::connect(&demoTimer, &QTimer::timeout,
this, &MainWindow::demoTimerTimeout);
QObject::connect(ui->actionDemoMode, &QAction::toggled,
this, &MainWindow::enableDemo);
}
MainWindow::~MainWindow()
{
for (auto curve : curves)
{
delete curve;
}
if (serialPort.isOpen())
{
serialPort.close();
}
delete ui;
}
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);
uiAboutDialog.lbAbout->setText(aboutText);
}
void MainWindow::loadPortList()
{
QString currentSelection = ui->cbPortList->currentText();
ui->cbPortList->clear();
for (auto port : QSerialPortInfo::availablePorts())
{
ui->cbPortList->addItem(port.portName());
}
// find current selection in the new list, maybe it doesn't exist anymore?
int currentSelectionIndex = ui->cbPortList->findText(currentSelection);
if (currentSelectionIndex >= 0)
{
ui->cbPortList->setCurrentIndex(currentSelectionIndex);
}
else // our port doesn't exist anymore, close port if it's open
{
if (serialPort.isOpen()) togglePort();
}
}
void MainWindow::loadBaudRateList()
{
ui->cbBaudRate->clear();
for (auto baudRate : QSerialPortInfo::standardBaudRates())
{
ui->cbBaudRate->addItem(QString::number(baudRate));
}
}
void MainWindow::togglePort()
{
if (serialPort.isOpen())
{
serialPort.close();
qDebug() << "Port closed, " << serialPort.portName();
emit portToggled(false);
}
else
{
serialPort.setPortName(ui->cbPortList->currentText());
// open port
if (serialPort.open(QIODevice::ReadWrite))
{
qDebug() << "Port opened, " << serialPort.portName();
emit portToggled(true);
// set baud rate
if (!serialPort.setBaudRate(ui->cbBaudRate->currentText().toInt()))
{
qDebug() << "Set baud rate failed during port opening: "
<< serialPort.error();
}
}
else
{
qDebug() << "Port open error: " << serialPort.error();
}
}
}
void MainWindow::selectPort(QString portName)
{
// has selection actually changed
if (portName != serialPort.portName())
{
// if another port is already open, close it by toggling
if (serialPort.isOpen())
{
togglePort();
// open new selection by toggling
togglePort();
}
}
}
void MainWindow::selectBaudRate(QString baudRate)
{
if (serialPort.isOpen())
{
if (!serialPort.setBaudRate(baudRate.toInt()))
{
qDebug() << "Set baud rate failed during select: "
<< serialPort.error();
}
else
{
qDebug() << "Baud rate changed: " << serialPort.baudRate();
}
}
}
void MainWindow::selectParity(int parity)
{
if (serialPort.isOpen())
{
if(!serialPort.setParity((QSerialPort::Parity) parity))
{
qDebug() << "Set parity failed: " << serialPort.error();
}
}
}
void MainWindow::selectDataBits(int dataBits)
{
if (serialPort.isOpen())
{
if(!serialPort.setDataBits((QSerialPort::DataBits) dataBits))
{
qDebug() << "Set data bits failed: " << serialPort.error();
}
}
}
void MainWindow::selectStopBits(int stopBits)
{
if (serialPort.isOpen())
{
if(!serialPort.setStopBits((QSerialPort::StopBits) stopBits))
{
qDebug() << "Set stop bits failed: " << serialPort.error();
}
}
}
void MainWindow::selectFlowControl(int flowControl)
{
if (serialPort.isOpen())
{
if(!serialPort.setFlowControl((QSerialPort::FlowControl) flowControl))
{
qDebug() << "Set flow control failed: " << serialPort.error();
}
}
}
void MainWindow::onPortToggled(bool open)
{
ui->pbOpenPort->setChecked(open);
// make sure demo mode is disabled
if (open && isDemoRunning()) enableDemo(false);
ui->actionDemoMode->setEnabled(!open);
}
void MainWindow::onDataReady()
{
if (!ui->actionPause->isChecked())
{
// a package is a set of channel data like {CHAN0_SAMPLE, CHAN1_SAMPLE...}
int packageSize = sampleSize * numOfChannels;
int bytesAvailable = serialPort.bytesAvailable();
if (bytesAvailable > 0 && skipByteRequested)
{
serialPort.read(1);
skipByteRequested = false;
bytesAvailable--;
}
if (bytesAvailable < packageSize)
{
return;
}
else
{
int numOfPackagesToRead =
(bytesAvailable - (bytesAvailable % packageSize)) / packageSize;
QVector channelSamples(numOfChannels);
for (int ci = 0; ci < numOfChannels; ci++)
{
channelSamples[ci].resize(numOfPackagesToRead);
}
int i = 0;
while(i < numOfPackagesToRead)
{
for (int ci = 0; ci < numOfChannels; ci++)
{
channelSamples[ci].replace(i, (this->*readSample)());
}
i++;
}
for (int ci = 0; ci < numOfChannels; ci++)
{
addChannelData(ci, channelSamples[ci]);
}
}
}
else
{
serialPort.clear(QSerialPort::Input);
}
}
void MainWindow::onPortError(QSerialPort::SerialPortError error)
{
switch(error)
{
case QSerialPort::NoError :
break;
case QSerialPort::ResourceError :
qDebug() << "Port error: resource unavaliable; most likely device removed.";
if (serialPort.isOpen())
{
qDebug() << "Closing port on resource error: " << serialPort.portName();
togglePort();
}
loadPortList();
break;
default:
qDebug() << "Unhandled port error: " << error;
break;
}
}
void MainWindow::skipByte()
{
skipByteRequested = true;
}
void MainWindow::addChannelData(unsigned int channel, DataArray data)
{
DataArray* channelDataArray = &(channelsData[channel]);
int offset = numOfSamples - data.size();
if (offset < 0)
{
for (int i = 0; i < numOfSamples; i++)
{
(*channelDataArray)[i] = data[i - offset];
}
}
else if (offset == 0)
{
(*channelDataArray) = data;
}
else
{
// shift old samples
int shift = data.size();
for (int i = 0; i < offset; i++)
{
(*channelDataArray)[i] = (*channelDataArray)[i + shift];
}
// place new samples
for (int i = 0; i < data.size(); i++)
{
(*channelDataArray)[offset + i] = data[i];
}
}
// update plot
curves[channel]->setSamples(dataX, (*channelDataArray));
ui->plot->replot(); // TODO: replot after all channel data updated
}
void MainWindow::clearPlot()
{
for (int ci = 0; ci < numOfChannels; ci++)
{
channelsData[ci].fill(0.0);
curves[ci]->setSamples(dataX, channelsData[ci]);
}
// update plot
ui->plot->replot();
}
void MainWindow::onNumOfSamplesChanged(int value)
{
unsigned int oldNum = this->numOfSamples;
numOfSamples = value;
// resize data arrays
if (numOfSamples < oldNum)
{
dataX.resize(numOfSamples);
for (int ci = 0; ci < numOfChannels; ci++)
{
channelsData[ci].remove(0, oldNum - numOfSamples);
}
}
else if(numOfSamples > oldNum)
{
dataX.resize(numOfSamples);
for (unsigned int i = oldNum; i < numOfSamples; i++)
{
dataX[i] = i;
for (int ci = 0; ci < numOfChannels; ci++)
{
channelsData[ci].prepend(0);
}
}
}
}
void MainWindow::onNumOfChannelsChanged(int value)
{
unsigned int oldNum = this->numOfChannels;
this->numOfChannels = value;
if (numOfChannels > oldNum)
{
// add new channels
for (int i = 0; i < numOfChannels - oldNum; i++)
{
channelsData.append(DataArray(numOfSamples, 0.0));
curves.append(new QwtPlotCurve());
curves.last()->setSamples(dataX, channelsData.last());
curves.last()->setPen(makeColor(curves.length()-1));
curves.last()->attach(ui->plot);
}
}
else if(numOfChannels < oldNum)
{
// remove channels
for (int i = 0; i < oldNum - numOfChannels; i++)
{
channelsData.removeLast();
auto curve = curves.takeLast();
curve->detach();
delete curve;
}
}
}
void MainWindow::onAutoScaleChecked(bool checked)
{
if (checked)
{
ui->plot->setAxisAutoScale(QwtPlot::yLeft);
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->setAxisScale(QwtPlot::yLeft, ui->spYmin->value(),
ui->spYmax->value());
}
}
void MainWindow::onYScaleChanged()
{
ui->plot->setAxisScale(QwtPlot::yLeft, ui->spYmin->value(),
ui->spYmax->value());
}
void MainWindow::onNumberFormatButtonToggled(int numberFormatId, bool checked)
{
if (checked) selectNumberFormat((NumberFormat) numberFormatId);
}
void MainWindow::selectNumberFormat(NumberFormat numberFormatId)
{
numberFormat = numberFormatId;
switch(numberFormat)
{
case NumberFormat_uint8:
sampleSize = 1;
readSample = &MainWindow::readSampleAs;
break;
case NumberFormat_int8:
sampleSize = 1;
readSample = &MainWindow::readSampleAs;
break;
case NumberFormat_uint16:
sampleSize = 2;
readSample = &MainWindow::readSampleAs;
break;
case NumberFormat_int16:
sampleSize = 2;
readSample = &MainWindow::readSampleAs;
break;
case NumberFormat_uint32:
sampleSize = 4;
readSample = &MainWindow::readSampleAs;
break;
case NumberFormat_int32:
sampleSize = 4;
readSample = &MainWindow::readSampleAs;
break;
}
}
template double MainWindow::readSampleAs()
{
T data;
this->serialPort.read((char*) &data, sizeof(data));
return double(data);
}
bool MainWindow::isDemoRunning()
{
return ui->actionDemoMode->isChecked();
}
void MainWindow::demoTimerTimeout()
{
demoCount++;
if (demoCount > 100) demoCount = 0;
for (int ci = 0; ci < numOfChannels; ci++)
{
DataArray data(1);
data.replace(0, (ci + 1)*demoCount);
addChannelData(ci, data);
}
}
void MainWindow::enableDemo(bool enabled)
{
if (enabled)
{
if (!serialPort.isOpen())
{
demoTimer.start();
ui->actionDemoMode->setChecked(true);
}
else
{
ui->actionDemoMode->setChecked(false);
}
}
else
{
demoTimer.stop();
ui->actionDemoMode->setChecked(false);
}
}
/*
Below crude drawing demostrates how color selection occurs for
given channel index
0° <--Hue Value--> 360°
|* . o . + . o . * . o . + . o . * . o . + . o . * . o . + . o . |
* -> 0-3
+ -> 4-7
o -> 8-15
. -> 16-31
*/
QColor MainWindow::makeColor(unsigned int channelIndex)
{
auto i = channelIndex;
if (i < 4)
{
return QColor::fromHsv(360*i/4, 255, 255);
}
else
{
double p = floor(log2(i));
double n = pow(2, p);
i = i - n;
return QColor::fromHsv(360*i/n + 360/pow(2,p+1), 255, 255);
}
}