diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -11,3 +11,4 @@ fd5f1eb480ec372b49df58b497458de05c30057c 9c9a11cd15fd094e2b2b65dc51805fd8fd1d2460 v0.8.1 4cf9a1ee1f107a38e03dbe17c4f2882c43d827c9 v0.9.0 ef003f7af8f37f760c22dae776f5ff8e1b526deb v0.9.1 +7ce85527f3f2f6d19afc0401d2ac723f6c072481 v0.10.0 diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright © 2017 Hasan Yavuz Özderya +# Copyright © 2018 Hasan Yavuz Özderya # # This file is part of serialplot. # @@ -52,10 +52,18 @@ else () find_package(QtColorWidgets REQUIRED) endif () +set(BUILD_LEDWIDGET true CACHE BOOL "Download and build LedWidget automatically.") +if (BUILD_LEDWIDGET) + include(BuildLedWidget) +else (BUILD_LEDWIDGET) + include(FindLedWidget) +endif (BUILD_LEDWIDGET) + # includes include_directories("./src" ${QWT_INCLUDE_DIR} ${QTCOLORWIDGETS_INCLUDE_DIRS} + ${LEDWIDGET_INCLUDE_DIR} ) # flags @@ -77,6 +85,8 @@ qt5_wrap_ui(UI_FILES src/binarystreamreadersettings.ui src/asciireadersettings.ui src/framedreadersettings.ui + src/demoreadersettings.ui + src/updatecheckdialog.ui ) if (WIN32) @@ -94,7 +104,6 @@ add_executable(${PROGRAM_NAME} WIN32 src/scrollzoomer.cpp src/scrollbar.cpp src/hidabletabwidget.cpp - src/framebuffer.cpp src/scalepicker.cpp src/scalezoomer.cpp src/portlist.cpp @@ -111,8 +120,13 @@ add_executable(${PROGRAM_NAME} WIN32 src/datarecorder.cpp src/tooltipfilter.cpp src/sneakylineedit.cpp - src/channelmanager.cpp + src/stream.cpp + src/streamchannel.cpp src/channelinfomodel.cpp + src/ringbuffer.cpp + src/ringbuffer.cpp + src/indexbuffer.cpp + src/readonlybuffer.cpp src/framebufferseries.cpp src/numberformatbox.cpp src/endiannessbox.cpp @@ -122,10 +136,22 @@ add_executable(${PROGRAM_NAME} WIN32 src/asciireader.cpp src/asciireadersettings.cpp src/demoreader.cpp + src/demoreadersettings.cpp src/framedreader.cpp src/framedreadersettings.cpp src/plotmanager.cpp + src/plotmenu.cpp + src/barplot.cpp + src/barchart.cpp + src/barscaledraw.cpp src/numberformat.cpp + src/updatechecker.cpp + src/versionnumber.cpp + src/updatecheckdialog.cpp + src/samplepack.cpp + src/source.cpp + src/sink.cpp + src/samplecounter.cpp misc/windows_icon.rc ${UI_FILES} ${RES_FILES} @@ -135,8 +161,9 @@ add_executable(${PROGRAM_NAME} WIN32 target_link_libraries(${PROGRAM_NAME} ${QWT_LIBRARY} ${QTCOLORWIDGETS_LIBRARIES} + ${LEDWIDGET_LIBRARY} ) -qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort) +qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort Network) if (BUILD_QWT) add_dependencies(${PROGRAM_NAME} QWT) @@ -146,6 +173,11 @@ if (BUILD_QTCOLORWIDGETS) add_dependencies(${PROGRAM_NAME} QCW) endif () +if (BUILD_LEDWIDGET) + add_dependencies(${PROGRAM_NAME} LEDW) +endif (BUILD_LEDWIDGET) + + # set compiler flags set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") @@ -162,13 +194,16 @@ else() endif() # default version -set(VERSION_STRING "0.9.1") +set(VERSION_STRING "0.10.0") set(VERSION_REVISION "0") # get revision number from mercurial and parse version string include(GetVersion) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_STRING=\\\"${VERSION_STRING}\\\" ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MAJOR=${VERSION_MAJOR} ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MINOR=${VERSION_MINOR} ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_PATCH=${VERSION_PATCH} ") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_REVISION=\\\"${VERSION_REVISION}\\\" ") # add make run target diff --git a/cmake/modules/BuildLedWidget.cmake b/cmake/modules/BuildLedWidget.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/BuildLedWidget.cmake @@ -0,0 +1,30 @@ +# +# 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(ExternalProject) + +ExternalProject_Add(LEDW + PREFIX ledw + HG_REPOSITORY https://bitbucket.org/hyOzd/ledwidget + UPDATE_COMMAND "" + INSTALL_COMMAND "") + +ExternalProject_Get_Property(LEDW binary_dir source_dir) +set(LEDWIDGET_INCLUDE_DIR ${source_dir}/src) +set(LEDWIDGET_LIBRARY ${binary_dir}/libledwidget.a) diff --git a/cmake/modules/BuildLinuxAppImage.cmake b/cmake/modules/BuildLinuxAppImage.cmake --- a/cmake/modules/BuildLinuxAppImage.cmake +++ b/cmake/modules/BuildLinuxAppImage.cmake @@ -1,7 +1,11 @@ # Based on: https://github.com/mhoeher/opentodolist +# +# Note: we extract linuxdeployqt appimage so that it can run in docker, that's +# because fuse doesn't work in docker. -set(LINUXDEPLOYQT_URL "https://github.com/probonopd/linuxdeployqt/releases/download/4/linuxdeployqt-4-x86_64.AppImage") -set(LINUXDEPLOYQT_TOOL ${CMAKE_CURRENT_BINARY_DIR}/linuxdeployqt-4-x86_64.AppImage) +set(LINUXDEPLOYQT_URL "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage") +set(LINUXDEPLOYQT_APPIMAGE ${CMAKE_CURRENT_BINARY_DIR}/linuxdeployqt-continuous-x86_64.AppImage) +set(LINUXDEPLOYQT_TOOL ${CMAKE_CURRENT_BINARY_DIR}/squashfs-root/AppRun) set(APPIMAGE_DIR ${CMAKE_CURRENT_BINARY_DIR}/${PROGRAM_NAME}-${VERSION_STRING}-${CMAKE_HOST_SYSTEM_PROCESSOR}) @@ -11,7 +15,9 @@ add_custom_command( COMMAND wget ${LINUXDEPLOYQT_URL} COMMAND - chmod a+x ${LINUXDEPLOYQT_TOOL}) + chmod a+x ${LINUXDEPLOYQT_APPIMAGE} + COMMAND + ${LINUXDEPLOYQT_APPIMAGE} --appimage-extract) add_custom_target( appimage diff --git a/cmake/modules/BuildQColorWidgets.cmake b/cmake/modules/BuildQColorWidgets.cmake --- a/cmake/modules/BuildQColorWidgets.cmake +++ b/cmake/modules/BuildQColorWidgets.cmake @@ -1,5 +1,5 @@ # -# Copyright © 2017 Hasan Yavuz Özderya +# Copyright © 2018 Hasan Yavuz Özderya # # This file is part of serialplot. # @@ -21,8 +21,8 @@ include(ExternalProject) ExternalProject_Add(QCW PREFIX qcw - GIT_REPOSITORY https://github.com/mbasaglia/Qt-Color-Widgets - PATCH_COMMAND patch -t -p1 -i ${CMAKE_CURRENT_LIST_DIR}/qt_5_2_moc_creation_namespace_fix.diff + GIT_REPOSITORY https://gitlab.com/mattia.basaglia/Qt-Color-Widgets.git + GIT_TAG 2c49e1bb4e1f591e720e2132cc2aaeef3ba73f14 CMAKE_CACHE_ARGS "-DCMAKE_CXX_FLAGS:string=-D QTCOLORWIDGETS_STATICALLY_LINKED" UPDATE_COMMAND "" INSTALL_COMMAND "") diff --git a/cmake/modules/FindLedWidget.cmake b/cmake/modules/FindLedWidget.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindLedWidget.cmake @@ -0,0 +1,25 @@ +# +# 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 . +# + +find_library(LEDWIDGET_LIBRARY "ledwidget") +find_path(LEDWIDGET_INCLUDE_DIR "ledwidget.h" PATH_SUFFIXES "ledwidget") + +mark_as_advanced(LEDWIDGET_LIBRARY LEDWIDGET_INCLUDE_DIR) + +find_package_handle_standard_args(LedWidget DEFAULT_MSG LEDWIDGET_LIBRARY LEDWIDGET_INCLUDE_DIR) diff --git a/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff b/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff deleted file mode 100644 --- a/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff +++ /dev/null @@ -1,39 +0,0 @@ -diff --git a/include/color_dialog.hpp b/include/color_dialog.hpp -index 5c7653d..895215c 100644 ---- a/include/color_dialog.hpp -+++ b/include/color_dialog.hpp -@@ -30,6 +30,8 @@ - - class QAbstractButton; - -+using namespace color_widgets; -+ - namespace color_widgets { - - class QCP_EXPORT ColorDialog : public QDialog -diff --git a/include/color_list_widget.hpp b/include/color_list_widget.hpp -index 282bea5..7d8e0c5 100644 ---- a/include/color_list_widget.hpp -+++ b/include/color_list_widget.hpp -@@ -25,6 +25,8 @@ - #include "abstract_widget_list.hpp" - #include "color_wheel.hpp" - -+using namespace color_widgets; -+ - namespace color_widgets { - - class QCP_EXPORT ColorListWidget : public AbstractWidgetList -diff --git a/include/color_selector.hpp b/include/color_selector.hpp -index db817d5..48b374d 100644 ---- a/include/color_selector.hpp -+++ b/include/color_selector.hpp -@@ -25,6 +25,8 @@ - #include "color_preview.hpp" - #include "color_wheel.hpp" - -+using namespace color_widgets; -+ - namespace color_widgets { - - /** diff --git a/misc/pseudo_device.py b/misc/pseudo_device.py --- a/misc/pseudo_device.py +++ b/misc/pseudo_device.py @@ -6,7 +6,7 @@ # # Currently this script only outputs ASCII(comma separated) data. # -# Copyright © 2015 Hasan Yavuz Özderya +# Copyright © 2018 Hasan Yavuz Özderya # # This file is part of serialplot. # @@ -26,6 +26,22 @@ import os, pty, time, struct, math +def ascii_test_str(port): + text = """ +1,2,3 +2,4,6 +3,8,11 +1,2,3 +-1,-1,-1 +nana +0,0,0 +1,na,na + """ + while True: + for line in text.splitlines(): + os.write(port, bytes(line+"\r\n", 'utf8')) + time.sleep(1) + def ascii_test(port): """Put ASCII test data through pseudo terminal.""" print("\n") @@ -109,8 +125,9 @@ def run(): try: # float_sine(master) - frame_test(master) + # frame_test(master) # ascii_test(master) + ascii_test_str(master) finally: # close the pseudo terminal files os.close(master) diff --git a/serialplot.pro b/serialplot.pro --- a/serialplot.pro +++ b/serialplot.pro @@ -68,7 +68,10 @@ SOURCES += \ src/framedreader.cpp \ src/plotmanager.cpp \ src/numberformat.cpp \ - src/recordpanel.cpp + src/recordpanel.cpp \ + src/updatechecker.cpp \ + src/updatecheckdialog.cpp \ + src/demoreadersettings.cpp HEADERS += \ src/mainwindow.h \ @@ -108,7 +111,10 @@ HEADERS += \ src/plotmanager.h \ src/setting_defines.h \ src/numberformat.h \ - src/recordpanel.h + src/recordpanel.h \ + src/updatechecker.h \ + src/updatecheckdialog.h \ + src/demoreadersettings.h FORMS += \ src/mainwindow.ui \ @@ -124,7 +130,9 @@ FORMS += \ src/framedreadersettings.ui \ src/binarystreamreadersettings.ui \ src/asciireadersettings.ui \ - src/recordpanel.ui + src/recordpanel.ui \ + src/updatecheckdialog.ui \ + src/demoreadersettings.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 © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -19,41 +19,27 @@ #include "abstractreader.h" -AbstractReader::AbstractReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject* parent) : +AbstractReader::AbstractReader(QIODevice* device, QObject* parent) : QObject(parent) { _device = device; - _channelMan = channelMan; - _recorder = recorder; - recording = false; +} - // initialize sps counter - sampleCount = 0; - samplesPerSecond = 0; - QObject::connect(&spsTimer, &QTimer::timeout, - this, &AbstractReader::spsTimerTimeout); - // TODO: start sps timer when reader is enabled - spsTimer.start(SPS_UPDATE_TIMEOUT * 1000); +void AbstractReader::pause(bool enabled) +{ + paused = enabled; } -void AbstractReader::spsTimerTimeout() +void AbstractReader::enable(bool enabled) { - unsigned currentSps = samplesPerSecond; - samplesPerSecond = (sampleCount/numOfChannels())/SPS_UPDATE_TIMEOUT; - if (currentSps != samplesPerSecond) + if (enabled) { - emit samplesPerSecondChanged(samplesPerSecond); + QObject::connect(_device, &QIODevice::readyRead, + this, &AbstractReader::onDataReady); } - sampleCount = 0; + else + { + QObject::disconnect(_device, 0, this, 0); + disconnectSinks(); + } } - -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 © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -25,20 +25,16 @@ #include #include -#include "channelmanager.h" -#include "datarecorder.h" +#include "source.h" /** * All reader classes must inherit this class. */ -class AbstractReader : public QObject +class AbstractReader : public QObject, public Source { Q_OBJECT public: - explicit AbstractReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject* parent = 0); - - bool recording; /// is recording started + explicit AbstractReader(QIODevice* device, QObject* parent = 0); /** * Returns a widget to be shown in data format panel when reader @@ -46,31 +42,16 @@ public: */ virtual QWidget* settingsWidget() = 0; - /** - * Number of channels being read. - * - * This number may be user selected or automatically determined - * from incoming stream. - */ - virtual unsigned numOfChannels() = 0; - /// Reader should only read when enabled. Default state should be /// 'disabled'. - virtual void enable(bool enabled = true) = 0; + virtual void enable(bool enabled = true); - /** - * @brief Starts sending data to recorder. - * - * @note recorder must have been started! - */ - void startRecording(); - - /// Stops recording. - void stopRecording(); + /// None of the current readers support X channel at the moment + bool hasX() const final { return false; }; signals: + // TODO: should we keep this? void numOfChannelsChanged(unsigned); - void samplesPerSecondChanged(unsigned); public slots: /** @@ -79,26 +60,15 @@ public slots: * Reader should actually continue reading to keep the * synchronization but shouldn't commit data. */ - virtual void pause(bool) = 0; + void pause(bool enabled); protected: QIODevice* _device; - - /// Should be called with read data - void addData(double* samples, unsigned length); - -private: - const int SPS_UPDATE_TIMEOUT = 1; // second + bool paused; - unsigned sampleCount; - unsigned samplesPerSecond; - - ChannelManager* _channelMan; - DataRecorder* _recorder; - QTimer spsTimer; - -private slots: - void spsTimerTimeout(); +protected slots: + /// all derived readers has to override this function + virtual void onDataReady() = 0; }; #endif // ABSTRACTREADER_H diff --git a/src/asciireader.cpp b/src/asciireader.cpp --- a/src/asciireader.cpp +++ b/src/asciireader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -24,28 +24,33 @@ /// If set to this value number of channels is determined from input #define NUMOFCHANNELS_AUTO (0) -AsciiReader::AsciiReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject* parent) : - AbstractReader(device, channelMan, recorder, parent) +AsciiReader::AsciiReader(QIODevice* device, QObject* parent) : + AbstractReader(device, parent) { paused = false; - discardFirstLine = true; - _numOfChannels = _settingsWidget.numOfChannels(); - autoNumOfChannels = (_numOfChannels == NUMOFCHANNELS_AUTO); + _numChannels = _settingsWidget.numOfChannels(); + autoNumOfChannels = (_numChannels == NUMOFCHANNELS_AUTO); + delimiter = _settingsWidget.delimiter(); connect(&_settingsWidget, &AsciiReaderSettings::numOfChannelsChanged, [this](unsigned value) { - _numOfChannels = value; - autoNumOfChannels = (_numOfChannels == NUMOFCHANNELS_AUTO); + _numChannels = value; + updateNumChannels(); // TODO: setting numchannels = 0, should remove all buffers + // do we want this? + autoNumOfChannels = (_numChannels == NUMOFCHANNELS_AUTO); if (!autoNumOfChannels) { emit numOfChannelsChanged(value); } }); - connect(device, &QIODevice::aboutToClose, [this](){discardFirstLine=true;}); + connect(&_settingsWidget, &AsciiReaderSettings::delimiterChanged, + [this](QChar d) + { + delimiter = d; + }); } QWidget* AsciiReader::settingsWidget() @@ -53,49 +58,38 @@ QWidget* AsciiReader::settingsWidget() return &_settingsWidget; } -unsigned AsciiReader::numOfChannels() +unsigned AsciiReader::numChannels() const { + // TODO: an alternative is to never set _numChannels to '0' // do not allow '0' - if (_numOfChannels == 0) - { - return 1; - } - else - { - return _numOfChannels; - } + return _numChannels == 0 ? 1 : _numChannels; } -// TODO: this could be a part of AbstractReader void AsciiReader::enable(bool enabled) { if (enabled) { - discardFirstLine = true; + firstReadAfterEnable = true; QObject::connect(_device, &QIODevice::readyRead, this, &AsciiReader::onDataReady); } else { QObject::disconnect(_device, 0, this, 0); + disconnectSinks(); } } -void AsciiReader::pause(bool enabled) -{ - paused = enabled; -} - void AsciiReader::onDataReady() { while(_device->canReadLine()) { - QByteArray line = _device->readLine(); + QString line = QString(_device->readLine()); // discard only once when we just started reading - if (discardFirstLine) + if (firstReadAfterEnable) { - discardFirstLine = false; + firstReadAfterEnable = false; continue; } @@ -116,51 +110,57 @@ void AsciiReader::onDataReady() continue; } - auto separatedValues = line.split(','); + const SamplePack* samples = parseLine(line); + if (samples != nullptr) { + // update number of channels if in auto mode + if (autoNumOfChannels ) { + unsigned nc = samples->numChannels(); + if (nc != _numChannels) { + _numChannels = nc; + updateNumChannels(); + // TODO: is `numOfChannelsChanged` signal still used? + emit numOfChannelsChanged(nc); + } + } - unsigned numReadChannels; // effective number of channels to read - unsigned numComingChannels = separatedValues.length(); + Q_ASSERT(samples->numChannels() == _numChannels); + + // commit data + feedOut(*samples); + } + } +} - if (autoNumOfChannels) - { - // did number of channels changed? - if (numComingChannels != _numOfChannels) - { - _numOfChannels = numComingChannels; - emit numOfChannelsChanged(numComingChannels); - } - numReadChannels = numComingChannels; - } - else if (numComingChannels >= _numOfChannels) - { - numReadChannels = _numOfChannels; - } - else // there is missing channel data +SamplePack* AsciiReader::parseLine(const QString& line) const +{ + auto separatedValues = line.split(delimiter, QString::SkipEmptyParts); + unsigned numComingChannels = separatedValues.length(); + + // check number of channels (skipped if auto num channels is enabled) + if ((!numComingChannels) || (!autoNumOfChannels && numComingChannels != _numChannels)) + { + qWarning() << "Line parsing error: invalid number of channels!"; + qWarning() << "Read line: " << line; + return nullptr; + } + + // parse data per channel + auto samples = new SamplePack(1, numComingChannels); + for (unsigned ci = 0; ci < numComingChannels; ci++) + { + bool ok; + samples->data(ci)[0] = separatedValues[ci].toDouble(&ok); + if (!ok) { - numReadChannels = separatedValues.length(); - qWarning() << "Incoming data is missing data for some channels!"; + qWarning() << "Data parsing error for channel: " << ci; qWarning() << "Read line: " << line; - } - // parse read line - double* channelSamples = new double[_numOfChannels](); - for (unsigned ci = 0; ci < numReadChannels; ci++) - { - bool ok; - channelSamples[ci] = separatedValues[ci].toDouble(&ok); - if (!ok) - { - qWarning() << "Data parsing error for channel: " << ci; - qWarning() << "Read line: " << line; - channelSamples[ci] = 0; - } + delete samples; + return nullptr; } + } - // commit data - addData(channelSamples, _numOfChannels); - - delete[] channelSamples; - } + return samples; } void AsciiReader::saveSettings(QSettings* settings) diff --git a/src/asciireader.h b/src/asciireader.h --- a/src/asciireader.h +++ b/src/asciireader.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,7 +21,9 @@ #define ASCIIREADER_H #include +#include +#include "samplepack.h" #include "abstractreader.h" #include "asciireadersettings.h" @@ -30,32 +32,32 @@ class AsciiReader : public AbstractReade Q_OBJECT public: - explicit AsciiReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject *parent = 0); + explicit AsciiReader(QIODevice* device, QObject *parent = 0); QWidget* settingsWidget(); - unsigned numOfChannels(); - void enable(bool enabled = true); + unsigned numChannels() const; + void enable(bool enabled) override; /// Stores settings into a `QSettings` void saveSettings(QSettings* settings); /// Loads settings from a `QSettings`. void loadSettings(QSettings* settings); -public slots: - void pause(bool); - private: AsciiReaderSettings _settingsWidget; - unsigned _numOfChannels; + unsigned _numChannels; /// number of channels will be determined from incoming data unsigned autoNumOfChannels; - bool paused; + QChar delimiter; ///< selected column delimiter - // We may have (usually true) started reading in the middle of a - // line, so its a better idea to just discard first line. - bool discardFirstLine; + bool firstReadAfterEnable = false; private slots: - void onDataReady(); + void onDataReady() override; + /** + * Parses given line and returns sample pack. + * + * Returns `nullptr` in case of error. + */ + SamplePack* parseLine(const QString& line) const; }; #endif // ASCIIREADER_H diff --git a/src/asciireadersettings.cpp b/src/asciireadersettings.cpp --- a/src/asciireadersettings.cpp +++ b/src/asciireadersettings.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,20 +17,35 @@ along with serialplot. If not, see . */ +#include +#include + #include "utils.h" #include "setting_defines.h" #include "asciireadersettings.h" #include "ui_asciireadersettings.h" -#include - AsciiReaderSettings::AsciiReaderSettings(QWidget *parent) : QWidget(parent), ui(new Ui::AsciiReaderSettings) { ui->setupUi(this); + auto validator = new QRegularExpressionValidator(QRegularExpression("[^\\d]?"), this); + ui->leDelimiter->setValidator(validator); + + connect(ui->rbComma, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->rbSpace, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->rbTab, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->rbOtherDelimiter, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->leDelimiter, &QLineEdit::textChanged, + this, &AsciiReaderSettings::customDelimiterChanged); + // Note: if directly connected we get a runtime warning on incompatible signal arguments connect(ui->spNumOfChannels, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged), [this](int value) @@ -44,11 +59,51 @@ AsciiReaderSettings::~AsciiReaderSetting delete ui; } -unsigned AsciiReaderSettings::numOfChannels() +unsigned AsciiReaderSettings::numOfChannels() const { return ui->spNumOfChannels->value(); } +QChar AsciiReaderSettings::delimiter() const +{ + if (ui->rbComma->isChecked()) + { + return QChar(','); + } + else if (ui->rbSpace->isChecked()) + { + return QChar(' '); + } + else if (ui->rbTab->isChecked()) + { + return QChar('\t'); + } + else // rbOther + { + auto t = ui->leDelimiter->text(); + return t.isEmpty() ? QChar() : t.at(0); + } +} + +void AsciiReaderSettings::delimiterToggled(bool checked) +{ + if (!checked) return; + + auto d = delimiter(); + if (!d.isNull()) + { + emit delimiterChanged(d); + } +} + +void AsciiReaderSettings::customDelimiterChanged(const QString text) +{ + if (ui->rbOtherDelimiter->isChecked()) + { + if (!text.isEmpty()) emit delimiterChanged(text.at(0)); + } +} + void AsciiReaderSettings::saveSettings(QSettings* settings) { settings->beginGroup(SettingGroup_ASCII); @@ -58,6 +113,25 @@ void AsciiReaderSettings::saveSettings(Q if (numOfChannelsSetting == "0") numOfChannelsSetting = "auto"; settings->setValue(SG_ASCII_NumOfChannels, numOfChannelsSetting); + // save delimiter + QString delimiterS; + if (ui->rbOtherDelimiter->isChecked()) + { + delimiterS = "other"; + } + else if (ui->rbTab->isChecked()) + { + // Note: \t is not correctly loaded + delimiterS = "TAB"; + } + else + { + delimiterS = delimiter(); + } + + settings->setValue(SG_ASCII_Delimiter, delimiterS); + settings->setValue(SG_ASCII_CustomDelimiter, ui->leDelimiter->text()); + settings->endGroup(); } @@ -83,5 +157,26 @@ void AsciiReaderSettings::loadSettings(Q } } + // load delimiter + auto delimiterS = settings->value(SG_ASCII_Delimiter, delimiter()).toString(); + auto customDelimiter = settings->value(SG_ASCII_CustomDelimiter, delimiter()).toString(); + if (!customDelimiter.isEmpty()) ui->leDelimiter->setText(customDelimiter); + if (delimiterS == ",") + { + ui->rbComma->setChecked(true); + } + else if (delimiterS == " ") + { + ui->rbSpace->setChecked(true); + } + else if (delimiterS == "TAB") + { + ui->rbTab->setChecked(true); + } + else + { + ui->rbOtherDelimiter->setChecked(true); + } + settings->endGroup(); } diff --git a/src/asciireadersettings.h b/src/asciireadersettings.h --- a/src/asciireadersettings.h +++ b/src/asciireadersettings.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -22,6 +22,7 @@ #include #include +#include namespace Ui { class AsciiReaderSettings; @@ -35,7 +36,8 @@ public: explicit AsciiReaderSettings(QWidget *parent = 0); ~AsciiReaderSettings(); - unsigned numOfChannels(); + unsigned numOfChannels() const; + QChar delimiter() const; /// Stores settings into a `QSettings` void saveSettings(QSettings* settings); /// Loads settings from a `QSettings`. @@ -43,9 +45,15 @@ public: signals: void numOfChannelsChanged(unsigned); + /// Signaled only with a valid delimiter + void delimiterChanged(QChar); private: Ui::AsciiReaderSettings *ui; + +private slots: + void delimiterToggled(bool checked); + void customDelimiterChanged(const QString text); }; #endif // ASCIIREADERSETTINGS_H diff --git a/src/asciireadersettings.ui b/src/asciireadersettings.ui --- a/src/asciireadersettings.ui +++ b/src/asciireadersettings.ui @@ -6,14 +6,17 @@ 0 0 - 414 + 493 171 Form - + + + QFormLayout::ExpandingFieldsGrow + 0 @@ -26,68 +29,105 @@ 0 - + + + + Number Of Channels: + + + + + + + + 60 + 0 + + + + Select number of channels or set to 0 for Auto (determined from incoming data) + + + Auto + + + false + + + 0 + + + 32 + + + + + + + Column Delimiter: + + + + - + - Number Of Channels: + comma + + + true - - - - 60 - 0 - - - - Select number of channels or set to 0 for Auto (determined from incoming data) + + + space - - Auto - - - false + + + + + + tab - - 0 - - - 32 + + + + + + other: - - - Qt::Horizontal + + + + 0 + 0 + - + - 1 - 20 + 30 + 16777215 - + + Enter a custom delimiter character + + + + + + | + + - - - - Qt::Vertical - - - - 20 - 1 - - - - diff --git a/src/barchart.cpp b/src/barchart.cpp new file mode 100644 --- /dev/null +++ b/src/barchart.cpp @@ -0,0 +1,96 @@ +/* + Copyright © 2018 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 "barchart.h" + +BarChart::BarChart(const Stream* stream) +{ + _stream = stream; + setSpacing(0); +} + +void BarChart::resample() +{ + setSamples(chartData()); +} + +QVector BarChart::chartData() const +{ + unsigned numChannels = _stream->numChannels(); + unsigned numSamples = _stream->numSamples(); + QVector data(numChannels); + for (unsigned i = 0; i < numChannels; i++) + { + data[i] = _stream->channel(i)->yData()->sample(numSamples-1); + } + return data; +} + +QwtColumnSymbol* BarChart::specialSymbol(int sampleIndex, const QPointF& sample) const +{ + unsigned numChannels = _stream->numChannels(); + if (sampleIndex < 0 || sampleIndex > (int) numChannels) + { + return NULL; + } + + auto color = _stream->channel(sampleIndex)->color(); + + QwtColumnSymbol* symbol = new QwtColumnSymbol(QwtColumnSymbol::Box); + symbol->setLineWidth(1); + symbol->setFrameStyle(QwtColumnSymbol::Plain); + symbol->setPalette(QPalette(color)); + + return symbol; +} + +void BarChart::drawSample( + QPainter *painter, const QwtScaleMap &xMap, const QwtScaleMap &yMap, + const QRectF &canvasRect, const QwtInterval &boundingInterval, + int index, const QPointF &sample ) const +{ + QwtPlotBarChart::drawSample(painter, xMap, yMap, canvasRect, boundingInterval, index, sample); + + // calculate bar size + const double barWidth = sampleWidth(xMap, canvasRect.width(), + boundingInterval.width(), sample.y()); + const double x = xMap.transform( sample.x() ); + const double x1 = x - 0.5 * barWidth; + // const double x2 = x + 0.5 * barWidth; + + const double y1 = yMap.transform(baseline()); + const double y2 = yMap.transform(sample.y()); + const double barHeight = fabs(y2 - y1); + + // create and calculate text size + const auto valueStr = QString::number(sample.y()); + auto valueText = QwtText(valueStr, QwtText::PlainText); + const auto textSize = valueText.textSize(); + auto r = QRectF(x1, y2, barWidth, textSize.height()); + if (y2 > y1) r.moveBottom(y2); + + // display text if there is enough space + if (barHeight >= textSize.height() && barWidth >= textSize.width()) + { + valueText.draw(painter, r); + } +} diff --git a/src/barchart.h b/src/barchart.h new file mode 100644 --- /dev/null +++ b/src/barchart.h @@ -0,0 +1,47 @@ +/* + Copyright © 2018 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 BARCHART_H +#define BARCHART_H + +#include +#include +#include "stream.h" + +class BarChart : public QwtPlotBarChart +{ +public: + explicit BarChart(const Stream* stream); + + void resample(); + QwtColumnSymbol* specialSymbol(int sampleIndex, const QPointF&) const; + + void drawSample( + QPainter *painter, const QwtScaleMap &xMap, const QwtScaleMap &yMap, + const QRectF &canvasRect, const QwtInterval &boundingInterval, + int index, const QPointF &sample ) const; + +private: + const Stream* _stream; + + QVector chartData() const; +}; + + +#endif // BARCHART_H diff --git a/src/barplot.cpp b/src/barplot.cpp new file mode 100644 --- /dev/null +++ b/src/barplot.cpp @@ -0,0 +1,74 @@ +/* + Copyright © 2018 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 "barplot.h" +#include "barscaledraw.h" +#include "utils.h" + +BarPlot::BarPlot(Stream* stream, PlotMenu* menu, QWidget* parent) : + QwtPlot(parent), _menu(menu), barChart(stream) +{ + _stream = stream; + barChart.attach(this); + setAxisMaxMinor(QwtPlot::xBottom, 0); + setAxisScaleDraw(QwtPlot::xBottom, new BarScaleDraw(stream)); + + update(); + connect(_stream, &Stream::dataAdded, this, &BarPlot::update); + connect(_stream, &Stream::numChannelsChanged, this, &BarPlot::update); + + // connect to menu + connect(&menu->darkBackgroundAction, SELECT::OVERLOAD_OF(&QAction::toggled), + this, &BarPlot::darkBackground); + darkBackground(menu->darkBackgroundAction.isChecked()); +} + +void BarPlot::update() +{ + // Note: -0.99 is used instead of -1 to handle the case of `numOfChannels==1` + setAxisScale(QwtPlot::xBottom, 0, _stream->numChannels()-0.99, 1); + barChart.resample(); + replot(); +} + +void BarPlot::setYAxis(bool autoScaled, double yMin, double yMax) +{ + if (autoScaled) + { + setAxisAutoScale(QwtPlot::yLeft); + } + else + { + setAxisScale(QwtPlot::yLeft, yMin, yMax); + } +} + + +void BarPlot::darkBackground(bool enabled) +{ + if (enabled) + { + setCanvasBackground(QBrush(Qt::black)); + } + else + { + setCanvasBackground(QBrush(Qt::white)); + } + replot(); +} diff --git a/src/barplot.h b/src/barplot.h new file mode 100644 --- /dev/null +++ b/src/barplot.h @@ -0,0 +1,55 @@ +/* + Copyright © 2018 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 BARPLOT_H +#define BARPLOT_H + +#include + +#include "stream.h" +#include "plotmenu.h" +#include "barchart.h" + +class BarPlot : public QwtPlot +{ + Q_OBJECT + +public: + explicit BarPlot(Stream* stream, + PlotMenu* menu, + QWidget* parent = 0); + +public slots: + /// Set the Y axis + void setYAxis(bool autoScaled, double yMin = 0, double yMax = 1); + /// Enable/disable dark background + void darkBackground(bool enabled); + +private: + Stream* _stream; + PlotMenu* _menu; + BarChart barChart; + + QVector chartData() const; + +private slots: + void update(); +}; + +#endif // BARPLOT_H diff --git a/src/barscaledraw.cpp b/src/barscaledraw.cpp new file mode 100644 --- /dev/null +++ b/src/barscaledraw.cpp @@ -0,0 +1,51 @@ +/* + Copyright © 2018 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 "barscaledraw.h" + +#include + +BarScaleDraw::BarScaleDraw(const Stream* stream) +{ + _stream = stream; + enableComponent(Backbone, false); + setLabelRotation(-90); + setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter); + + QObject::connect(_stream, &Stream::channelNameChanged, + [this]() + { + invalidateCache(); + }); +} + +QwtText BarScaleDraw::label(double value) const +{ + int index = value; + unsigned numChannels = _stream->numChannels(); + + if (index >=0 && index < (int) numChannels) + { + return _stream->channel(index)->name(); + } + else + { + return QString(""); + } +} diff --git a/src/barscaledraw.h b/src/barscaledraw.h new file mode 100644 --- /dev/null +++ b/src/barscaledraw.h @@ -0,0 +1,39 @@ +/* + Copyright © 2018 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 BARSCALEDRAW_H +#define BARSCALEDRAW_H + +#include +#include +#include + +#include "stream.h" + +class BarScaleDraw : public QwtScaleDraw +{ +public: + explicit BarScaleDraw(const Stream* stream); + QwtText label(double value) const; + +private: + const Stream* _stream; +}; + +#endif // BARSCALEDRAW_H diff --git a/src/binarystreamreader.cpp b/src/binarystreamreader.cpp --- a/src/binarystreamreader.cpp +++ b/src/binarystreamreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,19 +23,16 @@ #include "binarystreamreader.h" #include "floatswap.h" -BinaryStreamReader::BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject* parent) : - AbstractReader(device, channelMan, recorder, parent) +BinaryStreamReader::BinaryStreamReader(QIODevice* device, QObject* parent) : + AbstractReader(device, parent) { paused = false; skipByteRequested = false; skipSampleRequested = false; - _numOfChannels = _settingsWidget.numOfChannels(); + _numChannels = _settingsWidget.numOfChannels(); connect(&_settingsWidget, &BinaryStreamReaderSettings::numOfChannelsChanged, - this, &BinaryStreamReader::numOfChannelsChanged); - connect(&_settingsWidget, &BinaryStreamReaderSettings::numOfChannelsChanged, - this, &BinaryStreamReader::onNumOfChannelsChanged); + this, &BinaryStreamReader::onNumOfChannelsChanged); // initial number format selection onNumberFormatChanged(_settingsWidget.numberFormat()); @@ -60,27 +57,9 @@ QWidget* BinaryStreamReader::settingsWid return &_settingsWidget; } -unsigned BinaryStreamReader::numOfChannels() -{ - return _numOfChannels; -} - -void BinaryStreamReader::enable(bool enabled) +unsigned BinaryStreamReader::numChannels() const { - if (enabled) - { - QObject::connect(_device, &QIODevice::readyRead, - this, &BinaryStreamReader::onDataReady); - } - else - { - QObject::disconnect(_device, 0, this, 0); - } -} - -void BinaryStreamReader::pause(bool enabled) -{ - paused = enabled; + return _numChannels; } void BinaryStreamReader::onNumberFormatChanged(NumberFormat numberFormat) @@ -123,17 +102,19 @@ void BinaryStreamReader::onNumberFormatC void BinaryStreamReader::onNumOfChannelsChanged(unsigned value) { - _numOfChannels = value; + _numChannels = value; + updateNumChannels(); + emit numOfChannelsChanged(value); } void BinaryStreamReader::onDataReady() { // a package is a set of channel data like {CHAN0_SAMPLE, CHAN1_SAMPLE...} - int packageSize = sampleSize * _numOfChannels; + int packageSize = sampleSize * _numChannels; int bytesAvailable = _device->bytesAvailable(); // skip 1 byte if requested - if (bytesAvailable > 0 && skipByteRequested) + if (skipByteRequested && bytesAvailable > 0) { _device->read(1); skipByteRequested = false; @@ -141,11 +122,11 @@ void BinaryStreamReader::onDataReady() } // skip 1 sample (channel) if requested - if (bytesAvailable >= (int) sampleSize && skipSampleRequested) + if (skipSampleRequested && bytesAvailable >= (int) sampleSize) { _device->read(sampleSize); skipSampleRequested = false; - bytesAvailable--; + bytesAvailable -= sampleSize; } if (bytesAvailable < packageSize) return; @@ -160,20 +141,16 @@ void BinaryStreamReader::onDataReady() return; } - double* channelSamples = new double[numOfPackagesToRead*_numOfChannels]; - + // actual reading + SamplePack samples(numOfPackagesToRead, _numChannels); for (int i = 0; i < numOfPackagesToRead; i++) { - for (unsigned int ci = 0; ci < _numOfChannels; ci++) + for (unsigned int ci = 0; ci < _numChannels; ci++) { - // channelSamples[ci].replace(i, (this->*readSample)()); - channelSamples[ci*numOfPackagesToRead+i] = (this->*readSample)(); + samples.data(ci)[i] = (this->*readSample)(); } } - - addData(channelSamples, numOfPackagesToRead*_numOfChannels); - - delete[] channelSamples; + feedOut(samples); } template double BinaryStreamReader::readSampleAs() diff --git a/src/binarystreamreader.h b/src/binarystreamreader.h --- a/src/binarystreamreader.h +++ b/src/binarystreamreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -34,24 +34,18 @@ class BinaryStreamReader : public Abstra { Q_OBJECT public: - explicit BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject *parent = 0); + explicit BinaryStreamReader(QIODevice* device, QObject *parent = 0); QWidget* settingsWidget(); - unsigned numOfChannels(); - void enable(bool enabled = true); + unsigned numChannels() const; /// Stores settings into a `QSettings` void saveSettings(QSettings* settings); /// Loads settings from a `QSettings`. void loadSettings(QSettings* settings); -public slots: - void pause(bool); - private: BinaryStreamReaderSettings _settingsWidget; - unsigned _numOfChannels; + unsigned _numChannels; unsigned sampleSize; - bool paused; bool skipByteRequested; bool skipSampleRequested; @@ -69,7 +63,7 @@ private: private slots: void onNumberFormatChanged(NumberFormat numberFormat); void onNumOfChannelsChanged(unsigned value); - void onDataReady(); + void onDataReady() override; }; #endif // BINARYSTREAMREADER_H diff --git a/src/channelinfomodel.cpp b/src/channelinfomodel.cpp --- a/src/channelinfomodel.cpp +++ b/src/channelinfomodel.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -68,6 +68,7 @@ ChannelInfoModel::ChannelInfoModel(unsig ChannelInfoModel::ChannelInfoModel(const ChannelInfoModel& other) : ChannelInfoModel(other.rowCount(), other.parent()) { + // TODO: why not set (copy) info list directly instead? for (int i = 0; i < other.rowCount(); i++) { setData(index(i, COLUMN_NAME), @@ -76,9 +77,24 @@ ChannelInfoModel::ChannelInfoModel(const setData(index(i, COLUMN_NAME), other.data(other.index(i, COLUMN_NAME), Qt::ForegroundRole), Qt::ForegroundRole); + setData(index(i, COLUMN_VISIBILITY), other.data(other.index(i, COLUMN_VISIBILITY), Qt::CheckStateRole), Qt::CheckStateRole); + + setData(index(i, COLUMN_GAIN), + other.data(other.index(i, COLUMN_GAIN), Qt::CheckStateRole), + Qt::CheckStateRole); + setData(index(i, COLUMN_GAIN), + other.data(other.index(i, COLUMN_GAIN), Qt::EditRole), + Qt::EditRole); + + setData(index(i, COLUMN_OFFSET), + other.data(other.index(i, COLUMN_OFFSET), Qt::CheckStateRole), + Qt::CheckStateRole); + setData(index(i, COLUMN_OFFSET), + other.data(other.index(i, COLUMN_OFFSET), Qt::EditRole), + Qt::EditRole); } } @@ -96,6 +112,10 @@ ChannelInfoModel::ChannelInfo::ChannelIn name = tr("Channel %1").arg(index + 1); visibility = true; color = colors[index % NUMOF_COLORS]; + gain = 1.0; + offset = 0.0; + gainEn = false; + offsetEn = false; } QString ChannelInfoModel::name(unsigned i) const @@ -113,6 +133,26 @@ bool ChannelInfoModel::isVisible(unsigne return infos[i].visibility; } +bool ChannelInfoModel::gainEn (unsigned i) const +{ + return infos[i].gainEn; +} + +double ChannelInfoModel::gain (unsigned i) const +{ + return infos[i].gain; +} + +bool ChannelInfoModel::offsetEn (unsigned i) const +{ + return infos[i].offsetEn; +} + +double ChannelInfoModel::offset (unsigned i) const +{ + return infos[i].offset; +} + QStringList ChannelInfoModel::channelNames() const { QStringList r; @@ -143,6 +183,10 @@ Qt::ItemFlags ChannelInfoModel::flags(co { return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable; } + else if (index.column() == COLUMN_GAIN || index.column() == COLUMN_OFFSET) + { + return Qt::ItemIsEditable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable; + } return Qt::NoItemFlags; } @@ -155,27 +199,51 @@ QVariant ChannelInfoModel::data(const QM return QVariant(); } + auto &info = infos[index.row()]; + // get color if (role == Qt::ForegroundRole) { - return infos[index.row()].color; + return info.color; } - // get name + // name if (index.column() == COLUMN_NAME) { if (role == Qt::DisplayRole || role == Qt::EditRole) { - return QVariant(infos[index.row()].name); + return QVariant(info.name); } - } // get visibility + } // visibility else if (index.column() == COLUMN_VISIBILITY) { if (role == Qt::CheckStateRole) { - bool visible = infos[index.row()].visibility; + bool visible = info.visibility; return visible ? Qt::Checked : Qt::Unchecked; } + } // gain + else if (index.column() == COLUMN_GAIN) + { + if (role == Qt::CheckStateRole) + { + return info.gainEn ? Qt::Checked : Qt::Unchecked; + } + else if (role == Qt::DisplayRole || role == Qt::EditRole) + { + return QVariant(info.gain); + } + } // offset + else if (index.column() == COLUMN_OFFSET) + { + if (role == Qt::CheckStateRole) + { + return info.offsetEn ? Qt::Checked : Qt::Unchecked; + } + else if (role == Qt::DisplayRole || role == Qt::EditRole) + { + return QVariant(info.offset); + } } return QVariant(); @@ -195,6 +263,14 @@ QVariant ChannelInfoModel::headerData(in { return tr("Visible"); } + else if (section == COLUMN_GAIN) + { + return tr("Gain"); + } + else if (section == COLUMN_OFFSET) + { + return tr("Offset"); + } } } else // vertical @@ -216,22 +292,24 @@ bool ChannelInfoModel::setData(const QMo return false; } + auto &info = infos[index.row()]; + // set color if (role == Qt::ForegroundRole) { - infos[index.row()].color = value.value(); + info.color = value.value(); emit dataChanged(index, index, QVector({Qt::ForegroundRole})); return true; } // set name + bool r = false; if (index.column() == COLUMN_NAME) { if (role == Qt::DisplayRole || role == Qt::EditRole) { - infos[index.row()].name = value.toString(); - emit dataChanged(index, index, QVector({role})); - return true; + info.name = value.toString(); + r = true; } } // set visibility else if (index.column() == COLUMN_VISIBILITY) @@ -239,14 +317,47 @@ bool ChannelInfoModel::setData(const QMo if (role == Qt::CheckStateRole) { bool checked = value.toInt() == Qt::Checked; - infos[index.row()].visibility = checked; - emit dataChanged(index, index, QVector({role})); - return true; + info.visibility = checked; + r = true; + } + } + else if (index.column() == COLUMN_GAIN) + { + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + info.gain = value.toDouble(); + r = true; + } + else if (role == Qt::CheckStateRole) + { + bool checked = value.toInt() == Qt::Checked; + info.gainEn = checked; + if (_gainOrOffsetEn != checked) updateGainOrOffsetEn(); + r = true; + } + } + else if (index.column() == COLUMN_OFFSET) + { + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + info.offset = value.toDouble(); + r = true; + } + else if (role == Qt::CheckStateRole) + { + bool checked = value.toInt() == Qt::Checked; + info.offsetEn = checked; + if (_gainOrOffsetEn != checked) updateGainOrOffsetEn(); + r = true; } } - // invalid index/role - return false; + if (r) + { + emit dataChanged(index, index, QVector({role})); + } + + return r; } void ChannelInfoModel::setNumOfChannels(unsigned number) @@ -285,6 +396,7 @@ void ChannelInfoModel::setNumOfChannels( } _numOfChannels = number; + updateGainOrOffsetEn(); if (isInserting) { @@ -306,11 +418,13 @@ void ChannelInfoModel::resetInfos() endResetModel(); } +// TODO: fix repetitive code, ChannelInfoModel::reset* functions void ChannelInfoModel::resetNames() { beginResetModel(); for (unsigned ci = 0; (int) ci < infos.length(); ci++) { + // TODO: do not create a full object every time (applies to other reset methods as well) infos[ci].name = ChannelInfo(ci).name; } endResetModel(); @@ -326,17 +440,56 @@ void ChannelInfoModel::resetColors() endResetModel(); } -void ChannelInfoModel::resetVisibility() +void ChannelInfoModel::resetVisibility(bool visible) +{ + beginResetModel(); + for (unsigned ci = 0; (int) ci < infos.length(); ci++) + { + infos[ci].visibility = visible; + } + endResetModel(); +} + +void ChannelInfoModel::resetGains() { beginResetModel(); for (unsigned ci = 0; (int) ci < infos.length(); ci++) { - infos[ci].visibility = true; + infos[ci].gain = ChannelInfo(ci).gain; + infos[ci].gainEn = ChannelInfo(ci).gainEn; } + updateGainOrOffsetEn(); endResetModel(); } -void ChannelInfoModel::saveSettings(QSettings* settings) +void ChannelInfoModel::resetOffsets() +{ + beginResetModel(); + for (unsigned ci = 0; (int) ci < infos.length(); ci++) + { + infos[ci].offset = ChannelInfo(ci).offset; + infos[ci].offsetEn = ChannelInfo(ci).offsetEn; + } + updateGainOrOffsetEn(); + endResetModel(); +} + +bool ChannelInfoModel::gainOrOffsetEn() const +{ + return _gainOrOffsetEn; +} + +void ChannelInfoModel::updateGainOrOffsetEn() +{ + _gainOrOffsetEn = false; + for (unsigned ci = 0; ci < _numOfChannels; ci++) + { + auto& info = infos[ci]; + _gainOrOffsetEn |= (info.gainEn || info.offsetEn); + } +} + +void ChannelInfoModel::saveSettings(QSettings* settings) const { settings->beginGroup(SettingGroup_Channels); settings->beginWriteArray(SG_Channels_Channel); @@ -345,9 +498,14 @@ void ChannelInfoModel::saveSettings(QSet for (unsigned ci = 0; (int) ci < infos.length(); ci++) { settings->setArrayIndex(ci); - settings->setValue(SG_Channels_Name, infos[ci].name); - settings->setValue(SG_Channels_Color, infos[ci].color); - settings->setValue(SG_Channels_Visible, infos[ci].visibility); + auto& info = infos[ci]; + settings->setValue(SG_Channels_Name, info.name); + settings->setValue(SG_Channels_Color, info.color); + settings->setValue(SG_Channels_Visible, info.visibility); + settings->setValue(SG_Channels_Gain, info.gain); + settings->setValue(SG_Channels_GainEn, info.gainEn); + settings->setValue(SG_Channels_Offset, info.offset); + settings->setValue(SG_Channels_OffsetEn, info.offsetEn); } settings->endArray(); @@ -364,9 +522,13 @@ void ChannelInfoModel::loadSettings(QSet settings->setArrayIndex(ci); ChannelInfo chanInfo(ci); - chanInfo.name = settings->value(SG_Channels_Name, chanInfo.name).toString(); - chanInfo.color = settings->value(SG_Channels_Color, chanInfo.color).value(); - chanInfo.visibility = settings->value(SG_Channels_Visible, true).toBool(); + chanInfo.name = settings->value(SG_Channels_Name , chanInfo.name).toString(); + chanInfo.color = settings->value(SG_Channels_Color , chanInfo.color).value(); + chanInfo.visibility = settings->value(SG_Channels_Visible , chanInfo.visibility).toBool(); + chanInfo.gain = settings->value(SG_Channels_Gain , chanInfo.gain).toDouble(); + chanInfo.gainEn = settings->value(SG_Channels_GainEn , chanInfo.gainEn).toBool(); + chanInfo.offset = settings->value(SG_Channels_Offset , chanInfo.offset).toDouble(); + chanInfo.offsetEn = settings->value(SG_Channels_OffsetEn , chanInfo.offsetEn).toBool(); if ((int) ci < infos.size()) { @@ -385,6 +547,8 @@ void ChannelInfoModel::loadSettings(QSet } } + updateGainOrOffsetEn(); + settings->endArray(); settings->endGroup(); } diff --git a/src/channelinfomodel.h b/src/channelinfomodel.h --- a/src/channelinfomodel.h +++ b/src/channelinfomodel.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -34,7 +34,9 @@ public: { COLUMN_NAME = 0, COLUMN_VISIBILITY, - COLUMN_COUNT + COLUMN_GAIN, + COLUMN_OFFSET, + COLUMN_COUNT // MUST be last }; explicit ChannelInfoModel(unsigned numberOfChannels, QObject *parent = 0); @@ -44,6 +46,12 @@ public: QString name (unsigned i) const; QColor color (unsigned i) const; bool isVisible(unsigned i) const; + bool gainEn (unsigned i) const; + double gain (unsigned i) const; + bool offsetEn (unsigned i) const; + double offset (unsigned i) const; + /// Returns true if any of the channels have gain or offset enabled + bool gainOrOffsetEn() const; /// Returns a list of channel names QStringList channelNames() const; @@ -57,7 +65,7 @@ public: void setNumOfChannels(unsigned number); /// Stores all channel info into a `QSettings` - void saveSettings(QSettings* settings); + void saveSettings(QSettings* settings) const; /// Loads all channel info from a `QSettings`. void loadSettings(QSettings* settings); @@ -68,8 +76,12 @@ public slots: void resetNames(); /// reset all channel colors void resetColors(); + /// reset all channel gain values and disables gains + void resetGains(); + /// reset all channel offset values and disables offsets + void resetOffsets(); /// reset visibility - void resetVisibility(); + void resetVisibility(bool visible); private: struct ChannelInfo @@ -79,6 +91,8 @@ private: QString name; bool visibility; QColor color; + double gain, offset; + bool gainEn, offsetEn; }; unsigned _numOfChannels; ///< @note this is not necessarily the length of `infos` @@ -88,6 +102,16 @@ private: * remember user entered info (names, colors etc.). */ QList infos; + + /** + * Cache for gain and offset enabled variables of channels. If gain and/or + * offset is not enabled for *any* of the channels this is false otherwise + * true. + */ + bool _gainOrOffsetEn; + + /// Updates `_gainOrOffsetEn` by scanning all channel infos. + void updateGainOrOffsetEn(); }; #endif // CHANNELINFOMODEL_H diff --git a/src/channelmanager.cpp b/src/channelmanager.cpp deleted file mode 100644 --- a/src/channelmanager.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/* - 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 "channelmanager.h" -#include "setting_defines.h" - -ChannelManager::ChannelManager(unsigned numberOfChannels, unsigned numberOfSamples, QObject *parent) : - QObject(parent), - _infoModel(numberOfChannels) -{ - _numOfChannels = numberOfChannels; - _numOfSamples = numberOfSamples; - _paused = false; - - for (unsigned int i = 0; i < numberOfChannels; i++) - { - channelBuffers.append(new FrameBuffer(numberOfSamples)); - } - - connect(&_infoModel, &ChannelInfoModel::dataChanged, - this, &ChannelManager::onChannelInfoChanged); -} - -ChannelManager::~ChannelManager() -{ - for (auto buffer : channelBuffers) - { - delete buffer; - } -} - -unsigned ChannelManager::numOfChannels() -{ - return channelBuffers.size(); -} - -unsigned ChannelManager::numOfSamples() -{ - return _numOfSamples; -} - -void ChannelManager::setNumOfChannels(unsigned number) -{ - unsigned int oldNum = channelBuffers.size(); - - if (number > oldNum) - { - // add new channels - for (unsigned int i = 0; i < number - oldNum; i++) - { - channelBuffers.append(new FrameBuffer(_numOfSamples)); - } - } - else if(number < oldNum) - { - // remove channels - for (unsigned int i = oldNum-1; i > number-1; i--) - { - delete channelBuffers.takeLast(); - } - } - - _numOfChannels = number; - _infoModel.setNumOfChannels(number); - - emit numOfChannelsChanged(number); -} - -void ChannelManager::setNumOfSamples(unsigned number) -{ - _numOfSamples = number; - - for (int ci = 0; ci < channelBuffers.size(); ci++) - { - channelBuffers[ci]->resize(_numOfSamples); - } - - emit numOfSamplesChanged(number); -} - -void ChannelManager::pause(bool paused) -{ - _paused = paused; -} - -FrameBuffer* ChannelManager::channelBuffer(unsigned channel) -{ - return channelBuffers[channel]; -} - -ChannelInfoModel* ChannelManager::infoModel() -{ - return &_infoModel; -} - -QString ChannelManager::channelName(unsigned channel) -{ - return _infoModel.data(_infoModel.index(channel, ChannelInfoModel::COLUMN_NAME), - Qt::DisplayRole).toString(); -} - -QStringList ChannelManager::channelNames() -{ - QStringList list; - for (unsigned ci = 0; ci < _numOfChannels; ci++) - { - list << channelName(ci); - } - return list; -} - -void ChannelManager::onChannelInfoChanged(const QModelIndex & topLeft, - const QModelIndex & bottomRight, - const QVector & roles) -{ - int start = topLeft.row(); - int end = bottomRight.row(); - int col = topLeft.column(); - - for (int ci = start; ci <= end; ci++) - { - for (auto role : roles) - { - switch (role) - { - case Qt::EditRole: - if (col == ChannelInfoModel::COLUMN_NAME) - { - emit channelNameChanged(ci, channelName(ci)); - } - break; - case Qt::ForegroundRole: - if (col == ChannelInfoModel::COLUMN_NAME) - { - // TODO: emit channel color changed - } - break; - case Qt::CheckStateRole: - if (col == ChannelInfoModel::COLUMN_VISIBILITY) - { - // TODO: emit visibility - } - break; - } - } - // emit channelNameChanged(i, channelName(i)); - } -} - -void ChannelManager::addData(double* data, unsigned size) -{ - 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) -{ - _infoModel.saveSettings(settings); -} - -void ChannelManager::loadSettings(QSettings* settings) -{ - _infoModel.loadSettings(settings); -} diff --git a/src/channelmanager.h b/src/channelmanager.h deleted file mode 100644 --- a/src/channelmanager.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - 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 CHANNELMANAGER_H -#define CHANNELMANAGER_H - -#include -#include -#include -#include -#include - -#include "framebuffer.h" -#include "channelinfomodel.h" - -class ChannelManager : public QObject -{ - Q_OBJECT -public: - explicit ChannelManager(unsigned numberOfChannels, unsigned numberOfSamples, QObject *parent = 0); - ~ChannelManager(); - - unsigned numOfChannels(); - unsigned numOfSamples(); - FrameBuffer* channelBuffer(unsigned channel); - QString channelName(unsigned channel); - /// Stores channel names into a `QSettings` - void saveSettings(QSettings* settings); - /// Loads channel names from a `QSettings`. - void loadSettings(QSettings* settings); - /// Returns a model that manages channel information (name, color etc) - ChannelInfoModel* infoModel(); - /// Returns a list of channel names - QStringList channelNames(); - -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); - void setNumOfSamples(unsigned number); - /** - * Add data for all channels. - * - * All channels data is provided in a single array which contains equal - * number of samples for all channels. Structure is as shown below: - * - * [CH0_SMP0, CH0_SMP1 ... CH0_SMPN, CH1_SMP0, CH1_SMP1, ... , CHN_SMPN] - * - * @param data samples for all channels - * @param size size of `data`, must be multiple of `numOfChannels` - */ - 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; - - void addChannelName(QString name); ///< appends a new channel name at the end of list - -private slots: - void onChannelInfoChanged(const QModelIndex & topLeft, - const QModelIndex & bottomRight, - const QVector & roles = QVector ()); -}; - -#endif // CHANNELMANAGER_H diff --git a/src/commandedit.cpp b/src/commandedit.cpp --- a/src/commandedit.cpp +++ b/src/commandedit.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -62,7 +62,7 @@ CommandEdit::CommandEdit(QWidget *parent QLineEdit(parent) { hexValidator = new HexCommandValidator(this); - asciiValidator = new QRegExpValidator(QRegExp("[\\x0000-\\x007F]+")); + asciiValidator = new QRegExpValidator(QRegExp("[\\x0000-\\x007F]+"), this); ascii_mode = true; setValidator(asciiValidator); } diff --git a/src/commandwidget.cpp b/src/commandwidget.cpp --- a/src/commandwidget.cpp +++ b/src/commandwidget.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -103,7 +103,14 @@ bool CommandWidget::isASCIIMode() void CommandWidget::setASCIIMode(bool enabled) { - ui->pbASCII->setChecked(enabled); + if (enabled) + { + ui->pbASCII->setChecked(true); + } + else + { + ui->pbHEX->setChecked(true); + } } void CommandWidget::setName(QString name) diff --git a/src/dataformatpanel.cpp b/src/dataformatpanel.cpp --- a/src/dataformatpanel.cpp +++ b/src/dataformatpanel.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,41 +21,30 @@ #include "ui_dataformatpanel.h" #include -#include -#include #include #include "utils.h" #include "setting_defines.h" -#include "floatswap.h" -DataFormatPanel::DataFormatPanel(QSerialPort* port, - ChannelManager* channelMan, - DataRecorder* recorder, - QWidget *parent) : +DataFormatPanel::DataFormatPanel(QSerialPort* port, QWidget *parent) : QWidget(parent), ui(new Ui::DataFormatPanel), - bsReader(port, channelMan, recorder, this), - asciiReader(port, channelMan, recorder, this), - framedReader(port, channelMan, recorder, this), - demoReader(port, channelMan, recorder, this) + bsReader(port, this), + asciiReader(port, this), + framedReader(port, this), + demoReader(port, this) { ui->setupUi(this); serialPort = port; - _channelMan = channelMan; paused = false; - demoEnabled = false; + readerBeforeDemo = nullptr; // initalize default reader currentReader = &bsReader; bsReader.enable(); ui->rbBinary->setChecked(true); ui->horizontalLayout->addWidget(bsReader.settingsWidget(), 1); - connect(&bsReader, SIGNAL(numOfChannelsChanged(unsigned)), - this, SIGNAL(numOfChannelsChanged(unsigned))); - connect(&bsReader, SIGNAL(samplesPerSecondChanged(unsigned)), - this, SIGNAL(samplesPerSecondChanged(unsigned))); // initalize reader selection buttons connect(ui->rbBinary, &QRadioButton::toggled, [this](bool checked) @@ -72,10 +61,6 @@ DataFormatPanel::DataFormatPanel(QSerial { if (checked) selectReader(&framedReader); }); - - // re-purpose numofchannels settings from actual reader settings to demo reader - connect(this, &DataFormatPanel::numOfChannelsChanged, - &demoReader, &DemoReader::setNumOfChannels); } DataFormatPanel::~DataFormatPanel() @@ -83,9 +68,14 @@ DataFormatPanel::~DataFormatPanel() delete ui; } -unsigned DataFormatPanel::numOfChannels() +unsigned DataFormatPanel::numChannels() const { - return currentReader->numOfChannels(); + return currentReader->numChannels(); +} + +Source* DataFormatPanel::activeSource() +{ + return currentReader; } void DataFormatPanel::pause(bool enabled) @@ -95,33 +85,29 @@ void DataFormatPanel::pause(bool enabled demoReader.pause(enabled); } -void DataFormatPanel::enableDemo(bool enabled) +void DataFormatPanel::enableDemo(bool demoEnabled) { - if (enabled) + if (demoEnabled) { - demoReader.enable(); - demoReader.recording = currentReader->recording; - connect(&demoReader, &DemoReader::samplesPerSecondChanged, - this, &DataFormatPanel::samplesPerSecondChanged); + readerBeforeDemo = currentReader; + demoReader.setNumChannels(readerBeforeDemo->numChannels()); + selectReader(&demoReader); } else { - demoReader.enable(false); - disconnect(&demoReader, 0, this, 0); + Q_ASSERT(readerBeforeDemo != nullptr); + selectReader(readerBeforeDemo); } - demoEnabled = enabled; + + // disable/enable reader selection buttons during/after demo + ui->rbAscii->setDisabled(demoEnabled); + ui->rbBinary->setDisabled(demoEnabled); + ui->rbFramed->setDisabled(demoEnabled); } -void DataFormatPanel::startRecording() +bool DataFormatPanel::isDemoEnabled() const { - currentReader->recording = true; - if (demoEnabled) demoReader.recording = true; -} - -void DataFormatPanel::stopRecording() -{ - currentReader->recording = false; - if (demoEnabled) demoReader.recording = false; + return currentReader == &demoReader; } void DataFormatPanel::selectReader(AbstractReader* reader) @@ -131,10 +117,6 @@ void DataFormatPanel::selectReader(Abstr // re-connect signals disconnect(currentReader, 0, this, 0); - connect(reader, SIGNAL(numOfChannelsChanged(unsigned)), - this, SIGNAL(numOfChannelsChanged(unsigned))); - connect(reader, SIGNAL(samplesPerSecondChanged(unsigned)), - this, SIGNAL(samplesPerSecondChanged(unsigned))); // switch the settings widget ui->horizontalLayout->removeWidget(currentReader->settingsWidget()); @@ -142,29 +124,24 @@ void DataFormatPanel::selectReader(Abstr ui->horizontalLayout->addWidget(reader->settingsWidget(), 1); reader->settingsWidget()->show(); - // notify if number of channels is different - if (currentReader->numOfChannels() != reader->numOfChannels()) - { - emit numOfChannelsChanged(reader->numOfChannels()); - } - reader->pause(paused); - reader->recording = currentReader->recording; currentReader = reader; + emit sourceChanged(currentReader); } void DataFormatPanel::saveSettings(QSettings* settings) { settings->beginGroup(SettingGroup_DataFormat); - // save selected format + // save selected data format (current reader) QString format; - if (currentReader == &bsReader) + AbstractReader* selectedReader = isDemoEnabled() ? readerBeforeDemo : currentReader; + if (selectedReader == &bsReader) { format = "binary"; } - else if (currentReader == &asciiReader) + else if (selectedReader == &asciiReader) { format = "ascii"; } diff --git a/src/dataformatpanel.h b/src/dataformatpanel.h --- a/src/dataformatpanel.h +++ b/src/dataformatpanel.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -28,8 +28,6 @@ #include #include -#include "framebuffer.h" -#include "channelmanager.h" #include "binarystreamreader.h" #include "asciireader.h" #include "demoreader.h" @@ -45,14 +43,13 @@ class DataFormatPanel : public QWidget Q_OBJECT public: - explicit DataFormatPanel(QSerialPort* port, - ChannelManager* channelMan, - DataRecorder* recorder, - QWidget* parent = 0); + explicit DataFormatPanel(QSerialPort* port, QWidget* parent = 0); ~DataFormatPanel(); /// Returns currently selected number of channels - unsigned numOfChannels(); + unsigned numChannels() const; + /// Returns active source (reader) + Source* activeSource(); /// Stores data format panel settings into a `QSettings` void saveSettings(QSettings* settings); /// Loads data format panel settings from a `QSettings`. @@ -62,25 +59,14 @@ 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); + /// Active (selected) reader has changed. + void sourceChanged(Source* source); private: Ui::DataFormatPanel *ui; QSerialPort* serialPort; - ChannelManager* _channelMan; BinaryStreamReader bsReader; AsciiReader asciiReader; @@ -92,8 +78,10 @@ private: bool paused; - bool demoEnabled; DemoReader demoReader; + AbstractReader* readerBeforeDemo; + + bool isDemoEnabled() const; }; #endif // DATAFORMATPANEL_H diff --git a/src/datarecorder.cpp b/src/datarecorder.cpp --- a/src/datarecorder.cpp +++ b/src/datarecorder.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -19,6 +19,9 @@ #include "datarecorder.h" +#include +#include +#include #include DataRecorder::DataRecorder(QObject *parent) : @@ -28,12 +31,25 @@ DataRecorder::DataRecorder(QObject *pare lastNumChannels = 0; disableBuffering = false; windowsLE = false; + timestampEn = false; } -bool DataRecorder::startRecording(QString fileName, QString separator, QStringList channelNames) +bool DataRecorder::startRecording(QString fileName, QString separator, + QStringList channelNames, bool insertTime) { Q_ASSERT(!file.isOpen()); _sep = separator; + timestampEn = insertTime; + + // create directory if it doesn't exist + { + QFileInfo fi(fileName); + if (!fi.dir().mkpath(".")) + { + qCritical() << "Failed to create directory for: " << fileName; + return false; + } + } // open file file.setFileName(fileName); @@ -47,6 +63,10 @@ bool DataRecorder::startRecording(QStrin // write header line if (!channelNames.isEmpty()) { + if (timestampEn) + { + fileStream << tr("timestamp") << _sep; + } fileStream << channelNames.join(_sep); fileStream << le(); lastNumChannels = channelNames.length(); @@ -54,26 +74,35 @@ bool DataRecorder::startRecording(QStrin return true; } -void DataRecorder::addData(double* data, unsigned length, unsigned numOfChannels) +void DataRecorder::feedIn(const SamplePack& data) { - Q_ASSERT(length > 0); - Q_ASSERT(length % numOfChannels == 0); + Q_ASSERT(file.isOpen()); // recorder should be disconnected before stopping recording + Q_ASSERT(!data.hasX()); // NYI - if (lastNumChannels != 0 && numOfChannels != lastNumChannels) + // check if number of channels has changed during recording and warn + unsigned numChannels = data.numChannels(); + if (lastNumChannels != 0 && numChannels != lastNumChannels) { qWarning() << "Number of channels changed from " << lastNumChannels - << " to " << numOfChannels << + << " to " << numChannels << " during recording, CSV file is corrupted but no data will be lost."; } - lastNumChannels = numOfChannels; + lastNumChannels = numChannels; - unsigned numOfSamples = length / numOfChannels; // per channel - for (unsigned int i = 0; i < numOfSamples; i++) + // write data + qint64 timestamp; + if (timestampEn) timestamp = QDateTime::currentMSecsSinceEpoch(); + unsigned numSamples = data.numSamples(); + for (unsigned int i = 0; i < numSamples; i++) { - for (unsigned ci = 0; ci < numOfChannels; ci++) + if (timestampEn) { - fileStream << data[ci * numOfSamples + i]; - if (ci != numOfChannels-1) fileStream << _sep; + fileStream << timestamp << _sep; + } + for (unsigned ci = 0; ci < numChannels; ci++) + { + fileStream << data.data(ci)[i]; + if (ci != numChannels-1) fileStream << _sep; } fileStream << le(); } diff --git a/src/datarecorder.h b/src/datarecorder.h --- a/src/datarecorder.h +++ b/src/datarecorder.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -24,7 +24,15 @@ #include #include -class DataRecorder : public QObject +#include "sink.h" + +/** + * Implemented as a `Sink` that writes incoming data to a file. Before + * connecting a `Source` recording must be started with the `startRecording` + * method. Also before calling `stopRecording`, recorder should be disconnected + * from source. + */ +class DataRecorder : public QObject, public Sink { Q_OBJECT public: @@ -44,14 +52,17 @@ public: /** * @brief Starts recording data to a file in CSV format. * - * File is opened and header line (names of channels) is written. + * File is opened and header line (names of channels) is written. After + * calling this function recorder should be connected to a `Source`. * * @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 + * @param insertTime enable inserting timestamp * @return false if file operation fails (read only etc.) */ - bool startRecording(QString fileName, QString separator, QStringList channelNames); + bool startRecording(QString fileName, QString separator, + QStringList channelNames, bool insertTime); /** * @brief Adds data to a channel. @@ -75,11 +86,15 @@ public: /// Stops recording, closes file. void stopRecording(); +protected: + virtual void feedIn(const SamplePack& data); + private: unsigned lastNumChannels; ///< used for error message only QFile file; QTextStream fileStream; QString _sep; + bool timestampEn; /// Returns the selected line ending. const char* le() const; diff --git a/src/demoreader.cpp b/src/demoreader.cpp --- a/src/demoreader.cpp +++ b/src/demoreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -25,21 +25,23 @@ #define M_PI 3.14159265358979323846 #endif -DemoReader::DemoReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject* parent) : - AbstractReader(device, channelMan, recorder, parent) +DemoReader::DemoReader(QIODevice* device, QObject* parent) : + AbstractReader(device, parent) { paused = false; - _numOfChannels = 1; + _numChannels = _settingsWidget.numChannels(); + connect(&_settingsWidget, &DemoReaderSettings::numChannelsChanged, + this, &DemoReader::onNumChannelsChanged); + count = 0; timer.setInterval(100); - QObject::connect(&timer, &QTimer::timeout, - this, &DemoReader::demoTimerTimeout); + connect(&timer, &QTimer::timeout, + this, &DemoReader::demoTimerTimeout); } QWidget* DemoReader::settingsWidget() { - return NULL; + return &_settingsWidget; } void DemoReader::enable(bool enabled) @@ -51,22 +53,18 @@ void DemoReader::enable(bool enabled) else { timer.stop(); + disconnectSinks(); } } -unsigned DemoReader::numOfChannels() +unsigned DemoReader::numChannels() const { - return _numOfChannels; + return _numChannels; } -void DemoReader::setNumOfChannels(unsigned value) +void DemoReader::setNumChannels(unsigned value) { - _numOfChannels = value; -} - -void DemoReader::pause(bool enabled) -{ - paused = enabled; + _settingsWidget.setNumChannels(value); } void DemoReader::demoTimerTimeout() @@ -77,13 +75,23 @@ void DemoReader::demoTimerTimeout() if (!paused) { - double* samples = new double[_numOfChannels]; - for (unsigned ci = 0; ci < _numOfChannels; ci++) + SamplePack samples(1, _numChannels); + for (unsigned ci = 0; ci < _numChannels; ci++) { // 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); + samples.data(ci)[0] = 4*sin(2*M_PI*double((ci+1)*count)/period)/((2*(ci+1))*M_PI); } - addData(samples, _numOfChannels); - delete[] samples; + feedOut(samples); } } + +void DemoReader::onNumChannelsChanged(unsigned value) +{ + _numChannels = value; + updateNumChannels(); +} + +void DemoReader::onDataReady() +{ + // intentionally empty, required by AbstractReader +} diff --git a/src/demoreader.h b/src/demoreader.h --- a/src/demoreader.h +++ b/src/demoreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,6 +23,7 @@ #include #include "abstractreader.h" +#include "demoreadersettings.h" /** * This is a special case of reader implementation and should be used @@ -38,30 +39,26 @@ class DemoReader : public AbstractReader Q_OBJECT public: - explicit DemoReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject* parent = 0); + explicit DemoReader(QIODevice* device, QObject* parent = 0); - /// Demo reader is an exception so this function returns NULL QWidget* settingsWidget(); - - unsigned numOfChannels(); - - void enable(bool enabled = true); + unsigned numChannels() const; + void enable(bool enabled = true) override; public slots: - void pause(bool); - - /// Sets the number of channels, this doesn't trigger a `numOfChannelsChanged` signal. - void setNumOfChannels(unsigned value); + void setNumChannels(unsigned value); private: - bool paused; - unsigned _numOfChannels; + DemoReaderSettings _settingsWidget; + + unsigned _numChannels; QTimer timer; int count; private slots: void demoTimerTimeout(); + void onNumChannelsChanged(unsigned value); + void onDataReady() override; }; #endif // DEMOREADER_H diff --git a/src/demoreadersettings.cpp b/src/demoreadersettings.cpp new file mode 100644 --- /dev/null +++ b/src/demoreadersettings.cpp @@ -0,0 +1,51 @@ +/* + Copyright © 2018 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 "demoreadersettings.h" +#include "ui_demoreadersettings.h" + +#include "utils.h" + +DemoReaderSettings::DemoReaderSettings(QWidget *parent) : + QWidget(parent), + ui(new Ui::DemoReaderSettings) +{ + ui->setupUi(this); + + connect(ui->spNumChannels, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged), + [this](int value) + { + emit numChannelsChanged(value); + }); +} + +DemoReaderSettings::~DemoReaderSettings() +{ + delete ui; +} + +unsigned DemoReaderSettings::numChannels() const +{ + return ui->spNumChannels->value(); +} + +void DemoReaderSettings::setNumChannels(unsigned value) +{ + ui->spNumChannels->setValue(value); +} diff --git a/src/demoreadersettings.h b/src/demoreadersettings.h new file mode 100644 --- /dev/null +++ b/src/demoreadersettings.h @@ -0,0 +1,48 @@ +/* + Copyright © 2018 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 DEMOREADERSETTINGS_H +#define DEMOREADERSETTINGS_H + +#include + +namespace Ui { +class DemoReaderSettings; +} + +class DemoReaderSettings : public QWidget +{ + Q_OBJECT + +public: + explicit DemoReaderSettings(QWidget *parent = 0); + ~DemoReaderSettings(); + + unsigned numChannels() const; + /// Doesn't signal `numChannelsChanged`. + void setNumChannels(unsigned value); + +private: + Ui::DemoReaderSettings *ui; + +signals: + void numChannelsChanged(unsigned); +}; + +#endif // DEMOREADERSETTINGS_H diff --git a/src/demoreadersettings.ui b/src/demoreadersettings.ui new file mode 100644 --- /dev/null +++ b/src/demoreadersettings.ui @@ -0,0 +1,83 @@ + + + DemoReaderSettings + + + + 0 + 0 + 444 + 141 + + + + Form + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Number Of Channels: + + + + + + + + 60 + 0 + + + + Select number of channels or set to 0 for Auto (determined from incoming data) + + + + + + false + + + 1 + + + 32 + + + 1 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Demo is enabled, exit demo for reader settings. + + + + + + + + diff --git a/src/framebuffer.cpp b/src/framebuffer.cpp deleted file mode 100644 --- a/src/framebuffer.cpp +++ /dev/null @@ -1,154 +0,0 @@ -/* - 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 "framebuffer.h" - -FrameBuffer::FrameBuffer(size_t size) -{ - _size = size; - data = new double[_size](); - headIndex = 0; - - _boundingRect.setCoords(0, 0, size, 0); -} - -FrameBuffer::~FrameBuffer() -{ - delete[] data; -} - -void FrameBuffer::resize(size_t size) -{ - int offset = size - _size; - if (offset == 0) return; - - double* newData = new double[size]; - - // move data to new array - int fill_start = offset > 0 ? offset : 0; - - for (int i = fill_start; i < int(size); i++) - { - newData[i] = sample(i - offset); - } - - // fill the beginning of the new data - if (fill_start > 0) - { - for (int i = 0; i < fill_start; i++) - { - newData[i] = 0; - } - } - - // data is ready, clean and re-point - delete data; - data = newData; - headIndex = 0; - _size = size; - - // update the bounding rectangle - _boundingRect.setRight(_size); -} - -void FrameBuffer::addSamples(double* samples, size_t size) -{ - unsigned shift = size; - if (shift < _size) - { - unsigned x = _size - headIndex; // distance of `head` to end - - if (shift <= x) // there is enough room at the end of array - { - for (size_t i = 0; i < shift; i++) - { - data[i+headIndex] = samples[i]; - } - - if (shift == x) // we used all the room at the end - { - headIndex = 0; - } - else - { - headIndex += shift; - } - } - else // there isn't enough room - { - for (size_t i = 0; i < x; i++) // fill the end part - { - data[i+headIndex] = samples[i]; - } - for (size_t i = 0; i < (shift-x); i++) // continue from the beginning - { - data[i] = samples[i+x]; - } - headIndex = shift-x; - } - } - else // number of new samples equal or bigger than current size - { - int x = shift - _size; - for (size_t i = 0; i < _size; i++) - { - data[i] = samples[i+x]; - } - headIndex = 0; - } - - // update bounding rectangle - double minValue = data[0]; - double maxValue = data[0]; - for (size_t i = 0; i < _size; i++) - { - if (data[i] > maxValue) - { - maxValue = data[i]; - } - else if (data[i] < minValue) - { - minValue = data[i]; - } - } - _boundingRect.setTop(minValue); - _boundingRect.setBottom(maxValue); -} - -void FrameBuffer::clear() -{ - for (size_t i=0; i < _size; i++) data[i] = 0.; -} - -size_t FrameBuffer::size() const -{ - return _size; -} - -QRectF FrameBuffer::boundingRect() const -{ - return _boundingRect; -} - -double FrameBuffer::sample(size_t i) const -{ - size_t index = headIndex + i; - if (index >= _size) index -= _size; - return data[index]; -} diff --git a/src/framebuffer.h b/src/framebuffer.h --- a/src/framebuffer.h +++ b/src/framebuffer.h @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,33 +17,48 @@ along with serialplot. If not, see . */ +// IMPORTANT NOTE: this file will be renamed to "framebuffer.h" when +// stream work is complete + #ifndef FRAMEBUFFER_H #define FRAMEBUFFER_H -#include -#include +struct Range +{ + double start, end; +}; +/// Abstract base class for all frame buffers. class FrameBuffer { public: - FrameBuffer(size_t size); - ~FrameBuffer(); - - void resize(size_t size); - void addSamples(double* samples, size_t size); - void clear(); // fill 0 + /// Placeholder virtual destructor + virtual ~FrameBuffer() {}; + /// Returns size of the buffer. + virtual unsigned size() const = 0; + /// Returns a sample from given index. + virtual double sample(unsigned i) const = 0; + /// Returns minimum and maximum of the buffer values. + virtual Range limits() const = 0; +}; - // QwtSeriesData related implementations - size_t size() const; - QRectF boundingRect() const; - double sample(size_t i) const; +/// Common base class for index and writable frame buffers +class ResizableBuffer : public FrameBuffer +{ +public: + /// Resize the buffer. + /// + /// @important Resizing to same value is an error. + virtual void resize(unsigned n) = 0; +}; -private: - size_t _size; // size of `data` - double* data; - size_t headIndex; // indicates the actual `0` index of the ring buffer - - QRectF _boundingRect; +/// Abstract base class for writable frame buffers +class WFrameBuffer : public ResizableBuffer +{ + /// Add samples to the buffer + virtual void addSamples(double* samples, unsigned n) = 0; + /// Reset all data to 0 + virtual void clear() = 0; }; #endif // FRAMEBUFFER_H diff --git a/src/framebufferseries.cpp b/src/framebufferseries.cpp --- a/src/framebufferseries.cpp +++ b/src/framebufferseries.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,14 +17,17 @@ along with serialplot. If not, see . */ +#include #include "framebufferseries.h" -FrameBufferSeries::FrameBufferSeries(FrameBuffer* buffer) +FrameBufferSeries::FrameBufferSeries(const FrameBuffer* buffer) { xAsIndex = true; _xmin = 0; _xmax = 1; _buffer = buffer; + int_index_start = 0; + int_index_end = _buffer->size(); } void FrameBufferSeries::setXAxis(bool asIndex, double xmin, double xmax) @@ -36,32 +39,56 @@ void FrameBufferSeries::setXAxis(bool as size_t FrameBufferSeries::size() const { - return _buffer->size(); + return int_index_end - int_index_start; } QPointF FrameBufferSeries::sample(size_t i) const { + i += int_index_start; if (xAsIndex) { return QPointF(i, _buffer->sample(i)); } else { - return QPointF(i * (_xmax - _xmin) / size() + _xmin, _buffer->sample(i)); + return QPointF(i * (_xmax - _xmin) / _buffer->size() + _xmin, _buffer->sample(i)); } } QRectF FrameBufferSeries::boundingRect() const { + QRectF rect; + auto yLim = _buffer->limits(); + rect.setBottom(yLim.start); + rect.setTop(yLim.end); if (xAsIndex) { - return _buffer->boundingRect(); + rect.setLeft(0); + rect.setRight(size()); } else { - auto rect = _buffer->boundingRect(); rect.setLeft(_xmin); rect.setRight(_xmax); - return rect; + } + return rect.normalized(); +} + +void FrameBufferSeries::setRectOfInterest(const QRectF& rect) +{ + if (xAsIndex) + { + int_index_start = floor(rect.left())-1; + int_index_end = ceil(rect.right())+1; } + else + { + double xsize = _xmax - _xmin; + size_t bsize = _buffer->size(); + int_index_start = floor(bsize * (rect.left()-_xmin) / xsize)-1; + int_index_end = ceil(bsize * (rect.right()-_xmin) / xsize)+1; + } + + int_index_start = std::max(int_index_start, 0); + int_index_end = std::min((int) _buffer->size(), int_index_end); } diff --git a/src/framebufferseries.h b/src/framebufferseries.h --- a/src/framebufferseries.h +++ b/src/framebufferseries.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -35,7 +35,7 @@ class FrameBufferSeries : public QwtSeriesData { public: - FrameBufferSeries(FrameBuffer* buffer); + FrameBufferSeries(const FrameBuffer* buffer); /// Behavior of X axis void setXAxis(bool asIndex, double xmin, double xmax); @@ -44,12 +44,16 @@ public: size_t size() const; QPointF sample(size_t i) const; QRectF boundingRect() const; + void setRectOfInterest(const QRectF& rect); private: - FrameBuffer* _buffer; + const FrameBuffer* _buffer; bool xAsIndex; double _xmin; double _xmax; + + int int_index_start; ///< starting index of "rectangle of interest" + int int_index_end; ///< ending index of "rectangle of interest" }; #endif // FRAMEBUFFERSERIES_H diff --git a/src/framedreader.cpp b/src/framedreader.cpp --- a/src/framedreader.cpp +++ b/src/framedreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,15 +23,14 @@ #include "framedreader.h" -FramedReader::FramedReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject* parent) : - AbstractReader(device, channelMan, recorder, parent) +FramedReader::FramedReader(QIODevice* device, QObject* parent) : + AbstractReader(device, parent) { paused = false; // initial settings settingsInvalid = 0; - _numOfChannels = _settingsWidget.numOfChannels(); + _numChannels = _settingsWidget.numOfChannels(); hasSizeByte = _settingsWidget.frameSize() == 0; frameSize = _settingsWidget.frameSize(); syncWord = _settingsWidget.syncWord(); @@ -63,32 +62,14 @@ FramedReader::FramedReader(QIODevice* de reset(); } -void FramedReader::enable(bool enabled) -{ - if (enabled) - { - connect(_device, &QIODevice::readyRead, - this, &FramedReader::onDataReady); - } - else - { - QObject::disconnect(_device, 0, this, 0); - } -} - QWidget* FramedReader::settingsWidget() { return &_settingsWidget; } -unsigned FramedReader::numOfChannels() +unsigned FramedReader::numChannels() const { - return _numOfChannels; -} - -void FramedReader::pause(bool enabled) -{ - paused = enabled; + return _numChannels; } void FramedReader::onNumberFormatChanged(NumberFormat numberFormat) @@ -145,7 +126,7 @@ void FramedReader::checkSettings() } // check if fixed frame size is multiple of a sample set size - if (!hasSizeByte && frameSize % (_numOfChannels * sampleSize) != 0) + if (!hasSizeByte && frameSize % (_numChannels * sampleSize) != 0) { settingsInvalid |= FRAMESIZE_INVALID; } @@ -163,7 +144,7 @@ void FramedReader::checkSettings() { QString errorMessage = QString("Frame size must be multiple of %1 (#channels * sample size)!")\ - .arg(_numOfChannels * sampleSize); + .arg(_numChannels * sampleSize); _settingsWidget.showMessage(errorMessage, true); } @@ -175,7 +156,7 @@ void FramedReader::checkSettings() void FramedReader::onNumOfChannelsChanged(unsigned value) { - _numOfChannels = value; + _numChannels = value; checkSettings(); reset(); emit numOfChannelsChanged(value); @@ -238,11 +219,11 @@ void FramedReader::onDataReady() qCritical() << "Frame size is 0!"; reset(); } - else if (frameSize % (_numOfChannels * sampleSize) != 0) + else if (frameSize % (_numChannels * sampleSize) != 0) { qCritical() << QString("Frame size is not multiple of %1 (#channels * sample size)!") \ - .arg(_numOfChannels * sampleSize); + .arg(_numChannels * sampleSize); reset(); } else @@ -287,14 +268,13 @@ void FramedReader::readFrameDataAndCheck } // a package is 1 set of samples for all channels - unsigned numOfPackagesToRead = frameSize / (_numOfChannels * sampleSize); - double* channelSamples = new double[numOfPackagesToRead * _numOfChannels]; - + unsigned numOfPackagesToRead = frameSize / (_numChannels * sampleSize); + SamplePack samples(numOfPackagesToRead, _numChannels); for (unsigned i = 0; i < numOfPackagesToRead; i++) { - for (unsigned int ci = 0; ci < _numOfChannels; ci++) + for (unsigned int ci = 0; ci < _numChannels; ci++) { - channelSamples[ci*numOfPackagesToRead+i] = (this->*readSample)(); + samples.data(ci)[i] = (this->*readSample)(); } } @@ -311,14 +291,12 @@ void FramedReader::readFrameDataAndCheck if (!checksumEnabled || checksumPassed) { // commit data - addData(channelSamples, numOfPackagesToRead*_numOfChannels); + feedOut(samples); } else { qCritical() << "Checksum failed! Received:" << rChecksum << "Calculated:" << calcChecksum; } - - delete[] channelSamples; } template double FramedReader::readSampleAs() diff --git a/src/framedreader.h b/src/framedreader.h --- a/src/framedreader.h +++ b/src/framedreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -33,19 +33,14 @@ class FramedReader : public AbstractRead Q_OBJECT public: - explicit FramedReader(QIODevice* device, ChannelManager* channelMan, - DataRecorder* recorder, QObject *parent = 0); + explicit FramedReader(QIODevice* device, QObject *parent = 0); QWidget* settingsWidget(); - unsigned numOfChannels(); - void enable(bool enabled = true); + unsigned numChannels() const; /// Stores settings into a `QSettings` void saveSettings(QSettings* settings); /// Loads settings from a `QSettings`. void loadSettings(QSettings* settings); -public slots: - void pause(bool); - private: /// bit wise fields for `settingsValid` member enum SettingInvalidFlag @@ -56,9 +51,8 @@ private: // settings related members FramedReaderSettings _settingsWidget; - unsigned _numOfChannels; + unsigned _numChannels; unsigned sampleSize; - bool paused; unsigned settingsInvalid; /// settings are all valid if this is 0, if not no reading is done QByteArray syncWord; bool checksumEnabled; @@ -86,7 +80,7 @@ private: void readFrameDataAndCheck(); private slots: - void onDataReady(); + void onDataReady() override; void onNumberFormatChanged(NumberFormat numberFormat); void onNumOfChannelsChanged(unsigned value); diff --git a/src/indexbuffer.cpp b/src/indexbuffer.cpp new file mode 100644 --- /dev/null +++ b/src/indexbuffer.cpp @@ -0,0 +1,49 @@ +/* + 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 "indexbuffer.h" + +IndexBuffer::IndexBuffer(unsigned n) +{ + _size = n; +} + +unsigned IndexBuffer::size() const +{ + return _size; +} + +void IndexBuffer::resize(unsigned n) +{ + _size = n; +} + +double IndexBuffer::sample(unsigned i) const +{ + Q_ASSERT(i < _size); + + return i; +} + +Range IndexBuffer::limits() const +{ + return Range{0, _size-1.}; +} diff --git a/src/indexbuffer.h b/src/indexbuffer.h new file mode 100644 --- /dev/null +++ b/src/indexbuffer.h @@ -0,0 +1,43 @@ +/* + Copyright © 2018 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 INDEXBUFFER_H +#define INDEXBUFFER_H + +#include "framebuffer.h" + +/// A simple frame buffer that simply returns requested index as +/// sample value. +/// +/// @note This buffer isn't for storing data. +class IndexBuffer : public ResizableBuffer +{ +public: + IndexBuffer(unsigned n); + + unsigned size() const; + double sample(unsigned i) const; + Range limits() const; + void resize(unsigned n); + +private: + unsigned _size; +}; + +#endif diff --git a/src/linindexbuffer.cpp b/src/linindexbuffer.cpp new file mode 100644 --- /dev/null +++ b/src/linindexbuffer.cpp @@ -0,0 +1,57 @@ + /* + 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 "linindexbuffer.h" + +LinIndexBuffer::LinIndexBuffer(unsigned n, Range lim) +{ + Q_ASSERT(n > 0); + + _size = n; + setLimits(lim); +} + +unsigned LinIndexBuffer::size() const +{ + return _size; +} + +double LinIndexBuffer::sample(unsigned i) const +{ + return _limits.start + i * _step; +} + +Range LinIndexBuffer::limits() const +{ + return _limits; +} + +void LinIndexBuffer::resize(unsigned n) +{ + _size = n; + setLimits(_limits); // called to update `_step` +} + +void LinIndexBuffer::setLimits(Range lim) +{ + _limits = lim; + _step = (lim.end - lim.start) / (_size-1); +} diff --git a/src/linindexbuffer.h b/src/linindexbuffer.h new file mode 100644 --- /dev/null +++ b/src/linindexbuffer.h @@ -0,0 +1,49 @@ + /* + Copyright © 2018 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 LININDEXBUFFER_H +#define LININDEXBUFFER_H + +#include "framebuffer.h" + +/// A dynamic frame buffer that start and end values can be set and +/// intermediate values are calculated linearly. +/// +/// @note This buffer isn't for storing data. +class LinIndexBuffer : public ResizableBuffer +{ +public: + LinIndexBuffer(unsigned n, Range lim); + LinIndexBuffer(unsigned n, double min, double max) : + LinIndexBuffer(n, {min, max}) {}; + + unsigned size() const; + double sample(unsigned i) const; + Range limits() const; + void resize(unsigned n); + /// Sets minimum and maximum sample values of the buffer. + void setLimits(Range lim); + +private: + unsigned _size; + Range _limits; + double _step; +}; + +#endif diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -35,6 +35,7 @@ #include #include +#include #include "framebufferseries.h" #include "utils.h" @@ -47,11 +48,14 @@ Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) #endif +// TODO: depends on tab insertion order, a better solution would be to use object names const QMap panelSettingMap({ {0, "Port"}, {1, "DataFormat"}, {2, "Plot"}, - {3, "Commands"} + {3, "Commands"}, + {4, "Record"}, + {5, "Log"} }); MainWindow::MainWindow(QWidget *parent) : @@ -59,15 +63,16 @@ MainWindow::MainWindow(QWidget *parent) ui(new Ui::MainWindow), aboutDialog(this), portControl(&serialPort), - channelMan(1, 1, this), - snapshotMan(this, &channelMan), + secondaryPlot(NULL), + snapshotMan(this, &stream), commandPanel(&serialPort), - dataFormatPanel(&serialPort, &channelMan, &recorder), - recordPanel(&recorder, &channelMan) + dataFormatPanel(&serialPort), + recordPanel(&stream), + updateCheckDialog(this) { ui->setupUi(this); - plotMan = new PlotManager(ui->plotArea, channelMan.infoModel()); + plotMan = new PlotManager(ui->plotArea, &plotMenu, &stream); ui->tabWidget->insertTab(0, &portControl, "Port"); ui->tabWidget->insertTab(1, &dataFormatPanel, "Data Format"); @@ -80,8 +85,8 @@ MainWindow::MainWindow(QWidget *parent) addToolBar(recordPanel.toolbar()); ui->plotToolBar->addAction(snapshotMan.takeSnapshotAction()); - ui->menuBar->insertMenu(ui->menuHelp->menuAction(), snapshotMan.menu()); - ui->menuBar->insertMenu(ui->menuHelp->menuAction(), commandPanel.menu()); + menuBar()->insertMenu(ui->menuHelp->menuAction(), snapshotMan.menu()); + menuBar()->insertMenu(ui->menuHelp->menuAction(), commandPanel.menu()); connect(&commandPanel, &CommandPanel::focusRequested, [this]() { @@ -95,23 +100,42 @@ MainWindow::MainWindow(QWidget *parent) setupAboutDialog(); // init view menu - for (auto a : plotMan->menuActions()) - { - ui->menuView->addAction(a); - } - - ui->menuView->addSeparator(); - - QMenu* tbMenu = ui->menuView->addMenu("Toolbars"); + ui->menuBar->insertMenu(ui->menuSecondary->menuAction(), &plotMenu); + plotMenu.addSeparator(); + QMenu* tbMenu = plotMenu.addMenu("Toolbars"); tbMenu->addAction(ui->plotToolBar->toggleViewAction()); tbMenu->addAction(portControl.toolBar()->toggleViewAction()); + // init secondary plot menu + auto group = new QActionGroup(this); + group->addAction(ui->actionVertical); + group->addAction(ui->actionHorizontal); + // init UI signals + // Secondary plot menu signals + connect(ui->actionBarPlot, &QAction::triggered, + this, &MainWindow::showBarPlot); + + connect(ui->actionVertical, &QAction::triggered, + [this](bool checked) + { + if (checked) ui->splitter->setOrientation(Qt::Vertical); + }); + + connect(ui->actionHorizontal, &QAction::triggered, + [this](bool checked) + { + if (checked) ui->splitter->setOrientation(Qt::Horizontal); + }); + // Help menu signals QObject::connect(ui->actionHelpAbout, &QAction::triggered, &aboutDialog, &QWidget::show); + QObject::connect(ui->actionCheckUpdate, &QAction::triggered, + &updateCheckDialog, &QWidget::show); + QObject::connect(ui->actionReportBug, &QAction::triggered, [](){QDesktopServices::openUrl(QUrl(BUG_REPORT_URL));}); @@ -147,28 +171,18 @@ MainWindow::MainWindow(QWidget *parent) connect(&plotControlPanel, &PlotControlPanel::xScaleChanged, plotMan, &PlotManager::setXAxis); + connect(&plotControlPanel, &PlotControlPanel::plotWidthChanged, + plotMan, &PlotManager::setPlotWidth); + + // plot toolbar signals QObject::connect(ui->actionClear, SIGNAL(triggered(bool)), this, SLOT(clearPlot())); QObject::connect(snapshotMan.takeSnapshotAction(), &QAction::triggered, plotMan, &PlotManager::flashSnapshotOverlay); - // init port signals - QObject::connect(&(this->serialPort), SIGNAL(error(QSerialPort::SerialPortError)), - this, SLOT(onPortError(QSerialPort::SerialPortError))); - - // init data format and reader - QObject::connect(&channelMan, &ChannelManager::dataAdded, - plotMan, &PlotManager::replot); - QObject::connect(ui->actionPause, &QAction::triggered, - &channelMan, &ChannelManager::pause); - - QObject::connect(&recordPanel, &RecordPanel::recordStarted, - &dataFormatPanel, &DataFormatPanel::startRecording); - - QObject::connect(&recordPanel, &RecordPanel::recordStopped, - &dataFormatPanel, &DataFormatPanel::stopRecording); + &stream, &Stream::pause); QObject::connect(ui->actionPause, &QAction::triggered, [this](bool enabled) @@ -195,26 +209,10 @@ MainWindow::MainWindow(QWidget *parent) connect(&serialPort, &QIODevice::aboutToClose, &recordPanel, &RecordPanel::onPortClose); - // init data arrays and plot + // init plot numOfSamples = plotControlPanel.numOfSamples(); - unsigned numOfChannels = dataFormatPanel.numOfChannels(); - - channelMan.setNumOfSamples(numOfSamples); - channelMan.setNumOfChannels(dataFormatPanel.numOfChannels()); - - connect(&dataFormatPanel, &DataFormatPanel::numOfChannelsChanged, - &channelMan, &ChannelManager::setNumOfChannels); - - connect(&channelMan, &ChannelManager::numOfChannelsChanged, - this, &MainWindow::onNumOfChannelsChanged); - - plotControlPanel.setChannelInfoModel(channelMan.infoModel()); - - // init curve list - for (unsigned int i = 0; i < numOfChannels; i++) - { - plotMan->addCurve(channelMan.channelName(i), channelMan.channelBuffer(i)); - } + stream.setNumSamples(numOfSamples); + plotControlPanel.setChannelInfoModel(stream.infoModel()); // init scales plotMan->setYAxis(plotControlPanel.autoScale(), @@ -222,14 +220,14 @@ MainWindow::MainWindow(QWidget *parent) plotMan->setXAxis(plotControlPanel.xAxisAsIndex(), plotControlPanel.xMin(), plotControlPanel.xMax()); plotMan->setNumOfSamples(numOfSamples); + plotMan->setPlotWidth(plotControlPanel.plotWidth()); // Init sps (sample per second) counter spsLabel.setText("0sps"); spsLabel.setToolTip("samples per second (per channel)"); ui->statusBar->addPermanentWidget(&spsLabel); - QObject::connect(&dataFormatPanel, - &DataFormatPanel::samplesPerSecondChanged, - this, &MainWindow::onSpsChanged); + connect(&sampleCounter, &SampleCounter::spsChanged, + this, &MainWindow::onSpsChanged); // init demo QObject::connect(ui->actionDemoMode, &QAction::toggled, @@ -238,6 +236,11 @@ MainWindow::MainWindow(QWidget *parent) QObject::connect(ui->actionDemoMode, &QAction::toggled, plotMan, &PlotManager::showDemoIndicator); + // init stream connections + connect(&dataFormatPanel, &DataFormatPanel::sourceChanged, + this, &MainWindow::onSourceChanged); + onSourceChanged(dataFormatPanel.activeSource()); + // load default settings QSettings settings("serialplot", "serialplot"); loadAllSettings(&settings); @@ -279,8 +282,7 @@ void MainWindow::closeEvent(QCloseEvent auto clickedButton = QMessageBox::warning( this, "Closing SerialPlot", "There are un-saved snapshots. If you close you will loose the data.", - QMessageBox::Discard | QMessageBox::Discard, - QMessageBox::Cancel); + QMessageBox::Discard, QMessageBox::Cancel); if (clickedButton == QMessageBox::Cancel) { event->ignore(); @@ -346,104 +348,29 @@ void MainWindow::onPortToggled(bool open ui->actionDemoMode->setEnabled(!open); } -void MainWindow::onPortError(QSerialPort::SerialPortError error) +void MainWindow::onSourceChanged(Source* source) { - 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! Error: " << serialPort.errorString(); - break; - default: - qCritical() << "Unhandled port error: " << error; - break; - } + source->connectSink(&stream); + source->connectSink(&sampleCounter); } void MainWindow::clearPlot() { - for (unsigned ci = 0; ci < channelMan.numOfChannels(); ci++) - { - channelMan.channelBuffer(ci)->clear(); - } + stream.clear(); plotMan->replot(); } void MainWindow::onNumOfSamplesChanged(int value) { numOfSamples = value; - channelMan.setNumOfSamples(value); + stream.setNumSamples(value); plotMan->replot(); } -void MainWindow::onNumOfChannelsChanged(unsigned value) +void MainWindow::onSpsChanged(float sps) { - unsigned int oldNum = plotMan->numOfCurves(); - unsigned numOfChannels = value; - - if (numOfChannels > oldNum) - { - // add new channels - for (unsigned int i = oldNum; i < numOfChannels; i++) - { - plotMan->addCurve(channelMan.channelName(i), channelMan.channelBuffer(i)); - } - } - else if(numOfChannels < oldNum) - { - plotMan->removeCurves(oldNum - numOfChannels); - } - - plotMan->replot(); -} - -void MainWindow::onSpsChanged(unsigned sps) -{ - spsLabel.setText(QString::number(sps) + "sps"); + int precision = sps < 1. ? 3 : 0; + spsLabel.setText(QString::number(sps, 'f', precision) + "sps"); } bool MainWindow::isDemoRunning() @@ -471,6 +398,48 @@ void MainWindow::enableDemo(bool enabled } } +void MainWindow::showSecondary(QWidget* wid) +{ + if (secondaryPlot != NULL) + { + secondaryPlot->deleteLater(); + } + + secondaryPlot = wid; + ui->splitter->addWidget(wid); + ui->splitter->setStretchFactor(0, 1); + ui->splitter->setStretchFactor(1, 0); +} + +void MainWindow::hideSecondary() +{ + if (secondaryPlot == NULL) + { + qFatal("Secondary plot doesn't exist!"); + } + + secondaryPlot->deleteLater(); + secondaryPlot = NULL; +} + +void MainWindow::showBarPlot(bool show) +{ + if (show) + { + auto plot = new BarPlot(&stream, &plotMenu); + plot->setYAxis(plotControlPanel.autoScale(), + plotControlPanel.yMin(), + plotControlPanel.yMax()); + connect(&plotControlPanel, &PlotControlPanel::yScaleChanged, + plot, &BarPlot::setYAxis); + showSecondary(plot); + } + else + { + hideSecondary(); + } +} + void MainWindow::onExportCsv() { bool wasPaused = ui->actionPause->isChecked(); @@ -492,7 +461,7 @@ void MainWindow::onExportCsv() PlotViewSettings MainWindow::viewSettings() const { - return plotMan->viewSettings(); + return plotMenu.viewSettings(); } void MainWindow::messageHandler(QtMsgType type, @@ -541,11 +510,12 @@ void MainWindow::saveAllSettings(QSettin saveMWSettings(settings); portControl.saveSettings(settings); dataFormatPanel.saveSettings(settings); - channelMan.saveSettings(settings); + stream.saveSettings(settings); plotControlPanel.saveSettings(settings); - plotMan->saveSettings(settings); + plotMenu.saveSettings(settings); commandPanel.saveSettings(settings); recordPanel.saveSettings(settings); + updateCheckDialog.saveSettings(settings); } void MainWindow::loadAllSettings(QSettings* settings) @@ -553,11 +523,12 @@ void MainWindow::loadAllSettings(QSettin loadMWSettings(settings); portControl.loadSettings(settings); dataFormatPanel.loadSettings(settings); - channelMan.loadSettings(settings); + stream.loadSettings(settings); plotControlPanel.loadSettings(settings); - plotMan->loadSettings(settings); + plotMenu.loadSettings(settings); commandPanel.loadSettings(settings); recordPanel.loadSettings(settings); + updateCheckDialog.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 © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -40,11 +40,12 @@ #include "plotcontrolpanel.h" #include "recordpanel.h" #include "ui_about_dialog.h" -#include "framebuffer.h" -#include "channelmanager.h" +#include "stream.h" #include "snapshotmanager.h" #include "plotmanager.h" -#include "datarecorder.h" +#include "plotmenu.h" +#include "updatecheckdialog.h" +#include "samplecounter.h" namespace Ui { class MainWindow; @@ -75,18 +76,28 @@ private: unsigned int numOfSamples; QList curves; - ChannelManager channelMan; + // ChannelManager channelMan; + Stream stream; PlotManager* plotMan; + QWidget* secondaryPlot; SnapshotManager snapshotMan; - DataRecorder recorder; // operated by `recordPanel` + SampleCounter sampleCounter; QLabel spsLabel; CommandPanel commandPanel; DataFormatPanel dataFormatPanel; RecordPanel recordPanel; PlotControlPanel plotControlPanel; + PlotMenu plotMenu; + UpdateCheckDialog updateCheckDialog; + /// Returns true if demo is running bool isDemoRunning(); + /// Display a secondary plot in the splitter, removing and + /// deleting previous one if it exists + void showSecondary(QWidget* wid); + /// Hide secondary plot + void hideSecondary(); /// Stores settings for all modules void saveAllSettings(QSettings* settings); /// Load settings for all modules @@ -101,14 +112,14 @@ private: private slots: void onPortToggled(bool open); - void onPortError(QSerialPort::SerialPortError error); - + void onSourceChanged(Source* source); void onNumOfSamplesChanged(int value); - void onNumOfChannelsChanged(unsigned value); void clearPlot(); - void onSpsChanged(unsigned sps); + void onSpsChanged(float sps); void enableDemo(bool enabled); + void showBarPlot(bool show); + void onExportCsv(); void onSaveSettings(); void onLoadSettings(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -14,15 +14,26 @@ SerialPlot - + - + - + 0 0 + + Qt::Horizontal + + + + + 0 + 0 + + + @@ -90,7 +101,7 @@ 0 0 653 - 27 + 24 @@ -99,6 +110,7 @@ + @@ -111,13 +123,17 @@ - + - &View + Secondary + + + + - + @@ -142,7 +158,8 @@ false - + + .. Pause @@ -156,7 +173,8 @@ - + + .. Clear @@ -221,6 +239,38 @@ Load Settings from a File + + + &Check Update + + + + + true + + + Bar Plot + + + + + true + + + Vertical + + + + + true + + + true + + + Horizontal + + diff --git a/src/plot.cpp b/src/plot.cpp --- a/src/plot.cpp +++ b/src/plot.cpp @@ -39,6 +39,7 @@ Plot::Plot(QWidget* parent) : isAutoScaled = true; symbolSize = 0; numOfSamples = 1; + plotWidth = 1; showSymbols = Plot::ShowSymbolsAuto; QObject::connect(&zoomer, &Zoomer::unzoomed, this, &Plot::unzoomed); @@ -73,6 +74,16 @@ Plot::Plot(QWidget* parent) : demoIndicator.setText(demoText); demoIndicator.hide(); demoIndicator.attach(this); + + // init no channels are visible indicator + QwtText noChannelText(" No Visible Channels "); + noChannelText.setColor(QColor("white")); + noChannelText.setBackgroundBrush(Qt::darkBlue); + noChannelText.setBorderRadius(4); + noChannelText.setRenderFlags(Qt::AlignHCenter | Qt::AlignVCenter); + noChannelIndicator.setText(noChannelText); + noChannelIndicator.hide(); + noChannelIndicator.attach(this); } Plot::~Plot() @@ -99,17 +110,18 @@ void Plot::setXAxis(double xMin, double _xMin = xMin; _xMax = xMax; + zoomer.setXLimits(xMin, xMax); zoomer.zoom(0); // unzoom // set axis - setAxisScale(QwtPlot::xBottom, xMin, xMax); + // setAxisScale(QwtPlot::xBottom, xMin, xMax); replot(); // Note: if we don't replot here scale at startup isn't set correctly // reset zoom base - auto base = zoomer.zoomBase(); - base.setLeft(xMin); - base.setRight(xMax); - zoomer.setZoomBase(base); + // auto base = zoomer.zoomBase(); + // base.setLeft(xMin); + // base.setRight(xMax); + // zoomer.setZoomBase(base); onXScaleChanged(); } @@ -163,6 +175,12 @@ void Plot::showDemoIndicator(bool show) replot(); } +void Plot::showNoChannel(bool show) +{ + noChannelIndicator.setVisible(show); + replot(); +} + void Plot::unzoom() { zoomer.zoom(0); @@ -195,36 +213,6 @@ void Plot::darkBackground(bool enabled) replot(); } -/* - 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 Plot::makeColor(unsigned int channelIndex) -{ - auto i = channelIndex; - - if (i < 4) - { - return QColor::fromHsv(360*i/4, 255, 230); - } - 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, 230); - } -} - void Plot::flashSnapshotOverlay(bool light) { if (snapshotOverlay != NULL) delete snapshotOverlay; @@ -285,7 +273,8 @@ void Plot::calcSymbolSize() auto scaleDist = sw->scaleDraw()->scaleMap().sDist(); auto fullScaleDist = zoomer.zoomBase().width(); auto zoomRate = fullScaleDist / scaleDist; - float samplesInView = numOfSamples / zoomRate; + float plotWidthNumSamp = abs(numOfSamples * plotWidth / (_xMax - _xMin)); + float samplesInView = plotWidthNumSamp / zoomRate; int symDisPx = round(paintDist / samplesInView); if (symDisPx < SYMBOL_SHOW_AT_WIDTH) @@ -331,3 +320,9 @@ void Plot::setNumOfSamples(unsigned valu numOfSamples = value; onXScaleChanged(); } + +void Plot::setPlotWidth(double width) +{ + plotWidth = width; + zoomer.setHViewSize(width); +} diff --git a/src/plot.h b/src/plot.h --- a/src/plot.h +++ b/src/plot.h @@ -50,13 +50,12 @@ public: Plot(QWidget* parent = 0); ~Plot(); - static QColor makeColor(unsigned int channelIndex); - public slots: void showGrid(bool show = true); void showMinorGrid(bool show = true); void showLegend(bool show = true); void showDemoIndicator(bool show = true); + void showNoChannel(bool show = true); void unzoom(); void darkBackground(bool enabled = true); void setYAxis(bool autoScaled, double yMin = 0, double yMax = 1); @@ -72,6 +71,8 @@ public slots: void setNumOfSamples(unsigned value); + void setPlotWidth(double width); + protected: /// update the display of symbols depending on `symbolSize` void updateSymbols(); @@ -81,6 +82,7 @@ private: double yMin, yMax; double _xMin, _xMax; unsigned numOfSamples; + double plotWidth; int symbolSize; Zoomer zoomer; ScaleZoomer sZoomer; @@ -88,6 +90,7 @@ private: PlotSnapshotOverlay* snapshotOverlay; QwtPlotLegendItem legend; QwtPlotTextLabel demoIndicator; + QwtPlotTextLabel noChannelIndicator; ShowSymbols showSymbols; void resetAxes(); diff --git a/src/plotcontrolpanel.cpp b/src/plotcontrolpanel.cpp --- a/src/plotcontrolpanel.cpp +++ b/src/plotcontrolpanel.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -29,7 +30,9 @@ #include "setting_defines.h" /// Confirm if #samples is being set to a value greater than this -const int NUMSAMPLES_CONFIRM_AT = 10000; +const int NUMSAMPLES_CONFIRM_AT = 1000000; +/// Precision used for channel info table numbers +const int DOUBLESP_PRECISION = 6; /// Used for scale range selection combobox struct Range @@ -40,6 +43,25 @@ struct Range Q_DECLARE_METATYPE(Range); +/// Used for customizing double precision in tables +class SpinBoxDelegate : public QStyledItemDelegate +{ +public: + QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const Q_DECL_OVERRIDE + { + auto w = QStyledItemDelegate::createEditor( + parent, option, index); + + auto sp = qobject_cast(w); + if (sp) + { + sp->setDecimals(DOUBLESP_PRECISION); + } + return w; + } +}; + PlotControlPanel::PlotControlPanel(QWidget *parent) : QWidget(parent), ui(new Ui::PlotControlPanel), @@ -47,10 +69,16 @@ PlotControlPanel::PlotControlPanel(QWidg resetNamesAct(tr("Reset Names"), this), resetColorsAct(tr("Reset Colors"), this), showAllAct(tr("Show All"), this), + hideAllAct(tr("Hide All"), this), + resetGainsAct(tr("Reset All Gain"), this), + resetOffsetsAct(tr("Reset All Offset"), this), resetMenu(tr("Reset Menu"), this) { ui->setupUi(this); + delegate = new SpinBoxDelegate(); + ui->tvChannelInfo->setItemDelegate(delegate); + warnNumOfSamples = true; // TODO: load from settings _numOfSamples = ui->spNumOfSamples->value(); @@ -86,9 +114,28 @@ PlotControlPanel::PlotControlPanel(QWidg connect(ui->spXmax, SIGNAL(valueChanged(double)), this, SLOT(onXScaleChanged())); + connect(ui->spXmax, static_cast(&QDoubleSpinBox::valueChanged), + [this](double v) + { + // set limit just a little below + double step = pow(10, -1 * ui->spXmin->decimals()); + ui->spXmin->setMaximum(v - step); + }); + connect(ui->spXmin, SIGNAL(valueChanged(double)), this, SLOT(onXScaleChanged())); + connect(ui->spXmin, static_cast(&QDoubleSpinBox::valueChanged), + [this](double v) + { + // set limit just a little above + double step = pow(10, -1 * ui->spXmax->decimals()); + ui->spXmax->setMinimum(v + step); + }); + + connect(ui->spPlotWidth, SIGNAL(valueChanged(int)), + this, SLOT(onPlotWidthChanged())); + // init scale range preset list for (int nbits = 8; nbits <= 24; nbits++) // signed binary formats { @@ -119,12 +166,19 @@ PlotControlPanel::PlotControlPanel(QWidg ui->colorSelector->setDisplayMode(color_widgets::ColorPreview::AllAlpha); ui->colorSelector->setDisabled(true); - // reset button + // reset buttons + resetAct.setToolTip(tr("Reset channel names and colors")); resetMenu.addAction(&resetNamesAct); resetMenu.addAction(&resetColorsAct); - resetMenu.addAction(&showAllAct); + resetMenu.addAction(&resetGainsAct); + resetMenu.addAction(&resetOffsetsAct); resetAct.setMenu(&resetMenu); ui->tbReset->setDefaultAction(&resetAct); + + showAllAct.setToolTip(tr("Show all channels")); + hideAllAct.setToolTip(tr("Hide all channels")); + ui->tbShowAll->setDefaultAction(&showAllAct); + ui->tbHideAll->setDefaultAction(&hideAllAct); } PlotControlPanel::~PlotControlPanel() @@ -275,6 +329,7 @@ void PlotControlPanel::onIndexChecked(bo emit xScaleChanged(false, ui->spXmin->value(), ui->spXmax->value()); } + emit plotWidthChanged(plotWidth()); } void PlotControlPanel::onXScaleChanged() @@ -282,9 +337,29 @@ void PlotControlPanel::onXScaleChanged() if (!xAxisAsIndex()) { emit xScaleChanged(false, ui->spXmin->value(), ui->spXmax->value()); + emit plotWidthChanged(plotWidth()); } } +double PlotControlPanel::plotWidth() const +{ + double value = ui->spPlotWidth->value(); + if (!xAxisAsIndex()) + { + // scale by xmin and xmax + auto xmax = ui->spXmax->value(); + auto xmin = ui->spXmin->value(); + double scale = (xmax - xmin) / _numOfSamples; + value *= scale; + } + return value; +} + +void PlotControlPanel::onPlotWidthChanged() +{ + emit plotWidthChanged(plotWidth()); +} + void PlotControlPanel::setChannelInfoModel(ChannelInfoModel* model) { ui->tvChannelInfo->setModel(model); @@ -355,13 +430,17 @@ void PlotControlPanel::setChannelInfoMod connect(&resetAct, &QAction::triggered, model, &ChannelInfoModel::resetInfos); connect(&resetNamesAct, &QAction::triggered, model, &ChannelInfoModel::resetNames); connect(&resetColorsAct, &QAction::triggered, model, &ChannelInfoModel::resetColors); - connect(&showAllAct, &QAction::triggered, model, &ChannelInfoModel::resetVisibility); + connect(&resetGainsAct, &QAction::triggered, model, &ChannelInfoModel::resetGains); + connect(&resetOffsetsAct, &QAction::triggered, model, &ChannelInfoModel::resetOffsets); + connect(&showAllAct, &QAction::triggered, [model]{model->resetVisibility(true);}); + connect(&hideAllAct, &QAction::triggered, [model]{model->resetVisibility(false);}); } void PlotControlPanel::saveSettings(QSettings* settings) { settings->beginGroup(SettingGroup_Plot); settings->setValue(SG_Plot_NumOfSamples, numOfSamples()); + settings->setValue(SG_Plot_PlotWidth, ui->spPlotWidth->value()); settings->setValue(SG_Plot_IndexAsX, xAxisAsIndex()); settings->setValue(SG_Plot_XMax, xMax()); settings->setValue(SG_Plot_XMin, xMin()); @@ -376,6 +455,8 @@ void PlotControlPanel::loadSettings(QSet settings->beginGroup(SettingGroup_Plot); ui->spNumOfSamples->setValue( settings->value(SG_Plot_NumOfSamples, numOfSamples()).toInt()); + ui->spPlotWidth->setValue( + settings->value(SG_Plot_PlotWidth, ui->spPlotWidth->value()).toInt()); ui->cbIndex->setChecked( settings->value(SG_Plot_IndexAsX, xAxisAsIndex()).toBool()); ui->spXmax->setValue(settings->value(SG_Plot_XMax, xMax()).toDouble()); diff --git a/src/plotcontrolpanel.h b/src/plotcontrolpanel.h --- a/src/plotcontrolpanel.h +++ b/src/plotcontrolpanel.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -24,6 +24,7 @@ #include #include #include +#include #include "channelinfomodel.h" @@ -46,6 +47,8 @@ public: bool xAxisAsIndex() const; double xMax() const; double xMin() const; + /// Returns the plot width adjusted for x axis scaling. + double plotWidth() const; void setChannelInfoModel(ChannelInfoModel* model); @@ -58,6 +61,7 @@ signals: void numOfSamplesChanged(int value); void yScaleChanged(bool autoScaled, double yMin = 0, double yMax = 1); void xScaleChanged(bool asIndex, double xMin = 0, double xMax = 1); + void plotWidthChanged(double width); private: Ui::PlotControlPanel *ui; @@ -67,8 +71,10 @@ private: /// User can disable this setting in the checkbox bool warnNumOfSamples; - QAction resetAct, resetNamesAct, resetColorsAct, showAllAct; + QAction resetAct, resetNamesAct, resetColorsAct, showAllAct, + hideAllAct, resetGainsAct, resetOffsetsAct; QMenu resetMenu; + QStyledItemDelegate* delegate; /// Show a confirmation dialog before setting #samples to a big value bool askNSConfirmation(int value); @@ -80,6 +86,7 @@ private slots: void onRangeSelected(); void onIndexChecked(bool checked); void onXScaleChanged(); + void onPlotWidthChanged(); }; #endif // PLOTCONTROLPANEL_H diff --git a/src/plotcontrolpanel.ui b/src/plotcontrolpanel.ui --- a/src/plotcontrolpanel.ui +++ b/src/plotcontrolpanel.ui @@ -6,8 +6,8 @@ 0 0 - 706 - 187 + 704 + 195 @@ -17,7 +17,7 @@ - + 0 0 @@ -41,14 +41,14 @@ - + 0 0 - 300 + 30000 170 @@ -62,6 +62,9 @@ + + 3 + QLayout::SetMaximumSize @@ -95,12 +98,32 @@ 1 - 20 + 1 + + + Show all channels + + + Show All + + + + + + + Hide all channels + + + Hide All + + + + Reset @@ -132,15 +155,30 @@ + + + - Number Of Samples: + Buffer Size: + + + 100 + 0 + + + + + 100 + 16777215 + + - length of X axis + Length of acquisition as number of samples false @@ -149,14 +187,14 @@ 2 - 1000000 + 10000000 1000 - + Index as X AXis @@ -166,7 +204,7 @@ - + @@ -237,7 +275,7 @@ - + Auto Scale Y Axis @@ -247,7 +285,7 @@ - + @@ -318,34 +356,59 @@ - + Select Range Preset: - + + + + + + + + Plot Width: + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Width of X axis as maximum number of samples that are shown in plot + + + false + + + 2 + + + 100000 + + + 1000 + + + - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 1 - 20 - - - - diff --git a/src/plotmanager.cpp b/src/plotmanager.cpp --- a/src/plotmanager.cpp +++ b/src/plotmanager.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,7 +17,7 @@ along with serialplot. If not, see . */ -#include +#include #include #include #include "qwt_symbol.h" @@ -27,25 +27,69 @@ #include "utils.h" #include "setting_defines.h" -PlotManager::PlotManager(QWidget* plotArea, ChannelInfoModel* infoModel, QObject *parent) : - QObject(parent), - _plotArea(plotArea), - showGridAction("&Grid", this), - showMinorGridAction("&Minor Grid", this), - unzoomAction("&Unzoom", this), - darkBackgroundAction("&Dark Background", this), - showLegendAction("&Legend", this), - showMultiAction("Multi &Plot", this), - setSymbolsAction("Symbols", this) +PlotManager::PlotManager(QWidget* plotArea, PlotMenu* menu, + const Stream* stream, QObject* parent) : + QObject(parent) { + construct(plotArea, menu); + _stream = stream; + if (_stream == NULL) return; + + // connect to ChannelInfoModel + infoModel = _stream->infoModel(); + connect(infoModel, &QAbstractItemModel::dataChanged, + this, &PlotManager::onChannelInfoChanged); + connect(infoModel, &QAbstractItemModel::modelReset, + [this]() + { + onChannelInfoChanged(infoModel->index(0, 0), // start + infoModel->index(infoModel->rowCount()-1, 0), // end + {}); // roles ignored + }); + + connect(stream, &Stream::numChannelsChanged, this, &PlotManager::onNumChannelsChanged); + connect(stream, &Stream::dataAdded, this, &PlotManager::replot); + + // add initial curves if any? + for (unsigned int i = 0; i < stream->numChannels(); i++) + { + addCurve(stream->channel(i)->name(), stream->channel(i)->yData()); + } + +} + +PlotManager::PlotManager(QWidget* plotArea, PlotMenu* menu, + Snapshot* snapshot, QObject *parent) : + QObject(parent) +{ + construct(plotArea, menu); + + setNumOfSamples(snapshot->numSamples()); + setPlotWidth(snapshot->numSamples()); + infoModel = snapshot->infoModel(); + + for (unsigned ci = 0; ci < snapshot->numChannels(); ci++) + { + addCurve(snapshot->channelName(ci), snapshot->yData[ci]); + } + + connect(infoModel, &QAbstractItemModel::dataChanged, + this, &PlotManager::onChannelInfoChanged); +} + +void PlotManager::construct(QWidget* plotArea, PlotMenu* menu) +{ + _menu = menu; + _plotArea = plotArea; _autoScaled = true; _yMin = 0; _yMax = 1; _xAxisAsIndex = true; isDemoShown = false; - _infoModel = infoModel; _numOfSamples = 1; + _plotWidth = 1; showSymbols = Plot::ShowSymbolsAuto; + emptyPlot = NULL; // initalize layout and single widget isMulti = false; @@ -53,93 +97,28 @@ PlotManager::PlotManager(QWidget* plotAr setupLayout(isMulti); addPlotWidget(); - // initialize menu actions - showGridAction.setToolTip("Show Grid"); - showMinorGridAction.setToolTip("Show Minor Grid"); - unzoomAction.setToolTip("Unzoom the Plot"); - darkBackgroundAction.setToolTip("Enable Dark Plot Background"); - showLegendAction.setToolTip("Display the Legend on Plot"); - showMultiAction.setToolTip("Display All Channels Separately"); - setSymbolsAction.setToolTip("Show/Hide symbols"); - - showGridAction.setShortcut(QKeySequence("G")); - showMinorGridAction.setShortcut(QKeySequence("M")); - - showGridAction.setCheckable(true); - showMinorGridAction.setCheckable(true); - darkBackgroundAction.setCheckable(true); - showLegendAction.setCheckable(true); - showMultiAction.setCheckable(true); - - showGridAction.setChecked(false); - showMinorGridAction.setChecked(false); - darkBackgroundAction.setChecked(false); - showLegendAction.setChecked(true); - showMultiAction.setChecked(false); - - showMinorGridAction.setEnabled(false); + // connect to menu + connect(menu, &PlotMenu::symbolShowChanged, this, &PlotManager:: setSymbols); - // setup symbols menu - setSymbolsAutoAct = setSymbolsMenu.addAction("Show When Zoomed"); - setSymbolsAutoAct->setCheckable(true); - setSymbolsAutoAct->setChecked(true); - connect(setSymbolsAutoAct, SELECT::OVERLOAD_OF(&QAction::triggered), - [this](bool checked) - { - if (checked) setSymbols(Plot::ShowSymbolsAuto); - }); - setSymbolsShowAct = setSymbolsMenu.addAction("Always Show"); - setSymbolsShowAct->setCheckable(true); - connect(setSymbolsShowAct, SELECT::OVERLOAD_OF(&QAction::triggered), - [this](bool checked) - { - if (checked) setSymbols(Plot::ShowSymbolsShow); - }); - setSymbolsHideAct = setSymbolsMenu.addAction("Always Hide"); - setSymbolsHideAct->setCheckable(true); - connect(setSymbolsHideAct, SELECT::OVERLOAD_OF(&QAction::triggered), - [this](bool checked) - { - if (checked) setSymbols(Plot::ShowSymbolsHide); - }); - setSymbolsAction.setMenu(&setSymbolsMenu); - - // add symbol actions to same group so that they appear as radio buttons - auto group = new QActionGroup(this); - group->addAction(setSymbolsAutoAct); - group->addAction(setSymbolsShowAct); - group->addAction(setSymbolsHideAct); + connect(&menu->showGridAction, SELECT::OVERLOAD_OF(&QAction::toggled), + this, &PlotManager::showGrid); + connect(&menu->showMinorGridAction, SELECT::OVERLOAD_OF(&QAction::toggled), + this, &PlotManager::showMinorGrid); + connect(&menu->darkBackgroundAction, SELECT::OVERLOAD_OF(&QAction::toggled), + this, &PlotManager::darkBackground); + connect(&menu->showLegendAction, SELECT::OVERLOAD_OF(&QAction::toggled), + this, &PlotManager::showLegend); + connect(&menu->showMultiAction, SELECT::OVERLOAD_OF(&QAction::toggled), + this, &PlotManager::setMulti); + connect(&menu->unzoomAction, &QAction::triggered, + this, &PlotManager::unzoom); - connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered), - this, &PlotManager::showGrid); - connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered), - &showMinorGridAction, &QAction::setEnabled); - connect(&showMinorGridAction, SELECT::OVERLOAD_OF(&QAction::triggered), - this, &PlotManager::showMinorGrid); - connect(&unzoomAction, &QAction::triggered, this, &PlotManager::unzoom); - connect(&darkBackgroundAction, SELECT::OVERLOAD_OF(&QAction::triggered), - this, &PlotManager::darkBackground); - connect(&showLegendAction, SELECT::OVERLOAD_OF(&QAction::triggered), - this, &PlotManager::showLegend); - connect(&showLegendAction, SELECT::OVERLOAD_OF(&QAction::triggered), - this, &PlotManager::showLegend); - connect(&showMultiAction, SELECT::OVERLOAD_OF(&QAction::triggered), - this, &PlotManager::setMulti); - - // connect to channel info model - if (_infoModel != NULL) // TODO: remove when snapshots have infomodel - { - connect(_infoModel, &QAbstractItemModel::dataChanged, - this, &PlotManager::onChannelInfoChanged); - - connect(_infoModel, &QAbstractItemModel::modelReset, - [this]() - { - onChannelInfoChanged(_infoModel->index(0, 0), // start - _infoModel->index(_infoModel->rowCount()-1, 0), // end - {}); // roles ignored - }); - } + // initial settings from menu actions + showGrid(menu->showGridAction.isChecked()); + showMinorGrid(menu->showMinorGridAction.isChecked()); + darkBackground(menu->darkBackgroundAction.isChecked()); + showLegend(menu->showLegendAction.isChecked()); + setMulti(menu->showMultiAction.isChecked()); } PlotManager::~PlotManager() @@ -156,6 +135,28 @@ PlotManager::~PlotManager() } if (scrollArea != NULL) delete scrollArea; + if (emptyPlot != NULL) delete emptyPlot; +} + +void PlotManager::onNumChannelsChanged(unsigned value) +{ + unsigned int oldNum = numOfCurves(); + unsigned numOfChannels = value; + + if (numOfChannels > oldNum) + { + // add new channels + for (unsigned int i = oldNum; i < numOfChannels; i++) + { + addCurve(_stream->channel(i)->name(), _stream->channel(i)->yData()); + } + } + else if(numOfChannels < oldNum) + { + removeCurves(oldNum - numOfChannels); + } + + replot(); } void PlotManager::onChannelInfoChanged(const QModelIndex &topLeft, @@ -189,6 +190,8 @@ void PlotManager::onChannelInfoChanged(c } } + checkNoVisChannels(); + // replot single widget if (!isMulti) { @@ -198,6 +201,20 @@ void PlotManager::onChannelInfoChanged(c } } +void PlotManager::checkNoVisChannels() +{ + // if all channels are hidden show indicator + bool allhidden = std::none_of(curves.cbegin(), curves.cend(), + [](QwtPlotCurve* c) {return c->isVisible();}); + + plotWidgets[0]->showNoChannel(allhidden); + if (isMulti) + { + plotWidgets[0]->showNoChannel(allhidden); + plotWidgets[0]->setVisible(true); + } +} + void PlotManager::setMulti(bool enabled) { if (enabled == isMulti) return; @@ -224,7 +241,9 @@ void PlotManager::setMulti(bool enabled) // add new widgets and attach for (auto curve : curves) { - curve->attach(addPlotWidget()); + auto plot = addPlotWidget(); + plot->setVisible(curve->isVisible()); + curve->attach(plot); } } else @@ -238,6 +257,12 @@ void PlotManager::setMulti(bool enabled) curve->attach(plot); } } + + // will skip if no plot widgets exist (can happen during constructor) + if (plotWidgets.length()) + { + checkNoVisChannels(); + } } void PlotManager::setupLayout(bool multiPlot) @@ -284,15 +309,17 @@ Plot* PlotManager::addPlotWidget() plotWidgets.append(plot); layout->addWidget(plot); - plot->darkBackground(darkBackgroundAction.isChecked()); - plot->showGrid(showGridAction.isChecked()); - plot->showMinorGrid(showMinorGridAction.isChecked()); - plot->showLegend(showLegendAction.isChecked()); + plot->darkBackground(_menu->darkBackgroundAction.isChecked()); + plot->showGrid(_menu->showGridAction.isChecked()); + plot->showMinorGrid(_menu->showMinorGridAction.isChecked()); + plot->showLegend(_menu->showLegendAction.isChecked()); + plot->setSymbols(_menu->showSymbols()); + plot->showDemoIndicator(isDemoShown); plot->setYAxis(_autoScaled, _yMin, _yMax); plot->setNumOfSamples(_numOfSamples); - plot->setSymbols(showSymbols); + plot->setPlotWidth(_plotWidth); if (_xAxisAsIndex) { plot->setXAxis(0, _numOfSamples); @@ -305,7 +332,7 @@ Plot* PlotManager::addPlotWidget() return plot; } -void PlotManager::addCurve(QString title, FrameBuffer* buffer) +void PlotManager::addCurve(QString title, const FrameBuffer* buffer) { auto curve = new QwtPlotCurve(title); auto series = new FrameBufferSeries(buffer); @@ -314,20 +341,13 @@ void PlotManager::addCurve(QString title _addCurve(curve); } -void PlotManager::addCurve(QString title, QVector data) -{ - auto curve = new QwtPlotCurve(title); - curve->setSamples(data); - _addCurve(curve); -} - void PlotManager::_addCurve(QwtPlotCurve* curve) { // store and init the curve curves.append(curve); unsigned index = curves.size()-1; - auto color = _infoModel->color(index); + auto color = infoModel->color(index); curve->setPen(color); // create the plot for the curve if we are on multi display @@ -367,13 +387,6 @@ unsigned PlotManager::numOfCurves() return curves.size(); } -void PlotManager::setTitle(unsigned index, QString title) -{ - curves[index]->setTitle(title); - - plotWidget(index)->replot(); -} - Plot* PlotManager::plotWidget(unsigned curveIndex) { if (isMulti) @@ -394,19 +407,6 @@ void PlotManager::replot() } } -QList PlotManager::menuActions() -{ - QList actions; - actions << &showGridAction; - actions << &showMinorGridAction; - actions << &unzoomAction; - actions << &darkBackgroundAction; - actions << &showLegendAction; - actions << &showMultiAction; - actions << &setSymbolsAction; - return actions; -} - void PlotManager::showGrid(bool show) { for (auto plot : plotWidgets) @@ -483,7 +483,6 @@ void PlotManager::setXAxis(bool asIndex, _xMax = xMax; for (auto curve : curves) { - // TODO: what happens when addCurve(QVector) is used? FrameBufferSeries* series = static_cast(curve->data()); series->setXAxis(asIndex, xMin, xMax); } @@ -505,7 +504,7 @@ void PlotManager::flashSnapshotOverlay() { for (auto plot : plotWidgets) { - plot->flashSnapshotOverlay(darkBackgroundAction.isChecked()); + plot->flashSnapshotOverlay(_menu->darkBackgroundAction.isChecked()); } } @@ -519,114 +518,11 @@ void PlotManager::setNumOfSamples(unsign } } -PlotViewSettings PlotManager::viewSettings() const -{ - return PlotViewSettings( - { - showGridAction.isChecked(), - showMinorGridAction.isChecked(), - darkBackgroundAction.isChecked(), - showLegendAction.isChecked(), - showMultiAction.isChecked(), - showSymbols - }); -} - -void PlotManager::setViewSettings(const PlotViewSettings& settings) +void PlotManager::setPlotWidth(double width) { - showGridAction.setChecked(settings.showGrid); - showGrid(settings.showGrid); - showMinorGridAction.setChecked(settings.showMinorGrid); - showMinorGrid(settings.showMinorGrid); - darkBackgroundAction.setChecked(settings.darkBackground); - darkBackground(settings.darkBackground); - showLegendAction.setChecked(settings.showLegend); - showLegend(settings.showLegend); - showMultiAction.setChecked(settings.showMulti); - setMulti(settings.showMulti); - - setSymbols(settings.showSymbols); - if (showSymbols == Plot::ShowSymbolsAuto) + _plotWidth = width; + for (auto plot : plotWidgets) { - setSymbolsAutoAct->setChecked(true); - } - else if (showSymbols == Plot::ShowSymbolsShow) - { - setSymbolsShowAct->setChecked(true); - } - else - { - setSymbolsHideAct->setChecked(true); + plot->setPlotWidth(width); } } - -void PlotManager::saveSettings(QSettings* settings) -{ - settings->beginGroup(SettingGroup_Plot); - settings->setValue(SG_Plot_DarkBackground, darkBackgroundAction.isChecked()); - settings->setValue(SG_Plot_Grid, showGridAction.isChecked()); - settings->setValue(SG_Plot_MinorGrid, showMinorGridAction.isChecked()); - settings->setValue(SG_Plot_Legend, showLegendAction.isChecked()); - settings->setValue(SG_Plot_MultiPlot, showMultiAction.isChecked()); - - QString showSymbolsStr; - if (showSymbols == Plot::ShowSymbolsAuto) - { - showSymbolsStr = "auto"; - } - else if (showSymbols == Plot::ShowSymbolsShow) - { - showSymbolsStr = "show"; - } - else - { - showSymbolsStr = "hide"; - } - settings->setValue(SG_Plot_Symbols, showSymbolsStr); - - settings->endGroup(); -} - -void PlotManager::loadSettings(QSettings* settings) -{ - settings->beginGroup(SettingGroup_Plot); - darkBackgroundAction.setChecked( - settings->value(SG_Plot_DarkBackground, darkBackgroundAction.isChecked()).toBool()); - darkBackground(darkBackgroundAction.isChecked()); - showGridAction.setChecked( - settings->value(SG_Plot_Grid, showGridAction.isChecked()).toBool()); - showGrid(showGridAction.isChecked()); - showMinorGridAction.setChecked( - settings->value(SG_Plot_MinorGrid, showMinorGridAction.isChecked()).toBool()); - showMinorGridAction.setEnabled(showGridAction.isChecked()); - showMinorGrid(showMinorGridAction.isChecked()); - showLegendAction.setChecked( - settings->value(SG_Plot_Legend, showLegendAction.isChecked()).toBool()); - showLegend(showLegendAction.isChecked()); - showMultiAction.setChecked( - settings->value(SG_Plot_MultiPlot, showMultiAction.isChecked()).toBool()); - setMulti(showMultiAction.isChecked()); - - QString showSymbolsStr = settings->value(SG_Plot_Symbols, QString()).toString(); - if (showSymbolsStr == "auto") - { - setSymbols(Plot::ShowSymbolsAuto); - setSymbolsAutoAct->setChecked(true); - } - else if (showSymbolsStr == "show") - { - setSymbols(Plot::ShowSymbolsShow); - setSymbolsShowAct->setChecked(true); - } - else if (showSymbolsStr == "hide") - { - setSymbols(Plot::ShowSymbolsHide); - setSymbolsHideAct->setChecked(true); - } - else - { - qCritical() << "Invalid symbol setting:" << showSymbolsStr; - } - - settings->endGroup(); -} diff --git a/src/plotmanager.h b/src/plotmanager.h --- a/src/plotmanager.h +++ b/src/plotmanager.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -26,52 +26,34 @@ #include #include #include -#include #include #include #include "plot.h" #include "framebufferseries.h" -#include "channelinfomodel.h" - -struct PlotViewSettings -{ - bool showGrid; - bool showMinorGrid; - bool darkBackground; - bool showLegend; - bool showMulti; - Plot::ShowSymbols showSymbols; -}; +#include "stream.h" +#include "snapshot.h" +#include "plotmenu.h" class PlotManager : public QObject { Q_OBJECT public: - explicit PlotManager(QWidget* plotArea, ChannelInfoModel* infoModel = NULL, QObject *parent = 0); + explicit PlotManager(QWidget* plotArea, PlotMenu* menu, + const Stream* stream = NULL, + QObject *parent = 0); + explicit PlotManager(QWidget* plotArea, PlotMenu* menu, + Snapshot* snapshot, + QObject *parent = 0); ~PlotManager(); /// Add a new curve with title and buffer. A color is /// automatically chosen for curve. - void addCurve(QString title, FrameBuffer* buffer); - /// Alternative of `addCurve` for static curve data (snapshots). - void addCurve(QString title, QVector data); - /// Set the displayed title for a curve - void setTitle(unsigned index, QString title); + void addCurve(QString title, const FrameBuffer* buffer); /// Removes curves from the end void removeCurves(unsigned number); /// Returns current number of curves known by plot manager unsigned numOfCurves(); - /// Returns the list of actions to be inserted into the `View` menu - QList menuActions(); - /// Returns current status of menu actions - PlotViewSettings viewSettings() const; - /// Set the current state of view - void setViewSettings(const PlotViewSettings& settings); - /// Stores plot settings into a `QSettings`. - void saveSettings(QSettings* settings); - /// Loads plot settings from a `QSettings`. - void loadSettings(QSettings* settings); public slots: /// Enable/Disable multiple plot display @@ -88,15 +70,20 @@ public slots: void flashSnapshotOverlay(); /// Should be called to update zoom base void setNumOfSamples(unsigned value); + /// Maximum width of X axis (limit of hscroll) + void setPlotWidth(double width); private: bool isMulti; QWidget* _plotArea; + PlotMenu* _menu; QVBoxLayout* layout; ///< layout of the `plotArea` QScrollArea* scrollArea; QList curves; QList plotWidgets; - ChannelInfoModel* _infoModel; + Plot* emptyPlot; ///< for displaying when all channels are hidden + const Stream* _stream; ///< attached stream, can be `NULL` + const ChannelInfoModel* infoModel; bool isDemoShown; bool _autoScaled; double _yMin; @@ -105,21 +92,12 @@ private: double _xMin; double _xMax; unsigned _numOfSamples; + double _plotWidth; Plot::ShowSymbols showSymbols; - // menu actions - QAction showGridAction; - QAction showMinorGridAction; - QAction unzoomAction; - QAction darkBackgroundAction; - QAction showLegendAction; - QAction showMultiAction; - QAction setSymbolsAction; - QMenu setSymbolsMenu; - QAction* setSymbolsAutoAct; - QAction* setSymbolsShowAct; - QAction* setSymbolsHideAct; - + /// Common constructor + void construct(QWidget* plotArea, PlotMenu* menu); + /// Setups the layout for multi or single plot void setupLayout(bool multiPlot); /// Inserts a new plot widget to the current layout. Plot* addPlotWidget(); @@ -127,7 +105,8 @@ private: Plot* plotWidget(unsigned curveIndex); /// Common part of overloaded `addCurve` functions void _addCurve(QwtPlotCurve* curve); - void setSymbols(Plot::ShowSymbols shown); + /// Check and make sure "no visible channels" text is shown + void checkNoVisChannels(); private slots: void showGrid(bool show = true); @@ -135,7 +114,9 @@ private slots: void showLegend(bool show = true); void unzoom(); void darkBackground(bool enabled = true); + void setSymbols(Plot::ShowSymbols shown); + void onNumChannelsChanged(unsigned value); void onChannelInfoChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector & roles = QVector ()); diff --git a/src/plotmenu.cpp b/src/plotmenu.cpp new file mode 100644 --- /dev/null +++ b/src/plotmenu.cpp @@ -0,0 +1,224 @@ +/* + 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 "plotmenu.h" +#include "setting_defines.h" +#include "utils.h" + +PlotMenu::PlotMenu(QWidget* parent) : + QMenu(tr("&View"), parent), + showGridAction("&Grid", this), + showMinorGridAction("&Minor Grid", this), + unzoomAction("&Unzoom", this), + darkBackgroundAction("&Dark Background", this), + showLegendAction("&Legend", this), + showMultiAction("Multi &Plot", this), + setSymbolsAction("&Symbols", this), + setSymbolsAutoAct("Show When &Zoomed", this), + setSymbolsShowAct("Always &Show", this), + setSymbolsHideAct("Always &Hide", this) +{ + showGridAction.setToolTip("Show Grid"); + showMinorGridAction.setToolTip("Show Minor Grid"); + unzoomAction.setToolTip("Unzoom the Plot"); + darkBackgroundAction.setToolTip("Enable Dark Plot Background"); + showLegendAction.setToolTip("Display the Legend on Plot"); + showMultiAction.setToolTip("Display All Channels Separately"); + setSymbolsAction.setToolTip("Show/Hide symbols"); + + showGridAction.setShortcut(QKeySequence("G")); + showMinorGridAction.setShortcut(QKeySequence("M")); + + showGridAction.setCheckable(true); + showMinorGridAction.setCheckable(true); + darkBackgroundAction.setCheckable(true); + showLegendAction.setCheckable(true); + showMultiAction.setCheckable(true); + + showGridAction.setChecked(false); + showMinorGridAction.setChecked(false); + darkBackgroundAction.setChecked(false); + showLegendAction.setChecked(true); + showMultiAction.setChecked(false); + + // minor grid is only enabled when _major_ grid is enabled + showMinorGridAction.setEnabled(false); + connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered), + &showMinorGridAction, &QAction::setEnabled); + + // setup set symbols menu + setSymbolsMenu.addAction(&setSymbolsAutoAct); + setSymbolsAutoAct.setCheckable(true); + setSymbolsAutoAct.setChecked(true); + connect(&setSymbolsAutoAct, SELECT::OVERLOAD_OF(&QAction::triggered), + [this](bool checked) + { + if (checked) emit symbolShowChanged(Plot::ShowSymbolsAuto); + }); + + setSymbolsMenu.addAction(&setSymbolsShowAct); + setSymbolsShowAct.setCheckable(true); + connect(&setSymbolsShowAct, SELECT::OVERLOAD_OF(&QAction::triggered), + [this](bool checked) + { + if (checked) symbolShowChanged(Plot::ShowSymbolsShow); + }); + + setSymbolsMenu.addAction(&setSymbolsHideAct); + setSymbolsHideAct.setCheckable(true); + connect(&setSymbolsHideAct, SELECT::OVERLOAD_OF(&QAction::triggered), + [this](bool checked) + { + if (checked) symbolShowChanged(Plot::ShowSymbolsHide); + }); + + // add symbol actions to same group so that they appear as radio buttons + auto group = new QActionGroup(this); + group->addAction(&setSymbolsAutoAct); + group->addAction(&setSymbolsShowAct); + group->addAction(&setSymbolsHideAct); + + setSymbolsAction.setMenu(&setSymbolsMenu); + + // add all actions to create this menu + addAction(&showGridAction); + addAction(&showMinorGridAction); + addAction(&unzoomAction); + addAction(&darkBackgroundAction); + addAction(&showLegendAction); + addAction(&showMultiAction); + addAction(&setSymbolsAction); +} + +PlotMenu::PlotMenu(PlotViewSettings s, QWidget* parent) : + PlotMenu(parent) +{ + showGridAction.setChecked(s.showGrid); + showMinorGridAction.setChecked(s.showMinorGrid); + darkBackgroundAction.setChecked(s.darkBackground); + showLegendAction.setChecked(s.showLegend); + showMultiAction.setChecked(s.showMulti); + switch (s.showSymbols) + { + case Plot::ShowSymbolsAuto: + setSymbolsAutoAct.setChecked(true); + break; + case Plot::ShowSymbolsShow: + setSymbolsShowAct.setChecked(true); + break; + case Plot::ShowSymbolsHide: + setSymbolsHideAct.setChecked(true); + break; + } +} + +PlotViewSettings PlotMenu::viewSettings() const +{ + return PlotViewSettings( + { + showGridAction.isChecked(), + showMinorGridAction.isChecked(), + darkBackgroundAction.isChecked(), + showLegendAction.isChecked(), + showMultiAction.isChecked(), + showSymbols() + }); +} + +Plot::ShowSymbols PlotMenu::showSymbols() const +{ + if (setSymbolsAutoAct.isChecked()) + { + return Plot::ShowSymbolsAuto; + } + else if (setSymbolsShowAct.isChecked()) + { + return Plot::ShowSymbolsShow; + } + else // setSymbolsHideAct.isChecked() + { + return Plot::ShowSymbolsHide; + } +} + +void PlotMenu::saveSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Plot); + settings->setValue(SG_Plot_DarkBackground, darkBackgroundAction.isChecked()); + settings->setValue(SG_Plot_Grid, showGridAction.isChecked()); + settings->setValue(SG_Plot_MinorGrid, showMinorGridAction.isChecked()); + settings->setValue(SG_Plot_Legend, showLegendAction.isChecked()); + settings->setValue(SG_Plot_MultiPlot, showMultiAction.isChecked()); + + QString showSymbolsStr; + if (showSymbols() == Plot::ShowSymbolsAuto) + { + showSymbolsStr = "auto"; + } + else if (showSymbols() == Plot::ShowSymbolsShow) + { + showSymbolsStr = "show"; + } + else + { + showSymbolsStr = "hide"; + } + settings->setValue(SG_Plot_Symbols, showSymbolsStr); + + settings->endGroup(); +} + +void PlotMenu::loadSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Plot); + darkBackgroundAction.setChecked( + settings->value(SG_Plot_DarkBackground, darkBackgroundAction.isChecked()).toBool()); + showGridAction.setChecked( + settings->value(SG_Plot_Grid, showGridAction.isChecked()).toBool()); + showMinorGridAction.setChecked( + settings->value(SG_Plot_MinorGrid, showMinorGridAction.isChecked()).toBool()); + showMinorGridAction.setEnabled(showGridAction.isChecked()); + showLegendAction.setChecked( + settings->value(SG_Plot_Legend, showLegendAction.isChecked()).toBool()); + showMultiAction.setChecked( + settings->value(SG_Plot_MultiPlot, showMultiAction.isChecked()).toBool()); + + QString showSymbolsStr = settings->value(SG_Plot_Symbols, QString()).toString(); + if (showSymbolsStr == "auto") + { + // setSymbols(Plot::ShowSymbolsAuto); + setSymbolsAutoAct.setChecked(true); + } + else if (showSymbolsStr == "show") + { + // setSymbols(Plot::ShowSymbolsShow); + setSymbolsShowAct.setChecked(true); + } + else if (showSymbolsStr == "hide") + { + // setSymbols(Plot::ShowSymbolsHide); + setSymbolsHideAct.setChecked(true); + } + else + { + qCritical() << "Invalid symbol setting:" << showSymbolsStr; + } + + settings->endGroup(); +} diff --git a/src/plotmenu.h b/src/plotmenu.h new file mode 100644 --- /dev/null +++ b/src/plotmenu.h @@ -0,0 +1,73 @@ +/* + 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 PLOTMENU_H +#define PLOTMENU_H + +#include +#include +#include + +#include "plot.h" + +/// Used to quickly transfer view options between different menus +struct PlotViewSettings +{ + bool showGrid; + bool showMinorGrid; + bool darkBackground; + bool showLegend; + bool showMulti; + Plot::ShowSymbols showSymbols; +}; + +class PlotMenu : public QMenu +{ + Q_OBJECT + +public: + PlotMenu(QWidget* parent = 0); + PlotMenu(PlotViewSettings s, QWidget* parent = 0); + + QAction showGridAction; + QAction showMinorGridAction; + QAction unzoomAction; + QAction darkBackgroundAction; + QAction showLegendAction; + QAction showMultiAction; + QAction setSymbolsAction; + QMenu setSymbolsMenu; + QAction setSymbolsAutoAct; + QAction setSymbolsShowAct; + QAction setSymbolsHideAct; + + /// Returns a bundle of current view settings (menu selections) + PlotViewSettings viewSettings() const; + /// Selected "show symbol" option + Plot::ShowSymbols showSymbols() const; + /// Stores plot settings into a `QSettings`. + void saveSettings(QSettings* settings); + /// Loads plot settings from a `QSettings`. + void loadSettings(QSettings* settings); + +signals: + void symbolShowChanged(Plot::ShowSymbols shown); +}; + +#endif // PLOTMENU_H diff --git a/src/portcontrol.cpp b/src/portcontrol.cpp --- a/src/portcontrol.cpp +++ b/src/portcontrol.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -48,6 +48,8 @@ PortControl::PortControl(QSerialPort* po ui->setupUi(this); serialPort = port; + connect(serialPort, SIGNAL(error(QSerialPort::SerialPortError)), + this, SLOT(onPortError(QSerialPort::SerialPortError))); // setup actions openAction.setCheckable(true); @@ -130,6 +132,40 @@ PortControl::PortControl(QSerialPort* po SELECT::OVERLOAD_OF(&QButtonGroup::buttonClicked), this, &PortControl::selectFlowControl); + // initialize signal leds + ui->ledDTR->setOn(true); + ui->ledRTS->setOn(true); + + // connect output signals + connect(ui->pbDTR, &QPushButton::clicked, [this]() + { + // toggle DTR + ui->ledDTR->toggle(); + if (serialPort->isOpen()) + { + serialPort->setDataTerminalReady(ui->ledDTR->isOn()); + } + }); + + connect(ui->pbRTS, &QPushButton::clicked, [this]() + { + // toggle RTS + ui->ledRTS->toggle(); + if (serialPort->isOpen()) + { + serialPort->setRequestToSend(ui->ledRTS->isOn()); + } + }); + + // setup pin update leds + ui->ledDCD->setColor(Qt::yellow); + ui->ledDSR->setColor(Qt::yellow); + ui->ledRI->setColor(Qt::yellow); + ui->ledCTS->setColor(Qt::yellow); + + pinUpdateTimer.setInterval(1000); // ms + connect(&pinUpdateTimer, &QTimer::timeout, this, &PortControl::updatePinLeds); + loadPortList(); loadBaudRateList(); ui->cbBaudRate->setCurrentIndex(ui->cbBaudRate->findText("9600")); @@ -221,6 +257,7 @@ void PortControl::togglePort() { if (serialPort->isOpen()) { + pinUpdateTimer.stop(); serialPort->close(); qDebug() << "Closed port:" << serialPort->portName(); emit portToggled(false); @@ -258,6 +295,14 @@ void PortControl::togglePort() selectStopBits((QSerialPort::StopBits) stopBitsButtons.checkedId()); selectFlowControl((QSerialPort::FlowControl) flowControlButtons.checkedId()); + // set output signals + serialPort->setDataTerminalReady(ui->ledDTR->isOn()); + serialPort->setRequestToSend(ui->ledRTS->isOn()); + + // update pin signals + updatePinLeds(); + pinUpdateTimer.start(); + qDebug() << "Opened port:" << serialPort->portName(); emit portToggled(true); } @@ -319,6 +364,73 @@ void PortControl::onTbPortListActivated( ui->cbPortList->setCurrentIndex(index); } +void PortControl::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(); + togglePort(); + } + 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! Error: " << serialPort->errorString(); + break; + default: + qCritical() << "Unhandled port error: " << error; + break; + } +} + +void PortControl::updatePinLeds(void) +{ + auto pins = serialPort->pinoutSignals(); + ui->ledDCD->setOn(pins & QSerialPort::DataCarrierDetectSignal); + ui->ledDSR->setOn(pins & QSerialPort::DataSetReadySignal); + ui->ledRI->setOn(pins & QSerialPort::RingIndicatorSignal); + ui->ledCTS->setOn(pins & QSerialPort::ClearToSendSignal); +} + QString PortControl::currentParityText() { return paritySettingMap.value( diff --git a/src/portcontrol.h b/src/portcontrol.h --- a/src/portcontrol.h +++ b/src/portcontrol.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -28,6 +28,7 @@ #include #include #include +#include #include "portlist.h" @@ -65,6 +66,9 @@ private: QComboBox tbPortList; PortList portList; + /// Used to refresh pinout signal leds periodically + QTimer pinUpdateTimer; + /// Returns the currently selected (entered) "portName" in the UI QString selectedPortName(); /// Returns currently selected parity as text to be saved in settings @@ -86,9 +90,10 @@ public slots: private slots: void openActionTriggered(bool checked); - void onCbPortListActivated(int index); void onTbPortListActivated(int index); + void onPortError(QSerialPort::SerialPortError error); + void updatePinLeds(void); signals: void portToggled(bool open); diff --git a/src/portcontrol.ui b/src/portcontrol.ui --- a/src/portcontrol.ui +++ b/src/portcontrol.ui @@ -33,6 +33,9 @@ 0 + + You can enter a port name even if it's not listed, such as pseudo terminals. + true @@ -40,6 +43,9 @@ + + You can enter a custom baud rate if it's supported by your OS/adapter. + Qt::ImhPreferNumbers @@ -70,7 +76,7 @@ - + Reload the list of ports @@ -301,6 +307,224 @@ + + + 2 + + + + + + 15 + 15 + + + + + 15 + 15 + + + + Request To Send + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 40 + 20 + + + + Data Terminal Ready + + + DTR + + + + + + + + 15 + 15 + + + + + 15 + 15 + + + + Data Terminal Ready + + + + + + + + 15 + 15 + + + + + 15 + 15 + + + + Data Set Ready + + + + + + + Data Set Ready + + + DSR + + + Qt::AlignCenter + + + + + + + + 40 + 20 + + + + Request To Send + + + RTS + + + + + + + Data Carrier Detect + + + DCD + + + Qt::AlignCenter + + + + + + + + 15 + 15 + + + + + 15 + 15 + + + + Data Carrier Detect + + + + + + + Ring Indicator + + + RI + + + Qt::AlignCenter + + + + + + + + 15 + 15 + + + + + 15 + 15 + + + + Ring Indicator + + + + + + + + 15 + 15 + + + + + 15 + 15 + + + + Clear To Send + + + + + + + Clear To Send + + + CTS + + + Qt::AlignCenter + + + + + + Qt::Vertical @@ -308,7 +532,7 @@ 20 - 40 + 1 @@ -317,6 +541,14 @@ + + + LedWidget + QWidget +
ledwidget.h
+ 1 +
+
cbPortList pbReloadPorts diff --git a/src/portlist.cpp b/src/portlist.cpp --- a/src/portlist.cpp +++ b/src/portlist.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -50,7 +50,8 @@ void PortListItem::construct(QString nam { text += QString(" ") + description; } - if (vid && pid) + // Note: in some cases internal ports or RS232 ports may have VID&PID + if (vid && pid && !name.contains("tty")) { text += QString("[%1:").arg(vid, 4, 16, QChar('0')); text += QString("%1]").arg(pid, 4, 16, QChar('0')); diff --git a/src/readonlybuffer.cpp b/src/readonlybuffer.cpp new file mode 100644 --- /dev/null +++ b/src/readonlybuffer.cpp @@ -0,0 +1,103 @@ +/* + Copyright © 2018 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 "readonlybuffer.h" + +ReadOnlyBuffer::ReadOnlyBuffer(const FrameBuffer* source) : + ReadOnlyBuffer(source, 0, source->size()) +{ + // intentionally empty, see ↑ +} + +ReadOnlyBuffer::ReadOnlyBuffer(const FrameBuffer* source, unsigned start, unsigned n) +{ + Q_ASSERT(source->size() > 0); + Q_ASSERT(start + n <= source->size()); + + _size = n; + data = new double[_size]; + + for (unsigned i = 0; i < n; i++) + { + data[i] = source->sample(start + i); + } + + /// if not exact copy of source re-calculate limits + if (start == 0 && n == source->size()) + { + _limits = source->limits(); + } + else + { + updateLimits(); + } +} + +ReadOnlyBuffer::ReadOnlyBuffer(const double* source, unsigned ssize) +{ + Q_ASSERT(source != nullptr && ssize); + + _size = ssize; + data = new double[_size]; + memcpy(data, source, sizeof(double) * ssize); + updateLimits(); +} + +ReadOnlyBuffer::~ReadOnlyBuffer() +{ + delete[] data; +} + +unsigned ReadOnlyBuffer::size() const +{ + return _size; +} + +double ReadOnlyBuffer::sample(unsigned i) const +{ + return data[i]; +} + +Range ReadOnlyBuffer::limits() const +{ + return _limits; +} + +void ReadOnlyBuffer::updateLimits() +{ + Q_ASSERT(_size); + + _limits.start = data[0]; + _limits.end = data[0]; + + for (unsigned i = 0; i < _size; i++) + { + if (data[i] > _limits.end) + { + _limits.end = data[i]; + } + else if (data[i] < _limits.start) + { + _limits.start = data[i]; + } + } +} diff --git a/src/readonlybuffer.h b/src/readonlybuffer.h new file mode 100644 --- /dev/null +++ b/src/readonlybuffer.h @@ -0,0 +1,62 @@ +/* + Copyright © 2018 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 READONLYBUFFER_H +#define READONLYBUFFER_H + +#include "framebuffer.h" + +/// A read only frame buffer used for storing snapshot data. Main advantage of +/// this compared to `RingBuffer` is that reading data should be somewhat +/// faster. +class ReadOnlyBuffer : public FrameBuffer +{ +public: + /// Creates a buffer with data copied from `source`. Source buffer cannot be + /// empty. + ReadOnlyBuffer(const FrameBuffer* source); + + /// Creates a buffer from a slice of the `source`. + /// + /// @param start start of the slice + /// @param n number of samples + /// + /// @important (start + n) should be smaller or equal than `source->size()`, + /// otherwise it's an error. + ReadOnlyBuffer(const FrameBuffer* source, unsigned start, unsigned n); + + /// Creates a buffer with data copied from an array + ReadOnlyBuffer(const double* source, unsigned ssize); + + ~ReadOnlyBuffer(); + + virtual unsigned size() const; + virtual double sample(unsigned i) const; + virtual Range limits() const; + +private: + double* data; ///< data storage + unsigned _size; ///< data size + Range _limits; ///< limits cache + + // TODO: duplicate with `RingBuffer` + void updateLimits(); ///< Updates limits cache +}; + +#endif // READONLYBUFFER_H diff --git a/src/recordpanel.cpp b/src/recordpanel.cpp --- a/src/recordpanel.cpp +++ b/src/recordpanel.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,21 +23,25 @@ #include #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) : +RecordPanel::RecordPanel(Stream* stream, QWidget *parent) : QWidget(parent), ui(new Ui::RecordPanel), recordToolBar(tr("Record Toolbar")), - recordAction(QIcon::fromTheme("media-record"), tr("Record"), this) + recordAction(QIcon::fromTheme("media-record"), tr("Record"), this), + recorder(this) { overwriteSelected = false; - _recorder = recorder; - _channelMan = channelMan; + _stream = stream; ui->setupUi(this); @@ -58,16 +62,25 @@ RecordPanel::RecordPanel(DataRecorder* r connect(ui->cbDisableBuffering, &QCheckBox::toggled, [this](bool enabled) { - _recorder->disableBuffering = enabled; + recorder.disableBuffering = enabled; }); connect(ui->cbWindowsLE, &QCheckBox::toggled, [this](bool enabled) { - _recorder->windowsLE = enabled; + recorder.windowsLE = enabled; }); connect(&recordAction, &QAction::toggled, ui->cbWindowsLE, &QWidget::setDisabled); + connect(&recordAction, &QAction::toggled, ui->cbTimestamp, &QWidget::setDisabled); + connect(&recordAction, &QAction::toggled, ui->leSeparator, &QWidget::setDisabled); + connect(&recordAction, &QAction::toggled, ui->pbBrowse, &QWidget::setDisabled); + + QCompleter *completer = new QCompleter(this); + // TODO: QDirModel is deprecated, use QFileSystemModel (but it doesn't work) + completer->setModel(new QDirModel(completer)); + completer->setCaseSensitivity(Qt::CaseInsensitive); + ui->leFileName->setCompleter(completer); } RecordPanel::~RecordPanel() @@ -80,11 +93,6 @@ QToolBar* RecordPanel::toolbar() return &recordToolBar; } -bool RecordPanel::isRecording() -{ - return recordAction.isChecked(); -} - bool RecordPanel::recordPaused() { return ui->cbRecordPaused->isChecked(); @@ -101,13 +109,75 @@ bool RecordPanel::selectFile() } else { - selectedFile = fileName; - ui->lbFileName->setText(selectedFile); + setSelectedFile(fileName); overwriteSelected = QFile::exists(fileName); return true; } } +QString RecordPanel::selectedFile() const +{ + return ui->leFileName->text(); +} + +void RecordPanel::setSelectedFile(QString f) +{ + ui->leFileName->setText(f); +} + +QString RecordPanel::getSelectedFile() +{ + if (selectedFile().isEmpty()) + { + if (!selectFile()) return QString(); + } + + // assume that file name contains a time format specifier + if (selectedFile().contains("%")) + { + auto ts = formatTimeStamp(selectedFile()); + if (!QFile::exists(ts) || // file doesn't exists + confirmOverwrite(ts)) // exists but user accepted overwrite + { + return ts; + } + return QString(); + } + + // if no timestamp and file exists try autoincrement option + if (!overwriteSelected && QFile::exists(selectedFile())) + { + if (ui->cbAutoIncrement->isChecked()) + { + if (!incrementFileName()) return QString(); + } + else + { + if (!confirmOverwrite(selectedFile())) + return QString(); + } + } + + return selectedFile(); +} + +QString RecordPanel::formatTimeStamp(QString t) const +{ + auto maxSize = t.size() + 1024; + auto r = new char[maxSize]; + + time_t rawtime; + struct tm * timeinfo; + + time(&rawtime); + timeinfo = localtime (&rawtime); + strftime(r, maxSize, t.toLatin1().data(), timeinfo); + + auto rs = QString(r); + delete r; + return rs; +} + void RecordPanel::onRecord(bool start) { if (!start) @@ -126,22 +196,11 @@ void RecordPanel::onRecord(bool start) } // check file name - if (!canceled && selectedFile.isEmpty() && !selectFile()) - { - canceled = true; - } - - if (!canceled && !overwriteSelected && QFile::exists(selectedFile)) + QString fn; + if (!canceled) { - if (ui->cbAutoIncrement->isChecked()) - { - // TODO: should we increment even if user selected to replace? - canceled = !incrementFileName(); - } - else - { - canceled = !confirmOverwrite(selectedFile); - } + fn = getSelectedFile(); + canceled = fn.isEmpty(); } if (canceled) @@ -151,13 +210,15 @@ void RecordPanel::onRecord(bool start) else { overwriteSelected = false; - startRecording(); + // TODO: show more visible error message when recording fails + if (!startRecording(fn)) + recordAction.setChecked(false); } } bool RecordPanel::incrementFileName(void) { - QFileInfo fileInfo(selectedFile); + QFileInfo fileInfo(selectedFile()); QString base = fileInfo.completeBaseName(); QRegularExpression regex("(.*?)(\\d+)(?!.*\\d)(.*)"); @@ -192,10 +253,9 @@ bool RecordPanel::incrementFileName(void } else { - selectedFile = autoFileName; + setSelectedFile(autoFileName); } - ui->lbFileName->setText(selectedFile); return true; } @@ -222,7 +282,7 @@ bool RecordPanel::confirmOverwrite(QStri } else if (mb.clickedButton() == bOverwrite) { - selectedFile = fileName; + setSelectedFile(fileName); return true; } else // select button @@ -231,21 +291,29 @@ bool RecordPanel::confirmOverwrite(QStri } } -void RecordPanel::startRecording(void) +bool RecordPanel::startRecording(QString fileName) { QStringList channelNames; if (ui->cbHeader->isChecked()) { - channelNames = _channelMan->infoModel()->channelNames(); + channelNames = _stream->infoModel()->channelNames(); } - _recorder->startRecording(selectedFile, getSeparator(), channelNames); - emit recordStarted(); + if (recorder.startRecording(fileName, getSeparator(), + channelNames, ui->cbTimestamp->isChecked())) + { + _stream->connectFollower(&recorder); + return true; + } + else + { + return false; + } } void RecordPanel::stopRecording(void) { - emit recordStopped(); - _recorder->stopRecording(); + recorder.stopRecording(); + _stream->disconnectFollower(&recorder); } void RecordPanel::onPortClose() @@ -272,6 +340,7 @@ void RecordPanel::saveSettings(QSettings 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_Timestamp, ui->cbTimestamp->isChecked()); settings->setValue(SG_Record_Separator, ui->leSeparator->text()); settings->endGroup(); } @@ -289,6 +358,8 @@ void RecordPanel::loadSettings(QSettings settings->value(SG_Record_Header, ui->cbHeader->isChecked()).toBool()); ui->cbDisableBuffering->setChecked( settings->value(SG_Record_DisableBuffering, ui->cbDisableBuffering->isChecked()).toBool()); + ui->cbTimestamp->setChecked( + settings->value(SG_Record_Timestamp, ui->cbTimestamp->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 --- a/src/recordpanel.h +++ b/src/recordpanel.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -26,7 +26,7 @@ #include #include "datarecorder.h" -#include "channelmanager.h" +#include "stream.h" namespace Ui { class RecordPanel; @@ -37,13 +37,11 @@ class RecordPanel : public QWidget Q_OBJECT public: - explicit RecordPanel(DataRecorder* recorder, ChannelManager* channelMan, - QWidget* parent = 0); + explicit RecordPanel(Stream* stream, QWidget* parent = 0); ~RecordPanel(); QToolBar* toolbar(); - bool isRecording(); bool recordPaused(); /// Stores settings into a `QSettings` @@ -64,10 +62,9 @@ private: Ui::RecordPanel *ui; QToolBar recordToolBar; QAction recordAction; - QString selectedFile; bool overwriteSelected; - DataRecorder* _recorder; - ChannelManager* _channelMan; + DataRecorder recorder; + Stream* _stream; /** * @brief Increments the file name. @@ -92,7 +89,26 @@ private: */ bool confirmOverwrite(QString fileName); - void startRecording(void); + /// Returns filename in edit box. May be invalid! + QString selectedFile() const; + /// Sets the filename in edit box. + void setSelectedFile(QString f); + + /** + * Tries to get a valid file name by handling user interactions and + * automatic naming (increment, timestamp etc). + * + * Returned file name can be used immediately. File name box should also be + * set to selected file name. + * + * @return empty if failure otherwise valid filename + */ + QString getSelectedFile(); + + /// Formats timestamp in given text + QString formatTimeStamp(QString t) const; + + bool startRecording(QString fileName); void stopRecording(void); /// Returns separator text from ui. "\t" is converted to TAB diff --git a/src/recordpanel.ui b/src/recordpanel.ui --- a/src/recordpanel.ui +++ b/src/recordpanel.ui @@ -7,7 +7,7 @@ 0 0 627 - 261 + 207 @@ -19,6 +19,22 @@ + + + + 0 + 0 + + + + You can use C `strftime` function format specifiers for timestamps in your file name. + + + Enter file name or browse + + + + Select record file @@ -28,23 +44,56 @@ - - - - - 0 - 0 - - - - Select file... - - - + + + + Stop recording when port closed + + + true + + + + + + + Increments file name automatically everytime a new recording starts + + + Auto increment file name + + + true + + + + + + + Do not buffer when writing to file. Check this if you are using other software to open the file during recording. + + + Disable buffering + + + + + + + Use CR+LF as line endings. Some windows software may not show lines correctly otherwise. Can't be changed during recording. + + + Windows Style Line Endings + + + false + + + @@ -71,52 +120,6 @@ - - - - Do not buffer when writing to file. Check this if you are using other software to open the file during recording. - - - Disable buffering - - - - - - - Stop recording when port closed - - - true - - - - - - - Increments file name automatically everytime a new recording starts - - - Auto increment file name - - - true - - - - - - - Use CR+LF as line endings. Some windows software may not show lines correctly otherwise. Can't be changed during recording. - - - Windows Style Line Endings - - - false - - - @@ -130,6 +133,16 @@ + + + + Insert timestamp (milliseconds from epoch) as first column + + + Insert timestamp + + + diff --git a/src/ringbuffer.cpp b/src/ringbuffer.cpp new file mode 100644 --- /dev/null +++ b/src/ringbuffer.cpp @@ -0,0 +1,172 @@ +/* + 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 "ringbuffer.h" + +RingBuffer::RingBuffer(unsigned n) +{ + _size = n; + data = new double[_size](); + headIndex = 0; + + limInvalid = false; + limCache = {0, 0}; +} + +RingBuffer::~RingBuffer() +{ + delete[] data; +} + +unsigned RingBuffer::size() const +{ + return _size; +} + +double RingBuffer::sample(unsigned i) const +{ + unsigned index = headIndex + i; + if (index >= _size) index -= _size; + return data[index]; +} + +Range RingBuffer::limits() const +{ + if (limInvalid) updateLimits(); + return limCache; +} + +void RingBuffer::resize(unsigned n) +{ + Q_ASSERT(n != _size); + + int offset = (int) n - (int) _size; + if (offset == 0) return; + + double* newData = new double[n]; + + // move data to new array + int fill_start = offset > 0 ? offset : 0; + + for (int i = fill_start; i < int(n); i++) + { + newData[i] = sample(i - offset); + } + + // fill the beginning of the new data + if (fill_start > 0) + { + for (int i = 0; i < fill_start; i++) + { + newData[i] = 0; + } + } + + // data is ready, clean up and re-point + delete data; + data = newData; + headIndex = 0; + _size = n; + + // invalidate bounding rectangle + limInvalid = true; +} + +void RingBuffer::addSamples(double* samples, unsigned n) +{ + unsigned shift = n; + if (shift < _size) + { + unsigned x = _size - headIndex; // distance of `head` to end + + if (shift <= x) // there is enough room at the end of array + { + for (unsigned i = 0; i < shift; i++) + { + data[i+headIndex] = samples[i]; + } + + if (shift == x) // we used all the room at the end + { + headIndex = 0; + } + else + { + headIndex += shift; + } + } + else // there isn't enough room + { + for (unsigned i = 0; i < x; i++) // fill the end part + { + data[i+headIndex] = samples[i]; + } + for (unsigned i = 0; i < (shift-x); i++) // continue from the beginning + { + data[i] = samples[i+x]; + } + headIndex = shift-x; + } + } + else // number of new samples equal or bigger than current size (doesn't fit) + { + int x = shift - _size; + for (unsigned i = 0; i < _size; i++) + { + data[i] = samples[i+x]; + } + headIndex = 0; + } + + // invalidate cache + limInvalid = true; +} + +void RingBuffer::clear() +{ + for (unsigned i=0; i < _size; i++) + { + data[i] = 0.; + } + + limCache = {0, 0}; + limInvalid = false; +} + +void RingBuffer::updateLimits() const +{ + limCache.start = data[0]; + limCache.end = data[0]; + + for (unsigned i = 0; i < _size; i++) + { + if (data[i] > limCache.end) + { + limCache.end = data[i]; + } + else if (data[i] < limCache.start) + { + limCache.start = data[i]; + } + } + + limInvalid = false; +} diff --git a/src/ringbuffer.h b/src/ringbuffer.h new file mode 100644 --- /dev/null +++ b/src/ringbuffer.h @@ -0,0 +1,49 @@ +/* + Copyright © 2018 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 RINGBUFFER_H +#define RINGBUFFER_H + +#include "framebuffer.h" + +/// A fast buffer implementation for storing data. +class RingBuffer : public WFrameBuffer +{ +public: + RingBuffer(unsigned n); + ~RingBuffer(); + + virtual unsigned size() const; + virtual double sample(unsigned i) const; + virtual Range limits() const; + virtual void resize(unsigned n); + virtual void addSamples(double* samples, unsigned n); + virtual void clear(); + +private: + unsigned _size; ///< size of `data` + double* data; ///< storage + unsigned headIndex; ///< indicates the actual `0` index of the ring buffer + + mutable bool limInvalid; ///< Indicates that limits needs to be re-calculated + mutable Range limCache; ///< Cache for limits() + void updateLimits() const; ///< Updates limits cache +}; + +#endif diff --git a/src/samplecounter.cpp b/src/samplecounter.cpp new file mode 100644 --- /dev/null +++ b/src/samplecounter.cpp @@ -0,0 +1,44 @@ +/* + Copyright © 2018 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 "samplecounter.h" + +SampleCounter::SampleCounter() +{ + prevTimeMs = QDateTime::currentMSecsSinceEpoch(); + count = 0; +} + +#include + +void SampleCounter::feedIn(const SamplePack& data) +{ + count += data.numSamples(); + + qint64 current = QDateTime::currentMSecsSinceEpoch(); + auto diff = current - prevTimeMs; + if (diff > 1000) // 1sec + { + emit spsChanged(1000 * float(count) / diff); + + prevTimeMs = current; + count = 0; + } +} diff --git a/src/samplecounter.h b/src/samplecounter.h new file mode 100644 --- /dev/null +++ b/src/samplecounter.h @@ -0,0 +1,47 @@ +/* + Copyright © 2018 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 SAMPLECOUNTER_H +#define SAMPLECOUNTER_H + +#include +#include "sink.h" + +/// A `Sink` class for counting and reporting number of samples per second. +class SampleCounter : public QObject, public Sink +{ + Q_OBJECT + +public: + SampleCounter(); + +protected: + // implementations for `Sink` + virtual void feedIn(const SamplePack& data); + +signals: + /// Emitted per second if SPS value has changed. + void spsChanged(float value); + +private: + qint64 prevTimeMs; + unsigned count; +}; + +#endif // SAMPLECOUNTER_H diff --git a/src/samplepack.cpp b/src/samplepack.cpp new file mode 100644 --- /dev/null +++ b/src/samplepack.cpp @@ -0,0 +1,98 @@ +/* + Copyright © 2018 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 "samplepack.h" + +SamplePack::SamplePack(unsigned ns, unsigned nc, bool x) +{ + Q_ASSERT(ns > 0 && nc > 0); + + _numSamples = ns; + _numChannels = nc; + + _yData = new double[_numSamples * _numChannels](); + if (x) + { + _xData = new double[_numSamples](); + } + else + { + _xData = nullptr; + } +} + +SamplePack::SamplePack(const SamplePack& other) : + SamplePack(other.numSamples(), other.numChannels(), other.hasX()) +{ + size_t dataSize = sizeof(double) * numSamples(); + if (hasX()) + memcpy(xData(), other.xData(), dataSize); + memcpy(_yData, other._yData, dataSize * numChannels()); +} + +SamplePack::~SamplePack() +{ + delete[] _yData; + if (_xData != nullptr) + { + delete[] _xData; + } +} + +bool SamplePack::hasX() const +{ + return _xData != nullptr; +} + +unsigned SamplePack::numChannels() const +{ + return _numChannels; +} + +unsigned SamplePack::numSamples() const +{ + return _numSamples; +} + +double* SamplePack::xData() const +{ + Q_ASSERT(_xData != nullptr); + + return _xData; +} + +double* SamplePack::data(unsigned channel) const +{ + Q_ASSERT(channel < _numChannels); + + return &_yData[channel * _numSamples]; +} + +double* SamplePack::xData() +{ + return const_cast(static_cast(*this).xData()); +} + +double* SamplePack::data(unsigned channel) +{ + return const_cast(static_cast(*this).data(channel)); +} diff --git a/src/samplepack.h b/src/samplepack.h new file mode 100644 --- /dev/null +++ b/src/samplepack.h @@ -0,0 +1,50 @@ +/* + Copyright © 2018 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 SAMPLEPACK_H +#define SAMPLEPACK_H + +class SamplePack +{ +public: + /** + * @param ns number of samples + * @param nc number of channels + * @param x has X channel + */ + SamplePack(unsigned ns, unsigned nc, bool x = false); + SamplePack(const SamplePack& other); + ~SamplePack(); + + bool hasX() const; + unsigned numChannels() const; + unsigned numSamples() const; + double* xData() const; + double* data(unsigned channel) const; + + double* xData(); + double* data(unsigned channel); + +private: + unsigned _numSamples, _numChannels; + double* _xData; + double* _yData; +}; + +#endif // SAMPLEPACK_H diff --git a/src/scrollzoomer.cpp b/src/scrollzoomer.cpp --- a/src/scrollzoomer.cpp +++ b/src/scrollzoomer.cpp @@ -42,6 +42,10 @@ ScrollZoomer::ScrollZoomer( QWidget *can d_vScrollData( NULL ), d_inZoom( false ) { + xMin = 0.; + xMax = 10000.; + hViewSize = 10000; + for ( int axis = 0; axis < QwtPlot::axisCnt; axis++ ) d_alignCanvasToScales[ axis ] = false; @@ -50,6 +54,8 @@ ScrollZoomer::ScrollZoomer( QWidget *can d_hScrollData = new ScrollData; d_vScrollData = new ScrollData; + hscrollmove = false; + vscrollmove = false; } ScrollZoomer::~ScrollZoomer() @@ -59,6 +65,39 @@ ScrollZoomer::~ScrollZoomer() delete d_hScrollData; } +void ScrollZoomer::setXLimits(double min, double max) +{ + xMin = min; + xMax = max; + setZoomBase(); +} + +void ScrollZoomer::setHViewSize(double size) +{ + hscrollmove = true; + hViewSize = size; + setZoomBase(); + hscrollmove = false; +} + +void ScrollZoomer::setZoomBase(bool doReplot) +{ + QwtPlotZoomer::setZoomBase(doReplot); + auto zb = zoomBase(); + auto zs = zoomStack(); + zb.setRight(xMax); + if ((xMax - xMin) < hViewSize) + { + zb.setLeft(xMin); + } + else + { + zb.setLeft(xMax-hViewSize); + } + zs[0] = zb; + setZoomStack(zs); +} + void ScrollZoomer::rescale() { QwtScaleWidget *xScale = plot()->axisWidget( xAxis() ); @@ -112,7 +151,45 @@ void ScrollZoomer::rescale() } } - QwtPlotZoomer::rescale(); + // NOTE: Below snippet is copied from QwtPlotZoomer::rescale() just so that + // we can refrain from updating y axis when moving horizontal scrollbar, so + // that auto-scale isn't disrupted. Also we don't want to jump around in + // x-axis when moving vertical scroll. + { + QwtPlot *plt = plot(); + if ( !plt ) + return; + + const QRectF &rect = zoomStack()[zoomRectIndex()]; + if ( rect != scaleRect() ) + { + const bool doReplot = plt->autoReplot(); + plt->setAutoReplot( false ); + + if (!vscrollmove) + { + double x1 = rect.left(); + double x2 = rect.right(); + if ( !plt->axisScaleDiv( xAxis() ).isIncreasing() ) + qSwap( x1, x2 ); + + plt->setAxisScale( xAxis(), x1, x2 ); + } + + if (!hscrollmove) + { + double y1 = rect.top(); + double y2 = rect.bottom(); + if ( !plt->axisScaleDiv( yAxis() ).isIncreasing() ) + qSwap( y1, y2 ); + + plt->setAxisScale( yAxis(), y1, y2 ); + + plt->setAutoReplot( doReplot ); + } + plt->replot(); + } + } updateScrollBars(); } @@ -237,6 +314,11 @@ bool ScrollZoomer::eventFilter( QObject layoutScrollBars( rect ); break; } + case QEvent::Show: + { + layoutScrollBars( canvas()->contentsRect() ); + break; + } case QEvent::ChildRemoved: { const QObject *child = @@ -265,8 +347,8 @@ bool ScrollZoomer::needScrollBar( Qt::Or if ( orientation == Qt::Horizontal ) { mode = d_hScrollData->mode; - baseMin = zoomBase().left(); - baseMax = zoomBase().right(); + baseMin = xMin; + baseMax = xMax; zoomMin = zoomRect().left(); zoomMax = zoomRect().right(); } @@ -323,7 +405,7 @@ void ScrollZoomer::updateScrollBars() ScrollBar *sb = scrollBar( Qt::Horizontal ); sb->setPalette( plot()->palette() ); sb->setInverted( !plot()->axisScaleDiv( xAxis ).isIncreasing() ); - sb->setBase( zoomBase().left(), zoomBase().right() ); + sb->setBase( xMin, xMax ); sb->moveSlider( zoomRect().left(), zoomRect().right() ); if ( !sb->isVisibleTo( canvas() ) ) @@ -462,9 +544,17 @@ void ScrollZoomer::scrollBarMoved( Q_UNUSED( max ); if ( o == Qt::Horizontal ) + { + hscrollmove = true; moveTo( QPointF( min, zoomRect().top() ) ); + hscrollmove = false; + } else + { + vscrollmove = true; moveTo( QPointF( zoomRect().left(), min ) ); + vscrollmove = false; + } Q_EMIT zoomed( zoomRect() ); } @@ -487,3 +577,30 @@ int ScrollZoomer::oppositeAxis( int axis return axis; } + +void ScrollZoomer::moveTo( const QPointF &pos ) +{ + // QwtPlotZoomer::moveTo(pos); + // return; + + double x = pos.x(); + double y = pos.y(); + + if ( x < xMin ) + x = xMin; + if ( x > xMax - zoomRect().width() ) + x = xMax - zoomRect().width(); + + if ( y < zoomBase().top() ) + y = zoomBase().top(); + if ( y > zoomBase().bottom() - zoomRect().height() ) + y = zoomBase().bottom() - zoomRect().height(); + + if ( x != zoomRect().left() || y != zoomRect().top() ) + { + auto zs = zoomStack(); + zs[zoomRectIndex()].moveTo( x, y ); + setZoomStack(zs, zoomRectIndex()); + rescale(); + } +} diff --git a/src/scrollzoomer.h b/src/scrollzoomer.h --- a/src/scrollzoomer.h +++ b/src/scrollzoomer.h @@ -50,8 +50,14 @@ public: virtual bool eventFilter( QObject *, QEvent * ); + void setXLimits(double min, double max); + void setHViewSize(double size); + virtual void setZoomBase(bool doReplot = true); virtual void rescale(); +public Q_SLOTS: + virtual void moveTo( const QPointF & ); + protected: virtual ScrollBar *scrollBar( Qt::Orientation ); virtual void updateScrollBars(); @@ -61,6 +67,10 @@ private Q_SLOTS: void scrollBarMoved( Qt::Orientation o, double min, double max ); private: + QRectF d_limits; + double xMin, xMax; + double hViewSize; + bool needScrollBar( Qt::Orientation ) const; int oppositeAxis( int ) const; @@ -71,6 +81,8 @@ private: bool d_inZoom; bool d_alignCanvasToScales[ QwtPlot::axisCnt ]; + bool hscrollmove; + bool vscrollmove; }; #endif 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 © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -30,6 +30,7 @@ const char SettingGroup_Channels[] = "Ch const char SettingGroup_Plot[] = "Plot"; const char SettingGroup_Commands[] = "Commands"; const char SettingGroup_Record[] = "Record"; +const char SettingGroup_UpdateCheck[] = "UpdateCheck"; // mainwindow setting keys const char SG_MainWindow_Size[] = "size"; @@ -57,6 +58,8 @@ const char SG_Binary_Endianness[] = "end // ascii reader keys const char SG_ASCII_NumOfChannels[] = "numOfChannels"; +const char SG_ASCII_Delimiter[] = "delimiter"; +const char SG_ASCII_CustomDelimiter[] = "customDelimiter"; // framed reader keys const char SG_CustomFrame_NumOfChannels[] = "numOfChannels"; @@ -68,14 +71,19 @@ const char SG_CustomFrame_Endianness[] = const char SG_CustomFrame_Checksum[] = "checksum"; const char SG_CustomFrame_DebugMode[] = "debugMode"; -// channel manager keys +// channel info keys const char SG_Channels_Channel[] = "channel"; const char SG_Channels_Name[] = "name"; const char SG_Channels_Color[] = "color"; const char SG_Channels_Visible[] = "visible"; +const char SG_Channels_Gain[] = "gain"; +const char SG_Channels_GainEn[] = "gainEnabled"; +const char SG_Channels_Offset[] = "offset"; +const char SG_Channels_OffsetEn[] = "offsetEnabled"; // plot settings keys const char SG_Plot_NumOfSamples[] = "numOfSamples"; +const char SG_Plot_PlotWidth[] = "plotWidth"; const char SG_Plot_IndexAsX[] = "indexAsX"; const char SG_Plot_XMax[] = "xMax"; const char SG_Plot_XMin[] = "xMin"; @@ -102,5 +110,10 @@ const char SG_Record_StopOnClose[] const char SG_Record_Header[] = "header"; const char SG_Record_Separator[] = "separator"; const char SG_Record_DisableBuffering[] = "disableBuffering"; +const char SG_Record_Timestamp[] = "timestamp"; + +// update check settings keys +const char SG_UpdateCheck_Periodic[] = "periodicCheck"; +const char SG_UpdateCheck_LastCheck[] = "lastCheck"; #endif // SETTING_DEFINES_H diff --git a/src/sink.cpp b/src/sink.cpp new file mode 100644 --- /dev/null +++ b/src/sink.cpp @@ -0,0 +1,70 @@ +/* + Copyright © 2018 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 "sink.h" + +void Sink::connectFollower(Sink* sink) +{ + Q_ASSERT(!followers.contains(sink)); + + followers.append(sink); + sink->setNumChannels(_numChannels, _hasX); +} + +void Sink::disconnectFollower(Sink* sink) +{ + Q_ASSERT(followers.contains(sink)); + + followers.removeOne(sink); +} + +void Sink::feedIn(const SamplePack& data) +{ + for (auto sink : followers) + { + sink->feedIn(data); + } +} + +void Sink::setNumChannels(unsigned nc, bool x) +{ + _numChannels = nc; + _hasX = x; + for (auto sink : followers) + { + sink->setNumChannels(nc, x); + } +} + +void Sink::setSource(Source* s) +{ + Q_ASSERT((source == nullptr) != (s == nullptr)); + source = s; +} + +const Source* Sink::connectedSource() const +{ + return source; +} + +Source* Sink::connectedSource() +{ + return const_cast(static_cast(*this).connectedSource()); +} diff --git a/src/sink.h b/src/sink.h new file mode 100644 --- /dev/null +++ b/src/sink.h @@ -0,0 +1,73 @@ +/* + Copyright © 2018 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 SINK_H +#define SINK_H + +#include +#include "samplepack.h" + +class Source; + +class Sink +{ +public: + /// Placeholder virtual destructor + virtual ~Sink() {}; + + /// Connects a sink to get any data that this sink + /// gets. Connecting an already connected sink is an error. + void connectFollower(Sink* sink); + + /// Disconnects a follower. Disconnecting an unconnected sink is + /// an error. + void disconnectFollower(Sink* sink); + + /// Returns the connected source. `nullptr` if it's not connected. + const Source* connectedSource() const; + Source* connectedSource(); + +protected: + /// Entry point for incoming data. Re-implementations should + /// call this function to feed followers. + virtual void feedIn(const SamplePack& data); + + /// Is set by connected source. Re-implementations should call + /// this function to update followers. + virtual void setNumChannels(unsigned nc, bool x); + + /// Set by the connected source when its connected. When + /// disconnecting it's set to `nullptr`. + /// + /// @note Previous source is disconnected. + /// + /// @important Trying to connect a source while its already + /// connected is an error. + void setSource(Source* s); + + friend Source; + +private: + QList followers; + Source* source = nullptr; ///< source that this sink is connected to + bool _hasX; + unsigned _numChannels; +}; + +#endif // SINK_H diff --git a/src/snapshot.cpp b/src/snapshot.cpp --- a/src/snapshot.cpp +++ b/src/snapshot.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -25,14 +25,14 @@ #include "snapshot.h" #include "snapshotview.h" -Snapshot::Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel) : +Snapshot::Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved) : QObject(parent), cInfoModel(infoModel), _showAction(this), _deleteAction("&Delete", this) { _name = name; - _saved = false; + _saved = saved; view = NULL; mainWindow = parent; @@ -108,9 +108,24 @@ void Snapshot::setName(QString name) emit nameChanged(this); } +unsigned Snapshot::numChannels() const +{ + return yData.size(); +} + +unsigned Snapshot::numSamples() const +{ + return yData[0]->size(); +} + +const ChannelInfoModel* Snapshot::infoModel() const +{ + return &cInfoModel; +} + ChannelInfoModel* Snapshot::infoModel() { - return &cInfoModel; + return const_cast(static_cast(*this).infoModel()); } QString Snapshot::channelName(unsigned channel) @@ -120,31 +135,27 @@ QString Snapshot::channelName(unsigned c void Snapshot::save(QString fileName) { - // TODO: remove code duplication (MainWindow::onExportCsv) QSaveFile file(fileName); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream fileStream(&file); - unsigned numOfChannels = data.size(); - unsigned numOfSamples = data[0].size(); - // print header - for (unsigned int ci = 0; ci < numOfChannels; ci++) + for (unsigned int ci = 0; ci < numChannels(); ci++) { fileStream << channelName(ci); - if (ci != numOfChannels-1) fileStream << ","; + if (ci != numChannels()-1) fileStream << ","; } fileStream << '\n'; // print rows - for (unsigned int i = 0; i < numOfSamples; i++) + for (unsigned int i = 0; i < numSamples(); i++) { - for (unsigned int ci = 0; ci < numOfChannels; ci++) + for (unsigned int ci = 0; ci < numChannels(); ci++) { - fileStream << data[ci][i].y(); - if (ci != numOfChannels-1) fileStream << ","; + fileStream << yData[ci]->sample(i); + if (ci != numChannels()-1) fileStream << ","; } fileStream << '\n'; } diff --git a/src/snapshot.h b/src/snapshot.h --- a/src/snapshot.h +++ b/src/snapshot.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -27,6 +27,7 @@ #include #include "channelinfomodel.h" +#include "readonlybuffer.h" class SnapshotView; class MainWindow; @@ -36,15 +37,19 @@ class Snapshot : public QObject Q_OBJECT public: - Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel); + Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved = false); ~Snapshot(); - QVector> data; + // TODO: yData of snapshot shouldn't be public, preferable should be handled in constructor + QVector yData; QAction* showAction(); QAction* deleteAction(); QString name(); QString displayName(); ///< `name()` plus '*' if snapshot is not saved + unsigned numChannels() const; ///< number of channels in this snapshot + unsigned numSamples() const; ///< number of samples in every channel + const ChannelInfoModel* infoModel() const; ChannelInfoModel* infoModel(); void setName(QString name); QString channelName(unsigned channel); diff --git a/src/snapshotmanager.cpp b/src/snapshotmanager.cpp --- a/src/snapshotmanager.cpp +++ b/src/snapshotmanager.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -31,14 +32,14 @@ #include "snapshotmanager.h" SnapshotManager::SnapshotManager(MainWindow* mainWindow, - ChannelManager* channelMan) : + Stream* stream) : _menu("&Snapshots"), _takeSnapshotAction("&Take Snapshot", this), loadSnapshotAction("&Load Snapshots", this), clearAction("&Clear Snapshots", this) { _mainWindow = mainWindow; - _channelMan = channelMan; + _stream = stream; _takeSnapshotAction.setToolTip("Take a snapshot of current plot"); _takeSnapshotAction.setShortcut(QKeySequence("F5")); @@ -63,21 +64,14 @@ SnapshotManager::~SnapshotManager() } } -Snapshot* SnapshotManager::makeSnapshot() +Snapshot* SnapshotManager::makeSnapshot() const { QString name = QTime::currentTime().toString("'Snapshot ['HH:mm:ss']'"); - auto snapshot = new Snapshot(_mainWindow, name, *(_channelMan->infoModel())); - - unsigned numOfChannels = _channelMan->numOfChannels(); - unsigned numOfSamples = _channelMan->numOfSamples(); + auto snapshot = new Snapshot(_mainWindow, name, *(_stream->infoModel())); - for (unsigned ci = 0; ci < numOfChannels; ci++) + for (unsigned ci = 0; ci < _stream->numChannels(); ci++) { - snapshot->data.append(QVector(numOfSamples)); - for (unsigned i = 0; i < numOfSamples; i++) - { - snapshot->data[ci][i] = QPointF(i, _channelMan->channelBuffer(ci)->sample(i)); - } + snapshot->yData.append(new ReadOnlyBuffer(_stream->channel(ci)->yData())); } return snapshot; @@ -158,18 +152,20 @@ void SnapshotManager::loadSnapshotFromFi unsigned numOfChannels = channelNames.size(); // read data - QVector> data(numOfChannels); + QVector> data(numOfChannels); + QTextStream ts(&file); + QString line; unsigned lineNum = 1; - while (file.canReadLine()) + while (ts.readLineInto(&line)) { // parse line - auto line = QString(file.readLine()); auto split = line.split(','); if (split.size() != (int) numOfChannels) { qCritical() << "Parsing error at line " << lineNum << ": number of columns is not consistent."; + qCritical() << "Line " << lineNum << ": " << line; return; } @@ -186,16 +182,20 @@ void SnapshotManager::loadSnapshotFromFi << "\" to double."; return; } - data[ci].append(QPointF(lineNum-1, y)); + data[ci].append(y); } lineNum++; } - ChannelInfoModel channelInfo(channelNames); - + // create snapshot auto snapshot = new Snapshot( - _mainWindow, QFileInfo(fileName).baseName(), ChannelInfoModel(channelNames)); - snapshot->data = data; + _mainWindow, QFileInfo(fileName).baseName(), + ChannelInfoModel(channelNames), true); + + for (unsigned ci = 0; ci < numOfChannels; ci++) + { + snapshot->yData.append(new ReadOnlyBuffer(data[ci].data(), data[ci].size())); + } addSnapshot(snapshot, false); } diff --git a/src/snapshotmanager.h b/src/snapshotmanager.h --- a/src/snapshotmanager.h +++ b/src/snapshotmanager.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -24,8 +24,7 @@ #include #include -#include "framebuffer.h" -#include "channelmanager.h" +#include "stream.h" #include "snapshot.h" class MainWindow; @@ -35,7 +34,7 @@ class SnapshotManager : public QObject Q_OBJECT public: - SnapshotManager(MainWindow* mainWindow, ChannelManager* channelMan); + SnapshotManager(MainWindow* mainWindow, Stream* stream); ~SnapshotManager(); QMenu* menu(); @@ -43,13 +42,13 @@ public: /// Creates a dynamically allocated snapshot object but doesn't record it in snapshots list. /// @note Caller is responsible for deletion of the returned `Snapshot` object. - Snapshot* makeSnapshot(); + Snapshot* makeSnapshot() const; bool isAllSaved(); ///< returns `true` if all snapshots are saved to a file private: MainWindow* _mainWindow; - ChannelManager* _channelMan; + Stream* _stream; QList snapshots; diff --git a/src/snapshotview.cpp b/src/snapshotview.cpp --- a/src/snapshotview.cpp +++ b/src/snapshotview.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,26 +23,27 @@ SnapshotView::SnapshotView(MainWindow* parent, Snapshot* snapshot) : QMainWindow(parent), ui(new Ui::SnapshotView), - renameDialog(this) + renameDialog(this), + plotMenu(parent->viewSettings()) { _snapshot = snapshot; ui->setupUi(this); - plotMan = new PlotManager(ui->plotArea, snapshot->infoModel(), this); - plotMan->setViewSettings(parent->viewSettings()); + plotMan = new PlotManager(ui->plotArea, &plotMenu, snapshot, this); ui->menuSnapshot->insertAction(ui->actionClose, snapshot->deleteAction()); this->setWindowTitle(snapshot->displayName()); // initialize curves - unsigned numOfChannels = snapshot->data.size(); - unsigned numOfSamples = snapshot->data[0].size(); - for (unsigned ci = 0; ci < numOfChannels; ci++) - { - plotMan->addCurve(snapshot->channelName(ci), snapshot->data[ci]); - } - plotMan->setNumOfSamples(numOfSamples); + // unsigned numOfChannels = snapshot->data.size(); + // unsigned numOfSamples = snapshot->data[0].size(); + // for (unsigned ci = 0; ci < numOfChannels; ci++) + // { + // plotMan->addCurve(snapshot->channelName(ci), snapshot->data[ci]); + // } + // plotMan->setNumOfSamples(numOfSamples); + // plotMan->setPlotWidth(numOfSamples); renameDialog.setWindowTitle("Rename Snapshot"); renameDialog.setLabelText("Enter new name:"); @@ -52,11 +53,8 @@ SnapshotView::SnapshotView(MainWindow* p connect(ui->actionSave, &QAction::triggered, this, &SnapshotView::save); - // add 'View' menu items - for (auto a : plotMan->menuActions()) - { - ui->menuView->addAction(a); - } + // add "View" menu + menuBar()->insertMenu(NULL, &plotMenu); } SnapshotView::~SnapshotView() diff --git a/src/snapshotview.h b/src/snapshotview.h --- a/src/snapshotview.h +++ b/src/snapshotview.h @@ -31,6 +31,7 @@ #include "mainwindow.h" #include "plotmanager.h" +#include "plotmenu.h" #include "snapshot.h" namespace Ui { @@ -54,6 +55,7 @@ private: Snapshot* _snapshot; QInputDialog renameDialog; PlotManager* plotMan; + PlotMenu plotMenu; void closeEvent(QCloseEvent *event); diff --git a/src/snapshotview.ui b/src/snapshotview.ui --- a/src/snapshotview.ui +++ b/src/snapshotview.ui @@ -26,7 +26,7 @@ 0 0 544 - 25 + 24 @@ -37,13 +37,7 @@ - - - &View - - - diff --git a/src/source.cpp b/src/source.cpp new file mode 100644 --- /dev/null +++ b/src/source.cpp @@ -0,0 +1,79 @@ +/* + Copyright © 2018 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 "source.h" + +Source::~Source() +{ + for (auto sink : sinks) + { + sink->setSource(nullptr); + } +} + +void Source::connectSink(Sink* sink) +{ + Q_ASSERT(!sinks.contains(sink)); + + auto prevSource = sink->connectedSource(); + if (prevSource != nullptr) + { + prevSource->disconnect(sink); + } + + sinks.append(sink); + sink->setSource(this); + sink->setNumChannels(numChannels(), hasX()); +} + +void Source::disconnect(Sink* sink) +{ + Q_ASSERT(sinks.contains(sink)); + Q_ASSERT(sink->connectedSource() == this); + + sink->setSource(nullptr); + sinks.removeOne(sink); +} + +void Source::disconnectSinks() +{ + while (!sinks.isEmpty()) + { + auto sink = sinks.takeFirst(); + sink->setSource(nullptr); + } +} + +void Source::feedOut(const SamplePack& data) const +{ + for (auto sink : sinks) + { + sink->feedIn(data); + } +} + +void Source::updateNumChannels() const +{ + for (auto sink : sinks) + { + sink->setNumChannels(numChannels(), hasX()); + } +} diff --git a/src/source.h b/src/source.h new file mode 100644 --- /dev/null +++ b/src/source.h @@ -0,0 +1,64 @@ +/* + Copyright © 2018 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 SOURCE_H +#define SOURCE_H + +#include + +#include "sink.h" +#include "samplepack.h" + +class Source +{ +public: + /// Virtual destructor. Must be called by implementors to notify sinks. + virtual ~Source(); + + /// Returns true if source has X data + virtual bool hasX() const = 0; + + /// Returns number of channels + virtual unsigned numChannels() const = 0; + + /// Connects a sink to this source. + /// + /// If `Sink` is already connected to a source, it's disconnected first. + void connectSink(Sink* sink); + + /// Disconnects an already connected sink. Trying to disconnect an + /// unconnected sink is an error. + void disconnect(Sink* sink); + + /// Disconnects all connected sinks. + void disconnectSinks(); + +protected: + /// Feeds "in" given data to connected sinks + virtual void feedOut(const SamplePack& data) const; + + /// Updates "number of channels" of connected sinks. Must be + /// called when num. channels or hasX changes. + void updateNumChannels() const; + +private: + QList sinks; +}; + +#endif // SOURCE_H diff --git a/src/stream.cpp b/src/stream.cpp new file mode 100644 --- /dev/null +++ b/src/stream.cpp @@ -0,0 +1,248 @@ +/* + Copyright © 2018 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 "stream.h" +#include "ringbuffer.h" +#include "indexbuffer.h" + +Stream::Stream(unsigned nc, bool x, unsigned ns) : + _infoModel(nc) +{ + _numSamples = ns; + _paused = false; + + // create xdata buffer + _hasx = x; + if (x) + { + xData = new RingBuffer(ns); + } + else + { + xData = new IndexBuffer(ns); + } + + // create channels + for (unsigned i = 0; i < nc; i++) + { + auto c = new StreamChannel(i, xData, new RingBuffer(ns), &_infoModel); + channels.append(c); + } +} + +Stream::~Stream() +{ + for (auto ch : channels) + { + delete ch; + } + delete xData; +} + +bool Stream::hasX() const +{ + return _hasx; +} + +unsigned Stream::numChannels() const +{ + return channels.length(); +} + +unsigned Stream::numSamples() const +{ + return _numSamples; +} + +const StreamChannel* Stream::channel(unsigned index) const +{ + Q_ASSERT(index < numChannels()); + return channels[index]; +} + +StreamChannel* Stream::channel(unsigned index) +{ + return const_cast(static_cast(*this).channel(index)); +} + +const ChannelInfoModel* Stream::infoModel() const +{ + return &_infoModel; +} + +ChannelInfoModel* Stream::infoModel() +{ + return const_cast(static_cast(*this).infoModel()); +} + +void Stream::setNumChannels(unsigned nc, bool x) +{ + unsigned oldNum = numChannels(); + if (oldNum == nc && x == _hasx) return; + + // adjust the number of channels + if (nc > oldNum) + { + for (unsigned i = oldNum; i < nc; i++) + { + auto c = new StreamChannel(i, xData, new RingBuffer(_numSamples), &_infoModel); + channels.append(c); + } + } + else if (nc < oldNum) + { + for (unsigned i = oldNum-1; i > nc-1; i--) + { + delete channels.takeLast(); + } + } + + // change the xdata + if (x != _hasx) + { + if (x) + { + xData = new RingBuffer(_numSamples); + } + else + { + xData = new IndexBuffer(_numSamples); + } + + for (auto c : channels) + { + c->setX(xData); + } + _hasx = x; + } + + if (nc != oldNum) + { + _infoModel.setNumOfChannels(nc); + // TODO: how about X change? + emit numChannelsChanged(nc); + } + + Sink::setNumChannels(nc, x); +} + +const SamplePack* Stream::applyGainOffset(const SamplePack& pack) const +{ + Q_ASSERT(infoModel()->gainOrOffsetEn()); + + SamplePack* mPack = new SamplePack(pack); + unsigned ns = pack.numSamples(); + + for (unsigned ci = 0; ci < numChannels(); ci++) + { + // TODO: we could use some kind of map (int32, int64 would suffice) to speed things up + bool gainEn = infoModel()->gainEn(ci); + bool offsetEn = infoModel()->offsetEn(ci); + if (gainEn || offsetEn) + { + double* mdata = mPack->data(ci); + + double gain = infoModel()->gain(ci); + double offset = infoModel()->offset(ci); + + if (gainEn) + { + for (unsigned i = 0; i < ns; i++) + { + mdata[i] *= gain; + } + } + if (offsetEn) + { + for (unsigned i = 0; i < ns; i++) + { + mdata[i] += offset; + } + } + } + } + + return mPack; +} + +void Stream::feedIn(const SamplePack& pack) +{ + Q_ASSERT(pack.numChannels() == numChannels() && + pack.hasX() == hasX()); + + if (_paused) return; + + unsigned ns = pack.numSamples(); + if (_hasx) + { + static_cast(xData)->addSamples(pack.xData(), ns); + } + + // modified pack that gain and offset is applied to + const SamplePack* mPack = nullptr; + if (infoModel()->gainOrOffsetEn()) + mPack = applyGainOffset(pack); + + for (unsigned ci = 0; ci < numChannels(); ci++) + { + auto buf = static_cast(channels[ci]->yData()); + double* data = (mPack == nullptr) ? pack.data(ci) : mPack->data(ci); + buf->addSamples(data, ns); + } + + Sink::feedIn((mPack == nullptr) ? pack : *mPack); + + if (mPack != nullptr) delete mPack; + emit dataAdded(); +} + +void Stream::pause(bool paused) +{ + _paused = paused; +} + +void Stream::clear() +{ + for (auto c : channels) + { + static_cast(c->yData())->clear(); + } +} + +void Stream::setNumSamples(unsigned value) +{ + if (value == _numSamples) return; + _numSamples = value; + + xData->resize(value); + for (auto c : channels) + { + static_cast(c->yData())->resize(value); + } +} + +void Stream::saveSettings(QSettings* settings) const +{ + _infoModel.saveSettings(settings); +} + +void Stream::loadSettings(QSettings* settings) +{ + _infoModel.loadSettings(settings); +} diff --git a/src/stream.h b/src/stream.h new file mode 100644 --- /dev/null +++ b/src/stream.h @@ -0,0 +1,117 @@ +/* + Copyright © 2018 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 STREAM_H +#define STREAM_H + +#include +#include +#include +#include + +#include "sink.h" +#include "source.h" +#include "channelinfomodel.h" +#include "streamchannel.h" +#include "framebuffer.h" + +/** + * Main waveform storage class. It consists of channels. Channels are + * synchronized with each other. + * + * Implements `Sink` class for data entry. It's expected to be + * connected to a `Device` source. + */ +class Stream : public QObject, public Sink +{ + Q_OBJECT + +public: + /** + * @param nc number of channels + * @param x has X data input + * @param ns number of samples + */ + Stream(unsigned nc = 0, bool x = false, unsigned ns = 0); + ~Stream(); + + // implementations for `Source` + virtual bool hasX() const; + virtual unsigned numChannels() const; + + unsigned numSamples() const; + const StreamChannel* channel(unsigned index) const; + StreamChannel* channel(unsigned index); + const ChannelInfoModel* infoModel() const; + ChannelInfoModel* infoModel(); + + /// Saves channel information + void saveSettings(QSettings* settings) const; + /// Load channel information + void loadSettings(QSettings* settings); + +protected: + // implementations for `Sink` + virtual void setNumChannels(unsigned nc, bool x); + virtual void feedIn(const SamplePack& pack); + +signals: + void numChannelsChanged(unsigned value); + void numSamplesChanged(unsigned value); + void channelAdded(const StreamChannel* chan); + void channelNameChanged(unsigned channel, QString name); // TODO: does it stay? + void dataAdded(); ///< emitted when data added to channel man. + +public slots: + // TODO: these won't be public + // void setNumChannels(unsigned number); + void setNumSamples(unsigned value); + + /// When paused data feed is ignored + void pause(bool paused); + + /// Clears buffer data (fills with 0) + void clear(); + +private: + unsigned _numSamples; + bool _paused; + + bool _hasx; + ResizableBuffer* xData; + QList channels; + + ChannelInfoModel _infoModel; + + /** + * Applies gain and offset to given pack. + * + * Caller is responsible for deleting returned `SamplePack`. + * + * @note Should be called only when gain or offset is enabled. Guard with + * `ChannelInfoModel::gainOrOffsetEn()`. + * + * @param pack input data + * @return modified data + */ + const SamplePack* applyGainOffset(const SamplePack& pack) const; +}; + + +#endif // STREAM_H diff --git a/src/streamchannel.cpp b/src/streamchannel.cpp new file mode 100644 --- /dev/null +++ b/src/streamchannel.cpp @@ -0,0 +1,44 @@ +/* + Copyright © 2018 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 "streamchannel.h" + +StreamChannel::StreamChannel(unsigned i, const FrameBuffer* x, + FrameBuffer* y, ChannelInfoModel* info) +{ + _index = i; + _x = x; + _y = y; + _info = info; +} + +StreamChannel::~StreamChannel() +{ + delete _y; +} + +unsigned StreamChannel::index() const {return _index;} +QString StreamChannel::name() const {return _info->name(_index);}; +QColor StreamChannel::color() const {return _info->color(_index);}; +bool StreamChannel::visible() const {return _info->isVisible(_index);}; +const FrameBuffer* StreamChannel::xData() const {return _x;} +const FrameBuffer* StreamChannel::yData() const {return _y;} +FrameBuffer* StreamChannel::yData() {return _y;} +const ChannelInfoModel* StreamChannel::info() const {return _info;} +void StreamChannel::setX(const FrameBuffer* x) {_x = x;}; diff --git a/src/streamchannel.h b/src/streamchannel.h new file mode 100644 --- /dev/null +++ b/src/streamchannel.h @@ -0,0 +1,60 @@ +/* + Copyright © 2018 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 STREAMCHANNEL_H +#define STREAMCHANNEL_H + +#include "framebuffer.h" +#include "channelinfomodel.h" + +class StreamChannel +{ +public: + /** + * Creates a stream channel. + * + * @param i index of the channel + * @param x x axis buffer + * @param y data buffer of this channel, takes ownership + * @param info channel info model + */ + StreamChannel(unsigned i, + const FrameBuffer* x, + FrameBuffer* y, + ChannelInfoModel* info); + ~StreamChannel(); + + unsigned index() const; + QString name() const; + QColor color() const; + bool visible() const; + const FrameBuffer* xData() const; + FrameBuffer* yData(); + const FrameBuffer* yData() const; + const ChannelInfoModel* info() const; + void setX(const FrameBuffer* x); + +private: + unsigned _index; + const FrameBuffer* _x; + FrameBuffer* _y; + ChannelInfoModel* _info; +}; + +#endif // STREAMCHANNEL_H diff --git a/src/updatecheckdialog.cpp b/src/updatecheckdialog.cpp new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.cpp @@ -0,0 +1,105 @@ +/* + 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 "setting_defines.h" +#include "updatecheckdialog.h" +#include "ui_updatecheckdialog.h" + +UpdateCheckDialog::UpdateCheckDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::UpdateCheckDialog) +{ + ui->setupUi(this); + + // by default start from yesterday, so that we check at first run + lastCheck = QDate::currentDate().addDays(-1); + + connect(&updateChecker, &UpdateChecker::checkFailed, + [this](QString errorMessage) + { + lastCheck = QDate::currentDate(); + ui->label->setText(QString("Update check failed.\n") + errorMessage); + }); + + connect(&updateChecker, &UpdateChecker::checkFinished, + [this](bool found, QString newVersion, QString downloadUrl) + { + QString text; + if (!found) + { + text = "There is no update yet."; + } + else + { + show(); +#ifdef UPDATE_TYPE_PKGMAN + text = QString("There is a new version: %1. " + "Use your package manager to update" + " or click to download.")\ + .arg(newVersion).arg(downloadUrl); +#else + text = QString("Found update to version %1. Click to download.")\ + .arg(newVersion).arg(downloadUrl); +#endif + } + + lastCheck = QDate::currentDate(); + ui->label->setText(text); + }); +} + +UpdateCheckDialog::~UpdateCheckDialog() +{ + delete ui; +} + +void UpdateCheckDialog::showEvent(QShowEvent *event) +{ + updateChecker.checkUpdate(); + ui->label->setText("Checking update..."); +} + +void UpdateCheckDialog::closeEvent(QShowEvent *event) +{ + if (updateChecker.isChecking()) updateChecker.cancelCheck(); +} + +void UpdateCheckDialog::saveSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_UpdateCheck); + settings->setValue(SG_UpdateCheck_Periodic, ui->cbPeriodic->isChecked()); + settings->setValue(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate)); + settings->endGroup(); +} + +void UpdateCheckDialog::loadSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_UpdateCheck); + ui->cbPeriodic->setChecked(settings->value(SG_UpdateCheck_Periodic, + ui->cbPeriodic->isChecked()).toBool()); + auto lastCheckS = settings->value(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate)).toString(); + lastCheck = QDate::fromString(lastCheckS, Qt::ISODate); + settings->endGroup(); + + // start the periodic update if required + if (ui->cbPeriodic->isChecked() && lastCheck < QDate::currentDate()) + { + updateChecker.checkUpdate(); + } +} diff --git a/src/updatecheckdialog.h b/src/updatecheckdialog.h new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.h @@ -0,0 +1,54 @@ +/* + 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 UPDATECHECKDIALOG_H +#define UPDATECHECKDIALOG_H + +#include +#include +#include +#include "updatechecker.h" + +namespace Ui { +class UpdateCheckDialog; +} + +class UpdateCheckDialog : public QDialog +{ + Q_OBJECT + +public: + explicit UpdateCheckDialog(QWidget *parent = 0); + ~UpdateCheckDialog(); + + /// Stores update settings into a `QSettings`. + void saveSettings(QSettings* settings); + /// Loads update settings from a `QSettings`. + void loadSettings(QSettings* settings); + +private: + Ui::UpdateCheckDialog *ui; + UpdateChecker updateChecker; + QDate lastCheck; + + void showEvent(QShowEvent *event); + void closeEvent(QShowEvent *event); +}; + +#endif // UPDATECHECKDIALOG_H diff --git a/src/updatecheckdialog.ui b/src/updatecheckdialog.ui new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.ui @@ -0,0 +1,71 @@ + + + UpdateCheckDialog + + + + 0 + 0 + 400 + 148 + + + + Check Update + + + + + + Checking update... + + + true + + + + + + + Updates will be checked only once a day at first start of the application + + + Check updates periodically + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttonBox + clicked(QAbstractButton*) + UpdateCheckDialog + close() + + + 199 + 125 + + + 199 + 73 + + + + + diff --git a/src/updatechecker.cpp b/src/updatechecker.cpp new file mode 100644 --- /dev/null +++ b/src/updatechecker.cpp @@ -0,0 +1,222 @@ +/* + Copyright © 2018 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 "updatechecker.h" + +// This link returns the list of downloads in JSON format. Note that we only use +// the first page because results are sorted new to old. +const char BB_DOWNLOADS_URL[] = "https://api.bitbucket.org/2.0/repositories/hyozd/serialplot/downloads?fields=values.name,values.links.self.href"; + +UpdateChecker::UpdateChecker(QObject *parent) : + QObject(parent), nam(this) +{ + activeReply = NULL; + + connect(&nam, &QNetworkAccessManager::finished, + this, &UpdateChecker::onReqFinished); +} + +bool UpdateChecker::isChecking() const +{ + return activeReply != NULL && !activeReply->isFinished(); +} + +void UpdateChecker::checkUpdate() +{ + if (isChecking()) return; + + auto req = QNetworkRequest(QUrl(BB_DOWNLOADS_URL)); + activeReply = nam.get(req); +} + +void UpdateChecker::cancelCheck() +{ + if (activeReply != NULL) activeReply->abort(); +} + +void UpdateChecker::onReqFinished(QNetworkReply* reply) +{ + if (reply->error() != QNetworkReply::NoError) + { + emit checkFailed(QString("Network error: ") + reply->errorString()); + } + else + { + QJsonParseError error; + auto data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + emit checkFailed(QString("JSon parsing error: ") + error.errorString()); + } + else + { + QList files; + if (!parseData(data, files)) + { + // TODO: emit detailed data contents for logging + emit checkFailed("Data parsing error."); + } + else + { + FileInfo updateFile; + if (findUpdate(files, updateFile)) + { + emit checkFinished( + true, updateFile.version.toString(), updateFile.link); + } + else + { + emit checkFinished(false, "", ""); + } + } + } + } + reply->deleteLater(); + activeReply = NULL; +} + +bool UpdateChecker::parseData(const QJsonDocument& data, QList& files) const +{ + /* Data is expected to be in this form: + + { + "values": [ + { + "name": "serialplot-0.9.1-x86_64.AppImage", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hyOzd/serialplot/downloads/serialplot-0.9.1-x86_64.AppImage" + } + } + }, ... ] + } + */ + + if (!data.isObject()) return false; + + auto values = data.object()["values"]; + if (values == QJsonValue::Undefined || !values.isArray()) return false; + + for (auto value : values.toArray()) + { + if (!value.isObject()) return false; + + auto name = value.toObject().value("name"); + if (name.isUndefined() || !name.isString()) + return false; + + auto links = value.toObject().value("links"); + if (links.isUndefined() || !links.isObject()) + return false; + + auto self = links.toObject().value("self"); + if (self.isUndefined() || !self.isObject()) + return false; + + auto href = self.toObject().value("href"); + if (href.isUndefined() || !href.isString()) + return false; + + FileInfo finfo; + finfo.name = name.toString(); + finfo.link = href.toString(); + finfo.hasVersion = VersionNumber::extract(name.toString(), finfo.version); + + if (finfo.name.contains("amd64") || + finfo.name.contains("x86_64") || + finfo.name.contains("win64")) + { + finfo.arch = FileArch::amd64; + } + else if (finfo.name.contains("win32") || + finfo.name.contains("i386")) + { + finfo.arch = FileArch::_i386; + } + else + { + finfo.arch = FileArch::unknown; + } + + files += finfo; + } + + return true; +} + +bool UpdateChecker::findUpdate(const QList& files, FileInfo& foundFile) const +{ + QList fflist; + + // filter the file list according to extension and version number + for (int i = 0; i < files.length(); i++) + { + // file type to look +#if defined(Q_OS_WIN) + const char ext[] = ".exe"; +#else // of course linux + const char ext[] = ".appimage"; +#endif + + // file architecture to look +#if defined(Q_PROCESSOR_X86_64) + const FileArch arch = FileArch::amd64; +#elif defined(Q_PROCESSOR_X86_32) + const FileArch arch = FileArch::_i386; +#elif defined(Q_PROCESSOR_ARM) + const FileArch arch = FileArch::arm; +#else + #error Unknown architecture for update file detection. +#endif + + // filter the file list + auto file = files[i]; + if (file.name.contains(ext, Qt::CaseInsensitive) && + file.arch == arch && + file.hasVersion && file.version > CurrentVersion) + { + fflist += file; + } + } + + // sort and find most up to date file + if (!fflist.empty()) + { + std::sort(fflist.begin(), fflist.end(), + [](const FileInfo& a, const FileInfo& b) + { + return a.version > b.version; + }); + + foundFile = fflist[0]; + return true; + } + else + { + return false; + } +} diff --git a/src/updatechecker.h b/src/updatechecker.h new file mode 100644 --- /dev/null +++ b/src/updatechecker.h @@ -0,0 +1,77 @@ +/* + Copyright © 2018 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 UPDATECHECKER_H +#define UPDATECHECKER_H + +#include +#include +#include +#include + +#include "versionnumber.h" + +class UpdateChecker : public QObject +{ + Q_OBJECT +public: + explicit UpdateChecker(QObject *parent = 0); + + bool isChecking() const; + +signals: + void checkFinished(bool found, QString newVersion, QString downloadUrl); + void checkFailed(QString errorMessage); + +public slots: + void checkUpdate(); + void cancelCheck(); + +private: + enum class FileArch + { + unknown, + _i386, + amd64, + arm + }; + + struct FileInfo + { + QString name; + QString link; + bool hasVersion; + VersionNumber version; + FileArch arch; + }; + + QNetworkAccessManager nam; + QNetworkReply* activeReply; + + /// Parses json and creates a list of files + bool parseData(const QJsonDocument& data, QList& files) const; + /// Finds the update file in the file list. Returns `-1` if no new version + /// is found. + bool findUpdate(const QList& files, FileInfo& foundFile) const; + +private slots: + void onReqFinished(QNetworkReply* reply); +}; + +#endif // UPDATECHECKER_H diff --git a/src/versionnumber.cpp b/src/versionnumber.cpp new file mode 100644 --- /dev/null +++ b/src/versionnumber.cpp @@ -0,0 +1,103 @@ +/* + 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 "versionnumber.h" + +VersionNumber::VersionNumber(unsigned mj, unsigned mn, unsigned pt) +{ + major = mj; + minor = mn; + patch = pt; +} + +QString VersionNumber::toString() const +{ + return QString("%1.%2.%3").arg(major).arg(minor).arg(patch); +} + +bool VersionNumber::extract(const QString& str, VersionNumber& number) +{ + QRegularExpression regexp("(?:[-_vV \\t]|^)(?\\d+)" + "(?:\\.(?\\d+))(?:\\.(?\\d+))?[-_ \\t]?"); + auto match = regexp.match(str, 0, QRegularExpression::PartialPreferCompleteMatch); + + if (!(match.hasMatch() || match.hasPartialMatch())) return false; + + number.major = match.captured("major").toUInt(); + + auto zeroIfNull = [](QString str) -> unsigned + { + if (str.isNull()) return 0; + return str.toUInt(); + }; + + number.minor = zeroIfNull(match.captured("minor")); + number.patch = zeroIfNull(match.captured("patch")); + + return true; +} + +bool operator==(const VersionNumber& lhs, const VersionNumber& rhs) +{ + return lhs.major == rhs.major && + lhs.minor == rhs.minor && + lhs.patch == rhs.patch; +} + +bool operator<(const VersionNumber& lhs, const VersionNumber& rhs) +{ + if (lhs.major < rhs.major) + { + return true; + } + else if (lhs.major == rhs.major) + { + if (lhs.minor < rhs.minor) + { + return true; + } + else if (lhs.minor == rhs.minor) + { + if (lhs.patch < rhs.patch) return true; + } + } + return false; +} + +bool operator>(const VersionNumber& lhs, const VersionNumber& rhs) +{ + if (lhs.major > rhs.major) + { + return true; + } + else if (lhs.major == rhs.major) + { + if (lhs.minor > rhs.minor) + { + return true; + } + else if (lhs.minor == rhs.minor) + { + if (lhs.patch > rhs.patch) return true; + } + } + return false; +} diff --git a/src/versionnumber.h b/src/versionnumber.h new file mode 100644 --- /dev/null +++ b/src/versionnumber.h @@ -0,0 +1,46 @@ +/* + 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 VERSIONNUMBER_H +#define VERSIONNUMBER_H + +#include + +struct VersionNumber +{ + unsigned major = 0; + unsigned minor = 0; + unsigned patch = 0; + + VersionNumber(unsigned mj=0, unsigned mn=0, unsigned pt=0); + + /// Convert version number to string. + QString toString() const; + + /// Extracts the version number from given string. + static bool extract(const QString& str, VersionNumber& number); +}; + +bool operator==(const VersionNumber& lhs, const VersionNumber& rhs); +bool operator<(const VersionNumber& lhs, const VersionNumber& rhs); +bool operator>(const VersionNumber& lhs, const VersionNumber& rhs); + +const VersionNumber CurrentVersion(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + +#endif // VERSIONNUMBER_H diff --git a/src/zoomer.cpp b/src/zoomer.cpp --- a/src/zoomer.cpp +++ b/src/zoomer.cpp @@ -28,6 +28,8 @@ Zoomer::Zoomer(QWidget* widget, bool doR { is_panning = false; + setTrackerMode(AlwaysOn); + // set corner widget between the scrollbars with default background color auto cornerWidget = new QWidget(); auto bgColor = cornerWidget->palette().color(QPalette::Window).name(); @@ -62,7 +64,7 @@ QwtText Zoomer::trackerTextF(const QPoin QwtText b = ScrollZoomer::trackerTextF(pos); const QPolygon pa = selection(); - if (pa.count() < 2) + if (!isActive() || pa.count() < 2) { return b; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright © 2017 Hasan Yavuz Özderya +# Copyright © 2018 Hasan Yavuz Özderya # # This file is part of serialplot. # @@ -19,16 +19,77 @@ # Find the QtWidgets library find_package(Qt5Widgets) +find_package(Qt5Test) include_directories("../src") add_executable(Test EXCLUDE_FROM_ALL test.cpp + test_stream.cpp + ../src/samplepack.cpp + ../src/sink.cpp + ../src/source.cpp + ../src/indexbuffer.cpp + ../src/linindexbuffer.cpp + ../src/ringbuffer.cpp + ../src/readonlybuffer.cpp + ../src/stream.cpp + ../src/streamchannel.cpp + ../src/channelinfomodel.cpp ../src/datachunk.cpp - ../src/chunkedbuffer.cpp) + ../src/chunkedbuffer.cpp + ) add_test(NAME test1 COMMAND Test) qt5_use_modules(Test Widgets) +qt5_wrap_ui(UI_FILES_T + ../src/binarystreamreadersettings.ui + ../src/asciireadersettings.ui + ../src/framedreadersettings.ui + ../src/demoreadersettings.ui + ../src/numberformatbox.ui + ../src/endiannessbox.ui + ) + +# test for readers +add_executable(TestReaders EXCLUDE_FROM_ALL + test_readers.cpp + ../src/samplepack.cpp + ../src/sink.cpp + ../src/source.cpp + ../src/abstractreader.cpp + ../src/binarystreamreader.cpp + ../src/binarystreamreadersettings.cpp + ../src/asciireader.cpp + ../src/asciireadersettings.cpp + ../src/framedreader.cpp + ../src/framedreadersettings.cpp + ../src/demoreader.cpp + ../src/demoreadersettings.cpp + ../src/commandedit.cpp + ../src/endiannessbox.cpp + ../src/numberformatbox.cpp + ../src/numberformat.cpp + ${UI_FILES_T} + ) +qt5_use_modules(TestReaders Widgets Test) +add_test(NAME test_readers COMMAND TestReaders) + +# test for recroder +add_executable(TestRecorder EXCLUDE_FROM_ALL + test_recorder.cpp + ../src/samplepack.cpp + ../src/sink.cpp + ../src/source.cpp + ../src/datarecorder.cpp +) +qt5_use_modules(TestRecorder Widgets Test) +add_test(NAME test_recorder COMMAND TestRecorder) + set(CMAKE_CTEST_COMMAND ctest -V) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND}) -add_dependencies(check Test) +add_dependencies(check + Test + TestReaders + TestRecorder + ) diff --git a/tests/catch.hpp b/tests/catch.hpp --- a/tests/catch.hpp +++ b/tests/catch.hpp @@ -11542,4 +11542,3 @@ using Catch::Detail::Approx; #endif #endif // TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED - diff --git a/tests/test.cpp b/tests/test.cpp --- a/tests/test.cpp +++ b/tests/test.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,9 +21,370 @@ #define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file #include "catch.hpp" +#include "samplepack.h" +#include "source.h" +#include "indexbuffer.h" +#include "linindexbuffer.h" +#include "ringbuffer.h" +#include "readonlybuffer.h" #include "datachunk.h" #include "chunkedbuffer.h" +#include "test_helpers.h" + +TEST_CASE("samplepack with no X", "[memory]") +{ + SamplePack pack(100, 3, false); + + REQUIRE_FALSE(pack.hasX()); + REQUIRE(pack.numChannels() == 3); + REQUIRE(pack.numSamples() == 100); + + double* chan0 = pack.data(0); + double* chan1 = pack.data(1); + double* chan2 = pack.data(2); + + REQUIRE(chan0 == chan1 - 100); + REQUIRE(chan1 == chan2 - 100); +} + +TEST_CASE("samplepack with X", "[memory]") +{ + SamplePack pack(100, 3, true); + + REQUIRE(pack.hasX()); + REQUIRE(pack.numChannels() == 3); + REQUIRE(pack.numSamples() == 100); + REQUIRE(pack.xData() != nullptr); +} + +TEST_CASE("samplepack copy", "[memory]") +{ + SamplePack pack(10, 3, true); + + // fill test data + for (int i = 0; i < 10; i++) + { + pack.xData()[i] = i; + pack.data(0)[i] = i+5; + pack.data(1)[i] = i*2; + pack.data(2)[i] = i*3; + } + + SamplePack other = pack; + // compare + for (int i = 0; i < 10; i++) + { + REQUIRE(other.xData()[i] == i); + REQUIRE(other.data(0)[i] == i+5); + REQUIRE(other.data(1)[i] == i*2); + REQUIRE(other.data(2)[i] == i*3); + } +} + +TEST_CASE("sink", "[memory, stream]") +{ + TestSink sink; + SamplePack pack(100, 3, false); + + sink.setNumChannels(3, false); + REQUIRE(sink.numChannels() == 3); + + sink.feedIn(pack); + REQUIRE(sink.totalFed == 100); + sink.feedIn(pack); + REQUIRE(sink.totalFed == 200); + + TestSink follower; + + sink.connectFollower(&follower); + REQUIRE(follower.numChannels() == 3); + REQUIRE(follower.hasX() == false); + + sink.feedIn(pack); + REQUIRE(sink.totalFed == 300); + REQUIRE(follower.totalFed == 100); + + sink.setNumChannels(2, true); + REQUIRE(follower.numChannels() == 2); + REQUIRE(follower.hasX() == true); +} + +TEST_CASE("sink must be created unconnected", "[memory, stream]") +{ + TestSink sink; + REQUIRE(sink.connectedSource() == NULL); +} + +TEST_CASE("source", "[memory, stream]") +{ + TestSink sink; + + TestSource source(3, false); + + REQUIRE(source.numChannels() == 3); + REQUIRE(source.hasX() == false); + + source.connectSink(&sink); + REQUIRE(sink.numChannels() == 3); + REQUIRE(sink.hasX() == false); + + source._setNumChannels(5, true); + REQUIRE(sink.numChannels() == 5); + REQUIRE(sink.hasX() == true); + + SamplePack pack(100, 5, true); + source._feed(pack); + REQUIRE(sink.totalFed == 100); + + source.disconnect(&sink); + source._feed(pack); + REQUIRE(sink.totalFed == 100); +} + +TEST_CASE("source must set/unset sink 'source'", "[memory, stream]") +{ + TestSink sink; + TestSource source(3, false); + + source.connectSink(&sink); + REQUIRE(sink.connectedSource() == &source); + + source.disconnect(&sink); + REQUIRE(sink.connectedSource() == NULL); +} + +TEST_CASE("source disconnect all sinks", "[memory, stream]") +{ + TestSink sinks[3]; + TestSource source(3, false); + + // connect sinks + for (int i = 0; i < 3; i++) + { + source.connectSink(&sinks[i]); + } + + source.disconnectSinks(); + for (int i = 0; i < 3; i++) + { + REQUIRE(sinks[i].connectedSource() == NULL); + } +} + +TEST_CASE("IndexBuffer", "[memory, buffer]") +{ + IndexBuffer buf(10); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == i); + } + auto l = buf.limits(); + REQUIRE(l.start == 0); + REQUIRE(l.end == 9); + + buf.resize(20); + REQUIRE(buf.size() == 20); + REQUIRE(buf.sample(15) == 15); + l = buf.limits(); + REQUIRE(l.start == 0); + REQUIRE(l.end == 19); +} + +TEST_CASE("LinIndexBuffer", "[memory, buffer]") +{ + LinIndexBuffer buf(10, 0., 3.0); + + REQUIRE(buf.size() == 10); + REQUIRE(buf.sample(0) == 0.); + REQUIRE(buf.sample(9) == 3.0); + REQUIRE(buf.sample(4) == Approx(1+1/3.)); + + auto l = buf.limits(); + REQUIRE(l.start == 0.); + REQUIRE(l.end == 3.); + + buf.resize(20); + REQUIRE(buf.size() == 20); + REQUIRE(buf.sample(0) == 0.); + REQUIRE(buf.sample(9) == Approx(9.*3./19.)); + REQUIRE(buf.sample(4) == Approx(4.*3./19.)); + REQUIRE(buf.sample(19) == 3.0); + + l = buf.limits(); + REQUIRE(l.start == 0.); + REQUIRE(l.end == 3.0); + + buf.setLimits({-5., 5.}); + l = buf.limits(); + REQUIRE(l.start == -5.0); + REQUIRE(l.end == 5.0); + + REQUIRE(buf.sample(0) == -5.0); + REQUIRE(buf.sample(19) == 5.0); +} + +TEST_CASE("RingBuffer sizing", "[memory, buffer]") +{ + RingBuffer buf(10); + + REQUIRE(buf.size() == 10); + + buf.resize(5); + REQUIRE(buf.size() == 5); + + buf.resize(15); + REQUIRE(buf.size() == 15); +} + +TEST_CASE("RingBuffer initial values should be 0", "[memory, buffer]") +{ + RingBuffer buf(10); + + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == 0.); + } +} + +TEST_CASE("RingBuffer data access", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 10); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == values[i]); + } + + buf.addSamples(values, 5); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(buf.sample(i) == values[i+5]); + } + for (unsigned i = 5; i < 10; i++) + { + REQUIRE(buf.sample(i) == values[i-5]); + } +} + +TEST_CASE("making RingBuffer bigger should keep end values", "[memory, buffer]") +{ + RingBuffer buf(5); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 5); + buf.resize(10); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(buf.sample(i) == 0); + } + for (unsigned i = 5; i < 10; i++) + { + REQUIRE(buf.sample(i) == values[i-5]); + } +} + +TEST_CASE("making RingBuffer smaller should keep end values", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 10); + buf.resize(5); + + REQUIRE(buf.size() == 5); + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(buf.sample(i) == values[i+5]); + } +} + +TEST_CASE("RingBuffer limits", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + auto lim = buf.limits(); + REQUIRE(lim.start == 0.); + REQUIRE(lim.end == 0.); + + buf.addSamples(values, 10); + lim = buf.limits(); + REQUIRE(lim.start == 1.); + REQUIRE(lim.end == 10.); + + buf.addSamples(&values[9], 1); + lim = buf.limits(); + REQUIRE(lim.start == 2.); + REQUIRE(lim.end == 10.); + + buf.addSamples(values, 9); + buf.addSamples(values, 1); + lim = buf.limits(); + REQUIRE(lim.start == 1.); + REQUIRE(lim.end == 9.); +} + +TEST_CASE("RingBuffer clear", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 10); + buf.clear(); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == 0.); + } + auto lim = buf.limits(); + REQUIRE(lim.start == 0.); + REQUIRE(lim.end == 0.); +} + +TEST_CASE("ReadOnlyBuffer", "[memory, buffer]") +{ + IndexBuffer source(10); + + ReadOnlyBuffer buf(&source); + + REQUIRE(buf.size() == 10); + auto lim = buf.limits(); + REQUIRE(lim.start == 0.); + REQUIRE(lim.end == 9.); + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == i); + } +} + +TEST_CASE("ReadOnlyBuffer sliced constructor", "[memory, buffer]") +{ + IndexBuffer source(10); + + ReadOnlyBuffer buf(&source, 5, 4); + + REQUIRE(buf.size() == 4); + auto lim = buf.limits(); + REQUIRE(lim.start == 5.); + REQUIRE(lim.end == 8.); + for (unsigned i = 0; i < 4; i++) + { + REQUIRE(buf.sample(i) == (i + 5)); + } +} + TEST_CASE("DataChunk created empty", "[memory]") { DataChunk c(0, 1000); diff --git a/tests/test_helpers.h b/tests/test_helpers.h new file mode 100644 --- /dev/null +++ b/tests/test_helpers.h @@ -0,0 +1,86 @@ +#ifndef TEST_HELPERS_H +#define TEST_HELPERS_H + +#include "source.h" +#include "sink.h" + +class TestSink : public Sink +{ +public: + int totalFed; + int _numChannels; + bool _hasX; + + TestSink() + { + totalFed = 0; + _numChannels = 0; + _hasX = false; + }; + + void feedIn(const SamplePack& data) + { + REQUIRE(data.numChannels() == numChannels()); + + totalFed += data.numSamples(); + + Sink::feedIn(data); + }; + + void setNumChannels(unsigned nc, bool x) + { + _numChannels = nc; + _hasX = x; + + Sink::setNumChannels(nc, x); + }; + + virtual unsigned numChannels() const + { + return _numChannels; + }; + + virtual bool hasX() const + { + return _hasX; + }; +}; + +class TestSource : public Source +{ +public: + int _numChannels; + bool _hasX; + + TestSource(unsigned nc, bool x) + { + _numChannels = nc; + _hasX = x; + }; + + virtual unsigned numChannels() const + { + return _numChannels; + }; + + virtual bool hasX() const + { + return _hasX; + }; + + void _feed(const SamplePack& data) const + { + feedOut(data); + }; + + void _setNumChannels(unsigned nc, bool x) + { + _numChannels = nc; + _hasX = x; + + updateNumChannels(); + }; +}; + + +#endif // TEST_HELPERS_H diff --git a/tests/test_readers.cpp b/tests/test_readers.cpp new file mode 100644 --- /dev/null +++ b/tests/test_readers.cpp @@ -0,0 +1,210 @@ +/* + Copyright © 2018 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 . +*/ + +// This tells Catch to provide a main() - only do this in one cpp file per executable +#define CATCH_CONFIG_RUNNER +#include "catch.hpp" + +#include +#include +#include "binarystreamreader.h" +#include "asciireader.h" +#include "framedreader.h" +#include "demoreader.h" + +#include "test_helpers.h" + +static const int READYREAD_TIMEOUT = 10; // milliseconds + +TEST_CASE("reading data with BinaryStreamReader", "[reader]") +{ + QBuffer bufferDev; + BinaryStreamReader bs(&bufferDev); + bs.enable(true); + + TestSink sink; + bs.connectSink(&sink); + + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + + bufferDev.open(QIODevice::ReadWrite); + const char data[] = {0x01, 0x02, 0x03, 0x04}; + bufferDev.write(data, 4); + bufferDev.seek(0); + + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + REQUIRE(spy.wait(READYREAD_TIMEOUT)); + REQUIRE(sink.totalFed == 4); +} + +TEST_CASE("disabled BinaryStreamReader shouldn't read", "[reader]") +{ + QBuffer bufferDev; + BinaryStreamReader bs(&bufferDev); // disabled by default + + TestSink sink; + bs.connectSink(&sink); + + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + + bufferDev.open(QIODevice::ReadWrite); + const char data[] = {0x01, 0x02, 0x03, 0x04}; + bufferDev.write(data, 4); + bufferDev.seek(0); + + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + // readyRead isn't signaled because there are no connections to it + REQUIRE_FALSE(spy.wait(READYREAD_TIMEOUT)); + REQUIRE(sink.totalFed == 0); +} + +TEST_CASE("reading data with AsciiReader", "[reader, ascii]") +{ + QBuffer bufferDev; + AsciiReader reader(&bufferDev); + reader.enable(true); + + TestSink sink; + reader.connectSink(&sink); + + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + + // inject data to the buffer + bufferDev.open(QIODevice::ReadWrite); + bufferDev.write("0,1,3\n0,1,3\n0,1,3\n0,1,3\n"); + bufferDev.seek(0); + + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + REQUIRE(spy.wait(READYREAD_TIMEOUT)); + REQUIRE(sink._numChannels == 3); + REQUIRE(sink._hasX == false); + REQUIRE(sink.totalFed == 3); +} + +TEST_CASE("AsciiReader shouldn't read when disabled", "[reader, ascii]") +{ + QBuffer bufferDev; + AsciiReader reader(&bufferDev); // disabled by default + + TestSink sink; + reader.connectSink(&sink); + + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + + // inject data to the buffer + bufferDev.open(QIODevice::ReadWrite); + bufferDev.write("0,1,3\n0,1,3\n0,1,3\n0,1,3\n"); + bufferDev.seek(0); + + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + REQUIRE_FALSE(spy.wait(READYREAD_TIMEOUT)); + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + REQUIRE(sink.totalFed == 0); +} + +TEST_CASE("reading data with FramedReader", "[reader]") +{ + QBuffer bufferDev; + FramedReader reader(&bufferDev); + reader.enable(true); + + TestSink sink; + reader.connectSink(&sink); + + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + + bufferDev.open(QIODevice::ReadWrite); + const uint8_t data[] = {0xAA, 0xBB, 4, 0x01, 0x02, 0x03, 0x04}; + bufferDev.write((const char*) data, 7); + bufferDev.seek(0); + + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + REQUIRE(spy.wait(READYREAD_TIMEOUT)); + REQUIRE(sink.totalFed == 4); +} + +TEST_CASE("FramedReader shouldn't read when disabled", "[reader]") +{ + QBuffer bufferDev; + FramedReader reader(&bufferDev); + + TestSink sink; + reader.connectSink(&sink); + + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + + bufferDev.open(QIODevice::ReadWrite); + const uint8_t data[] = {0xAA, 0xBB, 4, 0x01, 0x02, 0x03, 0x04}; + bufferDev.write((const char*) data, 7); + bufferDev.seek(0); + + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + REQUIRE_FALSE(spy.wait(READYREAD_TIMEOUT)); + REQUIRE(sink.totalFed == 0); +} + +TEST_CASE("Generating data with DemoReader", "[reader, demo]") +{ + QBuffer bufferDev; // not actually used + DemoReader demoReader(&bufferDev); + demoReader.enable(true); + + TestSink sink; + demoReader.connectSink(&sink); + REQUIRE(sink._numChannels == 1); + REQUIRE(sink._hasX == false); + + // we need to wait somehow, we are not actually looking for signals + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + REQUIRE_FALSE(spy.wait(1000)); // we need some time for demoreader to produce data + REQUIRE(sink.totalFed >= 9); +} + +TEST_CASE("DemoReader shouldn't generate data when paused", "[reader, demo]") +{ + QBuffer bufferDev; // not actually used + DemoReader demoReader(&bufferDev); // paused by default + + TestSink sink; + demoReader.connectSink(&sink); + REQUIRE(sink._numChannels == 1); + + // we need to wait somehow, we are not actually looking for signals + QSignalSpy spy(&bufferDev, SIGNAL(readyRead())); + REQUIRE_FALSE(spy.wait(1000)); // we need some time for demoreader to produce data + REQUIRE(sink.totalFed == 0); +} + +// Note: this is added because `QApplication` must be created for widgets +#include +int main(int argc, char* argv[]) +{ + QApplication a(argc, argv); + + int result = Catch::Session().run( argc, argv ); + + return result; +} diff --git a/tests/test_recorder.cpp b/tests/test_recorder.cpp new file mode 100644 --- /dev/null +++ b/tests/test_recorder.cpp @@ -0,0 +1,107 @@ +/* + Copyright © 2018 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 . +*/ + +#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file +#include "catch.hpp" + +#include +#include "datarecorder.h" +#include "test_helpers.h" + +#define TEST_FILE_NAME "sp_test_recording.csv" + +TEST_CASE("test recording single channel", "[recorder]") +{ + DataRecorder rec; + TestSource source(1, false); + + // temporary file, remove if exists + auto fileName = QDir::tempPath() + QString("/" TEST_FILE_NAME); + if (QFile::exists(fileName)) QFile::remove(fileName); + + // connect source → sink + source.connectSink(&rec); + + // prepare data + QStringList channelNames({"Channel 1"}); + SamplePack samples(5, 1); + for (int i = 0; i < 5; i++) + { + samples.data(0)[i] = i+1; + } + + // test + rec.startRecording(fileName, ",", channelNames, false); + source._feed(samples); + rec.stopRecording(); + + // read file contents back + QFile recordFile(fileName); + REQUIRE(recordFile.open(QIODevice::ReadOnly | QIODevice::Text)); + // NOTE: mind the extra parantheses, otherwise 'catch' macros fail to compile + REQUIRE((recordFile.readLine() == "Channel 1\n")); + for (int i = 0; i < 5; i++) + REQUIRE((recordFile.readLine() == QString("%1\n").arg(i+1))); + + // cleanup + if (QFile::exists(fileName)) QFile::remove(fileName); +} + +TEST_CASE("test recording multiple channels", "[recorder]") +{ + DataRecorder rec; + TestSource source(3, false); + + // temporary file, remove if exists + auto fileName = QDir::tempPath() + QString("/" TEST_FILE_NAME); + if (QFile::exists(fileName)) QFile::remove(fileName); + + // connect source → sink + source.connectSink(&rec); + + // prepare data + QStringList channelNames({"Channel 1", "Channel 2", "Channel 3"}); + SamplePack samples(5, 3); + for (int ci = 0; ci < 3; ci++) + { + for (int i = 0; i < 5; i++) + { + samples.data(ci)[i] = (ci+1)*(i+1); + } + } + + // test + rec.startRecording(fileName, ",", channelNames, false); + source._feed(samples); + rec.stopRecording(); + + // read file contents back + QFile recordFile(fileName); + REQUIRE(recordFile.open(QIODevice::ReadOnly | QIODevice::Text)); + // NOTE: mind the extra parantheses, otherwise 'catch' macros fail to compile + REQUIRE((recordFile.readLine() == "Channel 1,Channel 2,Channel 3\n")); + REQUIRE((recordFile.readLine() == "1,2,3\n")); + REQUIRE((recordFile.readLine() == "2,4,6\n")); + REQUIRE((recordFile.readLine() == "3,6,9\n")); + REQUIRE((recordFile.readLine() == "4,8,12\n")); + REQUIRE((recordFile.readLine() == "5,10,15\n")); + + // cleanup + if (QFile::exists(fileName)) QFile::remove(fileName); +} diff --git a/tests/test_stream.cpp b/tests/test_stream.cpp new file mode 100644 --- /dev/null +++ b/tests/test_stream.cpp @@ -0,0 +1,246 @@ +/* + Copyright © 2018 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 "stream.h" + +#include "catch.hpp" +#include "test_helpers.h" + +TEST_CASE("construction of stream with default values", "[memory, stream]") +{ + // default values are an empty stream with no channels + Stream s; + + REQUIRE(s.numChannels() == 0); + REQUIRE(!s.hasX()); + REQUIRE(s.numSamples() == 0); +} + +TEST_CASE("construction of stream with parameters", "[memory, stream]") +{ + Stream s(4, true, 100); + + REQUIRE(s.numChannels() == 4); + REQUIRE(s.hasX()); + REQUIRE(s.numSamples() == 100); + + for (unsigned i = 0; i < 4; i++) + { + const StreamChannel* c = s.channel(i); + REQUIRE(c != NULL); + REQUIRE(c->index() == i); + } +} + +TEST_CASE("changing stream number of channels via sink", "[memory, stream, sink]") +{ + Stream s; + TestSource so(3, false); + so.connectSink(&s); + + // nc=3, x= false + REQUIRE(s.numChannels() == 3); + REQUIRE(!s.hasX()); + for (unsigned i = 0; i < 3; i++) + { + const StreamChannel* c = s.channel(i); + REQUIRE(c != NULL); + REQUIRE(c->index() == i); + } + + // increase nc value, add X + so._setNumChannels(5, true); + + REQUIRE(s.numChannels() == 5); + REQUIRE(s.hasX()); + + for (unsigned i = 0; i < 5; i++) + { + const StreamChannel* c = s.channel(i); + REQUIRE(c != NULL); + REQUIRE(c->index() == i); + } + + // reduce nc value, remove X + so._setNumChannels(1, false); + + REQUIRE(s.numChannels() == 1); + REQUIRE(!s.hasX()); + + for (unsigned i = 0; i < 1; i++) + { + const StreamChannel* c = s.channel(i); + REQUIRE(c != NULL); + REQUIRE(c->index() == i); + } +} + +TEST_CASE("adding data to a stream with no X", "[memory, stream, data, sink]") +{ + Stream s(3, false, 10); + + // prepare data + SamplePack pack(5, 3, false); + for (unsigned ci = 0; ci < 3; ci++) + { + for (unsigned i = 0; i < 5; i++) + { + pack.data(ci)[i] = i; + } + } + + TestSource so(3, false); + so.connectSink(&s); + + // test + so._feed(pack); + + for (unsigned ci = 0; ci < 3; ci++) + { + const StreamChannel* c = s.channel(ci); + const FrameBuffer* y = c->yData(); + + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(y->sample(i) == 0); + } + for (unsigned i = 5; i < 10; i++) + { + REQUIRE(y->sample(i) == i-5); + } + } +} + +TEST_CASE("adding data to a stream with X", "[memory, stream, data, sink]") +{ + Stream s(3, true, 10); + + // prepare data + SamplePack pack(5, 3, true); + for (unsigned ci = 0; ci < 3; ci++) + { + for (unsigned i = 0; i < 5; i++) + { + pack.data(ci)[i] = i; + } + } + // x data + for (unsigned i = 0; i < 5; i++) + { + pack.xData()[i] = i+10; + } + + TestSource so(3, true); + so.connectSink(&s); + + // test + so._feed(pack); + + for (unsigned ci = 0; ci < 3; ci++) + { + const StreamChannel* c = s.channel(ci); + const FrameBuffer* y = c->yData(); + + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(y->sample(i) == 0); + } + for (unsigned i = 5; i < 10; i++) + { + REQUIRE(y->sample(i) == i-5); + } + } + + // check x + const FrameBuffer* x = s.channel(0)->xData(); + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(x->sample(i) == 0); + } + for (unsigned i = 5; i < 10; i++) + { + REQUIRE(x->sample(i) == (i-5)+10); + } +} + +TEST_CASE("paused stream shouldn't store data", "[memory, stream, pause]") +{ + Stream s(3, false, 10); + + // prepare data + SamplePack pack(5, 3, false); + for (unsigned ci = 0; ci < 3; ci++) + { + for (unsigned i = 0; i < 5; i++) + { + pack.data(ci)[i] = i; + } + } + + TestSource so(3, false); + so.connectSink(&s); + + // test + s.pause(true); + so._feed(pack); + + for (unsigned ci = 0; ci < 3; ci++) + { + const StreamChannel* c = s.channel(ci); + const FrameBuffer* y = c->yData(); + + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(y->sample(i) == 0); + } + } +} + +TEST_CASE("clear stream data", "[memory, stream, pause]") +{ + Stream s(3, false, 10); + + // prepare data + SamplePack pack(5, 3, false); + for (unsigned ci = 0; ci < 3; ci++) + { + for (unsigned i = 0; i < 5; i++) + { + pack.data(ci)[i] = i; + } + } + + TestSource so(3, false); + so.connectSink(&s); + + // test + so._feed(pack); + s.clear(); + + for (unsigned ci = 0; ci < 3; ci++) + { + const StreamChannel* c = s.channel(ci); + const FrameBuffer* y = c->yData(); + + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(y->sample(i) == 0); + } + } +}