diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright © 2015-2016 Hasan Yavuz Özderya +# Copyright © 2017 Hasan Yavuz Özderya # # This file is part of serialplot. # @@ -59,6 +59,9 @@ include(BuildQColorWidgets) # includes include_directories("./src" ${QWT_INCLUDE_DIR} ${QCW_INCLUDE_DIR}) +# flags +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QCW_FLAGS}") + # wrap UI and resource files qt5_wrap_ui(UI_FILES src/mainwindow.ui @@ -69,6 +72,7 @@ qt5_wrap_ui(UI_FILES src/commandwidget.ui src/dataformatpanel.ui src/plotcontrolpanel.ui + src/recordpanel.ui src/numberformatbox.ui src/endiannessbox.ui src/binarystreamreadersettings.ui @@ -104,6 +108,8 @@ add_executable(${PROGRAM_NAME} WIN32 src/commandedit.cpp src/dataformatpanel.cpp src/plotcontrolpanel.cpp + src/recordpanel.cpp + src/datarecorder.cpp src/tooltipfilter.cpp src/sneakylineedit.cpp src/channelmanager.cpp diff --git a/cmake/modules/BuildQColorWidgets.cmake b/cmake/modules/BuildQColorWidgets.cmake --- a/cmake/modules/BuildQColorWidgets.cmake +++ b/cmake/modules/BuildQColorWidgets.cmake @@ -23,9 +23,11 @@ ExternalProject_Add(QCW PREFIX qcw GIT_REPOSITORY https://github.com/mbasaglia/Qt-Color-Widgets PATCH_COMMAND patch -t -N -p1 -i ${CMAKE_CURRENT_LIST_DIR}/qt_5_2_moc_creation_namespace_fix.diff + CMAKE_CACHE_ARGS "-DCMAKE_CXX_FLAGS:string=-D QTCOLORWIDGETS_STATICALLY_LINKED" UPDATE_COMMAND "" INSTALL_COMMAND "") ExternalProject_Get_Property(QCW binary_dir source_dir) +set(QCW_FLAGS "-D QTCOLORWIDGETS_STATICALLY_LINKED") set(QCW_LIBRARY ${binary_dir}/libColorWidgets-qt5.a) set(QCW_INCLUDE_DIR ${source_dir}/include) diff --git a/serialplot.pro b/serialplot.pro --- a/serialplot.pro +++ b/serialplot.pro @@ -67,7 +67,8 @@ SOURCES += \ src/demoreader.cpp \ src/framedreader.cpp \ src/plotmanager.cpp \ - src/numberformat.cpp + src/numberformat.cpp \ + src/recordpanel.cpp HEADERS += \ src/mainwindow.h \ @@ -106,7 +107,8 @@ HEADERS += \ src/framedreader.h \ src/plotmanager.h \ src/setting_defines.h \ - src/numberformat.h + src/numberformat.h \ + src/recordpanel.h FORMS += \ src/mainwindow.ui \ @@ -121,7 +123,8 @@ FORMS += \ src/endiannessbox.ui \ src/framedreadersettings.ui \ src/binarystreamreadersettings.ui \ - src/asciireadersettings.ui + src/asciireadersettings.ui \ + src/recordpanel.ui INCLUDEPATH += qmake/ src/ diff --git a/src/abstractreader.cpp b/src/abstractreader.cpp --- a/src/abstractreader.cpp +++ b/src/abstractreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -19,11 +19,14 @@ #include "abstractreader.h" -AbstractReader::AbstractReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : +AbstractReader::AbstractReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : QObject(parent) { _device = device; _channelMan = channelMan; + _recorder = recorder; + recording = false; // initialize sps counter sampleCount = 0; @@ -44,3 +47,13 @@ void AbstractReader::spsTimerTimeout() } sampleCount = 0; } + +void AbstractReader::addData(double* samples, unsigned length) +{ + _channelMan->addData(samples, length); + if (recording) + { + _recorder->addData(samples, length, numOfChannels()); + } + sampleCount += length; +} diff --git a/src/abstractreader.h b/src/abstractreader.h --- a/src/abstractreader.h +++ b/src/abstractreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -26,6 +26,7 @@ #include #include "channelmanager.h" +#include "datarecorder.h" /** * All reader classes must inherit this class. @@ -34,7 +35,10 @@ class AbstractReader : public QObject { Q_OBJECT public: - explicit AbstractReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit AbstractReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent = 0); + + bool recording; /// is recording started /** * Returns a widget to be shown in data format panel when reader @@ -54,11 +58,18 @@ public: /// 'disabled'. virtual void enable(bool enabled = true) = 0; + /** + * @brief Starts sending data to recorder. + * + * @note recorder must have been started! + */ + void startRecording(); + + /// Stops recording. + void stopRecording(); + signals: void numOfChannelsChanged(unsigned); - // TODO: this must be signaled by 'channel man' for better abstraction - void dataAdded(); ///< emitted when data added to channel man. - // TODO: this should be a part of 'channel man' void samplesPerSecondChanged(unsigned); public slots: @@ -72,14 +83,18 @@ public slots: protected: QIODevice* _device; - ChannelManager* _channelMan; - /// Implementing class should simply increase this count as samples are read - unsigned sampleCount; + /// Should be called with read data + void addData(double* samples, unsigned length); private: const int SPS_UPDATE_TIMEOUT = 1; // second + + unsigned sampleCount; unsigned samplesPerSecond; + + ChannelManager* _channelMan; + DataRecorder* _recorder; QTimer spsTimer; private slots: diff --git a/src/asciireader.cpp b/src/asciireader.cpp --- a/src/asciireader.cpp +++ b/src/asciireader.cpp @@ -24,12 +24,12 @@ /// If set to this value number of channels is determined from input #define NUMOFCHANNELS_AUTO (0) -AsciiReader::AsciiReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +AsciiReader::AsciiReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; discardFirstLine = true; - sampleCount = 0; _numOfChannels = _settingsWidget.numOfChannels(); autoNumOfChannels = (_numOfChannels == NUMOFCHANNELS_AUTO); @@ -155,9 +155,7 @@ void AsciiReader::onDataReady() } // commit data - _channelMan->addData(channelSamples, _numOfChannels); - sampleCount += numReadChannels; - emit dataAdded(); + addData(channelSamples, _numOfChannels); delete[] channelSamples; } diff --git a/src/asciireader.h b/src/asciireader.h --- a/src/asciireader.h +++ b/src/asciireader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -30,7 +30,8 @@ class AsciiReader : public AbstractReade Q_OBJECT public: - explicit AsciiReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit AsciiReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject *parent = 0); QWidget* settingsWidget(); unsigned numOfChannels(); void enable(bool enabled = true); diff --git a/src/binarystreamreader.cpp b/src/binarystreamreader.cpp --- a/src/binarystreamreader.cpp +++ b/src/binarystreamreader.cpp @@ -23,13 +23,13 @@ #include "binarystreamreader.h" #include "floatswap.h" -BinaryStreamReader::BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +BinaryStreamReader::BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; skipByteRequested = false; skipSampleRequested = false; - sampleCount = 0; _numOfChannels = _settingsWidget.numOfChannels(); connect(&_settingsWidget, &BinaryStreamReaderSettings::numOfChannelsChanged, @@ -171,9 +171,7 @@ void BinaryStreamReader::onDataReady() } } - _channelMan->addData(channelSamples, numOfPackagesToRead*_numOfChannels); - sampleCount += numOfPackagesToRead*_numOfChannels; - emit dataAdded(); + addData(channelSamples, numOfPackagesToRead*_numOfChannels); delete[] channelSamples; } diff --git a/src/binarystreamreader.h b/src/binarystreamreader.h --- a/src/binarystreamreader.h +++ b/src/binarystreamreader.h @@ -34,7 +34,8 @@ class BinaryStreamReader : public Abstra { Q_OBJECT public: - explicit BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject *parent = 0); QWidget* settingsWidget(); unsigned numOfChannels(); void enable(bool enabled = true); diff --git a/src/channelinfomodel.cpp b/src/channelinfomodel.cpp --- a/src/channelinfomodel.cpp +++ b/src/channelinfomodel.cpp @@ -98,21 +98,31 @@ ChannelInfoModel::ChannelInfo::ChannelIn color = colors[index % NUMOF_COLORS]; } -QString ChannelInfoModel::name(unsigned i) +QString ChannelInfoModel::name(unsigned i) const { return infos[i].name; } -QColor ChannelInfoModel::color(unsigned i) +QColor ChannelInfoModel::color(unsigned i) const { return infos[i].color; } -bool ChannelInfoModel::isVisible(unsigned i) +bool ChannelInfoModel::isVisible(unsigned i) const { return infos[i].visibility; } +QStringList ChannelInfoModel::channelNames() const +{ + QStringList r; + for (unsigned ci = 0; ci < _numOfChannels; ci++) + { + r << name(ci); + } + return r; +} + int ChannelInfoModel::rowCount(const QModelIndex &parent) const { return _numOfChannels; diff --git a/src/channelinfomodel.h b/src/channelinfomodel.h --- a/src/channelinfomodel.h +++ b/src/channelinfomodel.h @@ -41,9 +41,11 @@ public: ChannelInfoModel(const ChannelInfoModel& other); explicit ChannelInfoModel(const QStringList& channelNames); - QString name (unsigned i); - QColor color (unsigned i); - bool isVisible(unsigned i); + QString name (unsigned i) const; + QColor color (unsigned i) const; + bool isVisible(unsigned i) const; + /// Returns a list of channel names + QStringList channelNames() const; // implemented from QAbstractItemModel int rowCount(const QModelIndex &parent = QModelIndex()) const; diff --git a/src/channelmanager.cpp b/src/channelmanager.cpp --- a/src/channelmanager.cpp +++ b/src/channelmanager.cpp @@ -30,6 +30,7 @@ ChannelManager::ChannelManager(unsigned { _numOfChannels = numberOfChannels; _numOfSamples = numberOfSamples; + _paused = false; for (unsigned int i = 0; i < numberOfChannels; i++) { @@ -97,6 +98,11 @@ void ChannelManager::setNumOfSamples(uns emit numOfSamplesChanged(number); } +void ChannelManager::pause(bool paused) +{ + _paused = paused; +} + FrameBuffer* ChannelManager::channelBuffer(unsigned channel) { return channelBuffers[channel]; @@ -165,11 +171,15 @@ void ChannelManager::addData(double* dat { Q_ASSERT(size % _numOfChannels == 0); + if (_paused) return; + int n = size / _numOfChannels; for (unsigned ci = 0; ci < _numOfChannels; ci++) { channelBuffers[ci]->addSamples(&data[ci*n], n); } + + emit dataAdded(); } void ChannelManager::saveSettings(QSettings* settings) diff --git a/src/channelmanager.h b/src/channelmanager.h --- a/src/channelmanager.h +++ b/src/channelmanager.h @@ -39,7 +39,6 @@ public: unsigned numOfChannels(); unsigned numOfSamples(); FrameBuffer* channelBuffer(unsigned channel); - // QStringListModel* channelNames(); QString channelName(unsigned channel); /// Stores channel names into a `QSettings` void saveSettings(QSettings* settings); @@ -54,6 +53,7 @@ signals: void numOfChannelsChanged(unsigned value); void numOfSamplesChanged(unsigned value); void channelNameChanged(unsigned channel, QString name); + void dataAdded(); ///< emitted when data added to channel man. public slots: void setNumOfChannels(unsigned number); @@ -71,9 +71,13 @@ public slots: */ void addData(double* data, unsigned size); + /// When paused `addData` does nothing. + void pause(bool paused); + private: unsigned _numOfChannels; unsigned _numOfSamples; + bool _paused; QList channelBuffers; // QStringListModel _channelNames; ChannelInfoModel _infoModel; diff --git a/src/dataformatpanel.cpp b/src/dataformatpanel.cpp --- a/src/dataformatpanel.cpp +++ b/src/dataformatpanel.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -31,26 +31,27 @@ DataFormatPanel::DataFormatPanel(QSerialPort* port, ChannelManager* channelMan, + DataRecorder* recorder, QWidget *parent) : QWidget(parent), ui(new Ui::DataFormatPanel), - bsReader(port, channelMan, this), - asciiReader(port, channelMan, this), - framedReader(port, channelMan, this), - demoReader(port, channelMan, this) + bsReader(port, channelMan, recorder, this), + asciiReader(port, channelMan, recorder, this), + framedReader(port, channelMan, recorder, this), + demoReader(port, channelMan, recorder, this) { ui->setupUi(this); serialPort = port; _channelMan = channelMan; paused = false; + demoEnabled = false; // initalize default reader currentReader = &bsReader; bsReader.enable(); ui->rbBinary->setChecked(true); ui->horizontalLayout->addWidget(bsReader.settingsWidget(), 1); - connect(&bsReader, SIGNAL(dataAdded()), this, SIGNAL(dataAdded())); connect(&bsReader, SIGNAL(numOfChannelsChanged(unsigned)), this, SIGNAL(numOfChannelsChanged(unsigned))); connect(&bsReader, SIGNAL(samplesPerSecondChanged(unsigned)), @@ -99,8 +100,7 @@ void DataFormatPanel::enableDemo(bool en if (enabled) { demoReader.enable(); - connect(&demoReader, &DemoReader::dataAdded, - this, &DataFormatPanel::dataAdded); + demoReader.recording = currentReader->recording; connect(&demoReader, &DemoReader::samplesPerSecondChanged, this, &DataFormatPanel::samplesPerSecondChanged); } @@ -109,6 +109,19 @@ void DataFormatPanel::enableDemo(bool en demoReader.enable(false); disconnect(&demoReader, 0, this, 0); } + demoEnabled = enabled; +} + +void DataFormatPanel::startRecording() +{ + currentReader->recording = true; + if (demoEnabled) demoReader.recording = true; +} + +void DataFormatPanel::stopRecording() +{ + currentReader->recording = false; + if (demoEnabled) demoReader.recording = false; } void DataFormatPanel::selectReader(AbstractReader* reader) @@ -118,7 +131,6 @@ void DataFormatPanel::selectReader(Abstr // re-connect signals disconnect(currentReader, 0, this, 0); - connect(reader, SIGNAL(dataAdded()), this, SIGNAL(dataAdded())); connect(reader, SIGNAL(numOfChannelsChanged(unsigned)), this, SIGNAL(numOfChannelsChanged(unsigned))); connect(reader, SIGNAL(samplesPerSecondChanged(unsigned)), @@ -136,8 +148,8 @@ void DataFormatPanel::selectReader(Abstr emit numOfChannelsChanged(reader->numOfChannels()); } - // pause reader->pause(paused); + reader->recording = currentReader->recording; currentReader = reader; } diff --git a/src/dataformatpanel.h b/src/dataformatpanel.h --- a/src/dataformatpanel.h +++ b/src/dataformatpanel.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -34,6 +34,7 @@ #include "asciireader.h" #include "demoreader.h" #include "framedreader.h" +#include "datarecorder.h" namespace Ui { class DataFormatPanel; @@ -46,7 +47,8 @@ class DataFormatPanel : public QWidget public: explicit DataFormatPanel(QSerialPort* port, ChannelManager* channelMan, - QWidget *parent = 0); + DataRecorder* recorder, + QWidget* parent = 0); ~DataFormatPanel(); /// Returns currently selected number of channels @@ -60,10 +62,19 @@ public slots: void pause(bool); void enableDemo(bool); // demo shouldn't be enabled when port is open + /** + * @brief Starts sending data to recorder. + * + * @note recorder must have been started! + */ + void startRecording(); + + /// Stops recording. + void stopRecording(); + signals: void numOfChannelsChanged(unsigned); void samplesPerSecondChanged(unsigned); - void dataAdded(); private: Ui::DataFormatPanel *ui; @@ -81,6 +92,7 @@ private: bool paused; + bool demoEnabled; DemoReader demoReader; }; diff --git a/src/datarecorder.cpp b/src/datarecorder.cpp new file mode 100644 --- /dev/null +++ b/src/datarecorder.cpp @@ -0,0 +1,89 @@ +/* + Copyright © 2017 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 "datarecorder.h" + +#include + +DataRecorder::DataRecorder(QObject *parent) : + QObject(parent), + fileStream(&file) +{ + lastNumChannels = 0; + disableBuffering = false; +} + +bool DataRecorder::startRecording(QString fileName, QString separator, QStringList channelNames) +{ + Q_ASSERT(!file.isOpen()); + _sep = separator; + + // open file + file.setFileName(fileName); + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << "Opening file " << fileName + << " for recording failed with error: " << file.error(); + return false; + } + + // write header line + if (!channelNames.isEmpty()) + { + fileStream << channelNames.join(_sep); + fileStream << "\n"; + lastNumChannels = channelNames.length(); + } + return true; +} + +void DataRecorder::addData(double* data, unsigned length, unsigned numOfChannels) +{ + Q_ASSERT(length > 0); + Q_ASSERT(length % numOfChannels == 0); + + if (lastNumChannels != 0 && numOfChannels != lastNumChannels) + { + qWarning() << "Number of channels changed from " << lastNumChannels + << " to " << numOfChannels << + " during recording, CSV file is corrupted but no data will be lost."; + } + lastNumChannels = numOfChannels; + + unsigned numOfSamples = length / numOfChannels; // per channel + for (unsigned int i = 0; i < numOfSamples; i++) + { + for (unsigned ci = 0; ci < numOfChannels; ci++) + { + fileStream << data[ci * numOfSamples + i]; + if (ci != numOfChannels-1) fileStream << _sep; + } + fileStream << '\n'; + } + + if (disableBuffering) fileStream.flush(); +} + +void DataRecorder::stopRecording() +{ + Q_ASSERT(file.isOpen()); + + file.close(); + lastNumChannels = 0; +} diff --git a/src/datarecorder.h b/src/datarecorder.h new file mode 100644 --- /dev/null +++ b/src/datarecorder.h @@ -0,0 +1,77 @@ +/* + Copyright © 2017 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 . +*/ + +#ifndef DATARECORDER_H +#define DATARECORDER_H + +#include +#include +#include + +class DataRecorder : public QObject +{ + Q_OBJECT +public: + explicit DataRecorder(QObject *parent = 0); + + /// Disables file buffering + bool disableBuffering; + + /** + * @brief Starts recording data to a file in CSV format. + * + * File is opened and header line (names of channels) is written. + * + * @param fileName name of the recording file + * @param separator column separator + * @param channelNames names of the channels for header line, if empty no header line is written + * @return false if file operation fails (read only etc.) + */ + bool startRecording(QString fileName, QString separator, QStringList channelNames); + + /** + * @brief Adds data to a channel. + * + * Multiple rows of data can be added at a time. Each channels + * data should be ordered consecutively in the `data` array: + * + * [CH0_SMP0, CH0_SMP1 ... CH0_SMPN, CH1_SMP0, CH1_SMP1, ... , CHN_SMPN] + * + * If `numOfChannels` changes during recording, no data will be + * lost (ie. it will be written to the file) but this will produce + * an invalid CSV file. An error message will be written to the + * console. + * + * @param data samples array + * @param length number of samples in `data`, must be multiple of `numOfChannels` + * @param numOfChannels how many channels samples this data carries + */ + void addData(double* data, unsigned length, unsigned numOfChannels); + + /// Stops recording, closes file. + void stopRecording(); + +private: + unsigned lastNumChannels; ///< used for error message only + QFile file; + QTextStream fileStream; + QString _sep; +}; + +#endif // DATARECORDER_H diff --git a/src/demoreader.cpp b/src/demoreader.cpp --- a/src/demoreader.cpp +++ b/src/demoreader.cpp @@ -25,8 +25,9 @@ #define M_PI 3.14159265358979323846 #endif -DemoReader::DemoReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +DemoReader::DemoReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; _numOfChannels = 1; @@ -81,10 +82,8 @@ void DemoReader::demoTimerTimeout() { // we are calculating the fourier components of square wave samples[ci] = 4*sin(2*M_PI*double((ci+1)*count)/period)/((2*(ci+1))*M_PI); - sampleCount++; } - _channelMan->addData(samples, _numOfChannels); + addData(samples, _numOfChannels); delete[] samples; - emit dataAdded(); } } diff --git a/src/demoreader.h b/src/demoreader.h --- a/src/demoreader.h +++ b/src/demoreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -38,7 +38,8 @@ class DemoReader : public AbstractReader Q_OBJECT public: - explicit DemoReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit DemoReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent = 0); /// Demo reader is an exception so this function returns NULL QWidget* settingsWidget(); diff --git a/src/framedreader.cpp b/src/framedreader.cpp --- a/src/framedreader.cpp +++ b/src/framedreader.cpp @@ -23,8 +23,9 @@ #include "framedreader.h" -FramedReader::FramedReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +FramedReader::FramedReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; @@ -310,8 +311,7 @@ void FramedReader::readFrameDataAndCheck if (!checksumEnabled || checksumPassed) { // commit data - _channelMan->addData(channelSamples, numOfPackagesToRead * _numOfChannels); - emit dataAdded(); + addData(channelSamples, numOfPackagesToRead*_numOfChannels); } else { diff --git a/src/framedreader.h b/src/framedreader.h --- a/src/framedreader.h +++ b/src/framedreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -33,7 +33,8 @@ class FramedReader : public AbstractRead Q_OBJECT public: - explicit FramedReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit FramedReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject *parent = 0); QWidget* settingsWidget(); unsigned numOfChannels(); void enable(bool enabled = true); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -62,7 +62,8 @@ MainWindow::MainWindow(QWidget *parent) channelMan(1, 1, this), snapshotMan(this, &channelMan), commandPanel(&serialPort), - dataFormatPanel(&serialPort, &channelMan) + dataFormatPanel(&serialPort, &channelMan, &recorder), + recordPanel(&recorder, &channelMan) { ui->setupUi(this); @@ -72,9 +73,11 @@ MainWindow::MainWindow(QWidget *parent) ui->tabWidget->insertTab(1, &dataFormatPanel, "Data Format"); ui->tabWidget->insertTab(2, &plotControlPanel, "Plot"); ui->tabWidget->insertTab(3, &commandPanel, "Commands"); + ui->tabWidget->insertTab(4, &recordPanel, "Record"); ui->tabWidget->setCurrentIndex(0); auto tbPortControl = portControl.toolBar(); addToolBar(tbPortControl); + addToolBar(recordPanel.toolbar()); ui->plotToolBar->addAction(snapshotMan.takeSnapshotAction()); ui->menuBar->insertMenu(ui->menuHelp->menuAction(), snapshotMan.menu()); @@ -149,13 +152,43 @@ MainWindow::MainWindow(QWidget *parent) QObject::connect(&(this->serialPort), SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(onPortError(QSerialPort::SerialPortError))); - // TODO: `replot` must be triggered from ChannelManager // init data format and reader - QObject::connect(&dataFormatPanel, &DataFormatPanel::dataAdded, + QObject::connect(&channelMan, &ChannelManager::dataAdded, plotMan, &PlotManager::replot); QObject::connect(ui->actionPause, &QAction::triggered, - &dataFormatPanel, &DataFormatPanel::pause); + &channelMan, &ChannelManager::pause); + + QObject::connect(&recordPanel, &RecordPanel::recordStarted, + &dataFormatPanel, &DataFormatPanel::startRecording); + + QObject::connect(&recordPanel, &RecordPanel::recordStopped, + &dataFormatPanel, &DataFormatPanel::stopRecording); + + QObject::connect(ui->actionPause, &QAction::triggered, + [this](bool enabled) + { + if (enabled && !recordPanel.recordPaused()) + { + dataFormatPanel.pause(true); + } + else + { + dataFormatPanel.pause(false); + } + }); + + QObject::connect(&recordPanel, &RecordPanel::recordPausedChanged, + [this](bool enabled) + { + if (ui->actionPause->isChecked() && enabled) + { + dataFormatPanel.pause(false); + } + }); + + connect(&serialPort, &QIODevice::aboutToClose, + &recordPanel, &RecordPanel::onPortClose); // init data arrays and plot numOfSamples = plotControlPanel.numOfSamples(); @@ -466,6 +499,7 @@ void MainWindow::saveAllSettings(QSettin plotControlPanel.saveSettings(settings); plotMan->saveSettings(settings); commandPanel.saveSettings(settings); + recordPanel.saveSettings(settings); } void MainWindow::loadAllSettings(QSettings* settings) @@ -477,6 +511,7 @@ void MainWindow::loadAllSettings(QSettin plotControlPanel.loadSettings(settings); plotMan->loadSettings(settings); commandPanel.loadSettings(settings); + recordPanel.loadSettings(settings); } void MainWindow::saveMWSettings(QSettings* settings) diff --git a/src/mainwindow.h b/src/mainwindow.h --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -38,11 +38,13 @@ #include "commandpanel.h" #include "dataformatpanel.h" #include "plotcontrolpanel.h" +#include "recordpanel.h" #include "ui_about_dialog.h" #include "framebuffer.h" #include "channelmanager.h" #include "snapshotmanager.h" #include "plotmanager.h" +#include "datarecorder.h" namespace Ui { class MainWindow; @@ -74,10 +76,12 @@ private: ChannelManager channelMan; PlotManager* plotMan; SnapshotManager snapshotMan; + DataRecorder recorder; // operated by `recordPanel` QLabel spsLabel; CommandPanel commandPanel; DataFormatPanel dataFormatPanel; + RecordPanel recordPanel; PlotControlPanel plotControlPanel; bool isDemoRunning(); diff --git a/src/recordpanel.cpp b/src/recordpanel.cpp new file mode 100644 --- /dev/null +++ b/src/recordpanel.cpp @@ -0,0 +1,286 @@ +/* + Copyright © 2017 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 +#include +#include +#include +#include +#include +#include + +#include "recordpanel.h" +#include "ui_recordpanel.h" +#include "setting_defines.h" + +RecordPanel::RecordPanel(DataRecorder* recorder, ChannelManager* channelMan, QWidget *parent) : + QWidget(parent), + ui(new Ui::RecordPanel), + recordToolBar(tr("Record Toolbar")), + recordAction(QIcon::fromTheme("media-record"), tr("Record"), this) +{ + overwriteSelected = false; + _recorder = recorder; + _channelMan = channelMan; + + ui->setupUi(this); + + recordToolBar.setObjectName("tbRecord"); + + recordAction.setCheckable(true); + recordToolBar.addAction(&recordAction); + ui->pbRecord->setDefaultAction(&recordAction); + + connect(ui->pbBrowse, &QPushButton::clicked, + this, &RecordPanel::selectFile); + connect(&recordAction, &QAction::triggered, + this, &RecordPanel::onRecord); + + connect(ui->cbRecordPaused, SIGNAL(toggled(bool)), + this, SIGNAL(recordPausedChanged(bool))); + + connect(ui->cbDisableBuffering, &QCheckBox::toggled, + [this](bool enabled) + { + _recorder->disableBuffering = enabled; + }); +} + +RecordPanel::~RecordPanel() +{ + delete ui; +} + +QToolBar* RecordPanel::toolbar() +{ + return &recordToolBar; +} + +bool RecordPanel::isRecording() +{ + return recordAction.isChecked(); +} + +bool RecordPanel::recordPaused() +{ + return ui->cbRecordPaused->isChecked(); +} + +bool RecordPanel::selectFile() +{ + QString fileName = QFileDialog::getSaveFileName( + parentWidget(), tr("Select recording file")); + + if (fileName.isEmpty()) + { + return false; + } + else + { + selectedFile = fileName; + ui->lbFileName->setText(selectedFile); + overwriteSelected = QFile::exists(fileName); + return true; + } +} + +void RecordPanel::onRecord(bool start) +{ + if (!start) + { + stopRecording(); + return; + } + + bool canceled = false; + if (ui->leSeparator->text().isEmpty()) + { + QMessageBox::critical(this, "Error", + "Column separator cannot be empty! Please select a separator."); + ui->leSeparator->setFocus(Qt::OtherFocusReason); + canceled = true; + } + + // check file name + if (!canceled && selectedFile.isEmpty() && !selectFile()) + { + canceled = true; + } + + if (!canceled && !overwriteSelected && QFile::exists(selectedFile)) + { + if (ui->cbAutoIncrement->isChecked()) + { + // TODO: should we increment even if user selected to replace? + canceled = !incrementFileName(); + } + else + { + canceled = !confirmOverwrite(selectedFile); + } + } + + if (canceled) + { + recordAction.setChecked(false); + } + else + { + overwriteSelected = false; + startRecording(); + } +} + +bool RecordPanel::incrementFileName(void) +{ + QFileInfo fileInfo(selectedFile); + + QString base = fileInfo.completeBaseName(); + QRegularExpression regex("(.*?)(\\d+)(?!.*\\d)(.*)"); + auto match = regex.match(base); + + if (match.hasMatch()) + { + bool ok; + int fileNum = match.captured(2).toInt(&ok); + base = match.captured(1) + QString::number(fileNum + 1) + match.captured(3); + } + else + { + base += "_1"; + } + + QString suffix = fileInfo.suffix();; + if (!suffix.isEmpty()) + { + suffix = "." + suffix; + } + + QString autoFileName = fileInfo.path() + "/" + base + suffix; + + // check if auto generated file name exists, ask user another name + if (QFile::exists(autoFileName)) + { + if (!confirmOverwrite(autoFileName)) + { + return false; + } + } + else + { + selectedFile = autoFileName; + } + + ui->lbFileName->setText(selectedFile); + return true; +} + +bool RecordPanel::confirmOverwrite(QString fileName) +{ + // prepare message box + QMessageBox mb(parentWidget()); + mb.setWindowTitle(tr("File Already Exists")); + mb.setIcon(QMessageBox::Warning); + mb.setText(tr("File (%1) already exists. How to continue?").arg(fileName)); + + auto bCancel = mb.addButton(QMessageBox::Cancel); + auto bOverwrite = mb.addButton(tr("Overwrite"), QMessageBox::DestructiveRole); + mb.addButton(tr("Select Another File"), QMessageBox::YesRole); + + mb.setEscapeButton(bCancel); + + // show message box + mb.exec(); + + if (mb.clickedButton() == bCancel) + { + return false; + } + else if (mb.clickedButton() == bOverwrite) + { + selectedFile = fileName; + return true; + } + else // select button + { + return selectFile(); + } +} + +void RecordPanel::startRecording(void) +{ + QStringList channelNames; + if (ui->cbHeader->isChecked()) + { + channelNames = _channelMan->infoModel()->channelNames(); + } + _recorder->startRecording(selectedFile, getSeparator(), channelNames); + emit recordStarted(); +} + +void RecordPanel::stopRecording(void) +{ + emit recordStopped(); + _recorder->stopRecording(); +} + +void RecordPanel::onPortClose() +{ + if (recordAction.isChecked() && ui->cbStopOnClose->isChecked()) + { + stopRecording(); + recordAction.setChecked(false); + } +} + +QString RecordPanel::getSeparator() const +{ + QString sep = ui->leSeparator->text(); + sep.replace("\\t", "\t"); + return sep; +} + +void RecordPanel::saveSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Record); + settings->setValue(SG_Record_AutoIncrement, ui->cbAutoIncrement->isChecked()); + settings->setValue(SG_Record_RecordPaused, ui->cbRecordPaused->isChecked()); + settings->setValue(SG_Record_StopOnClose, ui->cbStopOnClose->isChecked()); + settings->setValue(SG_Record_Header, ui->cbHeader->isChecked()); + settings->setValue(SG_Record_DisableBuffering, ui->cbDisableBuffering->isChecked()); + settings->setValue(SG_Record_Separator, ui->leSeparator->text()); + settings->endGroup(); +} + +void RecordPanel::loadSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Record); + ui->cbAutoIncrement->setChecked( + settings->value(SG_Record_AutoIncrement, ui->cbAutoIncrement->isChecked()).toBool()); + ui->cbRecordPaused->setChecked( + settings->value(SG_Record_RecordPaused, ui->cbRecordPaused->isChecked()).toBool()); + ui->cbStopOnClose->setChecked( + settings->value(SG_Record_StopOnClose, ui->cbStopOnClose->isChecked()).toBool()); + ui->cbHeader->setChecked( + settings->value(SG_Record_Header, ui->cbHeader->isChecked()).toBool()); + ui->cbDisableBuffering->setChecked( + settings->value(SG_Record_DisableBuffering, ui->cbDisableBuffering->isChecked()).toBool()); + ui->leSeparator->setText(settings->value(SG_Record_Separator, ui->leSeparator->text()).toString()); + settings->endGroup(); +} diff --git a/src/recordpanel.h b/src/recordpanel.h new file mode 100644 --- /dev/null +++ b/src/recordpanel.h @@ -0,0 +1,117 @@ +/* + Copyright © 2017 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 . +*/ + +#ifndef RECORDPANEL_H +#define RECORDPANEL_H + +#include +#include +#include +#include + +#include "datarecorder.h" +#include "channelmanager.h" + +namespace Ui { +class RecordPanel; +} + +class RecordPanel : public QWidget +{ + Q_OBJECT + +public: + explicit RecordPanel(DataRecorder* recorder, ChannelManager* channelMan, + QWidget* parent = 0); + ~RecordPanel(); + + QToolBar* toolbar(); + + bool isRecording(); + bool recordPaused(); + + /// Stores settings into a `QSettings` + void saveSettings(QSettings* settings); + /// Loads settings from a `QSettings`. + void loadSettings(QSettings* settings); + +signals: + void recordStarted(); + void recordStopped(); + void recordPausedChanged(bool enabled); + +public slots: + /// Must be called when port is closed + void onPortClose(); + +private: + Ui::RecordPanel *ui; + QToolBar recordToolBar; + QAction recordAction; + QString selectedFile; + bool overwriteSelected; + DataRecorder* _recorder; + ChannelManager* _channelMan; + + /** + * @brief Increments the file name. + * + * If file name doesn't have a number at the end of it, a number is appended + * with underscore starting from 1. + * + * @return false if user cancels + */ + bool incrementFileName(void); + + /** + * @brief Used to ask user confirmation if auto generated file + * name exists. + * + * If user confirms overwrite, `selectedFile` is set to + * `fileName`. User is also given option to select file and is + * shown a file select dialog in this case. + * + * @param fileName auto generated file name. + * @return false if user cancels + */ + bool confirmOverwrite(QString fileName); + + void startRecording(void); + void stopRecording(void); + + /// Returns separator text from ui. "\t" is converted to TAB + /// character. + QString getSeparator() const; + +private slots: + /** + * @brief Opens up the file select dialog + * + * If you cancel the selection operation, currently selected file is not + * changed. + * + * @return true if file selected, false if user cancels + */ + bool selectFile(); + + void onRecord(bool start); + +}; + +#endif // RECORDPANEL_H diff --git a/src/recordpanel.ui b/src/recordpanel.ui new file mode 100644 --- /dev/null +++ b/src/recordpanel.ui @@ -0,0 +1,197 @@ + + + RecordPanel + + + + 0 + 0 + 532 + 261 + + + + Form + + + + + + + + + + Select record file + + + Browse + + + + + + + + 0 + 0 + + + + Select file... + + + + + + + + + Increments file name automatically everytime a new recording starts + + + Auto increment file name + + + true + + + + + + + Continue recording to file even when plotting is paused + + + Record while paused + + + true + + + + + + + Stop recording when port closed + + + true + + + + + + + Channel names are written to the first line of record file + + + Write header line + + + true + + + + + + + + + Column Separator: + + + + + + + + 30 + 16777215 + + + + For TAB character enter \t + + + , + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Do not buffer when writing to file. Check this if you are using other software to open the file during recording. + + + Disable buffering + + + + + + + Qt::Vertical + + + + 20 + 1 + + + + + + + + + + + + + 85 + 50 + + + + Start/Stop Recording + + + Record + + + + + + + Qt::Vertical + + + + 20 + 1 + + + + + + + + + + + diff --git a/src/setting_defines.h b/src/setting_defines.h --- a/src/setting_defines.h +++ b/src/setting_defines.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -29,6 +29,7 @@ const char SettingGroup_CustomFrame[] = const char SettingGroup_Channels[] = "Channels"; const char SettingGroup_Plot[] = "Plot"; const char SettingGroup_Commands[] = "Commands"; +const char SettingGroup_Record[] = "Record"; // mainwindow setting keys const char SG_MainWindow_Size[] = "size"; @@ -90,4 +91,12 @@ const char SG_Commands_Name[] = "name"; const char SG_Commands_Type[] = "type"; const char SG_Commands_Data[] = "data"; +// record panel settings keys +const char SG_Record_AutoIncrement[] = "autoIncrement"; +const char SG_Record_RecordPaused[] = "recordPaused"; +const char SG_Record_StopOnClose[] = "stopOnClose"; +const char SG_Record_Header[] = "header"; +const char SG_Record_Separator[] = "separator"; +const char SG_Record_DisableBuffering[] = "disableBuffering"; + #endif // SETTING_DEFINES_H