From aae596d3813bef43404613ba0e30b192b7ea3d3d Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Mon, 21 Sep 2026 17:12:36 +0200 Subject: [PATCH 1/8] CI: retry linuxdeployqt download on transient errors --- deploy/linux/build_linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/linux/build_linux.sh b/deploy/linux/build_linux.sh index a249e69..ac118a8 100755 --- a/deploy/linux/build_linux.sh +++ b/deploy/linux/build_linux.sh @@ -56,7 +56,7 @@ command -v dpkg-deb >/dev/null 2>&1 || fail "dpkg-deb is not installed." if [ ! -f "${LINUXDEPLOYQT}" ]; then info "Downloading linuxdeployqt" - curl -fsSL -o "${LINUXDEPLOYQT}" "${LINUXDEPLOYQT_URL}" + curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors -o "${LINUXDEPLOYQT}" "${LINUXDEPLOYQT_URL}" fi LINUXDEPLOYQT="$(realpath "${LINUXDEPLOYQT}")" chmod +x "${LINUXDEPLOYQT}" From cf3c87056337c7697a3218eb0a41635b13cc78e6 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Tue, 22 Sep 2026 16:16:34 +0200 Subject: [PATCH 2/8] Latest --- Source/device.cpp | 17 +++++++++++++++++ Source/device.h | 1 + 2 files changed, 18 insertions(+) diff --git a/Source/device.cpp b/Source/device.cpp index dec6417..4a0de3d 100644 --- a/Source/device.cpp +++ b/Source/device.cpp @@ -40,6 +40,7 @@ bool Device::acquisitionStart() { QString response; int streamID = m_params->getParamVariant("streamId").toInt(); + syncSamplesNoFromParameters(); streamLink->flush(); if(adc == DEVICE_ADC_UNKNOWN) return false; QString command = "device stream start -sid=" + QString::number(streamID) + " -adc=" + QString::number(adc-1); @@ -326,6 +327,22 @@ bool Device::getResolution(device_adc_resolution_t *resolution) return true; } +void Device::syncSamplesNoFromParameters() +{ + unsigned int packetSize; + bool ok = false; + + if(m_params == NULL) return; + + packetSize = m_params->getParamValue("streamPacketSize").toUInt(&ok); + if(!ok || packetSize == 0) return; + if(packetSize == samplesNo) return; + + samplesNo = packetSize; + dataProcessing->setSamplesNo(packetSize); + energyPointProcessing->setSamplesNo(packetSize); +} + bool Device::setSamplesNo(unsigned int aSamplesNo) { QString response; diff --git a/Source/device.h b/Source/device.h index d87d9a6..bcb6100 100644 --- a/Source/device.h +++ b/Source/device.h @@ -107,6 +107,7 @@ class Device : public QObject bool setResolution(device_adc_resolution_t resolution); bool getResolution(device_adc_resolution_t* resolution = NULL); bool setSamplesNo(unsigned int aSamplesNo); + void syncSamplesNoFromParameters(); bool setClockDiv(device_adc_clock_div_t clockDiv); bool getClockDiv(device_adc_clock_div_t* clockDiv = NULL); bool setChSampleTime(device_adc_ch_sampling_time_t sampleTime); From 4ff3827856df42cf236804900f590f1a1bb12ff4 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Fri, 25 Sep 2026 17:58:09 +0200 Subject: [PATCH 3/8] About added --- .github/workflows/linux-build.yml | 14 +- .github/workflows/windows-build.yml | 14 +- Source/OpenEPT.pro | 14 + .../Processing/Parameters/deviceparamdefs.cpp | 55 ++ .../Processing/Parameters/deviceparamdefs.h | 6 +- Source/Processing/Parameters/parameterdefs.h | 3 +- Source/Utility/log.cpp | 25 +- Source/Utility/log.h | 2 + Source/Windows/About/aboutwnd.cpp | 91 +++ Source/Windows/About/aboutwnd.h | 18 + Source/Windows/DataAnalyzer/dataanalyzer.cpp | 547 ++++-------------- Source/Windows/DataAnalyzer/dataanalyzer.h | 119 +--- .../DataAnalyzer/dataanalyzerprofile.cpp | 488 ++++++++++++++++ .../DataAnalyzer/dataanalyzerprofile.h | 91 +++ .../DataAnalyzer/dataanalyzerstatistics.cpp | 266 +++++++-- .../DataAnalyzer/dataanalyzerstatistics.h | 20 +- .../Windows/DataAnalyzer/dataanalyzerworker.h | 38 ++ Source/Windows/Device/configurationwnd.cpp | 50 +- Source/Windows/Device/logdockwidget.cpp | 27 + Source/Windows/Device/logdockwidget.h | 5 + Source/Windows/Plot/plot.cpp | 12 + Source/Windows/Plot/plot.h | 1 + Source/devicecontainer.cpp | 25 + Source/devicecontainer.h | 1 + Source/openept.cpp | 12 + Source/openept.h | 4 + Source/openept.ui | 6 + VERSION | 1 + deploy/set_version.sh | 42 ++ 29 files changed, 1417 insertions(+), 580 deletions(-) create mode 100644 Source/Windows/About/aboutwnd.cpp create mode 100644 Source/Windows/About/aboutwnd.h create mode 100644 Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp create mode 100644 Source/Windows/DataAnalyzer/dataanalyzerprofile.h create mode 100644 Source/Windows/DataAnalyzer/dataanalyzerworker.h create mode 100644 VERSION create mode 100755 deploy/set_version.sh diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 7fec620..90d1d01 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -34,17 +34,25 @@ jobs: - name: Determine version id: version run: | + BASE="$(head -n 1 VERSION | tr -d '[:space:]')" + [ -n "$BASE" ] || { echo "VERSION file is empty"; exit 1; } if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then - echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + TAG="${GITHUB_REF#refs/tags/v}" + if [ "$TAG" != "$BASE" ]; then + echo "Tag v$TAG does not match VERSION file ($BASE)." + echo "Update VERSION and commit it before tagging." + exit 1 + fi + echo "version=${BASE}" >> "$GITHUB_OUTPUT" else - echo "version=0.0.0+git.$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + echo "version=${BASE}+git.$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" fi - name: Build OpenEPT run: | mkdir -p build cd build - qmake ../Source/OpenEPT.pro CONFIG+=release + qmake ../Source/OpenEPT.pro CONFIG+=release APP_VERSION="${{ steps.version.outputs.version }}" make -j"$(nproc)" - name: Create AppImage and Debian package diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 4f6c5e5..63896d1 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -19,10 +19,18 @@ jobs: id: version shell: bash run: | + BASE="$(head -n 1 VERSION | tr -d '[:space:]')" + [ -n "$BASE" ] || { echo "VERSION file is empty"; exit 1; } if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then - echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + TAG="${GITHUB_REF#refs/tags/v}" + if [ "$TAG" != "$BASE" ]; then + echo "Tag v$TAG does not match VERSION file ($BASE)." + echo "Update VERSION and commit it before tagging." + exit 1 + fi + echo "version=${BASE}" >> "$GITHUB_OUTPUT" else - echo "version=0.0.0-git.$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + echo "version=${BASE}-git.$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" fi - name: Install Qt @@ -42,7 +50,7 @@ jobs: run: | mkdir build cd build - qmake ..\Source\OpenEPT.pro CONFIG+=release + qmake ..\Source\OpenEPT.pro CONFIG+=release APP_VERSION=${{ steps.version.outputs.version }} nmake - name: Deploy Qt runtime diff --git a/Source/OpenEPT.pro b/Source/OpenEPT.pro index d431d45..1b2434d 100644 --- a/Source/OpenEPT.pro +++ b/Source/OpenEPT.pro @@ -5,6 +5,15 @@ QT += svg greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport CONFIG += c++17 + +isEmpty(APP_VERSION) { + APP_VERSION = $$cat($$PWD/../VERSION, lines) +} +isEmpty(APP_VERSION) { + APP_VERSION = 0.0.0-dev +} +VERSION = $$replace(APP_VERSION, "[-+].*", "") +DEFINES += APP_VERSION=\\\"$$APP_VERSION\\\" #DEFINES += QCUSTOMPLOT_USE_OPENGL # You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. @@ -35,10 +44,12 @@ SOURCES += \ Processing/fileprocessing.cpp \ Processing/waveform.cpp \ Utility/log.cpp \ + Windows/About/aboutwnd.cpp \ Windows/AddDevice/adddevicewnd.cpp \ Windows/ApplicationConf/applicationconfwnd.cpp \ Windows/Console/consolewnd.cpp \ Windows/DataAnalyzer/dataanalyzer.cpp \ + Windows/DataAnalyzer/dataanalyzerprofile.cpp \ Windows/DataAnalyzer/dataanalyzerstatistics.cpp \ Windows/Device/calibrationwnd.cpp \ Windows/Device/configurationwnd.cpp \ @@ -74,10 +85,13 @@ HEADERS += \ Processing/fileprocessing.h \ Processing/waveform.h \ Utility/log.h \ + Windows/About/aboutwnd.h \ Windows/AddDevice/adddevicewnd.h \ Windows/ApplicationConf/applicationconfwnd.h \ Windows/Console/consolewnd.h \ Windows/DataAnalyzer/dataanalyzer.h \ + Windows/DataAnalyzer/dataanalyzerprofile.h \ + Windows/DataAnalyzer/dataanalyzerworker.h \ Windows/DataAnalyzer/dataanalyzerstatistics.h \ Windows/Device/calibrationwnd.h \ Windows/Device/configurationwnd.h \ diff --git a/Source/Processing/Parameters/deviceparamdefs.cpp b/Source/Processing/Parameters/deviceparamdefs.cpp index 6e87762..5565259 100644 --- a/Source/Processing/Parameters/deviceparamdefs.cpp +++ b/Source/Processing/Parameters/deviceparamdefs.cpp @@ -29,6 +29,12 @@ QList defaultGroupMeta() "Charger Configuration", "Charger COnfiguration if it is connected", 4 + }, + { + DeviceParamDefs::AcquisitionConfig, + "Acquisition", + "Application side acquisition control rules.", + 5 } }; } @@ -126,6 +132,12 @@ QList defaultSubGroupMeta() "Charger Memory Editing", 14 }, + { + DeviceParamDefs::AcquisitionControl, + "Acquisition Control", + "Rules that automatically control the acquisition.", + 15 + }, }; } @@ -1078,6 +1090,49 @@ QList defaultParams() }, 0, false + }, + { + { + "acqStopOnMarkerEnabled", + "Pause Acquisition On Marker", + "Pause the acquisition when the energy point marker with the configured name is received.", + "", + DeviceParamDefs::AcquisitionConfig, + DeviceParamDefs::AcquisitionControl, + Params::Access::ReadWrite, + Params::Storage::LoadSave, + Params::Target::Application, + false, + QVariant(), + QVariant(), + {}, + true, + 42, + Params::Editor::CheckBox + }, + false, + true + }, + { + { + "acqStopOnMarkerName", + "Pause Marker Name", + "Name of the energy point marker that pauses the acquisition.", + "", + DeviceParamDefs::AcquisitionConfig, + DeviceParamDefs::AcquisitionControl, + Params::Access::ReadWrite, + Params::Storage::LoadSave, + Params::Target::Application, + "", + QVariant(), + QVariant(), + {}, + true, + 43 + }, + "", + true } }; } diff --git a/Source/Processing/Parameters/deviceparamdefs.h b/Source/Processing/Parameters/deviceparamdefs.h index 69f86df..d58c8b4 100644 --- a/Source/Processing/Parameters/deviceparamdefs.h +++ b/Source/Processing/Parameters/deviceparamdefs.h @@ -11,7 +11,8 @@ namespace DeviceParamDefs ApplicationConfig = 1, RuntimeState = 2, Calculated = 3, - ChargerConfig = 4 + ChargerConfig = 4, + AcquisitionConfig = 5 }; enum SubGroup @@ -30,7 +31,8 @@ namespace DeviceParamDefs Statistics = 11, FileStorage = 12, ChargerConf = 13, - ChargerBD = 14 + ChargerBD = 14, + AcquisitionControl = 15 }; QList defaultGroupMeta(); diff --git a/Source/Processing/Parameters/parameterdefs.h b/Source/Processing/Parameters/parameterdefs.h index 192f7fd..0552cb5 100644 --- a/Source/Processing/Parameters/parameterdefs.h +++ b/Source/Processing/Parameters/parameterdefs.h @@ -37,7 +37,8 @@ namespace Params enum class Editor { LineEdit, - ComboBox + ComboBox, + CheckBox }; struct GroupMeta diff --git a/Source/Utility/log.cpp b/Source/Utility/log.cpp index 3ca715d..9d2b99b 100644 --- a/Source/Utility/log.cpp +++ b/Source/Utility/log.cpp @@ -1,11 +1,13 @@ #include "log.h" #include +#include Log::Log(QObject *parent) : QObject{parent} { plainTextWidget = NULL; filter = LOG_FILTER_ALL; + followOutput = true; } void Log::assignLogWidget(QPlainTextEdit *aWidget) @@ -33,9 +35,30 @@ void Log::appendEntry(log_entry_t entry) { if(plainTextWidget == NULL) return; if(!categoryVisible(entry.category, filter)) return; - plainTextWidget->moveCursor(QTextCursor::End); + + QScrollBar *scrollBar = plainTextWidget->verticalScrollBar(); + int scrollPosition = scrollBar->value(); + plainTextWidget->appendHtml(entry.html); + + if(followOutput) + { + plainTextWidget->moveCursor(QTextCursor::End); + scrollBar->setValue(scrollBar->maximum()); + } + else + { + scrollBar->setValue(scrollPosition); + } +} + +void Log::setFollowOutput(bool follow) +{ + followOutput = follow; + if(plainTextWidget == NULL) return; + if(!followOutput) return; plainTextWidget->moveCursor(QTextCursor::End); + plainTextWidget->verticalScrollBar()->setValue(plainTextWidget->verticalScrollBar()->maximum()); } void Log::setFilter(int aFilter) diff --git a/Source/Utility/log.h b/Source/Utility/log.h index ce0ec2b..81fc38f 100644 --- a/Source/Utility/log.h +++ b/Source/Utility/log.h @@ -53,6 +53,7 @@ class Log : public QObject public slots: void setFilter(int filter); + void setFollowOutput(bool follow); void clear(); private: @@ -61,6 +62,7 @@ public slots: QPlainTextEdit* plainTextWidget; QVector entries; log_filter_t filter; + bool followOutput; }; #endif // LOG_H diff --git a/Source/Windows/About/aboutwnd.cpp b/Source/Windows/About/aboutwnd.cpp new file mode 100644 index 0000000..0df46ea --- /dev/null +++ b/Source/Windows/About/aboutwnd.cpp @@ -0,0 +1,91 @@ +#include "aboutwnd.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +AboutWnd::AboutWnd(QWidget *parent) : + QDialog(parent) +{ + QFont defaultFont("Arial", 10); + setFont(defaultFont); + + setWindowTitle("About OpenEPT"); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + QVBoxLayout *mainVL = new QVBoxLayout(this); + + QHBoxLayout *headerHB = new QHBoxLayout(); + + QLabel *iconLabel = new QLabel(this); + iconLabel->setPixmap(QPixmap(":/images/main.png").scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + iconLabel->setFixedSize(72, 72); + + QVBoxLayout *titleVL = new QVBoxLayout(); + + QLabel *nameLabel = new QLabel("OpenEPT", this); + QFont nameFont("Arial", 16); + nameFont.setBold(true); + nameLabel->setFont(nameFont); + + QLabel *subtitleLabel = new QLabel("MCU Energy Profiling Tool", this); + + QLabel *versionLabel = new QLabel(QString("Version %1").arg(APP_VERSION), this); + versionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + + titleVL->addWidget(nameLabel); + titleVL->addWidget(subtitleLabel); + titleVL->addWidget(versionLabel); + titleVL->addStretch(); + + headerHB->addWidget(iconLabel); + headerHB->addSpacing(15); + headerHB->addLayout(titleVL); + headerHB->addStretch(); + + QLabel *descriptionLabel = new QLabel( + "OpenEPT is an open source energy profiler for embedded systems. It controls the " + "OpenEPT acquisition board, streams voltage and current samples in real time, and " + "provides tools for energy consumption measurement, energy debugging and offline " + "analysis of captured profiles.", + this); + descriptionLabel->setWordWrap(true); + descriptionLabel->setMinimumWidth(430); + descriptionLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + + QLabel *linksLabel = new QLabel( + "www.openept.net", + this); + linksLabel->setOpenExternalLinks(true); + + QLabel *creditsLabel = new QLabel( + "Supported by NLnet Foundation.", + this); + creditsLabel->setOpenExternalLinks(true); + + QLabel *copyrightLabel = new QLabel("Copyright © OpenEPT Team", this); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this); + buttonBox->button(QDialogButtonBox::Close)->setFixedSize(90, 30); + + connect(buttonBox, &QDialogButtonBox::rejected, this, &AboutWnd::reject); + + mainVL->addLayout(headerHB); + mainVL->addSpacing(10); + mainVL->addWidget(descriptionLabel); + mainVL->addSpacing(10); + mainVL->addWidget(linksLabel); + mainVL->addSpacing(10); + mainVL->addWidget(creditsLabel); + mainVL->addWidget(copyrightLabel); + mainVL->addSpacing(10); + mainVL->addWidget(buttonBox); + + setLayout(mainVL); + mainVL->setSizeConstraint(QLayout::SetFixedSize); +} diff --git a/Source/Windows/About/aboutwnd.h b/Source/Windows/About/aboutwnd.h new file mode 100644 index 0000000..44d9051 --- /dev/null +++ b/Source/Windows/About/aboutwnd.h @@ -0,0 +1,18 @@ +#ifndef ABOUTWND_H +#define ABOUTWND_H + +#include + +#ifndef APP_VERSION +#define APP_VERSION "0.0.0-dev" +#endif + +class AboutWnd : public QDialog +{ + Q_OBJECT + +public: + explicit AboutWnd(QWidget *parent = nullptr); +}; + +#endif // ABOUTWND_H diff --git a/Source/Windows/DataAnalyzer/dataanalyzer.cpp b/Source/Windows/DataAnalyzer/dataanalyzer.cpp index 8fd939d..4e61f3f 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzer.cpp +++ b/Source/Windows/DataAnalyzer/dataanalyzer.cpp @@ -4,33 +4,29 @@ #include #include #include - - -#define PLOT_MINIMUM_SIZE_HEIGHT 100 -#define PLOT_MINIMUM_SIZE_WIDTH 500 +#include +#include +#include +#include +#include DataAnalyzer::DataAnalyzer(QWidget *parent, QString aWsDirPath) : QWidget(parent), ui(new Ui::DataAnalyzer) { - ui->setupUi(this); + ui->setupUi(this); wsDirPath = aWsDirPath; - QFont defaultFont("Arial", 10); // Set desired font and size + QFont defaultFont("Arial", 10); setFont(defaultFont); - // Set up the main layout for DataAnalyzer QVBoxLayout *mainLayout = new QVBoxLayout(this); - - - // Create a horizontal layout for the button and line edit QHBoxLayout *topLayout = new QHBoxLayout; QPushButton *reloadProfileNamesPushb = new QPushButton(); QPushButton *processFilePushb = new QPushButton(); QPushButton *deleteProfilePushb = new QPushButton(); - detectedProfilesLabe = new QLabel("Detect consumption profiles", this); detectedProfilesLabe->setFixedSize(260, 30); detectedProfilesLabe->setFont(defaultFont); @@ -42,32 +38,24 @@ DataAnalyzer::DataAnalyzer(QWidget *parent, QString aWsDirPath) : connect(consumptionProfilesCB, SIGNAL(currentIndexChanged(int)), this, SLOT(onConsumptionProfileChanged(int))); realoadConsumptionProfiles(); - QPixmap buttonIconPng(":/images/NewSet/reload.png"); - QIcon buttonIcon(buttonIconPng); - reloadProfileNamesPushb->setIcon(buttonIcon); + reloadProfileNamesPushb->setIcon(QIcon(QPixmap(":/images/NewSet/reload.png"))); reloadProfileNamesPushb->setIconSize(QSize(30,30)); reloadProfileNamesPushb->setToolTip("Reload consumption profiles"); reloadProfileNamesPushb->setFixedSize(30, 30); - connect(reloadProfileNamesPushb, SIGNAL(clicked(bool)), this, SLOT(onRealoadConsumptionProfiles())); - QPixmap processIconPng(":/images/NewSet/load.png"); - QIcon processIcon(processIconPng); - processFilePushb->setIcon(processIcon); + processFilePushb->setIcon(QIcon(QPixmap(":/images/NewSet/load.png"))); processFilePushb->setIconSize(QSize(30,30)); - processFilePushb->setToolTip("Process consumption profile data"); + processFilePushb->setToolTip("Load selected consumption profile in a new tab"); processFilePushb->setFixedSize(30, 30); - connect(processFilePushb, SIGNAL(clicked(bool)), this, SLOT(onLoadConsumptionProfileData())); deleteProfilePushb->setIcon(QIcon(QPixmap(":/images/NewSet/stopHand.png"))); deleteProfilePushb->setIconSize(QSize(24,24)); deleteProfilePushb->setToolTip("Delete selected consumption profile from disk"); deleteProfilePushb->setFixedSize(30, 30); - connect(deleteProfilePushb, SIGNAL(clicked(bool)), this, SLOT(onDeleteConsumptionProfile())); - // Add the button and line edit to the horizontal layout topLayout->addWidget(detectedProfilesLabe); topLayout->addWidget(consumptionProfilesCB); topLayout->addWidget(reloadProfileNamesPushb); @@ -78,192 +66,58 @@ DataAnalyzer::DataAnalyzer(QWidget *parent, QString aWsDirPath) : mainLayout->addLayout(topLayout); - plotsToolBar = new QToolBar(this); - plotsToolBar->setIconSize(QSize(24, 24)); - QAction *saveAllPlotsAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/save.png")), "Save all plots"); - saveAllPlotsAction->setToolTip("Save Voltage, Current and Consumption plots (current view) to a folder"); - connect(saveAllPlotsAction, SIGNAL(triggered(bool)), this, SLOT(onSaveAllPlots())); - QAction *genStatisticsAction = plotsToolBar->addAction("Gen statistics"); - genStatisticsAction->setToolTip("Generate consumption statistics for segments between \" Start\" and \" Stop\" markers"); - connect(genStatisticsAction, SIGNAL(triggered(bool)), this, SLOT(onGenerateStatistics())); - mainLayout->addWidget(plotsToolBar); - - statisticsWnd = new DataAnalyzerStatisticsWnd(); qRegisterMetaType>("QVector"); qRegisterMetaType>>("QVector>"); - statisticsThread = new QThread(this); - statisticsWorker = new DataAnalyzerStatisticsWorker(); - statisticsWorker->moveToThread(statisticsThread); - statisticsThread->setObjectName("OpenEPT - Data Analyzer statistics"); - connect(this, SIGNAL(sigComputeStatistics(QVector,QVector,QVector,QVector,QVector>)), - statisticsWorker, SLOT(onComputeStatistics(QVector,QVector,QVector,QVector,QVector>)), Qt::QueuedConnection); qRegisterMetaType("dataanalyzer_segment_stat_t"); qRegisterMetaType>("QVector"); - connect(statisticsWorker, SIGNAL(sigStatisticsFinished(QVector,QVector,dataanalyzer_segment_stat_t,QStringList)), - this, SLOT(onStatisticsFinished(QVector,QVector,dataanalyzer_segment_stat_t,QStringList)), Qt::QueuedConnection); - statisticsThread->start(); - connect(statisticsWnd, SIGNAL(sigSegmentSelected(QString,int,int)), this, SLOT(onStatisticsSegmentSelected(QString,int,int))); - - // Create an internal QMainWindow to handle docking - mainWindow = new QMainWindow(this); - mainWindow->setWindowFlags(Qt::Widget); // Make QMainWindow behave as a regular widget - mainWindow->setDockNestingEnabled(true); // Allow nested docking if needed - - // Add QMainWindow to the layout - mainLayout->addWidget(mainWindow); - - - createVoltageSubWin(); - createCurrentSubWin(); - createConsumptionSubWin(); - - epEnabledFlag = false; - graphLoad = false; - - - thread = new QThread(this); - dataProcesingClass = new DataAnalyzerWorker(); - dataProcesingClass->moveToThread(thread); - thread->setObjectName("OpenEPT - Data Analyzer"); - qRegisterMetaType >("QVector>"); qRegisterMetaType >("QVector>"); - connect(this, SIGNAL(processVolCurConRequest(QString,QString)), - dataProcesingClass, SLOT(processVoltCurConData(QString,QString)), Qt::QueuedConnection); - - connect(dataProcesingClass, SIGNAL(processingVolCurConFinished(QVector>, QVector>)), - this, SLOT(processingVolCurConDone(QVector>, QVector>)), Qt::QueuedConnection); - - connect(this, SIGNAL(processEPRequest(QString,QString)), - dataProcesingClass, SLOT(processEPData(QString,QString)), Qt::QueuedConnection); - - - connect(dataProcesingClass, SIGNAL(processingEPFinished(QVector>)), - this, SLOT(processingEPDone(QVector>)), Qt::QueuedConnection); - - - connect(dataProcesingClass, SIGNAL(progressUpdated(int)), - this, SLOT(updateProgress(int)), Qt::QueuedConnection); - connect(dataProcesingClass, SIGNAL(updateProgressText(QString)), - this, SLOT(updateProgressText(QString)), Qt::QueuedConnection); - - thread->start(); + mdiArea = new QMdiArea(this); + mdiArea->setViewMode(QMdiArea::TabbedView); + mdiArea->setTabsClosable(true); + mdiArea->setTabsMovable(true); + mdiArea->setDocumentMode(true); + mdiArea->setTabPosition(QTabWidget::North); + mainLayout->addWidget(mdiArea); } -void DataAnalyzer::createVoltageSubWin() { - // Create a dock widget - QDockWidget *dockWidget = new QDockWidget("Voltage", this); - dockWidget->setAllowedAreas(Qt::AllDockWidgetAreas); - - // Create a content widget with a layout for the dock widget - QWidget *contentWidget = new QWidget; - QVBoxLayout *layout = new QVBoxLayout(contentWidget); - layout->setContentsMargins(2, 2, 2, 2); - - voltageChart = new Plot(PLOT_MINIMUM_SIZE_WIDTH/2, PLOT_MINIMUM_SIZE_HEIGHT, false); - voltageChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - voltageChart->setTitle("Voltage"); - voltageChart->setYLabel("[V]"); - voltageChart->setXLabel("[ms]"); - - layout->addWidget(voltageChart); - contentWidget->setLayout(layout); - - - // Set the content widget in the dock widget - dockWidget->setWidget(contentWidget); - - // Add the dock widget to the specified area in the main window - mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dockWidget); +DataAnalyzer::~DataAnalyzer() +{ + for(int i = profiles.size() - 1; i >= 0; i--) + { + if(subWindowFor(profiles[i]) != NULL) continue; + delete profiles[i]; + } - // Make dock widgets floatable and closable - dockWidget->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + delete ui; } -void DataAnalyzer::createCurrentSubWin() +void DataAnalyzer::showEvent(QShowEvent *event) { - // Create a dock widget - QDockWidget *dockWidget = new QDockWidget("Current", this); - dockWidget->setAllowedAreas(Qt::AllDockWidgetAreas); - - // Create a content widget with a layout for the dock widget - QWidget *contentWidget = new QWidget; - QVBoxLayout *layout = new QVBoxLayout(contentWidget); - layout->setContentsMargins(2, 2, 2, 2); - - - currentChart = new Plot(PLOT_MINIMUM_SIZE_WIDTH/2, PLOT_MINIMUM_SIZE_HEIGHT, false); - currentChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - currentChart->setTitle("Current"); - currentChart->setYLabel("[mA]"); - currentChart->setXLabel("[ms]"); - - layout->addWidget(currentChart); - contentWidget->setLayout(layout); - - - // Set the content widget in the dock widget - dockWidget->setWidget(contentWidget); - - // Add the dock widget to the specified area in the main window - mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dockWidget); - - // Make dock widgets floatable and closable - dockWidget->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + QWidget::showEvent(event); + realoadConsumptionProfiles(); } -void DataAnalyzer::createConsumptionSubWin() +void DataAnalyzer::setWorkspacePath(QString aWsDirPath) { - // Create a dock widget - QDockWidget *dockWidget = new QDockWidget("Consumption", this); - dockWidget->setAllowedAreas(Qt::AllDockWidgetAreas); - - // Create a content widget with a layout for the dock widget - QWidget *contentWidget = new QWidget; - QVBoxLayout *layout = new QVBoxLayout(contentWidget); - layout->setContentsMargins(2, 2, 2, 2); - - consumptionChart = new Plot(PLOT_MINIMUM_SIZE_WIDTH/2, PLOT_MINIMUM_SIZE_HEIGHT, false); - consumptionChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - consumptionChart->setTitle("Consumption"); - consumptionChart->setYLabel("[mA]"); - consumptionChart->setXLabel("[ms]"); - - connect(voltageChart, SIGNAL(sigXRangeChanged(QCPRange)), this, SLOT(onPlotXRangeChanged(QCPRange))); - connect(currentChart, SIGNAL(sigXRangeChanged(QCPRange)), this, SLOT(onPlotXRangeChanged(QCPRange))); - connect(consumptionChart, SIGNAL(sigXRangeChanged(QCPRange)), this, SLOT(onPlotXRangeChanged(QCPRange))); - - layout->addWidget(consumptionChart); - contentWidget->setLayout(layout); - - - // Set the content widget in the dock widget - dockWidget->setWidget(contentWidget); - - // Add the dock widget to the specified area in the main window - mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dockWidget); - - // Make dock widgets floatable and closable - dockWidget->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + wsDirPath = aWsDirPath; + realoadConsumptionProfiles(); } - - - QStringList DataAnalyzer::listConsumptionProfiles() { - QStringList profiles; + QStringList profileList; QDir dir(wsDirPath); - if(!dir.exists()) return profiles; + if(!dir.exists()) return profileList; - listConsumptionProfilesInDir(dir, "", profiles, 0); + listConsumptionProfilesInDir(dir, "", profileList, 0); - return profiles; + return profileList; } -void DataAnalyzer::listConsumptionProfilesInDir(QDir dir, QString relativePath, QStringList& profiles, int depth) +void DataAnalyzer::listConsumptionProfilesInDir(QDir dir, QString relativePath, QStringList& profileList, int depth) { if(depth > 3) return; @@ -275,11 +129,11 @@ void DataAnalyzer::listConsumptionProfilesInDir(QDir dir, QString relativePath, QString subPath = relativePath.isEmpty() ? entry.fileName() : relativePath + "/" + entry.fileName(); if(subDir.exists("OpenEPT.txt")) { - profiles << subPath; + profileList << subPath; } else { - listConsumptionProfilesInDir(subDir, subPath, profiles, depth + 1); + listConsumptionProfilesInDir(subDir, subPath, profileList, depth + 1); } } } @@ -301,119 +155,111 @@ void DataAnalyzer::realoadConsumptionProfiles() } } -DataAnalyzer::~DataAnalyzer() -{ - statisticsThread->quit(); - statisticsThread->wait(); - delete statisticsWorker; - delete statisticsWnd; - delete ui; -} - -void DataAnalyzer::showEvent(QShowEvent *event) +void DataAnalyzer::onRealoadConsumptionProfiles() { - QWidget::showEvent(event); realoadConsumptionProfiles(); } -void DataAnalyzer::setWorkspacePath(QString aWsDirPath) +void DataAnalyzer::onConsumptionProfileChanged(int index) { - wsDirPath = aWsDirPath; - realoadConsumptionProfiles(); + selectedConsumptionProfile = consumptionProfilesCB->itemText(index); } -void DataAnalyzer::onPlotXRangeChanged(QCPRange range) +void DataAnalyzer::attachProfile(DataAnalyzerProfile *profile, QString title) { - Plot *source = qobject_cast(sender()); - Plot *plots[3] = {voltageChart, currentChart, consumptionChart}; + QMdiSubWindow *subWindow = mdiArea->addSubWindow(profile); - for(int i = 0; i < 3; i++) - { - if(plots[i] == source) continue; - plots[i]->setXRangeSynced(range); - } + subWindow->setWindowTitle(title); + subWindow->setAttribute(Qt::WA_DeleteOnClose); + subWindow->showMaximized(); + mdiArea->setActiveSubWindow(subWindow); + profile->setDetached(false); + profile->show(); } -void DataAnalyzer::onGenerateStatistics() +void DataAnalyzer::onLoadConsumptionProfileData() { - if(!graphLoad) - { - QMessageBox::warning(this, "Statistics", "No consumption profile loaded"); - return; - } - if(loadedMarkers.isEmpty()) - { - QMessageBox::warning(this, "Statistics", "Loaded profile has no energy point markers (\" Start\" / \" Stop\" pairs are required)"); - return; - } - emit sigComputeStatistics(loadedVoltage, loadedVoltageKeys, loadedCurrent, loadedCurrentKeys, loadedMarkers); -} + DataAnalyzerProfile *profile; + QString title; + int counter; -void DataAnalyzer::onStatisticsFinished(QVector stats, QVector points, dataanalyzer_segment_stat_t total, QStringList warnings) -{ - if(stats.isEmpty() && points.isEmpty() && warnings.isEmpty()) + if(selectedConsumptionProfile.isEmpty() || !consumptionProfilesCB->isEnabled()) { - QMessageBox::information(this, "Statistics", "No \" Start\" / \" Stop\" marker pairs found"); + QMessageBox::warning(this, "Load profile", "No consumption profile selected"); return; } - statisticsWnd->setStatistics(selectedConsumptionProfile, stats, points, total, warnings); - statisticsWnd->show(); - statisticsWnd->raise(); - statisticsWnd->activateWindow(); -} -void DataAnalyzer::onStatisticsSegmentSelected(QString name, int startIndex, int endIndex) -{ - double min; - double max; + counter = profileCounters.value(selectedConsumptionProfile, 0) + 1; + profileCounters[selectedConsumptionProfile] = counter; + title = selectedConsumptionProfile.section('/', -1); + if(counter > 1) title += " (" + QString::number(counter) + ")"; - if(!graphLoad) return; - if(startIndex < 0 || endIndex >= loadedVoltageKeys.size() || endIndex <= startIndex) return; + profile = new DataAnalyzerProfile(wsDirPath, selectedConsumptionProfile); + profiles.append(profile); - min = loadedVoltageKeys[startIndex]; - max = loadedVoltageKeys[endIndex]; - voltageChart->zoomToKeyRange(min, max); - currentChart->zoomToKeyRange(min, max); - consumptionChart->zoomToKeyRange(min, max); + connect(profile, SIGNAL(sigDockStateToggleRequested()), this, SLOT(onProfileDockStateToggleRequested())); + connect(profile, SIGNAL(destroyed(QObject*)), this, SLOT(onProfileDestroyed(QObject*))); - raise(); - activateWindow(); + attachProfile(profile, title); + + profile->startLoading(); } -void DataAnalyzer::onSaveAllPlots() +QMdiSubWindow* DataAnalyzer::subWindowFor(DataAnalyzerProfile *profile) { - Plot *plots[3] = {voltageChart, currentChart, consumptionChart}; - QString defaultDir = wsDirPath; - QString dir; - QString prefix; - QString timeStamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); - QStringList failed; - - if(!graphLoad) - { - QMessageBox::warning(this, "Save plots", "No consumption profile loaded"); - return; - } - if(!selectedConsumptionProfile.isEmpty()) + QList subWindows = mdiArea->subWindowList(); + + for(int i = 0; i < subWindows.size(); i++) { - defaultDir = wsDirPath + "/" + selectedConsumptionProfile; + if(subWindows[i]->widget() == profile) return subWindows[i]; } - dir = QFileDialog::getExistingDirectory(this, "Select folder for plot images", defaultDir); - if(dir.isEmpty()) return; + return NULL; +} + +void DataAnalyzer::onProfileDockStateToggleRequested() +{ + DataAnalyzerProfile *profile = qobject_cast(sender()); + QMdiSubWindow *subWindow; - prefix = selectedConsumptionProfile.isEmpty() ? "profile" : selectedConsumptionProfile.section('/', -1).toLower().replace(' ', '_'); + if(profile == NULL) return; - for(int i = 0; i < 3; i++) + subWindow = subWindowFor(profile); + + if(subWindow != NULL) { - QString base = dir + "/oept_" + prefix + "_" + plots[i]->getTitle().toLower().replace(' ', '_') + "_" + timeStamp; - if(!plots[i]->saveImageToFile(base + ".png")) failed << base + ".png"; - if(!plots[i]->saveImageToFile(base + ".svg")) failed << base + ".svg"; + QString title = subWindow->windowTitle(); + + subWindow->setWidget(NULL); + profile->setParent(NULL); + mdiArea->removeSubWindow(subWindow); + subWindow->deleteLater(); + + profile->setWindowTitle("Data Analyzer - " + title); + profile->setWindowFlags(Qt::Window); + profile->setAttribute(Qt::WA_DeleteOnClose); + profile->setDetached(true); + profile->resize(1000, 700); + profile->show(); + profile->raise(); + profile->activateWindow(); + return; } - if(!failed.isEmpty()) + profile->setAttribute(Qt::WA_DeleteOnClose, false); + profile->setWindowFlags(Qt::Widget); + attachProfile(profile, profile->windowTitle().section(" - ", -1)); +} + +void DataAnalyzer::onProfileDestroyed(QObject *object) +{ + for(int i = 0; i < profiles.size(); i++) { - QMessageBox::warning(this, "Save plots", "Unable to save:\n" + failed.join("\n")); + if(profiles[i] == object) + { + profiles.remove(i); + return; + } } } @@ -444,13 +290,13 @@ void DataAnalyzer::onDeleteConsumptionProfile() return; } - if(graphLoad && selectedConsumptionProfile == profile) + for(int i = profiles.size() - 1; i >= 0; i--) { - voltageChart->clear(); - currentChart->clear(); - consumptionChart->clear(); - loadedMarkers.clear(); - graphLoad = false; + if(profiles[i]->getProfileName() != profile) continue; + + QMdiSubWindow *subWindow = subWindowFor(profiles[i]); + if(subWindow != NULL) subWindow->close(); + else profiles[i]->close(); } if(!dir.removeRecursively()) @@ -461,165 +307,6 @@ void DataAnalyzer::onDeleteConsumptionProfile() realoadConsumptionProfiles(); } -void DataAnalyzer::onRealoadConsumptionProfiles() -{ - realoadConsumptionProfiles(); -} - -void DataAnalyzer::onConsumptionProfileChanged(int index) -{ - selectedConsumptionProfile = consumptionProfilesCB->itemText(index); -} - -void DataAnalyzer::loadConsumptionProfileData() -{ - QString summaryFilePath = wsDirPath + "/" + selectedConsumptionProfile + "/OpenEPT.txt"; - QVector> summaryInfo = parseSummaryFile(summaryFilePath); - epEnabledFlag = false; - - QString epEnabled = getValueForKey(summaryInfo, "EP Enabled"); - if(epEnabled == "1") epEnabledFlag = true; - - if(epEnabledFlag) - { - dataProcesingClass->setLimits(3); - } - else - { - dataProcesingClass->setLimits(2); - } - - // Create a progress dialog without Cancel and Close buttons - progressDialog = new QProgressDialog("Loading Data... ", "Cancel", 0, 100, this); - progressDialog->setWindowModality(Qt::WindowModal); - progressDialog->setMinimumDuration(0); - progressDialog->setAutoClose(true); - - // Remove Close (X) button and Cancel button - progressDialog->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); - progressDialog->setWindowTitle("Load OpenEPT Data"); - - progressDialog->show(); - - emit processVolCurConRequest(wsDirPath, selectedConsumptionProfile); -} - -QString DataAnalyzer::getValueForKey(const QVector> &parsedData, const QString &key) -{ - for (const auto &pair : parsedData) { - if (pair.first == key) { - return pair.second; // Return the corresponding value - } - } - return ""; // Return empty string if key is not found -} - -QVector> DataAnalyzer::parseSummaryFile(const QString &filePath) -{ - QVector> data; // Store key-value pairs - - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qWarning() << "Failed to open file:" << filePath; - return data; - } - - QTextStream in(&file); - - // Skip the first two lines (header) - in.readLine(); - in.readLine(); - - // Read and parse key-value pairs - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); // Read and remove extra spaces - if (line.isEmpty()) continue; // Skip empty lines - - QStringList parts = line.split(":", Qt::SkipEmptyParts); // Split at ':' - if (parts.size() == 2) { - QString key = parts[0].trimmed(); // Extract and trim key - QString value = parts[1].trimmed(); // Extract and trim value - - data.append(qMakePair(key, value)); - } else { - qWarning() << "Invalid format in line:" << line; - } - } - - file.close(); - return data; -} -void DataAnalyzer::onLoadConsumptionProfileData() -{ - loadConsumptionProfileData(); - -} - -void DataAnalyzer::processingVolCurConDone(QVector > vc, QVector > cons) -{ - if(graphLoad) - { - voltageChart->clear(); - currentChart->clear(); - consumptionChart->clear(); - graphLoad = false; - } - - voltageChart->setData(vc[0],vc[1]); - currentChart->setData(vc[2],vc[3]); - consumptionChart->setData(cons[0], cons[1]); - - loadedVoltage = vc[0]; - loadedVoltageKeys = vc[1]; - loadedCurrent = vc[2]; - loadedCurrentKeys = vc[3]; - loadedMarkers.clear(); - - graphLoad = true; - - - if(epEnabledFlag){ - emit processEPRequest(wsDirPath, selectedConsumptionProfile); - } - else{ - if (progressDialog) { - progressDialog->close(); - } - } -} - -void DataAnalyzer::processingEPDone(QVector > epData) -{ - loadedMarkers = epData; - consumptionChart->scatterAddGraph(); - consumptionChart->scatterAddAllDataWithName(epData); - voltageChart->scatterAddGraph(); - voltageChart->scatterAddAllDataWithName(epData); - currentChart->scatterAddGraph(); - currentChart->scatterAddAllDataWithName(epData); - if (progressDialog) { - progressDialog->close(); - } -} - -void DataAnalyzer::updateProgress(int percentage) -{ - if (progressDialog) { - progressDialog->setValue(percentage); - if (percentage >= 100) { - progressDialog->close(); - } - } -} - -void DataAnalyzer::updateProgressText(QString text) -{ - if (progressDialog) { - progressDialog->setLabelText(text); - } -} - - DataAnalyzerWorker::DataAnalyzerWorker(QObject *parent) : QObject(parent) { vcDataProcessingStartPercentage = 0; diff --git a/Source/Windows/DataAnalyzer/dataanalyzer.h b/Source/Windows/DataAnalyzer/dataanalyzer.h index 524bf73..b9ee7f3 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzer.h +++ b/Source/Windows/DataAnalyzer/dataanalyzer.h @@ -5,138 +5,61 @@ #include #include #include -#include #include #include -#include +#include #include -#include -#include -#include "dataanalyzerstatistics.h" #include #include -#include -#include -#include -#include -#include -#include -#include "Windows/Plot/plot.h" +#include + +#include "dataanalyzerworker.h" +#include "dataanalyzerprofile.h" namespace Ui { class DataAnalyzer; } -class DataAnalyzerWorker : public QObject -{ - Q_OBJECT - -public: - explicit DataAnalyzerWorker(QObject *parent = nullptr); - bool setLimits(int limitsNumber); - -public slots: - void processVoltCurConData(const QString &wsDirPath, const QString &selectedConsumptionProfile); - void processEPData(const QString &wsDirPath, const QString &selectedConsumptionProfile); - -signals: - void processingVolCurConFinished(QVector > voltage, QVector> current); - void processingEPFinished(QVector> epata); - void progressUpdated(int percentage); - void updateProgressText(QString text); - -private: - QVector> parseVCData(const QString &filePath); - QVector> parseConsumptionData(const QString &filePath); - QVector> parseEPFile(const QString &filePath); - - int vcDataProcessingStartPercentage; - int consumptionDataProcessingStartPercentage; - int epDataProcessingStartPercentage; - double range; - -}; - - class DataAnalyzer : public QWidget { Q_OBJECT public: + explicit DataAnalyzer(QWidget *parent = nullptr, QString aWsDirPath=""); + ~DataAnalyzer(); + void setWorkspacePath(QString aWsDirPath); protected: void showEvent(QShowEvent *event) override; -public: - explicit DataAnalyzer(QWidget *parent = nullptr, QString aWsDirPath=""); - void loadConsumptionProfileData(); - ~DataAnalyzer(); - public slots: void onDeleteConsumptionProfile(); - void onGenerateStatistics(); - void onStatisticsSegmentSelected(QString name, int startIndex, int endIndex); - void onStatisticsFinished(QVector stats, QVector points, dataanalyzer_segment_stat_t total, QStringList warnings); - void onSaveAllPlots(); - void onPlotXRangeChanged(QCPRange range); - void onRealoadConsumptionProfiles(); - void onConsumptionProfileChanged(int index); - void onLoadConsumptionProfileData(); - void updateProgress(int percentage); - void updateProgressText(QString text); - void processingVolCurConDone(QVector > vc, QVector> cons); - void processingEPDone(QVector> epData); -signals: - void sigComputeStatistics(QVector voltage, QVector voltageKeys, QVector current, QVector currentKeys, QVector> markers); - void processVolCurConRequest(const QString &filePath, const QString &selectedConsumptionProfile); - void processEPRequest(const QString &filePath, const QString &selectedConsumptionProfile); + void onRealoadConsumptionProfiles(); + void onConsumptionProfileChanged(int index); + void onLoadConsumptionProfileData(); + void onProfileDockStateToggleRequested(); + void onProfileDestroyed(QObject *object); private: Ui::DataAnalyzer *ui; - QMainWindow *mainWindow; - QToolBar *plotsToolBar; - QThread *statisticsThread; - DataAnalyzerStatisticsWorker *statisticsWorker; - DataAnalyzerStatisticsWnd *statisticsWnd; - QVector loadedVoltage; - QVector loadedVoltageKeys; - QVector loadedCurrent; - QVector loadedCurrentKeys; - QVector> loadedMarkers; + QMdiArea *mdiArea; QLabel* detectedProfilesLabe; + QComboBox *consumptionProfilesCB; + QStringList consumptionProfilesName; QString wsDirPath; - - Plot *voltageChart; - Plot *currentChart; - Plot *consumptionChart; - bool epEnabledFlag; - bool graphLoad; - - QProgressDialog *progressDialog; - - QString selectedConsumptionProfile; - QThread *thread; - DataAnalyzerWorker *dataProcesingClass; - - void createVoltageSubWin(); - void createCurrentSubWin(); - void createConsumptionSubWin(); - - QComboBox *consumptionProfilesCB; - QStringList consumptionProfilesName; + QVector profiles; + QMap profileCounters; - QVector> parseSummaryFile(const QString &filePath); QStringList listConsumptionProfiles(); - QString getValueForKey(const QVector > &parsedData, const QString &key); - - - void realoadConsumptionProfiles(); void listConsumptionProfilesInDir(QDir dir, QString relativePath, QStringList& profiles, int depth); + void realoadConsumptionProfiles(); + QMdiSubWindow* subWindowFor(DataAnalyzerProfile *profile); + void attachProfile(DataAnalyzerProfile *profile, QString title); }; #endif // DATAANALYZER_H diff --git a/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp new file mode 100644 index 0000000..093e596 --- /dev/null +++ b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp @@ -0,0 +1,488 @@ +#include "dataanalyzerprofile.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define PLOT_MINIMUM_SIZE_HEIGHT 100 +#define PLOT_MINIMUM_SIZE_WIDTH 500 + +DataAnalyzerProfile::DataAnalyzerProfile(QString aWsDirPath, QString aProfileName, QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + setFont(defaultFont); + mainLayout->setContentsMargins(2, 2, 2, 2); + + wsDirPath = aWsDirPath; + profileName = aProfileName; + epEnabledFlag = false; + graphLoad = false; + progressDialog = NULL; + statisticsProgressDialog = NULL; + + plotsToolBar = new QToolBar(this); + plotsToolBar->setIconSize(QSize(24, 24)); + QAction *saveAllPlotsAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/save.png")), "Save all plots"); + saveAllPlotsAction->setToolTip("Save Voltage, Current and Consumption plots (current view) to a folder"); + connect(saveAllPlotsAction, SIGNAL(triggered(bool)), this, SLOT(onSaveAllPlots())); + QAction *genStatisticsAction = plotsToolBar->addAction("Gen statistics"); + genStatisticsAction->setToolTip("Generate consumption statistics for segments between \" Start\" and \" Stop\" markers"); + connect(genStatisticsAction, SIGNAL(triggered(bool)), this, SLOT(onGenerateStatistics())); + detachAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/expand.png")), "Detach"); + detachAction->setToolTip("Show this profile in a separate window"); + connect(detachAction, SIGNAL(triggered(bool)), this, SLOT(onDetachClicked())); + mainLayout->addWidget(plotsToolBar); + + statisticsWnd = new DataAnalyzerStatisticsWnd(); + statisticsThread = new QThread(this); + statisticsWorker = new DataAnalyzerStatisticsWorker(); + statisticsWorker->moveToThread(statisticsThread); + statisticsThread->setObjectName("OpenEPT - Data Analyzer statistics"); + connect(this, SIGNAL(sigComputeStatistics(QVector,QVector,QVector,QVector,QVector>)), + statisticsWorker, SLOT(onComputeStatistics(QVector,QVector,QVector,QVector,QVector>)), Qt::QueuedConnection); + connect(statisticsWorker, SIGNAL(sigStatisticsFinished(QVector,QVector,dataanalyzer_segment_stat_t,QStringList)), + this, SLOT(onStatisticsFinished(QVector,QVector,dataanalyzer_segment_stat_t,QStringList)), Qt::QueuedConnection); + connect(statisticsWorker, SIGNAL(sigStatisticsProgress(int,QString)), this, SLOT(onStatisticsProgress(int,QString)), Qt::QueuedConnection); + connect(statisticsWnd, SIGNAL(sigSegmentSelected(QString,int,int)), this, SLOT(onStatisticsSegmentSelected(QString,int,int))); + connect(statisticsWnd, SIGNAL(sigMarkerVisibilityChanged(QVector,QVector)), this, SLOT(onStatisticsMarkerVisibilityChanged(QVector,QVector))); + statisticsThread->start(); + + mainWindow = new QMainWindow(this); + mainWindow->setWindowFlags(Qt::Widget); + mainWindow->setDockNestingEnabled(true); + mainLayout->addWidget(mainWindow); + + createVoltageSubWin(); + createCurrentSubWin(); + createConsumptionSubWin(); + + thread = new QThread(this); + dataProcesingClass = new DataAnalyzerWorker(); + dataProcesingClass->moveToThread(thread); + thread->setObjectName("OpenEPT - Data Analyzer"); + + connect(this, SIGNAL(processVolCurConRequest(QString,QString)), + dataProcesingClass, SLOT(processVoltCurConData(QString,QString)), Qt::QueuedConnection); + connect(dataProcesingClass, SIGNAL(processingVolCurConFinished(QVector>, QVector>)), + this, SLOT(processingVolCurConDone(QVector>, QVector>)), Qt::QueuedConnection); + connect(this, SIGNAL(processEPRequest(QString,QString)), + dataProcesingClass, SLOT(processEPData(QString,QString)), Qt::QueuedConnection); + connect(dataProcesingClass, SIGNAL(processingEPFinished(QVector>)), + this, SLOT(processingEPDone(QVector>)), Qt::QueuedConnection); + connect(dataProcesingClass, SIGNAL(progressUpdated(int)), + this, SLOT(updateProgress(int)), Qt::QueuedConnection); + connect(dataProcesingClass, SIGNAL(updateProgressText(QString)), + this, SLOT(updateProgressText(QString)), Qt::QueuedConnection); + + thread->start(); +} + +DataAnalyzerProfile::~DataAnalyzerProfile() +{ + statisticsThread->quit(); + statisticsThread->wait(); + thread->quit(); + thread->wait(); + delete statisticsWorker; + delete dataProcesingClass; + delete statisticsWnd; +} + +QString DataAnalyzerProfile::getProfileName() +{ + return profileName; +} + +void DataAnalyzerProfile::setDetached(bool detached) +{ + if(detached) + { + detachAction->setText("Attach"); + detachAction->setToolTip("Show this profile back as a tab of the Data Analyzer"); + } + else + { + detachAction->setText("Detach"); + detachAction->setToolTip("Show this profile in a separate window"); + } +} + +void DataAnalyzerProfile::onDetachClicked() +{ + emit sigDockStateToggleRequested(); +} + +void DataAnalyzerProfile::createVoltageSubWin() +{ + QDockWidget *dockWidget = new QDockWidget("Voltage", this); + QWidget *contentWidget = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(contentWidget); + + dockWidget->setAllowedAreas(Qt::AllDockWidgetAreas); + layout->setContentsMargins(2, 2, 2, 2); + + voltageChart = new Plot(PLOT_MINIMUM_SIZE_WIDTH/2, PLOT_MINIMUM_SIZE_HEIGHT, false); + voltageChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + voltageChart->setTitle("Voltage"); + voltageChart->setYLabel("[V]"); + voltageChart->setXLabel("[ms]"); + + layout->addWidget(voltageChart); + contentWidget->setLayout(layout); + dockWidget->setWidget(contentWidget); + mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dockWidget); + dockWidget->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); +} + +void DataAnalyzerProfile::createCurrentSubWin() +{ + QDockWidget *dockWidget = new QDockWidget("Current", this); + QWidget *contentWidget = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(contentWidget); + + dockWidget->setAllowedAreas(Qt::AllDockWidgetAreas); + layout->setContentsMargins(2, 2, 2, 2); + + currentChart = new Plot(PLOT_MINIMUM_SIZE_WIDTH/2, PLOT_MINIMUM_SIZE_HEIGHT, false); + currentChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + currentChart->setTitle("Current"); + currentChart->setYLabel("[mA]"); + currentChart->setXLabel("[ms]"); + + layout->addWidget(currentChart); + contentWidget->setLayout(layout); + dockWidget->setWidget(contentWidget); + mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dockWidget); + dockWidget->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); +} + +void DataAnalyzerProfile::createConsumptionSubWin() +{ + QDockWidget *dockWidget = new QDockWidget("Consumption", this); + QWidget *contentWidget = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(contentWidget); + + dockWidget->setAllowedAreas(Qt::AllDockWidgetAreas); + layout->setContentsMargins(2, 2, 2, 2); + + consumptionChart = new Plot(PLOT_MINIMUM_SIZE_WIDTH/2, PLOT_MINIMUM_SIZE_HEIGHT, false); + consumptionChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + consumptionChart->setTitle("Consumption"); + consumptionChart->setYLabel("[mA]"); + consumptionChart->setXLabel("[ms]"); + + connect(voltageChart, SIGNAL(sigXRangeChanged(QCPRange)), this, SLOT(onPlotXRangeChanged(QCPRange))); + connect(currentChart, SIGNAL(sigXRangeChanged(QCPRange)), this, SLOT(onPlotXRangeChanged(QCPRange))); + connect(consumptionChart, SIGNAL(sigXRangeChanged(QCPRange)), this, SLOT(onPlotXRangeChanged(QCPRange))); + + layout->addWidget(consumptionChart); + contentWidget->setLayout(layout); + dockWidget->setWidget(contentWidget); + mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dockWidget); + dockWidget->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); +} + +void DataAnalyzerProfile::onPlotXRangeChanged(QCPRange range) +{ + Plot *source = qobject_cast(sender()); + Plot *plots[3] = {voltageChart, currentChart, consumptionChart}; + + for(int i = 0; i < 3; i++) + { + if(plots[i] == source) continue; + plots[i]->setXRangeSynced(range); + } +} + +void DataAnalyzerProfile::startLoading() +{ + QString summaryFilePath = wsDirPath + "/" + profileName + "/OpenEPT.txt"; + QVector> summaryInfo = parseSummaryFile(summaryFilePath); + QString epEnabled = getValueForKey(summaryInfo, "EP Enabled"); + + epEnabledFlag = (epEnabled == "1"); + + if(epEnabledFlag) + { + dataProcesingClass->setLimits(3); + } + else + { + dataProcesingClass->setLimits(2); + } + + progressDialog = new QProgressDialog("Loading Data... ", "Cancel", 0, 100, this); + progressDialog->setWindowModality(Qt::WindowModal); + progressDialog->setMinimumDuration(0); + progressDialog->setAutoClose(true); + progressDialog->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + progressDialog->setWindowTitle("Load OpenEPT Data"); + progressDialog->show(); + + emit processVolCurConRequest(wsDirPath, profileName); +} + +void DataAnalyzerProfile::processingVolCurConDone(QVector > vc, QVector > cons) +{ + if(graphLoad) + { + voltageChart->clear(); + currentChart->clear(); + consumptionChart->clear(); + graphLoad = false; + } + + voltageChart->setData(vc[0],vc[1]); + currentChart->setData(vc[2],vc[3]); + consumptionChart->setData(cons[0], cons[1]); + + loadedVoltage = vc[0]; + loadedVoltageKeys = vc[1]; + loadedCurrent = vc[2]; + loadedCurrentKeys = vc[3]; + loadedMarkers.clear(); + + graphLoad = true; + + if(epEnabledFlag) + { + emit processEPRequest(wsDirPath, profileName); + } + else + { + if(progressDialog) progressDialog->close(); + } +} + +void DataAnalyzerProfile::processingEPDone(QVector > epData) +{ + loadedMarkers = epData; + consumptionChart->scatterAddGraph(); + voltageChart->scatterAddGraph(); + currentChart->scatterAddGraph(); + applyMarkersToPlots(epData); + if(progressDialog) progressDialog->close(); +} + +void DataAnalyzerProfile::updateProgress(int percentage) +{ + if(progressDialog == NULL) return; + progressDialog->setValue(percentage); + if(percentage >= 100) progressDialog->close(); +} + +void DataAnalyzerProfile::updateProgressText(QString text) +{ + if(progressDialog == NULL) return; + progressDialog->setLabelText(text); +} + +void DataAnalyzerProfile::onGenerateStatistics() +{ + if(!graphLoad) + { + QMessageBox::warning(this, "Statistics", "No consumption profile loaded"); + return; + } + if(loadedMarkers.isEmpty()) + { + QMessageBox::warning(this, "Statistics", "Loaded profile has no energy point markers (\" Start\" / \" Stop\" pairs are required)"); + return; + } + + if(statisticsProgressDialog == NULL) + { + statisticsProgressDialog = new QProgressDialog("Generating statistics... ", QString(), 0, 100, this); + statisticsProgressDialog->setWindowModality(Qt::WindowModal); + statisticsProgressDialog->setMinimumDuration(0); + statisticsProgressDialog->setAutoClose(false); + statisticsProgressDialog->setAutoReset(false); + statisticsProgressDialog->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + statisticsProgressDialog->setWindowTitle("Consumption statistics"); + } + statisticsProgressDialog->setLabelText("Generating statistics... "); + statisticsProgressDialog->setValue(0); + statisticsProgressDialog->show(); + + emit sigComputeStatistics(loadedVoltage, loadedVoltageKeys, loadedCurrent, loadedCurrentKeys, loadedMarkers); +} + +void DataAnalyzerProfile::onStatisticsProgress(int percentage, QString text) +{ + if(statisticsProgressDialog == NULL) return; + statisticsProgressDialog->setLabelText(text); + statisticsProgressDialog->setValue(percentage); +} + +void DataAnalyzerProfile::onStatisticsFinished(QVector stats, QVector points, dataanalyzer_segment_stat_t total, QStringList warnings) +{ + if(statisticsProgressDialog != NULL) statisticsProgressDialog->hide(); + + if(stats.isEmpty() && points.isEmpty() && warnings.isEmpty()) + { + QMessageBox::information(this, "Statistics", "No \" Start\" / \" Stop\" marker pairs found"); + return; + } + statisticsWnd->setStatistics(profileName, stats, points, total, warnings); + statisticsWnd->show(); + statisticsWnd->raise(); + statisticsWnd->activateWindow(); +} + +void DataAnalyzerProfile::applyMarkersToPlots(QVector> markers) +{ + consumptionChart->scatterClearMarkers(); + voltageChart->scatterClearMarkers(); + currentChart->scatterClearMarkers(); + + consumptionChart->scatterAddAllDataWithName(markers); + voltageChart->scatterAddAllDataWithName(markers); + currentChart->scatterAddAllDataWithName(markers); +} + +void DataAnalyzerProfile::onStatisticsMarkerVisibilityChanged(QVector segments, QVector expanded) +{ + QVector> visibleMarkers; + + if(!graphLoad) return; + + for(int i = 0; i < loadedMarkers.size(); i++) + { + QString name = loadedMarkers[i].first.trimmed(); + int index = loadedMarkers[i].second; + int owner = -1; + + for(int k = 0; k < segments.size(); k++) + { + if(index < segments[k].startIndex || index > segments[k].endIndex) continue; + if(name.compare(segments[k].name + " Start", Qt::CaseInsensitive) != 0 && + name.compare(segments[k].name + " Stop", Qt::CaseInsensitive) != 0) continue; + owner = k; + break; + } + + if(owner < 0) + { + for(int k = 0; k < segments.size(); k++) + { + if(index < segments[k].startIndex || index > segments[k].endIndex) continue; + if(owner < 0 || (segments[k].endIndex - segments[k].startIndex) < (segments[owner].endIndex - segments[owner].startIndex)) + { + owner = k; + } + } + } + + if(owner >= 0 && expanded.value(owner, true) == false) continue; + + visibleMarkers.append(loadedMarkers[i]); + } + + applyMarkersToPlots(visibleMarkers); +} + +void DataAnalyzerProfile::onStatisticsSegmentSelected(QString name, int startIndex, int endIndex) +{ + double min; + double max; + + Q_UNUSED(name); + + if(!graphLoad) return; + if(startIndex < 0 || endIndex >= loadedVoltageKeys.size() || endIndex <= startIndex) return; + + min = loadedVoltageKeys[startIndex]; + max = loadedVoltageKeys[endIndex]; + voltageChart->zoomToKeyRange(min, max); + currentChart->zoomToKeyRange(min, max); + consumptionChart->zoomToKeyRange(min, max); + + window()->raise(); + window()->activateWindow(); +} + +void DataAnalyzerProfile::onSaveAllPlots() +{ + Plot *plots[3] = {voltageChart, currentChart, consumptionChart}; + QString defaultDir = wsDirPath; + QString dir; + QString prefix; + QString timeStamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); + QStringList failed; + + if(!graphLoad) + { + QMessageBox::warning(this, "Save plots", "No consumption profile loaded"); + return; + } + if(!profileName.isEmpty()) + { + defaultDir = wsDirPath + "/" + profileName; + } + + dir = QFileDialog::getExistingDirectory(this, "Select folder for plot images", defaultDir); + if(dir.isEmpty()) return; + + prefix = profileName.isEmpty() ? "profile" : profileName.section('/', -1).toLower().replace(' ', '_'); + + for(int i = 0; i < 3; i++) + { + QString base = dir + "/oept_" + prefix + "_" + plots[i]->getTitle().toLower().replace(' ', '_') + "_" + timeStamp; + if(!plots[i]->saveImageToFile(base + ".png")) failed << base + ".png"; + if(!plots[i]->saveImageToFile(base + ".svg")) failed << base + ".svg"; + } + + if(!failed.isEmpty()) + { + QMessageBox::warning(this, "Save plots", "Unable to save:\n" + failed.join("\n")); + } +} + +QString DataAnalyzerProfile::getValueForKey(const QVector> &parsedData, const QString &key) +{ + for(const auto &pair : parsedData) + { + if(pair.first == key) return pair.second; + } + return ""; +} + +QVector> DataAnalyzerProfile::parseSummaryFile(const QString &filePath) +{ + QVector> data; + + QFile file(filePath); + if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + qWarning() << "Failed to open file:" << filePath; + return data; + } + + QTextStream in(&file); + + in.readLine(); + in.readLine(); + + while(!in.atEnd()) + { + QString line = in.readLine().trimmed(); + if(line.isEmpty()) continue; + + QStringList parts = line.split(":", Qt::SkipEmptyParts); + if(parts.size() == 2) + { + data.append(qMakePair(parts[0].trimmed(), parts[1].trimmed())); + } + } + + file.close(); + return data; +} diff --git a/Source/Windows/DataAnalyzer/dataanalyzerprofile.h b/Source/Windows/DataAnalyzer/dataanalyzerprofile.h new file mode 100644 index 0000000..296ef22 --- /dev/null +++ b/Source/Windows/DataAnalyzer/dataanalyzerprofile.h @@ -0,0 +1,91 @@ +#ifndef DATAANALYZERPROFILE_H +#define DATAANALYZERPROFILE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dataanalyzerstatistics.h" +#include "dataanalyzerworker.h" +#include "Windows/Plot/plot.h" + +class DataAnalyzerProfile : public QWidget +{ + Q_OBJECT + +public: + explicit DataAnalyzerProfile(QString aWsDirPath, QString aProfileName, QWidget *parent = nullptr); + ~DataAnalyzerProfile(); + + QString getProfileName(); + void startLoading(); + void setDetached(bool detached); + +signals: + void sigDockStateToggleRequested(); + void sigCloseRequested(); + + void processVolCurConRequest(const QString &filePath, const QString &selectedConsumptionProfile); + void processEPRequest(const QString &filePath, const QString &selectedConsumptionProfile); + void sigComputeStatistics(QVector voltage, QVector voltageKeys, QVector current, QVector currentKeys, QVector> markers); + +public slots: + void onGenerateStatistics(); + void onStatisticsProgress(int percentage, QString text); + void onStatisticsFinished(QVector stats, QVector points, dataanalyzer_segment_stat_t total, QStringList warnings); + void onStatisticsSegmentSelected(QString name, int startIndex, int endIndex); + void onStatisticsMarkerVisibilityChanged(QVector segments, QVector expanded); + void onSaveAllPlots(); + void onPlotXRangeChanged(QCPRange range); + void onDetachClicked(); + void processingVolCurConDone(QVector > vc, QVector> cons); + void processingEPDone(QVector> epData); + void updateProgress(int percentage); + void updateProgressText(QString text); + +private: + void createVoltageSubWin(); + void createCurrentSubWin(); + void createConsumptionSubWin(); + void applyMarkersToPlots(QVector> markers); + QVector> parseSummaryFile(const QString &filePath); + QString getValueForKey(const QVector > &parsedData, const QString &key); + + QMainWindow *mainWindow; + QToolBar *plotsToolBar; + QAction *detachAction; + + Plot *voltageChart; + Plot *currentChart; + Plot *consumptionChart; + + QThread *statisticsThread; + DataAnalyzerStatisticsWorker *statisticsWorker; + DataAnalyzerStatisticsWnd *statisticsWnd; + + QThread *thread; + DataAnalyzerWorker *dataProcesingClass; + + QVector loadedVoltage; + QVector loadedVoltageKeys; + QVector loadedCurrent; + QVector loadedCurrentKeys; + QVector> loadedMarkers; + + QString wsDirPath; + QString profileName; + + bool epEnabledFlag; + bool graphLoad; + + QProgressDialog *progressDialog; + QProgressDialog *statisticsProgressDialog; +}; + +#endif // DATAANALYZERPROFILE_H diff --git a/Source/Windows/DataAnalyzer/dataanalyzerstatistics.cpp b/Source/Windows/DataAnalyzer/dataanalyzerstatistics.cpp index 6d9e4a4..5198908 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzerstatistics.cpp +++ b/Source/Windows/DataAnalyzer/dataanalyzerstatistics.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -44,10 +47,19 @@ void DataAnalyzerStatisticsWorker::onComputeStatistics(QVector voltage, QStringList warnings; QVector> openSegments; int sampleCount = qMin(qMin(voltage.size(), current.size()), qMin(voltageKeys.size(), currentKeys.size())); + int progressStep = qMax(1, markers.size() / 50); + + emit sigStatisticsProgress(0, "Pairing markers into segments..."); for(int i = 0; i < markers.size(); i++) { QString segmentName; + + if((i % progressStep) == 0) + { + emit sigStatisticsProgress(markers.size() > 0 ? (60 * i) / markers.size() : 60, "Pairing markers into segments..."); + } + QString markerName = markers[i].first.trimmed(); int index = markers[i].second; dataanalyzer_point_marker_t point; @@ -102,12 +114,24 @@ void DataAnalyzerStatisticsWorker::onComputeStatistics(QVector voltage, stat.endIndex = index; openSegments.removeAt(openIndex); - if(stat.startIndex < 0 || stat.endIndex >= sampleCount || stat.endIndex <= stat.startIndex) + if(stat.startIndex < 0 || stat.startIndex >= sampleCount) { warnings << "Segment \"" + stat.name + "\" is outside of loaded data range"; continue; } + if(stat.endIndex >= sampleCount) + { + warnings << "Segment \"" + stat.name + "\" stops after the loaded data, truncated at the last sample"; + stat.endIndex = sampleCount - 1; + } + + if(stat.endIndex < stat.startIndex) + { + warnings << "Segment \"" + stat.name + "\" stops before it starts"; + continue; + } + if(computeSegment(voltage, voltageKeys, current, &stat)) { stats.append(stat); @@ -145,6 +169,8 @@ void DataAnalyzerStatisticsWorker::onComputeStatistics(QVector voltage, stats[k + 1] = tmp; } + emit sigStatisticsProgress(70, "Resolving nested segments..."); + for(int i = 0; i < stats.size(); i++) { stats[i].parentIndex = -1; @@ -163,6 +189,8 @@ void DataAnalyzerStatisticsWorker::onComputeStatistics(QVector voltage, } } + emit sigStatisticsProgress(85, "Assigning single markers..."); + for(int i = 0; i < points.size(); i++) { points[i].parentIndex = -1; @@ -179,6 +207,8 @@ void DataAnalyzerStatisticsWorker::onComputeStatistics(QVector voltage, } } + emit sigStatisticsProgress(95, "Computing profile totals..."); + dataanalyzer_segment_stat_t total; total.parentIndex = -1; total.name = "Whole profile"; @@ -199,6 +229,7 @@ void DataAnalyzerStatisticsWorker::onComputeStatistics(QVector voltage, total.endTime = 0; } + emit sigStatisticsProgress(100, "Statistics generated"); emit sigStatisticsFinished(stats, points, total, warnings); } @@ -212,6 +243,22 @@ bool DataAnalyzerStatisticsWorker::computeSegment(QVector& voltage, QVec if(stat == NULL) return false; + if(stat->endIndex == stat->startIndex) + { + stat->maxCurrent = current[stat->startIndex]; + stat->minCurrent = current[stat->startIndex]; + stat->maxVoltage = voltage[stat->startIndex]; + stat->minVoltage = voltage[stat->startIndex]; + stat->duration = 0; + stat->startTime = keys[stat->startIndex]; + stat->endTime = keys[stat->endIndex]; + stat->consumption = 0; + stat->energy = 0; + stat->avgCurrent = current[stat->startIndex]; + stat->avgVoltage = voltage[stat->startIndex]; + return true; + } + stat->maxCurrent = current[stat->startIndex]; stat->minCurrent = current[stat->startIndex]; stat->maxVoltage = voltage[stat->startIndex]; @@ -405,21 +452,26 @@ bool DataAnalyzerStatisticsItem::operator<(const QTreeWidgetItem &other) const } #define STAT_COL_NAME 0 -#define STAT_COL_FIRST 1 -#define STAT_COL_LAST 2 -#define STAT_COL_CONSUMPTION 3 -#define STAT_COL_ENERGY 4 -#define STAT_COL_DURATION 5 -#define STAT_COL_SHARE 6 -#define STAT_COL_PARENT_SHARE 7 -#define STAT_COL_BATTERY 8 -#define STAT_COL_MAX_CURRENT 9 -#define STAT_COL_MIN_CURRENT 10 -#define STAT_COL_AVG_CURRENT 11 -#define STAT_COL_MAX_VOLTAGE 12 -#define STAT_COL_MIN_VOLTAGE 13 -#define STAT_COL_AVG_VOLTAGE 14 -#define STAT_COL_COUNT 15 +#define STAT_COL_MARKERS 1 +#define STAT_COL_FIRST 2 +#define STAT_COL_LAST 3 +#define STAT_COL_CONSUMPTION 4 +#define STAT_COL_ENERGY 5 +#define STAT_COL_DURATION 6 +#define STAT_COL_TRANSPORT 7 +#define STAT_COL_SHARE 8 +#define STAT_COL_PARENT_SHARE 9 +#define STAT_COL_BATTERY 10 +#define STAT_COL_MAX_CURRENT 11 +#define STAT_COL_MIN_CURRENT 12 +#define STAT_COL_AVG_CURRENT 13 +#define STAT_COL_MAX_VOLTAGE 14 +#define STAT_COL_MIN_VOLTAGE 15 +#define STAT_COL_AVG_VOLTAGE 16 +#define STAT_COL_COUNT 17 + +#define STAT_MARKER_FRAME_OVERHEAD 3 +#define STAT_UART_BITS_PER_BYTE 10 #define STAT_ROLE_SEGMENT (Qt::UserRole + 1) #define STAT_ROLE_POINT (Qt::UserRole + 2) @@ -437,16 +489,18 @@ DataAnalyzerStatisticsWnd::DataAnalyzerStatisticsWnd(QWidget *parent) QVBoxLayout *mainLayout = new QVBoxLayout(this); - header << "Segment" << "First action [s]" << "Last action [s]" << "Consumption [mAh]" << "Energy [mJ]" << "Duration [s]" << "Share of cycle [%]" << "Share of parent [%]" << "Battery [mAh]" + header << "Segment" << "Markers" << "First action [s]" << "Last action [s]" << "Consumption [mAh]" << "Energy [mJ]" << "Duration [s]" << "Transport [ms]" << "Share of cycle [%]" << "Share of parent [%]" << "Battery [mAh]" << "Max Current [mA]" << "Min Current [mA]" << "Avg Current [mA]" << "Max Voltage [V]" << "Min Voltage [V]" << "Avg Voltage [V]"; table = new QTreeWidget(this); table->setColumnCount(header.size()); table->setHeaderLabels(header); table->headerItem()->setToolTip(STAT_COL_NAME, "Segments nested in time are shown as sub-items, single markers are listed inside the segment where they occurred; double click to zoom plots"); + table->headerItem()->setToolTip(STAT_COL_MARKERS, "Checked: markers of the segment and of all its sub segments are drawn on the plots; unchecked: they are hidden and each sub segment can be turned back on separately"); table->headerItem()->setToolTip(STAT_COL_FIRST, "Time of segment start (or marker time) from the beginning of the recording"); table->headerItem()->setToolTip(STAT_COL_LAST, "Time of segment stop from the beginning of the recording"); table->headerItem()->setToolTip(STAT_COL_CONSUMPTION, "Editable: charge consumed by segment in one cycle (includes sub-segments)"); table->headerItem()->setToolTip(STAT_COL_DURATION, "Editable: segment duration; consumption is scaled with average current"); + table->headerItem()->setToolTip(STAT_COL_TRANSPORT, "Time needed to send the marker over the UART link at the selected baud rate (frame is \"1:\\r\", 10 bits per byte); for a segment it is the Start plus the Stop marker"); table->headerItem()->setToolTip(STAT_COL_SHARE, "Segment consumption / whole cycle consumption"); table->headerItem()->setToolTip(STAT_COL_PARENT_SHARE, "Segment consumption / parent segment consumption"); table->headerItem()->setToolTip(STAT_COL_BATTERY, "Part of battery capacity spent on this segment over the whole operating time"); @@ -459,13 +513,31 @@ DataAnalyzerStatisticsWnd::DataAnalyzerStatisticsWnd(QWidget *parent) table->setSortingEnabled(true); table->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); table->setAllColumnsShowFocus(true); + QToolBar *tableToolBar = new QToolBar(this); + QWidget *tableToolBarSpacer = new QWidget(this); + + tableToolBar->setIconSize(QSize(20, 20)); + tableToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + tableToolBarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + expandAction = tableToolBar->addAction(style()->standardIcon(QStyle::SP_ArrowDown), "Expand all"); + expandAction->setToolTip("Expand all segments and their sub segments"); + collapseAction = tableToolBar->addAction(style()->standardIcon(QStyle::SP_ArrowUp), "Collapse all"); + collapseAction->setToolTip("Collapse all segments"); + tableToolBar->addWidget(tableToolBarSpacer); + resetAction = tableToolBar->addAction(QIcon(QPixmap(":/images/NewSet/reload.png")), "Reset edits"); + resetAction->setToolTip("Restore measured values, discarding manual consumption and duration edits"); + exportAction = tableToolBar->addAction(QIcon(QPixmap(":/images/NewSet/export.png")), "Export CSV"); + exportAction->setToolTip("Export the statistics table to a CSV file"); + + mainLayout->addWidget(tableToolBar); mainLayout->addWidget(table, 3); unassignedLabel = new QLabel("Unassigned markers (outside of any segment)", this); unassignedLabel->setStyleSheet("font-weight: bold;"); unassignedTable = new QTreeWidget(this); - unassignedTable->setColumnCount(3); - unassignedTable->setHeaderLabels(QStringList() << "Marker" << "Time [s]" << "Sample index"); + unassignedTable->setColumnCount(4); + unassignedTable->setHeaderLabels(QStringList() << "Marker" << "Time [s]" << "Sample index" << "Transport [ms]"); unassignedTable->setRootIsDecorated(false); unassignedTable->setAlternatingRowColors(true); unassignedTable->setSortingEnabled(true); @@ -521,6 +593,18 @@ DataAnalyzerStatisticsWnd::DataAnalyzerStatisticsWnd(QWidget *parent) batteryRow->addSpacing(20); batteryRow->addWidget(cyclesLabel); batteryRow->addStretch(); + QLabel *baudRateLabel = new QLabel("Marker link baud rate:", this); + baudRateCombo = new QComboBox(this); + baudRateCombo->setFixedWidth(120); + baudRateCombo->addItems(QStringList() << "9600" << "19200" << "38400" << "57600" << "115200" << "230400" << "460800" << "921600"); + baudRateCombo->setCurrentText("115200"); + baudRateCombo->setToolTip("Baud rate of the UART link used to send energy point markers to the probe"); + transportInfoLabel = new QLabel(this); + transportInfoLabel->setStyleSheet("font-weight: bold;"); + batteryRow->addWidget(transportInfoLabel); + batteryRow->addSpacing(20); + batteryRow->addWidget(baudRateLabel); + batteryRow->addWidget(baudRateCombo); mainLayout->addLayout(batteryRow); QHBoxLayout *targetRow = new QHBoxLayout(); @@ -538,22 +622,11 @@ DataAnalyzerStatisticsWnd::DataAnalyzerStatisticsWnd(QWidget *parent) targetRow->addStretch(); mainLayout->addLayout(targetRow); - QHBoxLayout *buttonRow = new QHBoxLayout(); - expandButton = new QPushButton("Expand all", this); - collapseButton = new QPushButton("Collapse all", this); - buttonRow->addWidget(expandButton); - buttonRow->addWidget(collapseButton); - buttonRow->addStretch(); - resetButton = new QPushButton("Reset edits", this); - exportButton = new QPushButton("Export CSV", this); - buttonRow->addWidget(resetButton); - buttonRow->addWidget(exportButton); - mainLayout->addLayout(buttonRow); - - connect(expandButton, SIGNAL(clicked()), table, SLOT(expandAll())); - connect(collapseButton, SIGNAL(clicked()), table, SLOT(collapseAll())); - connect(exportButton, SIGNAL(clicked()), this, SLOT(onExportCsv())); - connect(resetButton, SIGNAL(clicked()), this, SLOT(onResetEdits())); + connect(baudRateCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(onBaudRateChanged())); + connect(expandAction, SIGNAL(triggered(bool)), table, SLOT(expandAll())); + connect(collapseAction, SIGNAL(triggered(bool)), table, SLOT(collapseAll())); + connect(exportAction, SIGNAL(triggered(bool)), this, SLOT(onExportCsv())); + connect(resetAction, SIGNAL(triggered(bool)), this, SLOT(onResetEdits())); connect(batteryCapacityEdit, SIGNAL(textChanged(QString)), this, SLOT(onBatteryCapacityChanged())); connect(targetTimeEdit, SIGNAL(textChanged(QString)), this, SLOT(onTargetTimeChanged())); connect(cycleReferenceCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCycleReferenceChanged())); @@ -701,6 +774,7 @@ void DataAnalyzerStatisticsWnd::setStatistics(QString aProfileName, QVectorblockSignals(true); @@ -716,9 +790,11 @@ void DataAnalyzerStatisticsWnd::setStatistics(QString aProfileName, QVectorresizeColumnToContents(i); - for(int i = 0; i < 3; i++) unassignedTable->resizeColumnToContents(i); + for(int i = 0; i < 4; i++) unassignedTable->resizeColumnToContents(i); if(!warnings.isEmpty()) { @@ -726,6 +802,101 @@ void DataAnalyzerStatisticsWnd::setStatistics(QString aProfileName, QVectorcurrentText().toDouble(); + + if(baudRate <= 0) return 0; + + return (markerName.length() + STAT_MARKER_FRAME_OVERHEAD) * STAT_UART_BITS_PER_BYTE * 1000.0 / baudRate; +} + +double DataAnalyzerStatisticsWnd::segmentTransportTime(QString segmentName) +{ + return markerTransportTime(segmentName + " Start") + markerTransportTime(segmentName + " Stop"); +} + +void DataAnalyzerStatisticsWnd::updateTransportTimes() +{ + double totalTransport = 0; + int markerNo = 0; + + tableUpdating = true; + + for(int i = 0; i < edited.size(); i++) + { + double transport = segmentTransportTime(edited[i].name); + + if(i < items.size() && items[i] != NULL) + { + setCell(items[i], STAT_COL_TRANSPORT, transport, QString::number(transport, 'f', 3), false); + } + + totalTransport += transport; + markerNo += 2; + } + + for(int i = 0; i < points.size(); i++) + { + totalTransport += markerTransportTime(points[i].name); + markerNo += 1; + } + + QTreeWidgetItemIterator iterator(table); + while(*iterator) + { + QTreeWidgetItem *item = *iterator; + int pointIndex = item->data(0, STAT_ROLE_POINT).toInt(); + + if(pointIndex >= 0 && pointIndex < points.size()) + { + double transport = markerTransportTime(points[pointIndex].name); + setCell(item, STAT_COL_TRANSPORT, transport, QString::number(transport, 'f', 3), false); + } + + ++iterator; + } + + tableUpdating = false; + + if(markerNo == 0) + { + transportInfoLabel->setText("Marker transport: no markers"); + return; + } + + transportInfoLabel->setText("Marker transport: avg " + QString::number(totalTransport / markerNo, 'f', 3) + + " ms, total " + formatDuration(totalTransport) + " (" + QString::number(markerNo) + " markers)"); +} + +void DataAnalyzerStatisticsWnd::onBaudRateChanged() +{ + updateTransportTimes(); + fillUnassigned(); +} + +void DataAnalyzerStatisticsWnd::emitMarkerVisibility() +{ + emit sigMarkerVisibilityChanged(statistics, markersExpanded); +} + +void DataAnalyzerStatisticsWnd::setMarkersExpanded(int index, bool expanded) +{ + if(index < 0 || index >= markersExpanded.size()) return; + + markersExpanded[index] = expanded; + + if(index < items.size() && items[index] != NULL) + { + items[index]->setCheckState(STAT_COL_MARKERS, expanded ? Qt::Checked : Qt::Unchecked); + } + + for(int i = 0; i < statistics.size(); i++) + { + if(statistics[i].parentIndex == index) setMarkersExpanded(i, expanded); + } +} + void DataAnalyzerStatisticsWnd::setCell(QTreeWidgetItem *item, int column, double value, QString text, bool editable) { item->setText(column, text); @@ -745,6 +916,7 @@ void DataAnalyzerStatisticsWnd::refreshItemValues(int i) setCell(item, STAT_COL_CONSUMPTION, edited[i].consumption, QString::number(edited[i].consumption, 'f', 6), true); setCell(item, STAT_COL_ENERGY, edited[i].energy, QString::number(edited[i].energy, 'f', 3), false); setCell(item, STAT_COL_DURATION, edited[i].duration / 1000.0, QString::number(edited[i].duration / 1000.0, 'f', 3), true); + setCell(item, STAT_COL_TRANSPORT, segmentTransportTime(edited[i].name), QString::number(segmentTransportTime(edited[i].name), 'f', 3), false); setCell(item, STAT_COL_MAX_CURRENT, edited[i].maxCurrent, QString::number(edited[i].maxCurrent, 'f', 3), false); setCell(item, STAT_COL_MIN_CURRENT, edited[i].minCurrent, QString::number(edited[i].minCurrent, 'f', 3), false); setCell(item, STAT_COL_AVG_CURRENT, edited[i].avgCurrent, QString::number(edited[i].avgCurrent, 'f', 3), false); @@ -777,6 +949,7 @@ QTreeWidgetItem* DataAnalyzerStatisticsWnd::createPointItem(int pointIndex) setCell(item, STAT_COL_NAME, 0, points[pointIndex].name, false); setCell(item, STAT_COL_FIRST, points[pointIndex].time / 1000.0, QString::number(points[pointIndex].time / 1000.0, 'f', 3), false); setCell(item, STAT_COL_LAST, points[pointIndex].time / 1000.0, "", false); + setCell(item, STAT_COL_TRANSPORT, markerTransportTime(points[pointIndex].name), QString::number(markerTransportTime(points[pointIndex].name), 'f', 3), false); return item; } @@ -798,6 +971,9 @@ void DataAnalyzerStatisticsWnd::fillUnassigned() item->setText(2, QString::number(points[i].index)); item->setData(2, Qt::UserRole, (double)points[i].index); item->setTextAlignment(2, Qt::AlignRight | Qt::AlignVCenter); + item->setText(3, QString::number(markerTransportTime(points[i].name), 'f', 3)); + item->setData(3, Qt::UserRole, markerTransportTime(points[i].name)); + item->setTextAlignment(3, Qt::AlignRight | Qt::AlignVCenter); unassignedTable->addTopLevelItem(item); count++; } @@ -824,6 +1000,9 @@ void DataAnalyzerStatisticsWnd::fillTable() swatch.fill(segmentColor(i)); items[i]->setIcon(STAT_COL_NAME, QIcon(swatch)); setCell(items[i], STAT_COL_NAME, i, edited[i].name, false); + items[i]->setFlags(items[i]->flags() | Qt::ItemIsUserCheckable); + items[i]->setCheckState(STAT_COL_MARKERS, markersExpanded.value(i, true) ? Qt::Checked : Qt::Unchecked); + items[i]->setToolTip(STAT_COL_MARKERS, "Uncheck to hide all markers of \"" + edited[i].name + "\" on the plots, sub segments included; sub segments can be turned back on one by one"); setCell(items[i], STAT_COL_SHARE, 0, "", false); setCell(items[i], STAT_COL_PARENT_SHARE, 0, "", false); setCell(items[i], STAT_COL_BATTERY, 0, "", false); @@ -1110,6 +1289,18 @@ void DataAnalyzerStatisticsWnd::onItemChanged(QTreeWidgetItem *item, int column) if(tableUpdating) return; if(item == NULL) return; + + if(column == STAT_COL_MARKERS) + { + i = item->data(0, STAT_ROLE_SEGMENT).toInt(); + if(i < 0 || i >= markersExpanded.size()) return; + tableUpdating = true; + setMarkersExpanded(i, item->checkState(STAT_COL_MARKERS) == Qt::Checked); + tableUpdating = false; + emitMarkerVisibility(); + return; + } + if(column != STAT_COL_CONSUMPTION && column != STAT_COL_DURATION) return; i = item->data(0, STAT_ROLE_SEGMENT).toInt(); @@ -1188,7 +1379,7 @@ void DataAnalyzerStatisticsWnd::onExportCsv() double capacity = batteryCapacityEdit->text().trimmed().toDouble(); double cycleConsumption = editedTotalConsumption(); - out << "Segment,First action [ms],Last action [ms],Consumption [mAh],Energy [mJ],Duration [ms],Share of cycle [%],Battery [mAh],Max Current [mA],Min Current [mA],Avg Current [mA],Max Voltage [V],Min Voltage [V],Avg Voltage [V]\n"; + out << "Segment,First action [ms],Last action [ms],Consumption [mAh],Energy [mJ],Duration [ms],Transport [ms],Share of cycle [%],Battery [mAh],Max Current [mA],Min Current [mA],Avg Current [mA],Max Voltage [V],Min Voltage [V],Avg Voltage [V]\n"; for(int i = 0; i < edited.size(); i++) { double share = (cycleConsumption > 0) ? edited[i].consumption / cycleConsumption * 100.0 : 0; @@ -1205,6 +1396,7 @@ void DataAnalyzerStatisticsWnd::onExportCsv() << QString::number(edited[i].consumption, 'g', 9) << "," << QString::number(edited[i].energy, 'g', 9) << "," << QString::number(edited[i].duration, 'g', 9) << "," + << QString::number(segmentTransportTime(edited[i].name), 'g', 9) << "," << QString::number(share, 'g', 9) << "," << QString::number(capacity * share / 100.0, 'g', 9) << "," << QString::number(edited[i].maxCurrent, 'g', 9) << "," diff --git a/Source/Windows/DataAnalyzer/dataanalyzerstatistics.h b/Source/Windows/DataAnalyzer/dataanalyzerstatistics.h index 12a0645..d4749d8 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzerstatistics.h +++ b/Source/Windows/DataAnalyzer/dataanalyzerstatistics.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,7 @@ class DataAnalyzerStatisticsWorker : public QObject signals: void sigStatisticsFinished(QVector stats, QVector points, dataanalyzer_segment_stat_t total, QStringList warnings); + void sigStatisticsProgress(int percentage, QString text); public slots: void onComputeStatistics(QVector voltage, QVector voltageKeys, QVector current, QVector currentKeys, QVector> markers); @@ -120,6 +122,7 @@ class DataAnalyzerStatisticsWnd : public QWidget signals: void sigSegmentSelected(QString name, int startIndex, int endIndex); + void sigMarkerVisibilityChanged(QVector segments, QVector expanded); private slots: void onExportCsv(); @@ -131,9 +134,15 @@ private slots: void onItemDoubleClicked(QTreeWidgetItem *item, int column); void onUnassignedDoubleClicked(QTreeWidgetItem *item, int column); void onHeaderContextMenu(QPoint pos); + void onBaudRateChanged(); private: void fillTable(); + void emitMarkerVisibility(); + double markerTransportTime(QString markerName); + double segmentTransportTime(QString segmentName); + void updateTransportTimes(); + void setMarkersExpanded(int index, bool expanded); void fillUnassigned(); void selectPoint(int index); QTreeWidgetItem* createPointItem(int pointIndex); @@ -152,13 +161,15 @@ private slots: QLabel *unassignedLabel; QTreeWidget *unassignedTable; QVector items; - QPushButton *exportButton; - QPushButton *resetButton; - QPushButton *expandButton; - QPushButton *collapseButton; + QAction *exportAction; + QAction *resetAction; + QAction *expandAction; + QAction *collapseAction; QLineEdit *batteryCapacityEdit; QLineEdit *targetTimeEdit; QComboBox *cycleReferenceCombo; + QComboBox *baudRateCombo; + QLabel *transportInfoLabel; QLabel *requiredCapacityLabel; DataAnalyzerShareBar *shareBar; QLabel *cycleDurationLabel; @@ -173,6 +184,7 @@ private slots: QVector statistics; QVector edited; QVector points; + QVector markersExpanded; dataanalyzer_segment_stat_t total; bool tableUpdating; }; diff --git a/Source/Windows/DataAnalyzer/dataanalyzerworker.h b/Source/Windows/DataAnalyzer/dataanalyzerworker.h new file mode 100644 index 0000000..524e69a --- /dev/null +++ b/Source/Windows/DataAnalyzer/dataanalyzerworker.h @@ -0,0 +1,38 @@ +#ifndef DATAANALYZERWORKER_H +#define DATAANALYZERWORKER_H + +#include +#include +#include +#include + +class DataAnalyzerWorker : public QObject +{ + Q_OBJECT + +public: + explicit DataAnalyzerWorker(QObject *parent = nullptr); + bool setLimits(int limitsNumber); + +public slots: + void processVoltCurConData(const QString &wsDirPath, const QString &selectedConsumptionProfile); + void processEPData(const QString &wsDirPath, const QString &selectedConsumptionProfile); + +signals: + void processingVolCurConFinished(QVector > voltage, QVector> current); + void processingEPFinished(QVector> epata); + void progressUpdated(int percentage); + void updateProgressText(QString text); + +private: + QVector> parseVCData(const QString &filePath); + QVector> parseConsumptionData(const QString &filePath); + QVector> parseEPFile(const QString &filePath); + + int vcDataProcessingStartPercentage; + int consumptionDataProcessingStartPercentage; + int epDataProcessingStartPercentage; + double range; +}; + +#endif // DATAANALYZERWORKER_H diff --git a/Source/Windows/Device/configurationwnd.cpp b/Source/Windows/Device/configurationwnd.cpp index 4563c82..211f7be 100644 --- a/Source/Windows/Device/configurationwnd.cpp +++ b/Source/Windows/Device/configurationwnd.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #define CONFIG_LABEL_WIDTH 150 #define CONFIG_FIELD_WIDTH 170 @@ -467,7 +468,16 @@ QWidget *ConfigurationWnd::createParamWidget(const Params::Param ¶m) QWidget *field = nullptr; - if(param.meta.editor == Params::Editor::ComboBox) + if(param.meta.editor == Params::Editor::CheckBox) + { + QCheckBox *checkBox = new QCheckBox(this); + + checkBox->setMinimumHeight(CONFIG_ROW_HEIGHT); + checkBox->setToolTip(param.meta.description); + + field = checkBox; + } + else if(param.meta.editor == Params::Editor::ComboBox) { QComboBox *comboBox = new QComboBox(this); @@ -516,10 +526,24 @@ QString ConfigurationWnd::getFieldValue(QWidget *field) const return comboBox->currentText(); } + if(QCheckBox *checkBox = qobject_cast(field)) + { + return checkBox->isChecked() ? QString("true") : QString("false"); + } + return QString(); } void ConfigurationWnd::setFieldWidgetValue(QWidget *field, const QString &value) { + if(QCheckBox *checkBox = qobject_cast(field)) + { + const QString normalized = value.trimmed().toLower(); + QSignalBlocker blocker(checkBox); + checkBox->setChecked((normalized == "true") || (normalized == "1") || + (normalized == "yes") || (normalized == "enabled")); + return; + } + if(QLineEdit *lineEdit = qobject_cast(field)) { lineEdit->setText(value); @@ -540,6 +564,12 @@ void ConfigurationWnd::setFieldWidgetValue(QWidget *field, const QString &value) } void ConfigurationWnd::setFieldWidgetEditable(QWidget *field, bool editable) { + if(QCheckBox *checkBox = qobject_cast(field)) + { + checkBox->setEnabled(editable); + return; + } + if(QLineEdit *lineEdit = qobject_cast(field)) { lineEdit->setReadOnly(!editable); @@ -652,6 +682,10 @@ void ConfigurationWnd::registerField(const Params::Param ¶m, QWidget *field) { connect(comboBox, &QComboBox::currentTextChanged, this, &ConfigurationWnd::onFieldChanged); } + else if(QCheckBox *checkBox = qobject_cast(field)) + { + connect(checkBox, &QCheckBox::toggled, this, &ConfigurationWnd::onFieldChanged); + } } void ConfigurationWnd::setFieldValue(const QString &key, const QString &value, bool markAsApplied) @@ -1379,6 +1413,20 @@ void ConfigurationWnd::onSetConfigClicked() emit sigDeviceConfigSet(changedChargerFields); } + QMap changedAcquisitionFields = + getChangedFields(static_cast(DeviceParamDefs::Group::AcquisitionConfig)); + + if((changedAcquisitionFields.isEmpty() == false) && (m_params != nullptr)) + { + for(auto it = changedAcquisitionFields.constBegin(); it != changedAcquisitionFields.constEnd(); ++it) + { + if(m_params->setParamValue(it.key(), it.value()) == true) + { + setFieldValue(it.key(), it.value(), true); + } + } + } + emit sigConfigSet(changedFields); } diff --git a/Source/Windows/Device/logdockwidget.cpp b/Source/Windows/Device/logdockwidget.cpp index f11f694..0296ec9 100644 --- a/Source/Windows/Device/logdockwidget.cpp +++ b/Source/Windows/Device/logdockwidget.cpp @@ -44,6 +44,15 @@ LogDockWidget::LogDockWidget(QString aTitle, QWidget *parent) } filterButton->setMenu(filterMenu); + followButton = new QToolButton(content); + followButton->setIcon(QIcon(":/images/NewSet/tracking_graph.png")); + followButton->setIconSize(QSize(18, 18)); + followButton->setFixedSize(24, 24); + followButton->setToolTip("Follow last message"); + followButton->setCheckable(true); + followButton->setChecked(true); + followButton->setAutoRaise(true); + floatButton = new QToolButton(content); floatButton->setIcon(style()->standardIcon(QStyle::SP_TitleBarNormalButton)); floatButton->setFixedSize(24, 24); @@ -58,6 +67,7 @@ LogDockWidget::LogDockWidget(QString aTitle, QWidget *parent) clearButton->setAutoRaise(true); toolRow->addStretch(); + toolRow->addWidget(followButton); toolRow->addWidget(filterButton); toolRow->addWidget(clearButton); toolRow->addWidget(floatButton); @@ -74,6 +84,7 @@ LogDockWidget::LogDockWidget(QString aTitle, QWidget *parent) connect(filterGroup, SIGNAL(triggered(QAction*)), this, SLOT(onFilterActionTriggered(QAction*))); connect(clearButton, SIGNAL(clicked()), this, SIGNAL(sigClearRequested())); + connect(followButton, SIGNAL(toggled(bool)), this, SLOT(onFollowButtonToggled(bool))); connect(floatButton, SIGNAL(clicked()), this, SLOT(onFloatClicked())); connect(this, SIGNAL(topLevelChanged(bool)), this, SLOT(onTopLevelChanged(bool))); } @@ -110,6 +121,22 @@ void LogDockWidget::onFilterActionTriggered(QAction* action) emit sigFilterChanged((int)filter); } +bool LogDockWidget::getFollowOutput() +{ + return followButton->isChecked(); +} + +void LogDockWidget::setFollowOutput(bool aFollow) +{ + followButton->setChecked(aFollow); +} + +void LogDockWidget::onFollowButtonToggled(bool checked) +{ + followButton->setToolTip(checked ? "Follow last message" : "Do not follow last message"); + emit sigFollowOutputChanged(checked); +} + void LogDockWidget::onFloatClicked() { setFloating(!isFloating()); diff --git a/Source/Windows/Device/logdockwidget.h b/Source/Windows/Device/logdockwidget.h index e3f6b58..d47b717 100644 --- a/Source/Windows/Device/logdockwidget.h +++ b/Source/Windows/Device/logdockwidget.h @@ -18,13 +18,17 @@ class LogDockWidget : public QDockWidget QPlainTextEdit* getTextWidget(); log_filter_t getFilter(); void setFilter(log_filter_t aFilter); + bool getFollowOutput(); + void setFollowOutput(bool aFollow); signals: void sigFilterChanged(int filter); void sigClearRequested(); + void sigFollowOutputChanged(bool follow); private slots: void onFilterActionTriggered(QAction* action); + void onFollowButtonToggled(bool checked); void onFloatClicked(); void onTopLevelChanged(bool floating); @@ -33,6 +37,7 @@ private slots: QToolButton *filterButton; QMenu *filterMenu; QActionGroup *filterGroup; + QToolButton *followButton; QToolButton *clearButton; QToolButton *floatButton; log_filter_t filter; diff --git a/Source/Windows/Plot/plot.cpp b/Source/Windows/Plot/plot.cpp index af01d69..241524a 100644 --- a/Source/Windows/Plot/plot.cpp +++ b/Source/Windows/Plot/plot.cpp @@ -202,6 +202,18 @@ void Plot::scatterAddAllDataWithName(QVector> data) plot->replot(); } +void Plot::scatterClearMarkers() +{ + if(!scatterGraphAdded) return; + plot->graph(1)->data()->clear(); + for(int i = 0; i < textData.size(); i++) + { + plot->removeItem(textData[i]); + } + textData.clear(); + plot->replot(); +} + QCPItemText* Plot::createMarkerLabel(double x, double y, QString name) { QCPItemText *textLabel = new QCPItemText(plot); diff --git a/Source/Windows/Plot/plot.h b/Source/Windows/Plot/plot.h index 82052fe..7cc5a2b 100644 --- a/Source/Windows/Plot/plot.h +++ b/Source/Windows/Plot/plot.h @@ -15,6 +15,7 @@ class Plot : public QWidget void scatterAddGraph(); void scatterAddData(QVector data, QVector keys); void scatterAddAllDataWithName(QVector> data); + void scatterClearMarkers(); void scatterAddDataWithName(double value, double keys, QString name); void scatterReplotDataWithName(); void setData(QVector data, QVector keys); diff --git a/Source/devicecontainer.cpp b/Source/devicecontainer.cpp index 21e3412..a137743 100644 --- a/Source/devicecontainer.cpp +++ b/Source/devicecontainer.cpp @@ -16,6 +16,8 @@ DeviceContainer::DeviceContainer(QObject *parent, log->assignLogWidget(deviceWnd->getLogWidget()); connect(deviceWnd->getLogDock(), SIGNAL(sigFilterChanged(int)), log, SLOT(setFilter(int))); connect(deviceWnd->getLogDock(), SIGNAL(sigClearRequested()), log, SLOT(clear())); + connect(deviceWnd->getLogDock(), SIGNAL(sigFollowOutputChanged(bool)), log, SLOT(setFollowOutput(bool))); + log->setFollowOutput(deviceWnd->getLogDock()->getFollowOutput()); m_AppParamsRef = appParam; consumptionProfileName = ""; consumptionProfileNameSet = false; @@ -1098,6 +1100,29 @@ void DeviceContainer::onDeviceNewEBPFull(double value, double key, QString name) fileProcessing->appendEPQueued(name, key); } log->printLogMessage(name + " (value: " + QString::number(value) + ", key: " + QString::number(key) + ")", LOG_MESSAGE_TYPE_INFO, LOG_MESSAGE_DEVICE_TYPE_DEVICE, LOG_MESSAGE_CATEGORY_ENERGY_POINT); + + checkAcquisitionPauseMarker(name); +} + +void DeviceContainer::checkAcquisitionPauseMarker(QString name) +{ + auto params = device->parameters(); + if(params == NULL) return; + if(params->getParamVariant("acqStopOnMarkerEnabled").toBool() == false) return; + + QString pauseMarker = params->getParamValue("acqStopOnMarkerName").trimmed(); + if(pauseMarker.isEmpty()) return; + if(name.trimmed().compare(pauseMarker, Qt::CaseInsensitive) != 0) return; + + log->printLogMessage("Pause marker \"" + pauseMarker + "\" received, pausing acquisition", LOG_MESSAGE_TYPE_INFO); + + onDeviceWndAcquisitionPause(); + + QMessageBox msgBox; + msgBox.setWindowIcon(QIcon(QPixmap(":/images/NewSet/pause.png"))); + msgBox.setWindowTitle("Acquisition paused"); + msgBox.setText("Acquisition paused because marker \"" + pauseMarker + "\" was detected."); + msgBox.exec(); } void DeviceContainer::onDeviceMeasurementEnergyFlowStatusChanged(charginganalysis_status_t status) diff --git a/Source/devicecontainer.h b/Source/devicecontainer.h index 22002c5..5972180 100644 --- a/Source/devicecontainer.h +++ b/Source/devicecontainer.h @@ -142,6 +142,7 @@ private slots: FileProcessing* fileProcessing; void fillDeviceSetFunctions(); + void checkAcquisitionPauseMarker(QString name); bool createSubDir(const QString &subDirName, QString &fullPath); void logResult(bool status, const QString& successMsg, const QString& errorMsg); diff --git a/Source/openept.cpp b/Source/openept.cpp index 7c59c3d..1723fb6 100644 --- a/Source/openept.cpp +++ b/Source/openept.cpp @@ -2,6 +2,7 @@ #include #include #include "openept.h" +#include "Windows/About/aboutwnd.h" #include "Windows/Device/devicewnd.h" #include "ui_openept.h" #include "Links/controllink.h" @@ -50,6 +51,7 @@ OpenEPT::OpenEPT(QString aWorkspacePath, QWidget *parent) connect(ui->actionAddSingleDevice, &QAction::triggered, this, &OpenEPT::onActionAddSingleDeviceTriggered); connect(ui->actionDataAnalyzer, &QAction::triggered, this, &OpenEPT::onActionOpenAndProcessData); connect(ui->actionApplicationSettings, &QAction::triggered, this, &OpenEPT::onActionAppSettings); + connect(ui->actionAbout, &QAction::triggered, this, &OpenEPT::onActionAbout); workspacePath = aWorkspacePath; @@ -68,6 +70,11 @@ OpenEPT::OpenEPT(QString aWorkspacePath, QWidget *parent) this, &OpenEPT::onAppConfigUpdated); + aboutWnd = new AboutWnd(this); + aboutWnd->setWindowModality(Qt::WindowModal); + + setWindowTitle(QString("Open EPT - MCU Energy profiler (v%1)").arg(APP_VERSION)); + connectedDeviceNumber = 0; } @@ -253,6 +260,11 @@ void OpenEPT::onActionAppSettings() appConfWnd->activateWindow(); } +void OpenEPT::onActionAbout() +{ + aboutWnd->exec(); +} + void OpenEPT::onAppConfigUpdated(QMap changedFields) { bool updateOk = true; diff --git a/Source/openept.h b/Source/openept.h index 7de797f..17380d0 100644 --- a/Source/openept.h +++ b/Source/openept.h @@ -5,6 +5,7 @@ #include #include #include +#include "Windows/About/aboutwnd.h" #include "Windows/AddDevice/adddevicewnd.h" #include "Windows/DataAnalyzer/dataanalyzer.h" #include "devicecontainer.h" @@ -35,6 +36,7 @@ private slots: void onDeviceContainerAllDeviceWndClosed(); void onActionOpenAndProcessData(); void onActionAppSettings(); + void onActionAbout(); void onAppConfigUpdated(QMap changedFields); @@ -44,6 +46,8 @@ private slots: /**/ AddDeviceWnd *addDeviceWnd; /**/ + AboutWnd *aboutWnd; + /**/ QList deviceList; /**/ QMenu *connectedDevicesMenu; diff --git a/Source/openept.ui b/Source/openept.ui index f3d1878..d8406ad 100644 --- a/Source/openept.ui +++ b/Source/openept.ui @@ -67,6 +67,7 @@ About + @@ -89,6 +90,11 @@ + + + About OpenEPT + + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..e9307ca --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.0.2 diff --git a/deploy/set_version.sh b/deploy/set_version.sh new file mode 100755 index 0000000..2a763cc --- /dev/null +++ b/deploy/set_version.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 2.0.3" + exit 1 +fi + +VERSION="$1" + +if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "ERROR: version must look like 2.0.3" >&2 + exit 1 +fi + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${REPO_DIR}" + +if [ -n "$(git status --porcelain)" ]; then + echo "ERROR: working tree is not clean. Commit or stash your changes first." >&2 + exit 1 +fi + +if git rev-parse "v${VERSION}" >/dev/null 2>&1; then + echo "ERROR: tag v${VERSION} already exists." >&2 + exit 1 +fi + +echo "${VERSION}" > VERSION + +git add VERSION +git commit -m "Release ${VERSION}" +git tag -a "v${VERSION}" -m "OpenEPT ${VERSION}" + +echo +echo "VERSION set to ${VERSION}, commit and tag v${VERSION} created." +echo +echo "Push with:" +echo +echo " git push && git push origin v${VERSION}" From aa89c969ec5d3dbbc930bad675730d79c64d409c Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Fri, 25 Sep 2026 18:08:26 +0200 Subject: [PATCH 4/8] Data analyzer fixed --- Source/Windows/DataAnalyzer/dataanalyzer.cpp | 67 ++++++++++++++++-- Source/Windows/DataAnalyzer/dataanalyzer.h | 7 ++ .../DataAnalyzer/dataanalyzerprofile.cpp | 18 ++--- Source/images/NewSet/delete.png | Bin 0 -> 576 bytes Source/resource.qrc | 1 + 5 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 Source/images/NewSet/delete.png diff --git a/Source/Windows/DataAnalyzer/dataanalyzer.cpp b/Source/Windows/DataAnalyzer/dataanalyzer.cpp index 4e61f3f..720aab0 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzer.cpp +++ b/Source/Windows/DataAnalyzer/dataanalyzer.cpp @@ -9,6 +9,7 @@ #include #include #include +#include DataAnalyzer::DataAnalyzer(QWidget *parent, QString aWsDirPath) : QWidget(parent), @@ -50,17 +51,16 @@ DataAnalyzer::DataAnalyzer(QWidget *parent, QString aWsDirPath) : processFilePushb->setFixedSize(30, 30); connect(processFilePushb, SIGNAL(clicked(bool)), this, SLOT(onLoadConsumptionProfileData())); - deleteProfilePushb->setIcon(QIcon(QPixmap(":/images/NewSet/stopHand.png"))); - deleteProfilePushb->setIconSize(QSize(24,24)); + deleteProfilePushb->setIcon(QIcon(QPixmap(":/images/NewSet/delete.png"))); + deleteProfilePushb->setIconSize(QSize(30,30)); deleteProfilePushb->setToolTip("Delete selected consumption profile from disk"); deleteProfilePushb->setFixedSize(30, 30); connect(deleteProfilePushb, SIGNAL(clicked(bool)), this, SLOT(onDeleteConsumptionProfile())); topLayout->addWidget(detectedProfilesLabe); - topLayout->addWidget(consumptionProfilesCB); topLayout->addWidget(reloadProfileNamesPushb); + topLayout->addWidget(consumptionProfilesCB); topLayout->addWidget(processFilePushb); - topLayout->addSpacing(20); topLayout->addWidget(deleteProfilePushb); topLayout->addStretch(); @@ -80,6 +80,17 @@ DataAnalyzer::DataAnalyzer(QWidget *parent, QString aWsDirPath) : mdiArea->setDocumentMode(true); mdiArea->setTabPosition(QTabWidget::North); mainLayout->addWidget(mdiArea); + + maximizeProfilePushb = new QToolButton(mdiArea); + maximizeProfilePushb->setIcon(QIcon(QPixmap(":/images/NewSet/expand.png"))); + maximizeProfilePushb->setIconSize(QSize(20,20)); + maximizeProfilePushb->setToolTip("Maximize selected profile in a separate window"); + maximizeProfilePushb->setAutoRaise(true); + maximizeProfilePushb->setFixedSize(24, 24); + maximizeProfilePushb->hide(); + connect(maximizeProfilePushb, SIGNAL(clicked(bool)), this, SLOT(onMaximizeProfile())); + + mdiArea->installEventFilter(this); } DataAnalyzer::~DataAnalyzer() @@ -175,6 +186,7 @@ void DataAnalyzer::attachProfile(DataAnalyzerProfile *profile, QString title) mdiArea->setActiveSubWindow(subWindow); profile->setDetached(false); profile->show(); + updateMaximizeButton(); } void DataAnalyzer::onLoadConsumptionProfileData() @@ -217,9 +229,52 @@ QMdiSubWindow* DataAnalyzer::subWindowFor(DataAnalyzerProfile *profile) return NULL; } +bool DataAnalyzer::eventFilter(QObject *object, QEvent *event) +{ + if(object == mdiArea && (event->type() == QEvent::Resize || + event->type() == QEvent::Show || + event->type() == QEvent::LayoutRequest)) + { + updateMaximizeButton(); + } + + return QWidget::eventFilter(object, event); +} + +void DataAnalyzer::updateMaximizeButton() +{ + QTabBar *tabBar = mdiArea->findChild(); + + if(tabBar == NULL || mdiArea->subWindowList().isEmpty()) + { + maximizeProfilePushb->hide(); + return; + } + + tabBar->setMaximumWidth(mdiArea->width() - maximizeProfilePushb->width() - 4); + + maximizeProfilePushb->move(mdiArea->width() - maximizeProfilePushb->width() - 2, + (tabBar->height() - maximizeProfilePushb->height()) / 2); + maximizeProfilePushb->show(); + maximizeProfilePushb->raise(); +} + +void DataAnalyzer::onMaximizeProfile() +{ + QMdiSubWindow *activeSubWindow = mdiArea->activeSubWindow(); + + if(activeSubWindow == NULL) return; + + toggleProfileDockState(qobject_cast(activeSubWindow->widget())); +} + void DataAnalyzer::onProfileDockStateToggleRequested() { - DataAnalyzerProfile *profile = qobject_cast(sender()); + toggleProfileDockState(qobject_cast(sender())); +} + +void DataAnalyzer::toggleProfileDockState(DataAnalyzerProfile *profile) +{ QMdiSubWindow *subWindow; if(profile == NULL) return; @@ -243,12 +298,14 @@ void DataAnalyzer::onProfileDockStateToggleRequested() profile->show(); profile->raise(); profile->activateWindow(); + updateMaximizeButton(); return; } profile->setAttribute(Qt::WA_DeleteOnClose, false); profile->setWindowFlags(Qt::Widget); attachProfile(profile, profile->windowTitle().section(" - ", -1)); + updateMaximizeButton(); } void DataAnalyzer::onProfileDestroyed(QObject *object) diff --git a/Source/Windows/DataAnalyzer/dataanalyzer.h b/Source/Windows/DataAnalyzer/dataanalyzer.h index b9ee7f3..8e06aa5 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzer.h +++ b/Source/Windows/DataAnalyzer/dataanalyzer.h @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "dataanalyzerworker.h" #include "dataanalyzerprofile.h" @@ -32,6 +34,7 @@ class DataAnalyzer : public QWidget protected: void showEvent(QShowEvent *event) override; + bool eventFilter(QObject *object, QEvent *event) override; public slots: void onDeleteConsumptionProfile(); @@ -39,12 +42,14 @@ public slots: void onConsumptionProfileChanged(int index); void onLoadConsumptionProfileData(); void onProfileDockStateToggleRequested(); + void onMaximizeProfile(); void onProfileDestroyed(QObject *object); private: Ui::DataAnalyzer *ui; QMdiArea *mdiArea; + QToolButton *maximizeProfilePushb; QLabel* detectedProfilesLabe; QComboBox *consumptionProfilesCB; QStringList consumptionProfilesName; @@ -60,6 +65,8 @@ public slots: void realoadConsumptionProfiles(); QMdiSubWindow* subWindowFor(DataAnalyzerProfile *profile); void attachProfile(DataAnalyzerProfile *profile, QString title); + void toggleProfileDockState(DataAnalyzerProfile *profile); + void updateMaximizeButton(); }; #endif // DATAANALYZER_H diff --git a/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp index 093e596..e74826b 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp +++ b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp @@ -33,11 +33,12 @@ DataAnalyzerProfile::DataAnalyzerProfile(QString aWsDirPath, QString aProfileNam QAction *saveAllPlotsAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/save.png")), "Save all plots"); saveAllPlotsAction->setToolTip("Save Voltage, Current and Consumption plots (current view) to a folder"); connect(saveAllPlotsAction, SIGNAL(triggered(bool)), this, SLOT(onSaveAllPlots())); - QAction *genStatisticsAction = plotsToolBar->addAction("Gen statistics"); + QAction *genStatisticsAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/analysis.png")), "Gen statistics"); genStatisticsAction->setToolTip("Generate consumption statistics for segments between \" Start\" and \" Stop\" markers"); connect(genStatisticsAction, SIGNAL(triggered(bool)), this, SLOT(onGenerateStatistics())); - detachAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/expand.png")), "Detach"); - detachAction->setToolTip("Show this profile in a separate window"); + detachAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/expand.png")), "Attach"); + detachAction->setToolTip("Show this profile back as a tab of the Data Analyzer"); + detachAction->setVisible(false); connect(detachAction, SIGNAL(triggered(bool)), this, SLOT(onDetachClicked())); mainLayout->addWidget(plotsToolBar); @@ -103,16 +104,7 @@ QString DataAnalyzerProfile::getProfileName() void DataAnalyzerProfile::setDetached(bool detached) { - if(detached) - { - detachAction->setText("Attach"); - detachAction->setToolTip("Show this profile back as a tab of the Data Analyzer"); - } - else - { - detachAction->setText("Detach"); - detachAction->setToolTip("Show this profile in a separate window"); - } + detachAction->setVisible(detached); } void DataAnalyzerProfile::onDetachClicked() diff --git a/Source/images/NewSet/delete.png b/Source/images/NewSet/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..c8401db9c05a74dc0a1edb50160aa205cabe3016 GIT binary patch literal 576 zcmV-G0>AxXGQ51)dfnv^?gu+e}cmjL^AtZYa@<)P|ja1O# z6Rd?TK7l@gG_=~2uD&ULt7a@v{u<<6bl-wz;> zNF)+}reJ)bjF-xIp@OSWca-T|g?hi{4C;;!WcvgE6Dqhmv}4sy|6Pv9K>O#=NcOih zUf9S9EeEEr=7Rs2{+b%JG;-m9+lgpVp04uYS(MF)eQ420=JJUdM9s~2W^<1Kn;ng+u8T0A?Pzp7lg zXk@eAqJzn1Di>}^Hsvq+a}J(mla&kayo50oy7rbW*RVSW<6wabeUCu+8(0y9THeAz zXwOVo1-HG0Rq&&ka1eYKf$)}@urj`mKzP$kxXrZUEnEmaHxmw$Kk*iB&*#%-!o}jl z32)(Y4RC*o0K5Wg)UgOK3I1^v0S3W8rXs*3_(xU*G#!NVwOR-NVao9BCOn@#1#4Q4 zMSw~0kEjT+3;vN50d~PZh9bZ;_{UKM7zY11iU7mlKTMlx!{H3CgBSaPj|0;WGg+pa z!v%T-o+#5>E8+4w3*Phq9+cP2!1P6#x)tndw|utCbuy7iBoc`PjFMZhU(0YQO0~2A O0000images/NewSet/filter.png images/NewSet/export.png images/NewSet/lock.png + images/NewSet/delete.png From be1b9a168d38484d9438e66b134e998c3ed8ede6 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Mon, 28 Sep 2026 15:20:39 +0200 Subject: [PATCH 5/8] Initial Version --- Source/OpenEPT.pro | 20 + Source/Processing/batteryparamsextraction.cpp | 716 +++++++++++ Source/Processing/batteryparamsextraction.h | 162 +++ .../Windows/BatteryParams/batterycyclewnd.cpp | 330 +++++ .../Windows/BatteryParams/batterycyclewnd.h | 60 + .../BatteryParams/batteryfitintervalwnd.cpp | 603 ++++++++++ .../BatteryParams/batteryfitintervalwnd.h | 122 ++ .../BatteryParams/batteryfitqualitywnd.cpp | 214 ++++ .../BatteryParams/batteryfitqualitywnd.h | 59 + .../BatteryParams/batteryocvanalysiswnd.cpp | 647 ++++++++++ .../BatteryParams/batteryocvanalysiswnd.h | 85 ++ .../BatteryParams/batteryparamsbarwnd.cpp | 113 ++ .../BatteryParams/batteryparamsbarwnd.h | 38 + .../batteryparamsplotsettings.cpp | 116 ++ .../BatteryParams/batteryparamsplotsettings.h | 41 + .../batteryparamssettingsdlg.cpp | 209 ++++ .../BatteryParams/batteryparamssettingsdlg.h | 54 + .../BatteryParams/batteryparamstrendwnd.cpp | 124 ++ .../BatteryParams/batteryparamstrendwnd.h | 44 + .../BatteryParams/batteryparamswnd.cpp | 1060 +++++++++++++++++ .../Windows/BatteryParams/batteryparamswnd.h | 104 ++ .../DataAnalyzer/dataanalyzerprofile.cpp | 137 +++ .../DataAnalyzer/dataanalyzerprofile.h | 4 + Source/Windows/Device/devicewnd.cpp | 3 + Source/Windows/Device/devicewnd.h | 3 + Source/Windows/Device/energycontrolwnd.cpp | 405 ++++++- Source/Windows/Device/energycontrolwnd.h | 20 +- Source/Windows/Plot/plot.cpp | 119 ++ Source/Windows/Plot/plot.h | 10 + Source/devicecontainer.cpp | 261 +++- Source/devicecontainer.h | 25 + 31 files changed, 5871 insertions(+), 37 deletions(-) create mode 100644 Source/Processing/batteryparamsextraction.cpp create mode 100644 Source/Processing/batteryparamsextraction.h create mode 100644 Source/Windows/BatteryParams/batterycyclewnd.cpp create mode 100644 Source/Windows/BatteryParams/batterycyclewnd.h create mode 100644 Source/Windows/BatteryParams/batteryfitintervalwnd.cpp create mode 100644 Source/Windows/BatteryParams/batteryfitintervalwnd.h create mode 100644 Source/Windows/BatteryParams/batteryfitqualitywnd.cpp create mode 100644 Source/Windows/BatteryParams/batteryfitqualitywnd.h create mode 100644 Source/Windows/BatteryParams/batteryocvanalysiswnd.cpp create mode 100644 Source/Windows/BatteryParams/batteryocvanalysiswnd.h create mode 100644 Source/Windows/BatteryParams/batteryparamsbarwnd.cpp create mode 100644 Source/Windows/BatteryParams/batteryparamsbarwnd.h create mode 100644 Source/Windows/BatteryParams/batteryparamsplotsettings.cpp create mode 100644 Source/Windows/BatteryParams/batteryparamsplotsettings.h create mode 100644 Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp create mode 100644 Source/Windows/BatteryParams/batteryparamssettingsdlg.h create mode 100644 Source/Windows/BatteryParams/batteryparamstrendwnd.cpp create mode 100644 Source/Windows/BatteryParams/batteryparamstrendwnd.h create mode 100644 Source/Windows/BatteryParams/batteryparamswnd.cpp create mode 100644 Source/Windows/BatteryParams/batteryparamswnd.h diff --git a/Source/OpenEPT.pro b/Source/OpenEPT.pro index 1b2434d..b57e9aa 100644 --- a/Source/OpenEPT.pro +++ b/Source/OpenEPT.pro @@ -37,6 +37,7 @@ SOURCES += \ Processing/Parameters/deviceparamdefs.cpp \ Processing/Parameters/deviceparameters.cpp \ Processing/Parameters/parameterstore.cpp \ + Processing/batteryparamsextraction.cpp \ Processing/calibrationdata.cpp \ Processing/charginganalysis.cpp \ Processing/dataprocessing.cpp \ @@ -45,6 +46,15 @@ SOURCES += \ Processing/waveform.cpp \ Utility/log.cpp \ Windows/About/aboutwnd.cpp \ + Windows/BatteryParams/batterycyclewnd.cpp \ + Windows/BatteryParams/batteryfitintervalwnd.cpp \ + Windows/BatteryParams/batteryfitqualitywnd.cpp \ + Windows/BatteryParams/batteryparamsbarwnd.cpp \ + Windows/BatteryParams/batteryparamsplotsettings.cpp \ + Windows/BatteryParams/batteryocvanalysiswnd.cpp \ + Windows/BatteryParams/batteryparamssettingsdlg.cpp \ + Windows/BatteryParams/batteryparamstrendwnd.cpp \ + Windows/BatteryParams/batteryparamswnd.cpp \ Windows/AddDevice/adddevicewnd.cpp \ Windows/ApplicationConf/applicationconfwnd.cpp \ Windows/Console/consolewnd.cpp \ @@ -77,6 +87,7 @@ HEADERS += \ Processing/Parameters/deviceparameters.h \ Processing/Parameters/parameterdefs.h \ Processing/Parameters/parameterstore.h \ + Processing/batteryparamsextraction.h \ Processing/calibrationdata.h \ Processing/charginganalysis.h \ Processing/dataprocessing.h \ @@ -86,6 +97,15 @@ HEADERS += \ Processing/waveform.h \ Utility/log.h \ Windows/About/aboutwnd.h \ + Windows/BatteryParams/batterycyclewnd.h \ + Windows/BatteryParams/batteryfitintervalwnd.h \ + Windows/BatteryParams/batteryfitqualitywnd.h \ + Windows/BatteryParams/batteryparamsbarwnd.h \ + Windows/BatteryParams/batteryparamsplotsettings.h \ + Windows/BatteryParams/batteryocvanalysiswnd.h \ + Windows/BatteryParams/batteryparamssettingsdlg.h \ + Windows/BatteryParams/batteryparamstrendwnd.h \ + Windows/BatteryParams/batteryparamswnd.h \ Windows/AddDevice/adddevicewnd.h \ Windows/ApplicationConf/applicationconfwnd.h \ Windows/Console/consolewnd.h \ diff --git a/Source/Processing/batteryparamsextraction.cpp b/Source/Processing/batteryparamsextraction.cpp new file mode 100644 index 0000000..63bbcce --- /dev/null +++ b/Source/Processing/batteryparamsextraction.cpp @@ -0,0 +1,716 @@ +#include "batteryparamsextraction.h" + +#include +#include + +BatteryParamsExtraction::BatteryParamsExtraction(QObject *parent) + : QObject{parent} +{ + pulseStartMarker = BATTERYPARAMS_DEFAULT_PULSE_START_MARKER; + pulseEndMarker = BATTERYPARAMS_DEFAULT_PULSE_END_MARKER; + pauseStartMarker = BATTERYPARAMS_DEFAULT_PAUSE_START_MARKER; + pauseEndMarker = BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER; + + samplingPeriod = 0; + settings = settingsDefault(); + analysisDeferred = false; + chargeTotal = 0; + cycleActive = false; + cycleCounter = 0; + cycleReset(); +} + +void BatteryParamsExtraction::setMarkerNames(QString pulseStart, QString pulseEnd, QString pauseStart, QString pauseEnd) +{ + pulseStartMarker = pulseStart; + pulseEndMarker = pulseEnd; + pauseStartMarker = pauseStart; + pauseEndMarker = pauseEnd; +} + +void BatteryParamsExtraction::setSamplingPeriod(double aSamplingPeriod) +{ + samplingPeriod = aSamplingPeriod; +} + +batteryparams_settings_t BatteryParamsExtraction::settingsDefault() +{ + batteryparams_settings_t defaults; + + defaults.relaxationThreshold = BATTERYPARAMS_RELAX_THRESHOLD_DEFAULT; + defaults.relaxationWindow = BATTERYPARAMS_RELAX_WINDOW_DEFAULT; + defaults.model = BATTERYPARAMS_MODEL_SECOND_ORDER; + defaults.fitEnd = BATTERYPARAMS_FIT_END_PAUSE; + defaults.tauGridPoints = BATTERYPARAMS_TAU_GRID_POINTS; + defaults.tauRefinementNo = BATTERYPARAMS_TAU_REFINEMENT_NO; + defaults.tauSeparation = BATTERYPARAMS_TAU_SEPARATION; + defaults.capacity = 0; + defaults.initialSoc = BATTERYPARAMS_INITIAL_SOC_DEFAULT; + + return defaults; +} + +void BatteryParamsExtraction::setSettings(batteryparams_settings_t aSettings) +{ + settings = aSettings; +} + +/*Cycle detection and the model fitting can be separated, so that a whole + recording is first split into cycles and the cycles are then fitted together, + in parallel if asked for*/ +void BatteryParamsExtraction::setAnalysisDeferred(bool aDeferred) +{ + analysisDeferred = aDeferred; +} + +batteryparams_settings_t BatteryParamsExtraction::getSettings() +{ + return settings; +} + +/*Battery is considered relaxed once the terminal voltage stops changing, which + means the span between the highest and the lowest sample from that moment to + the end of the pause stays below the threshold. Looking only at a sliding + window is not enough: a slow exponential tail always has an early window that + happens to change little while the voltage still has tens of millivolts to go, + and a window wide drift accumulates unnoticed. The window is what the point + still has to be away from the end of the pause, so the condition is backed by + at least that much data*/ +static int prvBATTERYPARAMS_RelaxationPositionGet(const QVector &keys, const QVector &voltage, + int start, int end, double threshold, double window) +{ + QVector suffixMaximum(end - start + 1); + QVector suffixMinimum(end - start + 1); + + if(end < start) return -1; + + for(int i = end; i >= start; i--) + { + int position = i - start; + + if(i == end) + { + suffixMaximum[position] = voltage[i]; + suffixMinimum[position] = voltage[i]; + continue; + } + + suffixMaximum[position] = qMax(voltage[i], suffixMaximum[position + 1]); + suffixMinimum[position] = qMin(voltage[i], suffixMinimum[position + 1]); + } + + for(int i = start; i <= end; i++) + { + if((keys[end] - keys[i]) < window) break; + if((suffixMaximum[i - start] - suffixMinimum[i - start]) > threshold) continue; + + return i; + } + + return -1; +} + +/*With the time constants fixed the relaxation curve is linear in its remaining + parameters, so the open circuit voltage the cell is heading to and the branch + amplitudes are obtained by least squares. Fitting the asymptote instead of + taking it from the last sample keeps the result usable even when the pause is + cut short*/ +static bool prvBATTERYPARAMS_RelaxationFit(const QVector &time, const QVector &voltage, + double tauFast, double tauSlow, bool secondOrder, + double *openCircuitVoltage, double *amplitudeFast, double *amplitudeSlow) +{ + int size = secondOrder ? 3 : 2; + double matrix[3][4]; + + for(int row = 0; row < size; row++) + { + for(int column = 0; column < size + 1; column++) + { + matrix[row][column] = 0; + } + } + + for(int i = 0; i < time.size(); i++) + { + double basis[3]; + + basis[0] = 1.0; + basis[1] = exp(-time[i] / tauFast); + basis[2] = secondOrder ? exp(-time[i] / tauSlow) : 0; + + for(int row = 0; row < size; row++) + { + for(int column = 0; column < size; column++) + { + matrix[row][column] += basis[row] * basis[column]; + } + matrix[row][size] += basis[row] * voltage[i]; + } + } + + for(int column = 0; column < size; column++) + { + int pivot = column; + + for(int row = column + 1; row < size; row++) + { + if(fabs(matrix[row][column]) > fabs(matrix[pivot][column])) pivot = row; + } + + if(fabs(matrix[pivot][column]) < 1e-18) return false; + + for(int i = 0; i < size + 1; i++) + { + double temporary = matrix[column][i]; + matrix[column][i] = matrix[pivot][i]; + matrix[pivot][i] = temporary; + } + + for(int row = 0; row < size; row++) + { + double factor; + + if(row == column) continue; + + factor = matrix[row][column] / matrix[column][column]; + + for(int i = column; i < size + 1; i++) + { + matrix[row][i] -= factor * matrix[column][i]; + } + } + } + + *openCircuitVoltage = matrix[0][size] / matrix[0][0]; + *amplitudeFast = -matrix[1][size] / matrix[1][1]; + *amplitudeSlow = secondOrder ? (-matrix[2][size] / matrix[2][2]) : 0; + + return true; +} + +bool BatteryParamsExtraction::analyzeCycle(batteryparams_cycle_t *cycle, batteryparams_settings_t settings) +{ + batteryparams_model_t model = settings.model; + int pauseStart = -1; + int pauseEnd = -1; + int relaxPosition; + int fastPosition; + bool secondOrder = (model == BATTERYPARAMS_MODEL_SECOND_ORDER); + double threshold = settings.relaxationThreshold / 1000.0; + double window = settings.relaxationWindow * 1000.0; + double pulseDuration; + double pulseCurrent; + double amplitudeFast = 0; + double amplitudeSlow = 0; + double openCircuitVoltage = 0; + QVector relaxTime; + QVector relaxValue; + + if(cycle == NULL) return false; + + cycle->relaxationReached = false; + cycle->relaxationKey = 0; + cycle->relaxationVoltage = 0; + cycle->ocv = 0; + cycle->model = model; + cycle->modelValid = false; + cycle->tauFast = 0; + cycle->tauSlow = 0; + cycle->r1 = 0; + cycle->c1 = 0; + cycle->r2 = 0; + cycle->c2 = 0; + cycle->fitRmse = 0; + cycle->fitMaxError = 0; + cycle->fitKeys.clear(); + cycle->fitVoltage.clear(); + + for(int i = 0; i < cycle->plotKeys.size(); i++) + { + if((pauseStart < 0) && (cycle->plotKeys[i] >= cycle->pauseStartKey)) pauseStart = i; + if(cycle->plotKeys[i] <= cycle->pauseEndKey) pauseEnd = i; + } + + if((pauseStart < 0) || (pauseEnd <= pauseStart)) return false; + + relaxPosition = prvBATTERYPARAMS_RelaxationPositionGet(cycle->plotKeys, cycle->plotVoltage, + pauseStart, pauseEnd, threshold, window); + + if(relaxPosition < 0) return false; + + cycle->relaxationReached = true; + cycle->relaxationKey = cycle->plotKeys[relaxPosition]; + cycle->relaxationVoltage = cycle->plotVoltage[relaxPosition]; + + /*Cell is at rest once it relaxed, so the voltage measured there is its open + circuit voltage*/ + cycle->ocv = cycle->relaxationVoltage; + + /*Overpotential left on the RC branches when the pulse ends, decaying to zero + as the cell relaxes towards its open circuit voltage*/ + /*Parameters are taken either from the part of the pause up to the point where + the cell relaxed, or from the whole pause. Errors and the drawn curve always + cover the whole pause, so the two settings stay comparable*/ + int fitEndPosition = (settings.fitEnd == BATTERYPARAMS_FIT_END_RELAXATION) ? relaxPosition : pauseEnd; + + for(int i = pauseStart; i <= fitEndPosition; i++) + { + relaxTime.append((cycle->plotKeys[i] - cycle->pauseStartKey) / 1000.0); + relaxValue.append(cycle->plotVoltage[i]); + } + + if(relaxTime.size() < 4) return false; + + /*Time constants are searched for on a logarithmic grid, with the asymptote + and the amplitudes solved by least squares for every candidate. The grid is + then narrowed around the best pair a few times to refine the result. + Settling takes about five time constants, so nothing much longer than the + relaxation itself is worth trying*/ + { + double relaxDuration = relaxTime.last(); + double tauFastLow = relaxTime[1] - relaxTime[0]; + double tauFastHigh = relaxDuration / BATTERYPARAMS_SETTLING_TAU_NO * 2.0; + double tauSlowLow; + double tauSlowHigh; + double bestError = -1; + + if(tauFastLow <= 0) tauFastLow = relaxDuration / 1000.0; + if(tauFastHigh <= tauFastLow) tauFastHigh = tauFastLow * 10.0; + + tauSlowLow = tauFastLow; + tauSlowHigh = tauFastHigh; + + for(int refinement = 0; refinement < settings.tauRefinementNo; refinement++) + { + double bestTauFast = 0; + double bestTauSlow = 0; + double bestAmplitudeFast = 0; + double bestAmplitudeSlow = 0; + double bestVoltage = 0; + + bestError = -1; + + for(int fastStep = 0; fastStep < settings.tauGridPoints; fastStep++) + { + double tauFastCandidate = tauFastLow * pow(tauFastHigh / tauFastLow, + (double)fastStep / (settings.tauGridPoints - 1)); + int slowSteps = secondOrder ? settings.tauGridPoints : 1; + + for(int slowStep = 0; slowStep < slowSteps; slowStep++) + { + double tauSlowCandidate = 0; + double candidateFast = 0; + double candidateSlow = 0; + double candidateVoltage = 0; + double errorSum = 0; + + if(secondOrder) + { + tauSlowCandidate = tauSlowLow * pow(tauSlowHigh / tauSlowLow, + (double)slowStep / (settings.tauGridPoints - 1)); + + /*Branches have to be clearly separated, otherwise the two + exponentials describe the same thing*/ + if(tauSlowCandidate < tauFastCandidate * settings.tauSeparation) continue; + } + + if(!prvBATTERYPARAMS_RelaxationFit(relaxTime, relaxValue, tauFastCandidate, tauSlowCandidate, + secondOrder, &candidateVoltage, &candidateFast, &candidateSlow)) + { + continue; + } + + if(candidateFast < 0) continue; + if(secondOrder && (candidateSlow < 0)) continue; + + for(int i = 0; i < relaxTime.size(); i++) + { + double fitted = candidateVoltage - candidateFast * exp(-relaxTime[i] / tauFastCandidate); + double error; + + if(secondOrder) fitted -= candidateSlow * exp(-relaxTime[i] / tauSlowCandidate); + + error = relaxValue[i] - fitted; + errorSum += error * error; + } + + if((bestError < 0) || (errorSum < bestError)) + { + bestError = errorSum; + bestTauFast = tauFastCandidate; + bestTauSlow = tauSlowCandidate; + bestAmplitudeFast = candidateFast; + bestAmplitudeSlow = candidateSlow; + bestVoltage = candidateVoltage; + } + } + } + + if(bestError < 0) return false; + + cycle->tauFast = bestTauFast; + cycle->tauSlow = bestTauSlow; + amplitudeFast = bestAmplitudeFast; + amplitudeSlow = bestAmplitudeSlow; + openCircuitVoltage = bestVoltage; + + tauFastLow = bestTauFast / BATTERYPARAMS_TAU_REFINEMENT_SPAN; + tauFastHigh = bestTauFast * BATTERYPARAMS_TAU_REFINEMENT_SPAN; + + if(secondOrder) + { + tauSlowLow = bestTauSlow / BATTERYPARAMS_TAU_REFINEMENT_SPAN; + tauSlowHigh = bestTauSlow * BATTERYPARAMS_TAU_REFINEMENT_SPAN; + } + } + } + + if(secondOrder && (cycle->tauSlow <= cycle->tauFast)) + { + secondOrder = false; + cycle->model = BATTERYPARAMS_MODEL_FIRST_ORDER; + cycle->tauSlow = 0; + amplitudeSlow = 0; + } + + /*Overpotential left on the RC branches when the pulse ends, decaying to zero + as the cell relaxes towards its open circuit voltage*/ + pulseDuration = (cycle->pulseEndKey - cycle->pulseStartKey) / 1000.0; + pulseCurrent = cycle->pulseEndCurrent / 1000.0; + + if((pulseDuration <= 0) || (fabs(pulseCurrent) < 1e-9)) return false; + + /*During the pulse each branch charges towards I*R, so the amplitude seen at + the beginning of the relaxation is scaled by how far it got*/ + double chargedFast = 1.0 - exp(-pulseDuration / cycle->tauFast); + + if(chargedFast <= 0) return false; + + cycle->r1 = amplitudeFast / (pulseCurrent * chargedFast); + cycle->c1 = (fabs(cycle->r1) > 1e-9) ? cycle->tauFast / cycle->r1 : 0; + + if(secondOrder) + { + double chargedSlow = 1.0 - exp(-pulseDuration / cycle->tauSlow); + + if(chargedSlow <= 0) return false; + + cycle->r2 = amplitudeSlow / (pulseCurrent * chargedSlow); + cycle->c2 = (fabs(cycle->r2) > 1e-9) ? cycle->tauSlow / cycle->r2 : 0; + } + + double squareErrorSum = 0; + int errorSampleNo = 0; + + /*Evaluated over the whole pause no matter where the parameters came from*/ + for(int i = pauseStart; i <= pauseEnd; i++) + { + double time = (cycle->plotKeys[i] - cycle->pauseStartKey) / 1000.0; + double fitted = openCircuitVoltage - amplitudeFast * exp(-time / cycle->tauFast); + double error; + + if(secondOrder) + { + fitted -= amplitudeSlow * exp(-time / cycle->tauSlow); + } + + error = cycle->plotVoltage[i] - fitted; + + squareErrorSum += error * error; + errorSampleNo++; + if(fabs(error) > cycle->fitMaxError) cycle->fitMaxError = fabs(error); + + cycle->fitKeys.append(cycle->plotKeys[i]); + cycle->fitVoltage.append(fitted); + } + + if(errorSampleNo == 0) return false; + + cycle->fitRmse = sqrt(squareErrorSum / errorSampleNo); + cycle->modelValid = true; + + return true; +} + +QVector BatteryParamsExtraction::getCycles() +{ + return cycles; +} + +void BatteryParamsExtraction::cycleReset() +{ + activeCycle.index = 0; + activeCycle.pulseStartKey = 0; + activeCycle.pulseEndKey = 0; + activeCycle.pauseStartKey = 0; + activeCycle.pauseEndKey = 0; + activeCycle.pulseEndVoltage = 0; + activeCycle.pulseEndCurrent = 0; + activeCycle.pauseStartVoltage = 0; + activeCycle.pauseStartCurrent = 0; + activeCycle.deltaVoltage = 0; + activeCycle.deltaCurrent = 0; + activeCycle.resistance = 0; + activeCycle.charge = 0; + activeCycle.chargeTotal = 0; + activeCycle.socUsed = 0; + activeCycle.soc = 0; + activeCycle.resistanceValid = false; + activeCycle.socValid = false; + activeCycle.relaxationReached = false; + activeCycle.relaxationKey = 0; + activeCycle.relaxationVoltage = 0; + activeCycle.ocv = 0; + activeCycle.model = settings.model; + activeCycle.modelValid = false; + activeCycle.tauFast = 0; + activeCycle.tauSlow = 0; + activeCycle.r1 = 0; + activeCycle.c1 = 0; + activeCycle.r2 = 0; + activeCycle.c2 = 0; + activeCycle.fitRmse = 0; + activeCycle.fitMaxError = 0; + activeCycle.fitKeys.clear(); + activeCycle.fitVoltage.clear(); + activeCycle.plotKeys.clear(); + activeCycle.plotVoltage.clear(); + activeCycle.plotCurrent.clear(); +} + +void BatteryParamsExtraction::storeCycleSamples() +{ + int startPosition = samplePositionGet(activeCycle.pulseStartKey); + int endPosition = samplePositionGet(activeCycle.pauseEndKey); + int samplesNo; + int stride; + double stepWindowStart = activeCycle.pulseEndKey - BATTERYPARAMS_CYCLE_PLOT_STEP_WINDOW; + double stepWindowEnd = activeCycle.pauseStartKey + BATTERYPARAMS_CYCLE_PLOT_STEP_WINDOW; + + if(startPosition < 0 || endPosition < startPosition) return; + + samplesNo = endPosition - startPosition + 1; + + /*Charge taken out of the battery during the cycle, obtained by integrating + the measured current. Sampling period is in ms and current in mA*/ + for(int i = startPosition; i < endPosition; i++) + { + activeCycle.charge += currentSamples[i] * (keySamples[i + 1] - keySamples[i]) / 3600000.0; + } + + chargeTotal += activeCycle.charge; + activeCycle.chargeTotal = chargeTotal; + + if(settings.capacity > 0) + { + activeCycle.socUsed = chargeTotal / settings.capacity * 100.0; + activeCycle.soc = settings.initialSoc - activeCycle.socUsed; + activeCycle.socValid = true; + } + + stride = samplesNo / BATTERYPARAMS_CYCLE_PLOT_POINTS_MAX; + if(stride < 1) stride = 1; + + /*A full cycle can hold millions of samples, so the plotted waveform is + decimated. Samples around the current step are kept untouched because the + step is what the resistance is calculated from*/ + for(int i = startPosition; i <= endPosition; i++) + { + bool inStepWindow = (keySamples[i] >= stepWindowStart) && (keySamples[i] <= stepWindowEnd); + + if(!inStepWindow && (((i - startPosition) % stride) != 0) && (i != endPosition)) continue; + + activeCycle.plotKeys.append(keySamples[i]); + activeCycle.plotVoltage.append(voltageSamples[i]); + activeCycle.plotCurrent.append(currentSamples[i]); + } +} + +void BatteryParamsExtraction::onReset() +{ + voltageSamples.clear(); + currentSamples.clear(); + keySamples.clear(); + pendingMarkers.clear(); + cycles.clear(); + chargeTotal = 0; + cycleActive = false; + cycleCounter = 0; + cycleReset(); +} + +void BatteryParamsExtraction::onNewSamplesReceived(QVector voltage, QVector current, QVector voltageKeys, QVector currentKeys) +{ + int samplesNo = voltage.size(); + + Q_UNUSED(currentKeys); + + if(current.size() < samplesNo) samplesNo = current.size(); + if(voltageKeys.size() < samplesNo) samplesNo = voltageKeys.size(); + + for(int i = 0; i < samplesNo; i++) + { + voltageSamples.append(voltage[i]); + currentSamples.append(current[i]); + keySamples.append(voltageKeys[i]); + } + + /*Marker position is reported as a sample index counted from the acquisition + start, while collected samples carry an absolute key. Sampling period is + the link between the two and is taken from the samples themselves*/ + if((samplingPeriod <= 0) && (samplesNo > 1) && (voltageKeys[1] > voltageKeys[0])) + { + samplingPeriod = voltageKeys[1] - voltageKeys[0]; + } + + processPendingMarkers(); +} + +int BatteryParamsExtraction::samplePositionGet(double key) +{ + int low = 0; + int high = keySamples.size() - 1; + int position; + + if(keySamples.isEmpty()) return -1; + + while(low < high) + { + int middle = (low + high) / 2; + if(keySamples[middle] < key) + { + low = middle + 1; + } + else + { + high = middle; + } + } + + position = low; + + /*Keys are sample times, so the closest one to the marker time is used*/ + if((position > 0) && (qAbs(keySamples[position - 1] - key) <= qAbs(keySamples[position] - key))) + { + position -= 1; + } + + return position; +} + +void BatteryParamsExtraction::trimSamples(double key) +{ + int position = samplePositionGet(key); + + if(position <= 0) return; + + voltageSamples.remove(0, position); + currentSamples.remove(0, position); + keySamples.remove(0, position); +} + +void BatteryParamsExtraction::onNewMarkerReceived(double value, double key, QString name) +{ + batteryparams_marker_t marker; + + Q_UNUSED(value); + + if(key < 0) return; + + marker.name = name.trimmed(); + marker.key = key; + + pendingMarkers.append(marker); + + processPendingMarkers(); +} + +void BatteryParamsExtraction::processPendingMarkers() +{ + /*Markers and samples are produced by two independent links, so a marker can + arrive before the samples it points to. Markers are processed in order and + kept until the sample they reference is collected*/ + while(!pendingMarkers.isEmpty()) + { + double markerKey; + + if(samplingPeriod <= 0) return; + if(keySamples.isEmpty()) return; + + markerKey = pendingMarkers.first().key * samplingPeriod; + + if(markerKey > keySamples.last()) return; + + batteryparams_marker_t marker = pendingMarkers.takeFirst(); + + /*Marker older than the collected samples cannot be resolved any more*/ + if(markerKey < keySamples.first()) continue; + + processMarker(marker.name, samplePositionGet(markerKey)); + } +} + +void BatteryParamsExtraction::processMarker(QString name, int position) +{ + double voltage; + double current; + double key; + + if(position < 0 || position >= keySamples.size()) return; + + voltage = voltageSamples[position]; + current = currentSamples[position]; + key = keySamples[position]; + + if(name == pulseStartMarker) + { + cycleReset(); + cycleActive = true; + cycleCounter++; + activeCycle.index = cycleCounter; + activeCycle.pulseStartKey = key; + emit sigCycleStarted(activeCycle.index, key); + return; + } + + if(!cycleActive) return; + + if(name == pulseEndMarker) + { + activeCycle.pulseEndKey = key; + activeCycle.pulseEndVoltage = voltage; + activeCycle.pulseEndCurrent = current; + return; + } + + if(name == pauseStartMarker) + { + activeCycle.pauseStartKey = key; + activeCycle.pauseStartVoltage = voltage; + activeCycle.pauseStartCurrent = current; + + /*Internal resistance is obtained from the current step between the end of + the pulse and the beginning of the pause. Voltage is in V, current in mA*/ + activeCycle.deltaVoltage = activeCycle.pauseStartVoltage - activeCycle.pulseEndVoltage; + activeCycle.deltaCurrent = activeCycle.pulseEndCurrent - activeCycle.pauseStartCurrent; + + if(fabs(activeCycle.deltaCurrent) > 0.000001) + { + activeCycle.resistance = activeCycle.deltaVoltage / (activeCycle.deltaCurrent / 1000.0); + activeCycle.resistanceValid = true; + } + return; + } + + if(name == pauseEndMarker) + { + activeCycle.pauseEndKey = key; + storeCycleSamples(); + if(!analysisDeferred) analyzeCycle(&activeCycle, settings); + cycles.append(activeCycle); + cycleActive = false; + emit sigCycleFinished(activeCycle); + trimSamples(key); + return; + } +} diff --git a/Source/Processing/batteryparamsextraction.h b/Source/Processing/batteryparamsextraction.h new file mode 100644 index 0000000..32d2ba7 --- /dev/null +++ b/Source/Processing/batteryparamsextraction.h @@ -0,0 +1,162 @@ +#ifndef BATTERYPARAMSEXTRACTION_H +#define BATTERYPARAMSEXTRACTION_H + +#include +#include +#include + +#define BATTERYPARAMS_DEFAULT_PULSE_START_MARKER "Pulse Start" +#define BATTERYPARAMS_DEFAULT_PULSE_END_MARKER "Pulse End" +#define BATTERYPARAMS_DEFAULT_PAUSE_START_MARKER "Pause Start" +#define BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER "Pause End" + +#define BATTERYPARAMS_CYCLE_PLOT_POINTS_MAX 4000 +#define BATTERYPARAMS_RELAX_THRESHOLD_DEFAULT 5.0 +#define BATTERYPARAMS_RELAX_WINDOW_DEFAULT 60.0 +#define BATTERYPARAMS_RELAX_FAST_WINDOW_DIVIDER 60.0 +#define BATTERYPARAMS_SETTLING_TAU_NO 5.0 +#define BATTERYPARAMS_TAU_GRID_POINTS 60 +#define BATTERYPARAMS_TAU_SEPARATION 3.0 +#define BATTERYPARAMS_TAU_REFINEMENT_NO 3 +#define BATTERYPARAMS_TAU_REFINEMENT_SPAN 1.6 +#define BATTERYPARAMS_INITIAL_SOC_DEFAULT 100.0 +#define BATTERYPARAMS_CYCLE_PLOT_STEP_WINDOW 50.0 + +typedef enum +{ + BATTERYPARAMS_FIT_END_RELAXATION = 0, + BATTERYPARAMS_FIT_END_PAUSE +}batteryparams_fit_end_t; + +typedef enum +{ + BATTERYPARAMS_MODEL_FIRST_ORDER = 1, + BATTERYPARAMS_MODEL_SECOND_ORDER = 2 +}batteryparams_model_t; + +typedef struct +{ + double relaxationThreshold; + double relaxationWindow; + batteryparams_model_t model; + batteryparams_fit_end_t fitEnd; + int tauGridPoints; + int tauRefinementNo; + double tauSeparation; + double capacity; + double initialSoc; +}batteryparams_settings_t; + +typedef struct +{ + unsigned int index; + + double pulseStartKey; + double pulseEndKey; + double pauseStartKey; + double pauseEndKey; + + double pulseEndVoltage; + double pulseEndCurrent; + double pauseStartVoltage; + double pauseStartCurrent; + + double deltaVoltage; + double deltaCurrent; + double resistance; + + double charge; + double chargeTotal; + double socUsed; + double soc; + + bool resistanceValid; + bool socValid; + + bool relaxationReached; + double relaxationKey; + double relaxationVoltage; + double ocv; + + batteryparams_model_t model; + bool modelValid; + double tauFast; + double tauSlow; + double r1; + double c1; + double r2; + double c2; + double fitRmse; + double fitMaxError; + + QVector fitKeys; + QVector fitVoltage; + + QVector plotKeys; + QVector plotVoltage; + QVector plotCurrent; +}batteryparams_cycle_t; + +Q_DECLARE_METATYPE(batteryparams_cycle_t); + +class BatteryParamsExtraction : public QObject +{ + Q_OBJECT +public: + explicit BatteryParamsExtraction(QObject *parent = nullptr); + + void setMarkerNames(QString pulseStart, QString pulseEnd, QString pauseStart, QString pauseEnd); + void setSamplingPeriod(double aSamplingPeriod); + void setSettings(batteryparams_settings_t aSettings); + void setAnalysisDeferred(bool aDeferred); + batteryparams_settings_t getSettings(); + + static batteryparams_settings_t settingsDefault(); + static bool analyzeCycle(batteryparams_cycle_t *cycle, batteryparams_settings_t settings); + QVector getCycles(); + +signals: + void sigCycleStarted(unsigned int index, double key); + void sigCycleFinished(batteryparams_cycle_t cycle); + +public slots: + void onNewSamplesReceived(QVector voltage, QVector current, QVector voltageKeys, QVector currentKeys); + void onNewMarkerReceived(double value, double key, QString name); + void onReset(); + +private: + typedef struct + { + QString name; + double key; + }batteryparams_marker_t; + + QString pulseStartMarker; + QString pulseEndMarker; + QString pauseStartMarker; + QString pauseEndMarker; + + QVector voltageSamples; + QVector currentSamples; + QVector keySamples; + double samplingPeriod; + batteryparams_settings_t settings; + bool analysisDeferred; + double chargeTotal; + + QVector pendingMarkers; + QVector cycles; + + batteryparams_cycle_t activeCycle; + bool cycleActive; + unsigned int cycleCounter; + + int samplePositionGet(double key); + void processPendingMarkers(); + void processMarker(QString name, int position); + void trimSamples(double key); + void storeCycleSamples(); + void cycleReset(); +}; + +#endif // BATTERYPARAMSEXTRACTION_H diff --git a/Source/Windows/BatteryParams/batterycyclewnd.cpp b/Source/Windows/BatteryParams/batterycyclewnd.cpp new file mode 100644 index 0000000..b1aa929 --- /dev/null +++ b/Source/Windows/BatteryParams/batterycyclewnd.cpp @@ -0,0 +1,330 @@ +#include "batterycyclewnd.h" + +#include +#include +#include + +#define BATTERYCYCLE_PLOT_MIN_WIDTH 600 +#define BATTERYCYCLE_PLOT_MIN_HEIGHT 180 +#define BATTERYCYCLE_EDIT_WIDTH 100 +#define BATTERYCYCLE_STEP_BUTTON_WIDTH 30 +#define BATTERYCYCLE_BUTTON_WIDTH 90 +#define BATTERYCYCLE_ROW_HEIGHT 28 + +BatteryCycleWnd::BatteryCycleWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QHBoxLayout *markerLayout = new QHBoxLayout(); + QHBoxLayout *buttonLayout = new QHBoxLayout(); + + setFont(defaultFont); + setWindowTitle("Battery cycle"); + + infoLabel = new QLabel(this); + mainLayout->addWidget(infoLabel); + + modelLabel = new QLabel(this); + mainLayout->addWidget(modelLabel); + + markerLayout->addLayout(createMarkerRow("Pulse Start", &pulseStartEdit)); + markerLayout->addSpacing(15); + markerLayout->addLayout(createMarkerRow("Pulse End", &pulseEndEdit)); + markerLayout->addSpacing(15); + markerLayout->addLayout(createMarkerRow("Pause Start", &pauseStartEdit)); + markerLayout->addSpacing(15); + markerLayout->addLayout(createMarkerRow("Pause End", &pauseEndEdit)); + markerLayout->addStretch(); + mainLayout->addLayout(markerLayout); + + pulseEndEdit->setToolTip("Pulse End position. Resistance is calculated between this point and Pause Start"); + pauseStartEdit->setToolTip("Pause Start position. Resistance is calculated between Pulse End and this point"); + + restoreButton = new QPushButton("Restore", this); + restoreButton->setToolTip("Return all markers to the positions reported by the device"); + saveButton = new QPushButton("Save", this); + saveButton->setToolTip("Store adjusted markers and the recalculated resistance in the cycles table"); + restoreButton->setFixedSize(BATTERYCYCLE_BUTTON_WIDTH, BATTERYCYCLE_ROW_HEIGHT); + saveButton->setFixedSize(BATTERYCYCLE_BUTTON_WIDTH, BATTERYCYCLE_ROW_HEIGHT); + + buttonLayout->addStretch(); + buttonLayout->addWidget(restoreButton); + buttonLayout->addWidget(saveButton); + mainLayout->addLayout(buttonLayout); + + voltagePlot = new Plot(BATTERYCYCLE_PLOT_MIN_WIDTH, BATTERYCYCLE_PLOT_MIN_HEIGHT, false, this); + voltagePlot->setTitle("Voltage"); + voltagePlot->setYLabel("[V]"); + voltagePlot->setXLabel("[ms]"); + voltagePlot->scatterAddGraph(); + mainLayout->addWidget(voltagePlot, 1); + + currentPlot = new Plot(BATTERYCYCLE_PLOT_MIN_WIDTH, BATTERYCYCLE_PLOT_MIN_HEIGHT, false, this); + currentPlot->setTitle("Current"); + currentPlot->setYLabel("[mA]"); + currentPlot->setXLabel("[ms]"); + currentPlot->scatterAddGraph(); + mainLayout->addWidget(currentPlot, 1); + + connect(restoreButton, &QPushButton::clicked, this, &BatteryCycleWnd::onRestore); + connect(saveButton, &QPushButton::clicked, this, &BatteryCycleWnd::onSave); + + plotSettings = BATTERYPARAMSPLOT_SettingsDefault(); + + resize(900, 650); +} + +int BatteryCycleWnd::keyPositionGet(QVector keys, double key) +{ + int position = -1; + double distance = 0; + + for(int i = 0; i < keys.size(); i++) + { + double currentDistance = qAbs(keys[i] - key); + + if((position < 0) || (currentDistance < distance)) + { + position = i; + distance = currentDistance; + } + } + + return position; +} + +void BatteryCycleWnd::addMarker(Plot *plot, QVector values, double key, QString name) +{ + int position = keyPositionGet(cycle.plotKeys, key); + + if(position < 0 || position >= values.size()) return; + + plot->scatterAddDataWithName(values[position], position, name); +} + +QHBoxLayout* BatteryCycleWnd::createMarkerRow(QString name, QLineEdit **edit) +{ + QHBoxLayout *rowLayout = new QHBoxLayout(); + QPushButton *stepBack = new QPushButton("<", this); + QPushButton *stepForward = new QPushButton(">", this); + + *edit = new QLineEdit(this); + (*edit)->setFixedSize(BATTERYCYCLE_EDIT_WIDTH, BATTERYCYCLE_ROW_HEIGHT); + stepBack->setFixedSize(BATTERYCYCLE_STEP_BUTTON_WIDTH, BATTERYCYCLE_ROW_HEIGHT); + stepForward->setFixedSize(BATTERYCYCLE_STEP_BUTTON_WIDTH, BATTERYCYCLE_ROW_HEIGHT); + stepBack->setToolTip("Move " + name + " one sample back"); + stepForward->setToolTip("Move " + name + " one sample forward"); + + /*Step buttons act on the edit box next to them*/ + stepBack->setProperty("markerEdit", QVariant::fromValue((QObject*)(*edit))); + stepForward->setProperty("markerEdit", QVariant::fromValue((QObject*)(*edit))); + + rowLayout->addWidget(new QLabel(name + " [ms]", this)); + rowLayout->addWidget(stepBack); + rowLayout->addWidget(*edit); + rowLayout->addWidget(stepForward); + + connect(stepBack, &QPushButton::clicked, this, &BatteryCycleWnd::onMarkerStepBack); + connect(stepForward, &QPushButton::clicked, this, &BatteryCycleWnd::onMarkerStepForward); + connect(*edit, &QLineEdit::editingFinished, this, &BatteryCycleWnd::onMarkersEdited); + + return rowLayout; +} + +void BatteryCycleWnd::markerStep(QLineEdit *edit, int step) +{ + int position; + + if(edit == NULL) return; + + position = keyPositionGet(cycle.plotKeys, edit->text().toDouble()); + + if(position < 0) return; + + position += step; + if(position < 0) position = 0; + if(position >= cycle.plotKeys.size()) position = cycle.plotKeys.size() - 1; + + edit->setText(QString::number(cycle.plotKeys[position], 'f', 3)); + + applyMarkers(); +} + +void BatteryCycleWnd::onMarkerStepBack() +{ + markerStep(qobject_cast(sender()->property("markerEdit").value()), -1); +} + +void BatteryCycleWnd::onMarkerStepForward() +{ + markerStep(qobject_cast(sender()->property("markerEdit").value()), 1); +} + +void BatteryCycleWnd::onMarkersEdited() +{ + applyMarkers(); +} + +void BatteryCycleWnd::onRestore() +{ + setCycle(originalCycle); +} + +void BatteryCycleWnd::onSave() +{ + emit sigCycleUpdated(cycle); +} + +void BatteryCycleWnd::applyMarkers() +{ + int pulseStartPosition = keyPositionGet(cycle.plotKeys, pulseStartEdit->text().toDouble()); + int pulseEndPosition = keyPositionGet(cycle.plotKeys, pulseEndEdit->text().toDouble()); + int pauseStartPosition = keyPositionGet(cycle.plotKeys, pauseStartEdit->text().toDouble()); + int pauseEndPosition = keyPositionGet(cycle.plotKeys, pauseEndEdit->text().toDouble()); + + if(pulseStartPosition < 0 || pulseEndPosition < 0) return; + if(pauseStartPosition < 0 || pauseEndPosition < 0) return; + + cycle.pulseStartKey = cycle.plotKeys[pulseStartPosition]; + + cycle.pulseEndKey = cycle.plotKeys[pulseEndPosition]; + cycle.pulseEndVoltage = cycle.plotVoltage[pulseEndPosition]; + cycle.pulseEndCurrent = cycle.plotCurrent[pulseEndPosition]; + + cycle.pauseStartKey = cycle.plotKeys[pauseStartPosition]; + cycle.pauseStartVoltage = cycle.plotVoltage[pauseStartPosition]; + cycle.pauseStartCurrent = cycle.plotCurrent[pauseStartPosition]; + + cycle.pauseEndKey = cycle.plotKeys[pauseEndPosition]; + + cycle.deltaVoltage = cycle.pauseStartVoltage - cycle.pulseEndVoltage; + cycle.deltaCurrent = cycle.pulseEndCurrent - cycle.pauseStartCurrent; + cycle.resistanceValid = fabs(cycle.deltaCurrent) > 0.000001; + cycle.resistance = cycle.resistanceValid ? cycle.deltaVoltage / (cycle.deltaCurrent / 1000.0) : 0; + + pulseStartEdit->setText(QString::number(cycle.pulseStartKey, 'f', 3)); + pulseEndEdit->setText(QString::number(cycle.pulseEndKey, 'f', 3)); + pulseStartEdit->setText(QString::number(cycle.pulseStartKey, 'f', 3)); + pulseEndEdit->setText(QString::number(cycle.pulseEndKey, 'f', 3)); + pauseStartEdit->setText(QString::number(cycle.pauseStartKey, 'f', 3)); + pauseEndEdit->setText(QString::number(cycle.pauseEndKey, 'f', 3)); + + updateInfo(); + updatePlots(); +} + +void BatteryCycleWnd::updateInfo() +{ + bool moved = (cycle.pulseStartKey != originalCycle.pulseStartKey) || + (cycle.pulseEndKey != originalCycle.pulseEndKey) || + (cycle.pauseStartKey != originalCycle.pauseStartKey) || + (cycle.pauseEndKey != originalCycle.pauseEndKey); + + infoLabel->setText("Cycle " + QString::number(cycle.index) + + " | " + QString::number(cycle.charge, 'f', 3) + " mAh this cycle" + + " | " + QString::number(cycle.chargeTotal, 'f', 3) + " mAh total" + + (cycle.socValid ? " | " + QString::number(cycle.socUsed, 'f', 2) + " % SoC used" : "") + + " | duration " + QString::number(cycle.pauseEndKey - cycle.pulseStartKey, 'f', 1) + " ms" + + " | dV " + QString::number(cycle.deltaVoltage * 1000.0, 'f', 2) + " mV" + + " | dI " + QString::number(cycle.deltaCurrent, 'f', 2) + " mA" + + " | R0 " + (cycle.resistanceValid ? QString::number(cycle.resistance * 1000.0, 'f', 2) + " mOhm" : "-") + + (moved ? " | markers moved, not saved" : "")); + + if(!cycle.relaxationReached) + { + modelLabel->setText("Relaxation not reached, RC model not calculated"); + modelLabel->setStyleSheet("color: rgb(170, 130, 0);"); + return; + } + + if(!cycle.modelValid) + { + modelLabel->setText("Relaxed at " + QString::number(cycle.relaxationKey, 'f', 1) + " ms, RC model could not be fitted"); + modelLabel->setStyleSheet("color: rgb(170, 130, 0);"); + return; + } + + modelLabel->setStyleSheet("color: rgb(0, 120, 0);"); + modelLabel->setText("Relaxed at " + QString::number(cycle.relaxationKey, 'f', 1) + " ms | " + + QString(cycle.model == BATTERYPARAMS_MODEL_SECOND_ORDER ? "2nd order" : "1st order") + + " | R1 " + QString::number(cycle.r1 * 1000.0, 'f', 2) + " mOhm" + + " C1 " + QString::number(cycle.c1, 'f', 1) + " F" + + " tau1 " + QString::number(cycle.tauFast, 'f', 2) + " s" + + (cycle.model == BATTERYPARAMS_MODEL_SECOND_ORDER ? + " | R2 " + QString::number(cycle.r2 * 1000.0, 'f', 2) + " mOhm" + + " C2 " + QString::number(cycle.c2, 'f', 1) + " F" + + " tau2 " + QString::number(cycle.tauSlow, 'f', 2) + " s" : "") + + " | RMSE " + QString::number(cycle.fitRmse * 1000.0, 'f', 3) + " mV" + + " max " + QString::number(cycle.fitMaxError * 1000.0, 'f', 3) + " mV"); + + saveButton->setEnabled(moved); + restoreButton->setEnabled(moved); +} + +void BatteryCycleWnd::setPlotSettings(batteryparams_plot_settings_t settings) +{ + plotSettings = settings; + + voltagePlot->applyStyle(settings.fontFamily, settings.labelFontSize, settings.tickFontSize, + settings.lineWidth, settings.gridVisible, settings.minorGridVisible); + currentPlot->applyStyle(settings.fontFamily, settings.labelFontSize, settings.tickFontSize, + settings.lineWidth, settings.gridVisible, settings.minorGridVisible); +} + +void BatteryCycleWnd::updatePlots() +{ + voltagePlot->scatterClearMarkers(); + currentPlot->scatterClearMarkers(); + voltagePlot->markersAtKeyClear(); + currentPlot->markersAtKeyClear(); + voltagePlot->overlayClear(); + + voltagePlot->setData(cycle.plotVoltage, cycle.plotKeys); + currentPlot->setData(cycle.plotCurrent, cycle.plotKeys); + + addMarker(voltagePlot, cycle.plotVoltage, cycle.pulseStartKey, "Pulse Start"); + addMarker(voltagePlot, cycle.plotVoltage, cycle.pulseEndKey, "Pulse End"); + addMarker(voltagePlot, cycle.plotVoltage, cycle.pauseStartKey, "Pause Start"); + addMarker(voltagePlot, cycle.plotVoltage, cycle.pauseEndKey, "Pause End"); + + addMarker(currentPlot, cycle.plotCurrent, cycle.pulseStartKey, "Pulse Start"); + addMarker(currentPlot, cycle.plotCurrent, cycle.pulseEndKey, "Pulse End"); + addMarker(currentPlot, cycle.plotCurrent, cycle.pauseStartKey, "Pause Start"); + addMarker(currentPlot, cycle.plotCurrent, cycle.pauseEndKey, "Pause End"); + + if(cycle.relaxationReached) + { + voltagePlot->markerAddAtKey(cycle.relaxationKey, cycle.relaxationVoltage, "Relaxed", QColor(230, 180, 0)); + currentPlot->markerAddAtKey(cycle.relaxationKey, 0, "Relaxed", QColor(230, 180, 0)); + } + + if(cycle.modelValid && !cycle.fitKeys.isEmpty()) + { + voltagePlot->overlaySetData(cycle.fitVoltage, cycle.fitKeys); + } + + if(!cycle.plotKeys.isEmpty()) + { + voltagePlot->zoomToKeyRange(cycle.plotKeys.first(), cycle.plotKeys.last()); + currentPlot->zoomToKeyRange(cycle.plotKeys.first(), cycle.plotKeys.last()); + } + + setPlotSettings(plotSettings); +} + +void BatteryCycleWnd::setCycle(batteryparams_cycle_t aCycle) +{ + cycle = aCycle; + originalCycle = aCycle; + + setWindowTitle("Battery cycle " + QString::number(cycle.index)); + + pulseStartEdit->setText(QString::number(cycle.pulseStartKey, 'f', 3)); + pulseEndEdit->setText(QString::number(cycle.pulseEndKey, 'f', 3)); + pauseStartEdit->setText(QString::number(cycle.pauseStartKey, 'f', 3)); + pauseEndEdit->setText(QString::number(cycle.pauseEndKey, 'f', 3)); + + updateInfo(); + updatePlots(); +} diff --git a/Source/Windows/BatteryParams/batterycyclewnd.h b/Source/Windows/BatteryParams/batterycyclewnd.h new file mode 100644 index 0000000..667d2f1 --- /dev/null +++ b/Source/Windows/BatteryParams/batterycyclewnd.h @@ -0,0 +1,60 @@ +#ifndef BATTERYCYCLEWND_H +#define BATTERYCYCLEWND_H + +#include +#include +#include +#include +#include + +#include "Processing/batteryparamsextraction.h" +#include "Windows/BatteryParams/batteryparamsplotsettings.h" +#include "Windows/Plot/plot.h" + +class BatteryCycleWnd : public QWidget +{ + Q_OBJECT + +public: + explicit BatteryCycleWnd(QWidget *parent = nullptr); + + void setCycle(batteryparams_cycle_t aCycle); + void setPlotSettings(batteryparams_plot_settings_t settings); + +signals: + void sigCycleUpdated(batteryparams_cycle_t cycle); + +private slots: + void onMarkerStepBack(); + void onMarkerStepForward(); + void onMarkersEdited(); + void onRestore(); + void onSave(); + +private: + QLabel *infoLabel; + QLabel *modelLabel; + Plot *voltagePlot; + Plot *currentPlot; + + QLineEdit *pulseStartEdit; + QLineEdit *pulseEndEdit; + QLineEdit *pauseStartEdit; + QLineEdit *pauseEndEdit; + QPushButton *saveButton; + QPushButton *restoreButton; + + batteryparams_plot_settings_t plotSettings; + batteryparams_cycle_t cycle; + batteryparams_cycle_t originalCycle; + + void addMarker(Plot *plot, QVector values, double key, QString name); + int keyPositionGet(QVector keys, double key); + QHBoxLayout* createMarkerRow(QString name, QLineEdit **edit); + void markerStep(QLineEdit *edit, int step); + void applyMarkers(); + void updateInfo(); + void updatePlots(); +}; + +#endif // BATTERYCYCLEWND_H diff --git a/Source/Windows/BatteryParams/batteryfitintervalwnd.cpp b/Source/Windows/BatteryParams/batteryfitintervalwnd.cpp new file mode 100644 index 0000000..cc255cf --- /dev/null +++ b/Source/Windows/BatteryParams/batteryfitintervalwnd.cpp @@ -0,0 +1,603 @@ +#include "batteryfitintervalwnd.h" + +#include +#include +#include +#include +#include + +#define BATTERYFITINTERVAL_BAR_WIDTH 0.2 + +static QTableWidgetItem* prvBATTERYFITINTERVAL_ItemCreate(QString text) +{ + QTableWidgetItem *item = new QTableWidgetItem(text); + + item->setTextAlignment(Qt::AlignCenter); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + + return item; +} + +static double prvBATTERYFITINTERVAL_Difference(double reference, double value) +{ + if(fabs(reference) < 1e-12) return 0; + + return (value - reference) / reference * 100.0; +} + +BatteryFitIntervalWnd::BatteryFitIntervalWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QHBoxLayout *bottomLayout = new QHBoxLayout(); + QWidget *errorTab = new QWidget(this); + QVBoxLayout *errorLayout = new QVBoxLayout(errorTab); + QHBoxLayout *errorControlLayout = new QHBoxLayout(); + QWidget *differenceTab = new QWidget(this); + QVBoxLayout *differenceLayout = new QVBoxLayout(differenceTab); + QHBoxLayout *differenceControlLayout = new QHBoxLayout(); + QPushButton *saveButton = new QPushButton("Save image", this); + QStringList header; + + setFont(defaultFont); + setWindowTitle("Fit interval comparison"); + + plotSettings = BATTERYPARAMSPLOT_SettingsDefault(); + tabWidget = new QTabWidget(this); + + errorPlot = new QCustomPlot(errorTab); + errorPlot->setInteraction(QCP::iRangeDrag, true); + errorPlot->setInteraction(QCP::iRangeZoom, true); + errorPlot->legend->setVisible(true); + errorPlot->xAxis->setLabel("Cycle"); + errorPlot->yAxis->setLabel("Fit error [mV]"); + errorGroup = new QCPBarsGroup(errorPlot); + errorGroup->setSpacingType(QCPBarsGroup::stAbsolute); + errorGroup->setSpacing(1); + + errorBars.append(barsCreate(errorPlot, errorGroup, "To relaxation: RMSE", QColor(150, 200, 150))); + errorBars.append(barsCreate(errorPlot, errorGroup, "To relaxation: max", QColor(30, 150, 60))); + errorBars.append(barsCreate(errorPlot, errorGroup, "To pause end: RMSE", QColor(240, 180, 130))); + errorBars.append(barsCreate(errorPlot, errorGroup, "To pause end: max", QColor(200, 90, 20))); + + for(int i = 0; i < errorBars.size(); i++) + { + QCheckBox *checkBox = new QCheckBox(errorBars[i]->name(), errorTab); + + checkBox->setChecked(true); + errorCheckBoxes.append(checkBox); + errorControlLayout->addWidget(checkBox); + connect(checkBox, &QCheckBox::toggled, this, &BatteryFitIntervalWnd::onSeriesToggled); + } + errorControlLayout->addStretch(); + errorLayout->addLayout(errorControlLayout); + errorLayout->addWidget(errorPlot, 1); + tabWidget->addTab(errorTab, "Fit error"); + + differencePlot = new QCustomPlot(differenceTab); + differencePlot->setInteraction(QCP::iRangeDrag, true); + differencePlot->setInteraction(QCP::iRangeZoom, true); + differencePlot->legend->setVisible(true); + differencePlot->xAxis->setLabel("Cycle"); + differencePlot->yAxis->setLabel("Difference to relaxation point fit [%]"); + differenceGroup = new QCPBarsGroup(differencePlot); + differenceGroup->setSpacingType(QCPBarsGroup::stAbsolute); + differenceGroup->setSpacing(1); + + differenceBars.append(barsCreate(differencePlot, differenceGroup, "R1", QColor(200, 60, 20))); + differenceBars.append(barsCreate(differencePlot, differenceGroup, "C1", QColor(30, 110, 200))); + differenceBars.append(barsCreate(differencePlot, differenceGroup, "R2", QColor(30, 150, 60))); + differenceBars.append(barsCreate(differencePlot, differenceGroup, "C2", QColor(120, 60, 180))); + + for(int i = 0; i < differenceBars.size(); i++) + { + QCheckBox *checkBox = new QCheckBox(differenceBars[i]->name(), differenceTab); + + checkBox->setChecked(true); + differenceCheckBoxes.append(checkBox); + differenceControlLayout->addWidget(checkBox); + connect(checkBox, &QCheckBox::toggled, this, &BatteryFitIntervalWnd::onSeriesToggled); + } + differenceControlLayout->addStretch(); + differenceLayout->addLayout(differenceControlLayout); + differenceLayout->addWidget(differencePlot, 1); + tabWidget->addTab(differenceTab, "Parameter difference"); + + header << "Cycle" + << "R1 relax [mOhm]" << "R1 pause [mOhm]" << "dR1 [%]" + << "C1 relax [F]" << "C1 pause [F]" << "dC1 [%]" + << "R2 relax [mOhm]" << "R2 pause [mOhm]" << "dR2 [%]" + << "C2 relax [F]" << "C2 pause [F]" << "dC2 [%]" + << "RMSE relax [mV]" << "RMSE pause [mV]" + << "Max relax [mV]" << "Max pause [mV]"; + table = new QTableWidget(0, header.size(), this); + table->setHorizontalHeaderLabels(header); + table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + table->horizontalHeader()->setSectionsMovable(true); + table->verticalHeader()->setVisible(false); + table->verticalHeader()->setDefaultSectionSize(20); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + tabWidget->addTab(table, "Table"); + + QWidget *sweepTab = new QWidget(this); + + sweepTabCreate(sweepTab); + tabWidget->addTab(sweepTab, "Relaxation sweep"); + + summaryLabel = new QLabel(this); + summaryLabel->setStyleSheet("color: gray;"); + + bottomLayout->addWidget(summaryLabel, 1); + bottomLayout->addWidget(saveButton); + + mainLayout->addWidget(tabWidget, 1); + mainLayout->addLayout(bottomLayout); + + connect(saveButton, &QPushButton::clicked, this, &BatteryFitIntervalWnd::onSaveImage); + + resize(1250, 700); +} + +QCPBars* BatteryFitIntervalWnd::barsCreate(QCustomPlot *plot, QCPBarsGroup *group, QString name, QColor color) +{ + QCPBars *bars = new QCPBars(plot->xAxis, plot->yAxis); + + bars->setName(name); + bars->setBarsGroup(group); + bars->setWidth(BATTERYFITINTERVAL_BAR_WIDTH); + bars->setPen(QPen(color.darker(130))); + bars->setBrush(QBrush(color)); + + return bars; +} + +void BatteryFitIntervalWnd::setPlotSettings(batteryparams_plot_settings_t settings) +{ + plotSettings = settings; + + BATTERYPARAMSPLOT_Apply(errorPlot, plotSettings); + BATTERYPARAMSPLOT_Apply(differencePlot, plotSettings); + BATTERYPARAMSPLOT_Apply(sweepPlot, plotSettings); +} + +/*Relaxation is decided by how much the voltage is allowed to move (dV) over how + long (dt). One of the two is held and the other swept, so it is visible how the + fit error follows the criterion*/ +void BatteryFitIntervalWnd::sweepTabCreate(QWidget *parent) +{ + QVBoxLayout *layout = new QVBoxLayout(parent); + QHBoxLayout *controlLayout = new QHBoxLayout(); + QHBoxLayout *sliderLayout = new QHBoxLayout(); + QPushButton *runButton = new QPushButton("Run sweep", parent); + + sweepModeCombo = new QComboBox(parent); + sweepModeCombo->addItem("Sweep dV, hold dt"); + sweepModeCombo->addItem("Sweep dt, hold dV"); + sweepModeCombo->setToolTip("Which half of the relaxation criterion is swept"); + + sweepFixedEdit = new QLineEdit("60", parent); + sweepFixedEdit->setFixedWidth(70); + sweepFixedUnitLabel = new QLabel("[s]", parent); + + sweepFromEdit = new QLineEdit("1", parent); + sweepFromEdit->setFixedWidth(70); + sweepToEdit = new QLineEdit("20", parent); + sweepToEdit->setFixedWidth(70); + sweepStepsEdit = new QLineEdit("10", parent); + sweepStepsEdit->setFixedWidth(70); + + sweepViewCombo = new QComboBox(parent); + sweepViewCombo->addItem("Average over cycles"); + sweepViewCombo->addItem("Per cycle: RMSE"); + sweepViewCombo->addItem("Per cycle: max error"); + sweepViewCombo->addItem("Per cycle: relaxation time"); + sweepViewCombo->setToolTip("Average of all cycles, or one curve per cycle"); + + sweepCompareCheckBox = new QCheckBox("Compare with pause end fit", parent); + sweepCompareCheckBox->setChecked(true); + sweepCompareCheckBox->setToolTip("Draws the error of the fit over the whole pause as a reference line"); + + controlLayout->addWidget(sweepModeCombo); + controlLayout->addSpacing(10); + controlLayout->addWidget(new QLabel("Hold", parent)); + controlLayout->addWidget(sweepFixedEdit); + controlLayout->addWidget(sweepFixedUnitLabel); + controlLayout->addSpacing(10); + controlLayout->addWidget(new QLabel("From", parent)); + controlLayout->addWidget(sweepFromEdit); + controlLayout->addWidget(new QLabel("to", parent)); + controlLayout->addWidget(sweepToEdit); + controlLayout->addWidget(new QLabel("steps", parent)); + controlLayout->addWidget(sweepStepsEdit); + controlLayout->addSpacing(10); + controlLayout->addWidget(sweepViewCombo); + controlLayout->addSpacing(10); + controlLayout->addWidget(sweepCompareCheckBox); + controlLayout->addStretch(); + controlLayout->addWidget(runButton); + layout->addLayout(controlLayout); + + sweepPlot = new QCustomPlot(parent); + sweepPlot->setInteraction(QCP::iRangeDrag, true); + sweepPlot->setInteraction(QCP::iRangeZoom, true); + sweepPlot->legend->setVisible(true); + sweepPlot->xAxis->setLabel("Relaxation threshold dV [mV]"); + sweepPlot->yAxis->setLabel("Fit error over the whole pause [mV]"); + layout->addWidget(sweepPlot, 1); + + sweepMarker = new QCPItemStraightLine(sweepPlot); + sweepMarker->setPen(QPen(QColor(120, 120, 120), 1, Qt::DashLine)); + sweepMarker->setVisible(false); + + sweepSlider = new QSlider(Qt::Horizontal, parent); + sweepSlider->setEnabled(false); + sweepPointLabel = new QLabel("Run the sweep to see how the error follows the criterion", parent); + + sliderLayout->addWidget(sweepSlider, 1); + sliderLayout->addWidget(sweepPointLabel); + layout->addLayout(sliderLayout); + + connect(sweepModeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &BatteryFitIntervalWnd::onSweepModeChanged); + connect(runButton, &QPushButton::clicked, this, &BatteryFitIntervalWnd::onSweepRun); + connect(sweepSlider, &QSlider::valueChanged, this, &BatteryFitIntervalWnd::onSweepSliderMoved); + connect(sweepViewCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &BatteryFitIntervalWnd::onSweepViewChanged); +} + +void BatteryFitIntervalWnd::onSweepModeChanged() +{ + bool thresholdSwept = (sweepModeCombo->currentIndex() == 0); + + sweepFixedUnitLabel->setText(thresholdSwept ? "[s]" : "[mV]"); + sweepFixedEdit->setText(thresholdSwept ? "60" : "5"); + sweepFromEdit->setText(thresholdSwept ? "1" : "10"); + sweepToEdit->setText(thresholdSwept ? "20" : "600"); + sweepPlot->xAxis->setLabel(thresholdSwept ? "Relaxation threshold dV [mV]" : "Relaxation window dt [s]"); + sweepPlot->replot(); +} + +void BatteryFitIntervalWnd::onSweepRun() +{ + batteryfitinterval_sweep_t mode = (sweepModeCombo->currentIndex() == 0) ? + BATTERYFITINTERVAL_SWEEP_THRESHOLD : BATTERYFITINTERVAL_SWEEP_WINDOW; + + emit sigSweepRequested(mode, sweepFixedEdit->text().toDouble(), + sweepFromEdit->text().toDouble(), sweepToEdit->text().toDouble(), + sweepStepsEdit->text().toInt(), sweepCompareCheckBox->isChecked()); +} + +void BatteryFitIntervalWnd::setSweep(QVector aSweepValues, QVector rmse, QVector maxError, + QVector relaxedNo, double referenceRmse, double referenceMaxError, + QVector cycleIndexes, + QVector> cycleRmse, QVector> cycleMaxError, + QVector relaxationTime, QVector> cycleRelaxationTime) +{ + sweepValues = aSweepValues; + sweepRmse = rmse; + sweepMaxError = maxError; + sweepRelaxedNo = relaxedNo; + sweepCycleIndexes = cycleIndexes; + sweepCycleRmse = cycleRmse; + sweepCycleMaxError = cycleMaxError; + sweepRelaxationTime = relaxationTime; + sweepCycleRelaxationTime = cycleRelaxationTime; + sweepReferenceRmse = referenceRmse; + sweepReferenceMaxError = referenceMaxError; + + sweepPlotUpdate(); + + if(sweepValues.isEmpty()) return; + + sweepSlider->setEnabled(true); + sweepSlider->setRange(0, sweepValues.size() - 1); + sweepSlider->setValue(0); + onSweepSliderMoved(0); +} + +void BatteryFitIntervalWnd::sweepPlotUpdate() +{ + int view = sweepViewCombo->currentIndex(); + + sweepPlot->clearGraphs(); + + if(sweepValues.isEmpty()) + { + sweepPlot->replot(); + return; + } + + sweepPlot->yAxis2->setVisible(view == 0); + + if(view == 0) + { + QCPGraph *graph = sweepPlot->addGraph(); + + graph->setName("RMSE"); + graph->setPen(QPen(QColor(200, 60, 20), 2)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(200, 60, 20), QColor(200, 60, 20), 6)); + graph->setData(sweepValues, sweepRmse, true); + + graph = sweepPlot->addGraph(); + graph->setName("Max error"); + graph->setPen(QPen(QColor(30, 110, 200), 2)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(30, 110, 200), QColor(30, 110, 200), 6)); + graph->setData(sweepValues, sweepMaxError, true); + + /*Relaxation time is seconds, so it gets its own axis on the right*/ + sweepPlot->yAxis2->setLabel("Average time to relaxation [s]"); + sweepPlot->yAxis2->setTickLabels(true); + + graph = sweepPlot->addGraph(sweepPlot->xAxis, sweepPlot->yAxis2); + graph->setName("Average relaxation time"); + graph->setPen(QPen(QColor(30, 150, 60), 2, Qt::DashDotLine)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssSquare, QColor(30, 150, 60), QColor(30, 150, 60), 6)); + graph->setData(sweepValues, sweepRelaxationTime, true); + } + else + { + const QVector> &source = (view == 1) ? sweepCycleRmse : + ((view == 2) ? sweepCycleMaxError : sweepCycleRelaxationTime); + + /*One curve per cycle, so a cycle that behaves differently from the rest + stands out instead of disappearing into the average*/ + for(int i = 0; i < source.size() && i < sweepCycleIndexes.size(); i++) + { + QVector keys; + QVector values; + QCPGraph *graph; + QColor color = QColor::fromHsv((i * 360) / qMax(1, source.size()), 200, 200); + + for(int point = 0; point < source[i].size() && point < sweepValues.size(); point++) + { + if(source[i][point] < 0) continue; + + keys.append(sweepValues[point]); + values.append(source[i][point]); + } + + if(keys.isEmpty()) continue; + + graph = sweepPlot->addGraph(); + graph->setName("Cycle " + QString::number(sweepCycleIndexes[i])); + graph->setPen(QPen(color, 1)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, color, color, 4)); + graph->setData(keys, values, true); + } + + QCPGraph *averageGraph = sweepPlot->addGraph(); + + averageGraph->setName("Average"); + averageGraph->setPen(QPen(Qt::black, 2)); + averageGraph->setData(sweepValues, (view == 1) ? sweepRmse : + ((view == 2) ? sweepMaxError : sweepRelaxationTime), true); + } + + sweepPlot->yAxis->setLabel((view == 3) ? "Time to relaxation [s]" : "Fit error over the whole pause [mV]"); + + /*The reference is a fit error, so it says nothing about the relaxation time*/ + if((sweepReferenceRmse > 0) && (view != 3)) + { + QVector referenceValues; + QVector referenceLine; + QCPGraph *graph; + + referenceValues << sweepValues.first() << sweepValues.last(); + + if(view != 2) + { + referenceLine.clear(); + referenceLine << sweepReferenceRmse << sweepReferenceRmse; + graph = sweepPlot->addGraph(); + graph->setName("RMSE, fit to pause end"); + graph->setPen(QPen(QColor(200, 60, 20), 1, Qt::DashLine)); + graph->setData(referenceValues, referenceLine, true); + } + + if(view != 1) + { + referenceLine.clear(); + referenceLine << sweepReferenceMaxError << sweepReferenceMaxError; + graph = sweepPlot->addGraph(); + graph->setName("Max error, fit to pause end"); + graph->setPen(QPen(QColor(30, 110, 200), 1, Qt::DashLine)); + graph->setData(referenceValues, referenceLine, true); + } + } + + sweepPlot->rescaleAxes(); + BATTERYPARAMSPLOT_Apply(sweepPlot, plotSettings); +} + +void BatteryFitIntervalWnd::onSweepViewChanged() +{ + sweepPlotUpdate(); +} + +void BatteryFitIntervalWnd::onSweepSliderMoved(int position) +{ + bool thresholdSwept = (sweepModeCombo->currentIndex() == 0); + + if(position < 0 || position >= sweepValues.size()) return; + + sweepMarker->setVisible(true); + sweepMarker->point1->setCoords(sweepValues[position], 0); + sweepMarker->point2->setCoords(sweepValues[position], 1); + sweepPlot->replot(); + + sweepPointLabel->setText(QString("%1 %2 | RMSE %3 mV | max %4 mV | relaxation %5 s | %6 cycles relaxed") + .arg(sweepValues[position], 0, 'f', thresholdSwept ? 2 : 1) + .arg(thresholdSwept ? "mV" : "s") + .arg(sweepRmse[position], 0, 'f', 3) + .arg(sweepMaxError[position], 0, 'f', 3) + .arg(sweepRelaxationTime.value(position), 0, 'f', 1) + .arg((int)sweepRelaxedNo[position])); +} + +void BatteryFitIntervalWnd::setData(QVector aCycles) +{ + cycles = aCycles; + + updatePlots(); + updateTable(); + updateSummary(); +} + +/*Only the visible series take part in the scaling, so switching one off fills + the plot with what is left*/ +void BatteryFitIntervalWnd::rescale(QCustomPlot *plot, QVector &bars, QVector &checkBoxes) +{ + bool first = true; + + plot->xAxis->rescale(); + + for(int i = 0; i < bars.size() && i < checkBoxes.size(); i++) + { + bars[i]->setVisible(checkBoxes[i]->isChecked()); + bars[i]->removeFromLegend(); + + if(!checkBoxes[i]->isChecked()) continue; + + bars[i]->addToLegend(); + bars[i]->rescaleValueAxis(!first); + first = false; + } + + if(!first) + { + QCPRange range = plot->yAxis->range(); + + plot->yAxis->setRange(range.lower - range.size() * 0.1, range.upper + range.size() * 0.1); + if(plot->yAxis->range().lower > 0) plot->yAxis->setRangeLower(0); + } + + plot->xAxis->setRange(plot->xAxis->range().lower - 0.5, plot->xAxis->range().upper + 0.5); + plot->replot(); +} + +void BatteryFitIntervalWnd::updatePlots() +{ + QVector keys; + QVector> errorValues(4); + QVector> differenceValues(4); + QSharedPointer errorTicker(new QCPAxisTickerFixed); + QSharedPointer differenceTicker(new QCPAxisTickerFixed); + + for(int i = 0; i < cycles.size(); i++) + { + keys.append(cycles[i].index); + + errorValues[0].append(cycles[i].relaxationValid ? cycles[i].relaxationRmse : 0); + errorValues[1].append(cycles[i].relaxationValid ? cycles[i].relaxationMaxError : 0); + errorValues[2].append(cycles[i].pauseValid ? cycles[i].pauseRmse : 0); + errorValues[3].append(cycles[i].pauseValid ? cycles[i].pauseMaxError : 0); + + bool both = cycles[i].relaxationValid && cycles[i].pauseValid; + + differenceValues[0].append(both ? prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationR1, cycles[i].pauseR1) : 0); + differenceValues[1].append(both ? prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationC1, cycles[i].pauseC1) : 0); + differenceValues[2].append(both ? prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationR2, cycles[i].pauseR2) : 0); + differenceValues[3].append(both ? prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationC2, cycles[i].pauseC2) : 0); + } + + for(int i = 0; i < 4; i++) + { + errorBars[i]->setData(keys, errorValues[i], true); + differenceBars[i]->setData(keys, differenceValues[i], true); + } + + errorTicker->setTickStep(1.0); + errorTicker->setScaleStrategy(QCPAxisTickerFixed::ssNone); + errorPlot->xAxis->setTicker(errorTicker); + + differenceTicker->setTickStep(1.0); + differenceTicker->setScaleStrategy(QCPAxisTickerFixed::ssNone); + differencePlot->xAxis->setTicker(differenceTicker); + + rescale(errorPlot, errorBars, errorCheckBoxes); + rescale(differencePlot, differenceBars, differenceCheckBoxes); + + BATTERYPARAMSPLOT_Apply(errorPlot, plotSettings); + BATTERYPARAMSPLOT_Apply(differencePlot, plotSettings); +} + +void BatteryFitIntervalWnd::updateTable() +{ + table->setRowCount(0); + + for(int i = 0; i < cycles.size(); i++) + { + int row = table->rowCount(); + bool both = cycles[i].relaxationValid && cycles[i].pauseValid; + + table->insertRow(row); + table->setItem(row, 0, prvBATTERYFITINTERVAL_ItemCreate(QString::number(cycles[i].index))); + table->setItem(row, 1, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].relaxationValid ? QString::number(cycles[i].relaxationR1 * 1000.0, 'f', 2) : "-")); + table->setItem(row, 2, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].pauseValid ? QString::number(cycles[i].pauseR1 * 1000.0, 'f', 2) : "-")); + table->setItem(row, 3, prvBATTERYFITINTERVAL_ItemCreate(both ? QString::number(prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationR1, cycles[i].pauseR1), 'f', 1) : "-")); + table->setItem(row, 4, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].relaxationValid ? QString::number(cycles[i].relaxationC1, 'f', 1) : "-")); + table->setItem(row, 5, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].pauseValid ? QString::number(cycles[i].pauseC1, 'f', 1) : "-")); + table->setItem(row, 6, prvBATTERYFITINTERVAL_ItemCreate(both ? QString::number(prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationC1, cycles[i].pauseC1), 'f', 1) : "-")); + table->setItem(row, 7, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].relaxationValid ? QString::number(cycles[i].relaxationR2 * 1000.0, 'f', 2) : "-")); + table->setItem(row, 8, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].pauseValid ? QString::number(cycles[i].pauseR2 * 1000.0, 'f', 2) : "-")); + table->setItem(row, 9, prvBATTERYFITINTERVAL_ItemCreate(both ? QString::number(prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationR2, cycles[i].pauseR2), 'f', 1) : "-")); + table->setItem(row, 10, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].relaxationValid ? QString::number(cycles[i].relaxationC2, 'f', 1) : "-")); + table->setItem(row, 11, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].pauseValid ? QString::number(cycles[i].pauseC2, 'f', 1) : "-")); + table->setItem(row, 12, prvBATTERYFITINTERVAL_ItemCreate(both ? QString::number(prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationC2, cycles[i].pauseC2), 'f', 1) : "-")); + table->setItem(row, 13, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].relaxationValid ? QString::number(cycles[i].relaxationRmse, 'f', 3) : "-")); + table->setItem(row, 14, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].pauseValid ? QString::number(cycles[i].pauseRmse, 'f', 3) : "-")); + table->setItem(row, 15, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].relaxationValid ? QString::number(cycles[i].relaxationMaxError, 'f', 3) : "-")); + table->setItem(row, 16, prvBATTERYFITINTERVAL_ItemCreate(cycles[i].pauseValid ? QString::number(cycles[i].pauseMaxError, 'f', 3) : "-")); + } + + table->resizeColumnsToContents(); +} + +void BatteryFitIntervalWnd::updateSummary() +{ + double relaxationRmseSum = 0; + double pauseRmseSum = 0; + double r1DifferenceSum = 0; + double c1DifferenceSum = 0; + int validNo = 0; + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].relaxationValid || !cycles[i].pauseValid) continue; + + relaxationRmseSum += cycles[i].relaxationRmse; + pauseRmseSum += cycles[i].pauseRmse; + r1DifferenceSum += fabs(prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationR1, cycles[i].pauseR1)); + c1DifferenceSum += fabs(prvBATTERYFITINTERVAL_Difference(cycles[i].relaxationC1, cycles[i].pauseC1)); + validNo++; + } + + if(validNo == 0) + { + summaryLabel->setText("No cycle could be fitted over both intervals"); + return; + } + + summaryLabel->setText(QString("%1 cycles | average RMSE %2 mV to relaxation, %3 mV to pause end | " + "average difference R1 %4 %, C1 %5 %") + .arg(validNo) + .arg(relaxationRmseSum / validNo, 0, 'f', 3) + .arg(pauseRmseSum / validNo, 0, 'f', 3) + .arg(r1DifferenceSum / validNo, 0, 'f', 1) + .arg(c1DifferenceSum / validNo, 0, 'f', 1)); +} + +void BatteryFitIntervalWnd::onSeriesToggled() +{ + rescale(errorPlot, errorBars, errorCheckBoxes); + rescale(differencePlot, differenceBars, differenceCheckBoxes); +} + +void BatteryFitIntervalWnd::onSaveImage() +{ + QCustomPlot *plot = qobject_cast(tabWidget->currentWidget()); + + if(plot == NULL) plot = (tabWidget->currentIndex() == 1) ? differencePlot : errorPlot; + + BATTERYPARAMSPLOT_Save(plot, this, "fit_interval_comparison"); +} diff --git a/Source/Windows/BatteryParams/batteryfitintervalwnd.h b/Source/Windows/BatteryParams/batteryfitintervalwnd.h new file mode 100644 index 0000000..f8fb2c5 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryfitintervalwnd.h @@ -0,0 +1,122 @@ +#ifndef BATTERYFITINTERVALWND_H +#define BATTERYFITINTERVALWND_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Chart/qcustomplot.h" +#include "batteryparamsplotsettings.h" + +typedef enum +{ + BATTERYFITINTERVAL_SWEEP_THRESHOLD = 0, + BATTERYFITINTERVAL_SWEEP_WINDOW +}batteryfitinterval_sweep_t; + +typedef struct +{ + unsigned int index; + + bool relaxationValid; + double relaxationR1; + double relaxationC1; + double relaxationR2; + double relaxationC2; + double relaxationRmse; + double relaxationMaxError; + + bool pauseValid; + double pauseR1; + double pauseC1; + double pauseR2; + double pauseC2; + double pauseRmse; + double pauseMaxError; +}batteryfitinterval_cycle_t; + +class BatteryFitIntervalWnd : public QWidget +{ + Q_OBJECT + +public: + explicit BatteryFitIntervalWnd(QWidget *parent = nullptr); + + void setData(QVector cycles); + void setPlotSettings(batteryparams_plot_settings_t settings); + void setSweep(QVector sweepValues, QVector rmse, QVector maxError, + QVector relaxedNo, double referenceRmse, double referenceMaxError, + QVector cycleIndexes, + QVector> cycleRmse, QVector> cycleMaxError, + QVector relaxationTime, QVector> cycleRelaxationTime); + +signals: + void sigSweepRequested(batteryfitinterval_sweep_t mode, double fixedValue, + double from, double to, int steps, bool compare); + +private slots: + void onSeriesToggled(); + void onSweepModeChanged(); + void onSweepRun(); + void onSweepSliderMoved(int position); + void onSweepViewChanged(); + void onSaveImage(); + +private: + QTabWidget *tabWidget; + QCustomPlot *errorPlot; + QCustomPlot *differencePlot; + QTableWidget *table; + + QVector errorBars; + QVector differenceBars; + QVector errorCheckBoxes; + QVector differenceCheckBoxes; + QCPBarsGroup *errorGroup; + QCPBarsGroup *differenceGroup; + QLabel *summaryLabel; + + QCustomPlot *sweepPlot; + QComboBox *sweepModeCombo; + QLineEdit *sweepFixedEdit; + QLabel *sweepFixedUnitLabel; + QLineEdit *sweepFromEdit; + QLineEdit *sweepToEdit; + QLineEdit *sweepStepsEdit; + QCheckBox *sweepCompareCheckBox; + QComboBox *sweepViewCombo; + QSlider *sweepSlider; + QLabel *sweepPointLabel; + QCPItemStraightLine *sweepMarker; + + QVector sweepValues; + QVector sweepRmse; + QVector sweepMaxError; + QVector sweepRelaxedNo; + QVector sweepCycleIndexes; + QVector> sweepCycleRmse; + QVector> sweepCycleMaxError; + QVector sweepRelaxationTime; + QVector> sweepCycleRelaxationTime; + double sweepReferenceRmse; + double sweepReferenceMaxError; + + QVector cycles; + batteryparams_plot_settings_t plotSettings; + + QCPBars* barsCreate(QCustomPlot *plot, QCPBarsGroup *group, QString name, QColor color); + void rescale(QCustomPlot *plot, QVector &bars, QVector &checkBoxes); + void updatePlots(); + void updateTable(); + void updateSummary(); + void sweepTabCreate(QWidget *parent); + void sweepPlotUpdate(); +}; + +#endif // BATTERYFITINTERVALWND_H diff --git a/Source/Windows/BatteryParams/batteryfitqualitywnd.cpp b/Source/Windows/BatteryParams/batteryfitqualitywnd.cpp new file mode 100644 index 0000000..f74564e --- /dev/null +++ b/Source/Windows/BatteryParams/batteryfitqualitywnd.cpp @@ -0,0 +1,214 @@ +#include "batteryfitqualitywnd.h" + +#include +#include +#include +#include +#include + +#define BATTERYFITQUALITY_BAR_WIDTH 0.18 + +BatteryFitQualityWnd::BatteryFitQualityWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QHBoxLayout *controlLayout = new QHBoxLayout(); + QPushButton *saveButton = new QPushButton("Save image", this); + + setFont(defaultFont); + setWindowTitle("Estimation quality"); + + plot = new QCustomPlot(this); + plot->setInteraction(QCP::iRangeDrag, true); + plot->setInteraction(QCP::iRangeZoom, true); + plot->legend->setVisible(true); + plot->legend->setFont(defaultFont); + plot->xAxis->setLabel("Cycle"); + plot->yAxis->setLabel("Relaxation fit error [mV]"); + + barsGroup = new QCPBarsGroup(plot); + barsGroup->setSpacingType(QCPBarsGroup::stAbsolute); + barsGroup->setSpacing(1); + + firstOrderRmseBars = barsCreate("1st order RMSE", QColor(120, 170, 220)); + firstOrderMaxBars = barsCreate("1st order max", QColor(30, 110, 200)); + secondOrderRmseBars = barsCreate("2nd order RMSE", QColor(240, 180, 130)); + secondOrderMaxBars = barsCreate("2nd order max", QColor(200, 90, 20)); + + firstOrderRmseCheckBox = new QCheckBox("1st order RMSE", this); + firstOrderMaxCheckBox = new QCheckBox("1st order max", this); + secondOrderRmseCheckBox = new QCheckBox("2nd order RMSE", this); + secondOrderMaxCheckBox = new QCheckBox("2nd order max", this); + + firstOrderRmseCheckBox->setChecked(true); + firstOrderMaxCheckBox->setChecked(true); + secondOrderRmseCheckBox->setChecked(true); + secondOrderMaxCheckBox->setChecked(true); + + controlLayout->addWidget(firstOrderRmseCheckBox); + controlLayout->addWidget(firstOrderMaxCheckBox); + controlLayout->addWidget(secondOrderRmseCheckBox); + controlLayout->addWidget(secondOrderMaxCheckBox); + controlLayout->addStretch(); + controlLayout->addWidget(saveButton); + + summaryLabel = new QLabel(this); + summaryLabel->setStyleSheet("color: gray;"); + + mainLayout->addLayout(controlLayout); + mainLayout->addWidget(plot, 1); + mainLayout->addWidget(summaryLabel); + + connect(firstOrderRmseCheckBox, &QCheckBox::toggled, this, &BatteryFitQualityWnd::onSeriesToggled); + connect(firstOrderMaxCheckBox, &QCheckBox::toggled, this, &BatteryFitQualityWnd::onSeriesToggled); + connect(secondOrderRmseCheckBox, &QCheckBox::toggled, this, &BatteryFitQualityWnd::onSeriesToggled); + connect(secondOrderMaxCheckBox, &QCheckBox::toggled, this, &BatteryFitQualityWnd::onSeriesToggled); + connect(saveButton, &QPushButton::clicked, this, &BatteryFitQualityWnd::onSaveImage); + + plotSettings = BATTERYPARAMSPLOT_SettingsDefault(); + + resize(1100, 600); +} + +QCPBars* BatteryFitQualityWnd::barsCreate(QString name, QColor color) +{ + QCPBars *bars = new QCPBars(plot->xAxis, plot->yAxis); + + bars->setName(name); + bars->setBarsGroup(barsGroup); + bars->setWidth(BATTERYFITQUALITY_BAR_WIDTH); + bars->setPen(QPen(color.darker(130))); + bars->setBrush(QBrush(color)); + + return bars; +} + +void BatteryFitQualityWnd::setPlotSettings(batteryparams_plot_settings_t settings) +{ + plotSettings = settings; + BATTERYPARAMSPLOT_Apply(plot, plotSettings); +} + +void BatteryFitQualityWnd::setData(QVector aCycles) +{ + cycles = aCycles; + + updateBars(); + updateSummary(); +} + +void BatteryFitQualityWnd::updateBars() +{ + QVector keys; + QVector firstRmse; + QVector firstMax; + QVector secondRmse; + QVector secondMax; + QSharedPointer ticker(new QCPAxisTickerFixed); + + for(int i = 0; i < cycles.size(); i++) + { + keys.append(cycles[i].index); + firstRmse.append(cycles[i].firstOrderValid ? cycles[i].firstOrderRmse : 0); + firstMax.append(cycles[i].firstOrderValid ? cycles[i].firstOrderMaxError : 0); + secondRmse.append(cycles[i].secondOrderValid ? cycles[i].secondOrderRmse : 0); + secondMax.append(cycles[i].secondOrderValid ? cycles[i].secondOrderMaxError : 0); + } + + firstOrderRmseBars->setData(keys, firstRmse, true); + firstOrderMaxBars->setData(keys, firstMax, true); + secondOrderRmseBars->setData(keys, secondRmse, true); + secondOrderMaxBars->setData(keys, secondMax, true); + + firstOrderRmseBars->setVisible(firstOrderRmseCheckBox->isChecked()); + firstOrderMaxBars->setVisible(firstOrderMaxCheckBox->isChecked()); + secondOrderRmseBars->setVisible(secondOrderRmseCheckBox->isChecked()); + secondOrderMaxBars->setVisible(secondOrderMaxCheckBox->isChecked()); + + /*Hidden series are taken out of the legend and out of the scaling, so the + visible ones fill the plot*/ + firstOrderRmseBars->removeFromLegend(); + firstOrderMaxBars->removeFromLegend(); + secondOrderRmseBars->removeFromLegend(); + secondOrderMaxBars->removeFromLegend(); + + if(firstOrderRmseCheckBox->isChecked()) firstOrderRmseBars->addToLegend(); + if(firstOrderMaxCheckBox->isChecked()) firstOrderMaxBars->addToLegend(); + if(secondOrderRmseCheckBox->isChecked()) secondOrderRmseBars->addToLegend(); + if(secondOrderMaxCheckBox->isChecked()) secondOrderMaxBars->addToLegend(); + + ticker->setTickStep(1.0); + ticker->setScaleStrategy(QCPAxisTickerFixed::ssNone); + plot->xAxis->setTicker(ticker); + + plot->xAxis->rescale(); + + { + bool first = true; + QCPBars *series[4] = {firstOrderRmseBars, firstOrderMaxBars, secondOrderRmseBars, secondOrderMaxBars}; + + for(int i = 0; i < 4; i++) + { + if(!series[i]->visible()) continue; + series[i]->rescaleValueAxis(!first); + first = false; + } + } + + plot->yAxis->setRangeLower(0); + plot->xAxis->setRange(plot->xAxis->range().lower - 0.5, plot->xAxis->range().upper + 0.5); + + BATTERYPARAMSPLOT_Apply(plot, plotSettings); +} + +void BatteryFitQualityWnd::updateSummary() +{ + double firstRmseSum = 0; + double secondRmseSum = 0; + double firstMaxWorst = 0; + double secondMaxWorst = 0; + int firstNo = 0; + int secondNo = 0; + + for(int i = 0; i < cycles.size(); i++) + { + if(cycles[i].firstOrderValid) + { + firstRmseSum += cycles[i].firstOrderRmse; + firstNo++; + if(cycles[i].firstOrderMaxError > firstMaxWorst) firstMaxWorst = cycles[i].firstOrderMaxError; + } + + if(!cycles[i].secondOrderValid) continue; + + secondRmseSum += cycles[i].secondOrderRmse; + secondNo++; + if(cycles[i].secondOrderMaxError > secondMaxWorst) secondMaxWorst = cycles[i].secondOrderMaxError; + } + + if((firstNo == 0) && (secondNo == 0)) + { + summaryLabel->setText("No cycle could be fitted"); + return; + } + + summaryLabel->setText(QString("1st order: average RMSE %1 mV, worst deviation %2 mV over %3 cycles | " + "2nd order: average RMSE %4 mV, worst deviation %5 mV over %6 cycles") + .arg(firstNo > 0 ? firstRmseSum / firstNo : 0, 0, 'f', 3) + .arg(firstMaxWorst, 0, 'f', 3) + .arg(firstNo) + .arg(secondNo > 0 ? secondRmseSum / secondNo : 0, 0, 'f', 3) + .arg(secondMaxWorst, 0, 'f', 3) + .arg(secondNo)); +} + +void BatteryFitQualityWnd::onSeriesToggled() +{ + updateBars(); +} + +void BatteryFitQualityWnd::onSaveImage() +{ + BATTERYPARAMSPLOT_Save(plot, this, "estimation_quality"); +} diff --git a/Source/Windows/BatteryParams/batteryfitqualitywnd.h b/Source/Windows/BatteryParams/batteryfitqualitywnd.h new file mode 100644 index 0000000..24eee84 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryfitqualitywnd.h @@ -0,0 +1,59 @@ +#ifndef BATTERYFITQUALITYWND_H +#define BATTERYFITQUALITYWND_H + +#include +#include +#include +#include + +#include "Chart/qcustomplot.h" +#include "batteryparamsplotsettings.h" + +typedef struct +{ + unsigned int index; + bool firstOrderValid; + double firstOrderRmse; + double firstOrderMaxError; + bool secondOrderValid; + double secondOrderRmse; + double secondOrderMaxError; +}batteryfitquality_cycle_t; + +class BatteryFitQualityWnd : public QWidget +{ + Q_OBJECT + +public: + explicit BatteryFitQualityWnd(QWidget *parent = nullptr); + + void setData(QVector cycles); + void setPlotSettings(batteryparams_plot_settings_t settings); + +private slots: + void onSeriesToggled(); + void onSaveImage(); + +private: + QCustomPlot *plot; + QCPBars *firstOrderRmseBars; + QCPBars *firstOrderMaxBars; + QCPBars *secondOrderRmseBars; + QCPBars *secondOrderMaxBars; + QCPBarsGroup *barsGroup; + + QCheckBox *firstOrderRmseCheckBox; + QCheckBox *firstOrderMaxCheckBox; + QCheckBox *secondOrderRmseCheckBox; + QCheckBox *secondOrderMaxCheckBox; + QLabel *summaryLabel; + + QVector cycles; + batteryparams_plot_settings_t plotSettings; + + QCPBars* barsCreate(QString name, QColor color); + void updateBars(); + void updateSummary(); +}; + +#endif // BATTERYFITQUALITYWND_H diff --git a/Source/Windows/BatteryParams/batteryocvanalysiswnd.cpp b/Source/Windows/BatteryParams/batteryocvanalysiswnd.cpp new file mode 100644 index 0000000..0853b67 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryocvanalysiswnd.cpp @@ -0,0 +1,647 @@ +#include "batteryocvanalysiswnd.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define BATTERYOCV_EDIT_WIDTH 70 +#define BATTERYOCV_ROW_HEIGHT 26 + +static QTableWidgetItem* prvBATTERYOCV_ItemCreate(QString text) +{ + QTableWidgetItem *item = new QTableWidgetItem(text); + + item->setTextAlignment(Qt::AlignCenter); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + + return item; +} + +BatteryOcvAnalysisWnd::BatteryOcvAnalysisWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QHBoxLayout *mainLayout = new QHBoxLayout(this); + QVBoxLayout *controlLayout = new QVBoxLayout(); + QGroupBox *degreeGroup = new QGroupBox("Polynomial degrees", this); + QVBoxLayout *degreeGroupLayout = new QVBoxLayout(degreeGroup); + QGroupBox *noiseGroup = new QGroupBox("Noise sensitivity", this); + QVBoxLayout *noiseGroupLayout = new QVBoxLayout(noiseGroup); + QHBoxLayout *rangeLayout = new QHBoxLayout(); + QHBoxLayout *noiseLayout = new QHBoxLayout(); + QPushButton *saveButton = new QPushButton("Save image", this); + QStringList degreeHeader; + + setFont(defaultFont); + setWindowTitle("OCV analysis"); + + tabWidget = new QTabWidget(this); + + ocvPlot = new QCustomPlot(this); + ocvPlot->setInteraction(QCP::iRangeDrag, true); + ocvPlot->setInteraction(QCP::iRangeZoom, true); + ocvPlot->legend->setVisible(true); + ocvPlot->legend->setFont(defaultFont); + ocvPlot->xAxis->setLabel("SoC [%]"); + ocvPlot->yAxis->setLabel("OCV [V]"); + tabWidget->addTab(ocvPlot, "OCV(SoC)"); + + sensitivityPlot = new QCustomPlot(this); + sensitivityPlot->setInteraction(QCP::iRangeDrag, true); + sensitivityPlot->setInteraction(QCP::iRangeZoom, true); + sensitivityPlot->legend->setVisible(true); + sensitivityPlot->legend->setFont(defaultFont); + sensitivityPlot->xAxis->setLabel("SoC [%]"); + sensitivityPlot->yAxis->setLabel("dSoC/dV [%/V]"); + tabWidget->addTab(sensitivityPlot, "Sensitivity dSoC/dV"); + + errorPlot = new QCustomPlot(this); + errorPlot->setInteraction(QCP::iRangeDrag, true); + errorPlot->setInteraction(QCP::iRangeZoom, true); + errorPlot->legend->setVisible(true); + errorPlot->legend->setFont(defaultFont); + errorPlot->xAxis->setLabel("Polynomial degree"); + errorPlot->yAxis->setLabel("Fit error [mV]"); + tabWidget->addTab(errorPlot, "Fit error"); + + degreeMinEdit = new QLineEdit(QString::number(BATTERYOCV_DEGREE_MIN_DEFAULT), this); + degreeMinEdit->setFixedSize(BATTERYOCV_EDIT_WIDTH, BATTERYOCV_ROW_HEIGHT); + degreeMaxEdit = new QLineEdit(QString::number(BATTERYOCV_DEGREE_MAX_DEFAULT), this); + degreeMaxEdit->setFixedSize(BATTERYOCV_EDIT_WIDTH, BATTERYOCV_ROW_HEIGHT); + + rangeLayout->addWidget(new QLabel("From", this)); + rangeLayout->addWidget(degreeMinEdit); + rangeLayout->addWidget(new QLabel("to", this)); + rangeLayout->addWidget(degreeMaxEdit); + rangeLayout->addStretch(); + degreeGroupLayout->addLayout(rangeLayout); + + degreeHeader << "Draw" << "Degree" << "RMSE [mV]" << "Mean [mV]" << "Std [mV]"; + degreeTable = new QTableWidget(0, 5, this); + degreeTable->setHorizontalHeaderLabels(degreeHeader); + degreeTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + degreeTable->verticalHeader()->setVisible(false); + degreeTable->verticalHeader()->setDefaultSectionSize(20); + degreeTable->setSelectionBehavior(QAbstractItemView::SelectRows); + degreeTable->setMinimumHeight(320); + degreeTable->setMinimumWidth(330); + degreeTable->setToolTip("Fit error of every polynomial degree.\nTick a degree to draw it next to the measured curve"); + degreeGroupLayout->addWidget(degreeTable, 1); + + measuredCheckBox = new QCheckBox("Draw measured points", this); + measuredCheckBox->setChecked(true); + degreeGroupLayout->addWidget(measuredCheckBox); + + noiseEdit = new QLineEdit(QString::number(BATTERYOCV_NOISE_DEFAULT), this); + noiseEdit->setFixedSize(BATTERYOCV_EDIT_WIDTH, BATTERYOCV_ROW_HEIGHT); + noiseLayout->addWidget(new QLabel("Voltage noise [mV]", this)); + noiseLayout->addStretch(); + noiseLayout->addWidget(noiseEdit); + noiseGroupLayout->addLayout(noiseLayout); + + summaryLabel = new QLabel(this); + summaryLabel->setWordWrap(true); + summaryLabel->setMinimumWidth(300); + noiseGroupLayout->addWidget(summaryLabel); + + controlLayout->addWidget(degreeGroup, 1); + controlLayout->addWidget(noiseGroup); + controlLayout->addWidget(saveButton); + + mainLayout->addWidget(tabWidget, 1); + mainLayout->addLayout(controlLayout); + + connect(degreeMinEdit, &QLineEdit::editingFinished, this, &BatteryOcvAnalysisWnd::onDegreeRangeChanged); + connect(degreeMaxEdit, &QLineEdit::editingFinished, this, &BatteryOcvAnalysisWnd::onDegreeRangeChanged); + connect(degreeTable, &QTableWidget::itemChanged, this, &BatteryOcvAnalysisWnd::onSelectionChanged); + connect(measuredCheckBox, &QCheckBox::toggled, this, &BatteryOcvAnalysisWnd::onSelectionChanged); + connect(noiseEdit, &QLineEdit::editingFinished, this, &BatteryOcvAnalysisWnd::onNoiseChanged); + connect(saveButton, &QPushButton::clicked, this, &BatteryOcvAnalysisWnd::onSaveImage); + + plotSettings = BATTERYPARAMSPLOT_SettingsDefault(); + socOffset = 0; + socScale = 1; + tableUpdating = false; + + resize(1250, 720); +} + +double BatteryOcvAnalysisWnd::noiseGet() +{ + double noise = noiseEdit->text().toDouble(); + + return (noise > 0) ? noise : 0; +} + +QColor BatteryOcvAnalysisWnd::degreeColor(int index) +{ + static const QColor colors[] = { + QColor(200, 60, 20), QColor(30, 110, 200), QColor(30, 150, 60), + QColor(120, 60, 180), QColor(210, 150, 20), QColor(20, 150, 170) + }; + + return colors[index % 6]; +} + +/*OCV is fitted as a function of the state of charge, the same way the reference + scripts do it. SoC is normalised first, otherwise the normal equations of a + twentieth order polynomial are hopeless*/ +bool BatteryOcvAnalysisWnd::polynomialFit(int degree, QVector *coefficients) +{ + int size = degree + 1; + QVector> matrix(size, QVector(size + 1, 0)); + + coefficients->clear(); + + if(measuredSoc.size() < size + 1) return false; + + for(int i = 0; i < measuredSoc.size(); i++) + { + QVector basis(size); + double normalized = (measuredSoc[i] - socOffset) / socScale; + + basis[0] = 1.0; + for(int power = 1; power < size; power++) basis[power] = basis[power - 1] * normalized; + + for(int row = 0; row < size; row++) + { + for(int column = 0; column < size; column++) + { + matrix[row][column] += basis[row] * basis[column]; + } + matrix[row][size] += basis[row] * measuredOcv[i]; + } + } + + for(int column = 0; column < size; column++) + { + int pivot = column; + + for(int row = column + 1; row < size; row++) + { + if(fabs(matrix[row][column]) > fabs(matrix[pivot][column])) pivot = row; + } + + if(fabs(matrix[pivot][column]) < 1e-18) return false; + + matrix.swapItemsAt(column, pivot); + + for(int row = 0; row < size; row++) + { + double factor; + + if(row == column) continue; + + factor = matrix[row][column] / matrix[column][column]; + + for(int i = column; i < size + 1; i++) + { + matrix[row][i] -= factor * matrix[column][i]; + } + } + } + + for(int row = 0; row < size; row++) + { + coefficients->append(matrix[row][size] / matrix[row][row]); + } + + return true; +} + +double BatteryOcvAnalysisWnd::polynomialValue(const QVector &coefficients, double soc) +{ + double normalized = (soc - socOffset) / socScale; + double power = 1.0; + double value = 0; + + for(int i = 0; i < coefficients.size(); i++) + { + value += coefficients[i] * power; + power *= normalized; + } + + return value; +} + +/*dV/dSoC in volts per percent*/ +double BatteryOcvAnalysisWnd::polynomialSlope(const QVector &coefficients, double soc) +{ + double normalized = (soc - socOffset) / socScale; + double power = 1.0; + double value = 0; + + for(int i = 1; i < coefficients.size(); i++) + { + value += i * coefficients[i] * power; + power *= normalized; + } + + return value / socScale; +} + +void BatteryOcvAnalysisWnd::setData(QVector soc, QVector ocv) +{ + QVector order; + + measuredSoc.clear(); + measuredOcv.clear(); + + for(int i = 0; i < soc.size() && i < ocv.size(); i++) order.append(i); + + std::sort(order.begin(), order.end(), [&](int a, int b){ return soc[a] < soc[b]; }); + + for(int i = 0; i < order.size(); i++) + { + measuredSoc.append(soc[order[i]]); + measuredOcv.append(ocv[order[i]]); + } + + socOffset = 0; + socScale = 0; + + for(int i = 0; i < measuredSoc.size(); i++) socOffset += measuredSoc[i]; + if(!measuredSoc.isEmpty()) socOffset /= measuredSoc.size(); + + for(int i = 0; i < measuredSoc.size(); i++) + { + if(fabs(measuredSoc[i] - socOffset) > socScale) socScale = fabs(measuredSoc[i] - socOffset); + } + + if(socScale <= 0) socScale = 1; + + buildFits(); + updateAll(); +} + +void BatteryOcvAnalysisWnd::buildFits() +{ + int minimumDegree = degreeMinEdit->text().toInt(); + int maximumDegree = degreeMaxEdit->text().toInt(); + int bestRow = -1; + double bestRmse = -1; + + if(minimumDegree < 1) minimumDegree = 1; + if(maximumDegree > BATTERYOCV_DEGREE_LIMIT) maximumDegree = BATTERYOCV_DEGREE_LIMIT; + if(maximumDegree > measuredSoc.size() - 2) maximumDegree = measuredSoc.size() - 2; + if(maximumDegree < minimumDegree) maximumDegree = minimumDegree; + + degreeMinEdit->setText(QString::number(minimumDegree)); + degreeMaxEdit->setText(QString::number(maximumDegree)); + + fits.clear(); + tableUpdating = true; + degreeTable->setRowCount(0); + + for(int degree = minimumDegree; degree <= maximumDegree; degree++) + { + batteryocv_fit_t fit; + double squareSum = 0; + double absoluteSum = 0; + double errorSum = 0; + int row; + + fit.degree = degree; + fit.valid = polynomialFit(degree, &fit.coefficients); + fit.rmse = 0; + fit.meanError = 0; + fit.standardDeviation = 0; + + if(!fit.valid) continue; + + for(int i = 0; i < measuredSoc.size(); i++) + { + double error = polynomialValue(fit.coefficients, measuredSoc[i]) - measuredOcv[i]; + + squareSum += error * error; + absoluteSum += fabs(error); + errorSum += error; + } + + fit.rmse = sqrt(squareSum / measuredSoc.size()) * 1000.0; + fit.meanError = absoluteSum / measuredSoc.size() * 1000.0; + fit.standardDeviation = sqrt(squareSum / measuredSoc.size() - + (errorSum / measuredSoc.size()) * (errorSum / measuredSoc.size())) * 1000.0; + + fits.append(fit); + + row = degreeTable->rowCount(); + degreeTable->insertRow(row); + + QTableWidgetItem *drawItem = new QTableWidgetItem(); + drawItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable); + drawItem->setCheckState(Qt::Unchecked); + degreeTable->setItem(row, 0, drawItem); + degreeTable->setItem(row, 1, prvBATTERYOCV_ItemCreate(QString::number(degree))); + degreeTable->setItem(row, 2, prvBATTERYOCV_ItemCreate(QString::number(fit.rmse, 'f', 3))); + degreeTable->setItem(row, 3, prvBATTERYOCV_ItemCreate(QString::number(fit.meanError, 'f', 3))); + degreeTable->setItem(row, 4, prvBATTERYOCV_ItemCreate(QString::number(fit.standardDeviation, 'f', 3))); + + if((bestRmse < 0) || (fit.rmse < bestRmse)) + { + bestRmse = fit.rmse; + bestRow = row; + } + } + + /*The degree with the smallest error starts ticked, everything else is left + to the user*/ + if(bestRow >= 0) + { + degreeTable->item(bestRow, 0)->setCheckState(Qt::Checked); + } + + tableUpdating = false; +} + +void BatteryOcvAnalysisWnd::updateOcvPlot() +{ + int colorIndex = 0; + + ocvPlot->clearGraphs(); + + if(measuredCheckBox->isChecked()) + { + QCPGraph *measuredGraph = ocvPlot->addGraph(); + + measuredGraph->setName("Measured"); + measuredGraph->setPen(QPen(QColor(60, 60, 60), 2)); + measuredGraph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(60, 60, 60), + QColor(60, 60, 60), 7)); + measuredGraph->setData(measuredSoc, measuredOcv, true); + } + + for(int i = 0; i < fits.size() && i < degreeTable->rowCount(); i++) + { + QVector curveSoc; + QVector curveOcv; + QCPGraph *graph; + + if(degreeTable->item(i, 0)->checkState() != Qt::Checked) continue; + + for(int point = 0; point < BATTERYOCV_CURVE_POINTS; point++) + { + double soc = measuredSoc.first() + (measuredSoc.last() - measuredSoc.first()) * + (double)point / (BATTERYOCV_CURVE_POINTS - 1); + + curveSoc.append(soc); + curveOcv.append(polynomialValue(fits[i].coefficients, soc)); + } + + graph = ocvPlot->addGraph(); + graph->setName("Degree " + QString::number(fits[i].degree)); + graph->setPen(QPen(degreeColor(colorIndex), 2, Qt::DashLine)); + graph->setData(curveSoc, curveOcv, true); + colorIndex++; + } + + ocvPlot->rescaleAxes(); + BATTERYPARAMSPLOT_Apply(ocvPlot, plotSettings); +} + +void BatteryOcvAnalysisWnd::updateSensitivityPlot() +{ + int colorIndex = 0; + QVector magnitudes; + + sensitivityPlot->clearGraphs(); + + /*Sensitivity of the measured points, taken between neighbouring cycles*/ + if(measuredCheckBox->isChecked() && (measuredSoc.size() > 1)) + { + QVector rawSoc; + QVector rawSensitivity; + QCPGraph *graph; + + for(int i = 0; i < measuredSoc.size() - 1; i++) + { + double deltaVoltage = measuredOcv[i + 1] - measuredOcv[i]; + double deltaSoc = measuredSoc[i + 1] - measuredSoc[i]; + + if(fabs(deltaVoltage) < 1e-9) continue; + + rawSoc.append((measuredSoc[i] + measuredSoc[i + 1]) / 2.0); + rawSensitivity.append(deltaSoc / deltaVoltage); + magnitudes.append(fabs(deltaSoc / deltaVoltage)); + } + + graph = sensitivityPlot->addGraph(); + graph->setName("Measured"); + graph->setPen(QPen(QColor(60, 60, 60), 2)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(60, 60, 60), + QColor(60, 60, 60), 6)); + graph->setData(rawSoc, rawSensitivity, true); + } + + for(int i = 0; i < fits.size() && i < degreeTable->rowCount(); i++) + { + QVector curveSoc; + QVector curveSensitivity; + QCPGraph *graph; + + if(degreeTable->item(i, 0)->checkState() != Qt::Checked) continue; + + for(int point = 0; point < BATTERYOCV_CURVE_POINTS; point++) + { + double soc = measuredSoc.first() + (measuredSoc.last() - measuredSoc.first()) * + (double)point / (BATTERYOCV_CURVE_POINTS - 1); + double slope = polynomialSlope(fits[i].coefficients, soc); + + if(fabs(slope) < 1e-9) continue; + + curveSoc.append(soc); + curveSensitivity.append(1.0 / slope); + magnitudes.append(fabs(1.0 / slope)); + } + + graph = sensitivityPlot->addGraph(); + graph->setName("Degree " + QString::number(fits[i].degree)); + graph->setPen(QPen(degreeColor(colorIndex), 2, Qt::DashLine)); + graph->setData(curveSoc, curveSensitivity, true); + colorIndex++; + } + + sensitivityPlot->rescaleAxes(); + + /*Where the fitted curve turns, its slope passes through zero and the + sensitivity goes to infinity. Those spikes would flatten everything else, + so the visible range follows the bulk of the values and the spikes are left + to be reached by zooming out*/ + if(!magnitudes.isEmpty()) + { + double limit; + + std::sort(magnitudes.begin(), magnitudes.end()); + limit = magnitudes[(int)(magnitudes.size() * BATTERYOCV_SENSITIVITY_PERCENTILE)] * BATTERYOCV_SENSITIVITY_MARGIN; + + if(limit > 0) sensitivityPlot->yAxis->setRange(-limit, limit); + } + + BATTERYPARAMSPLOT_Apply(sensitivityPlot, plotSettings); +} + +void BatteryOcvAnalysisWnd::updateErrorPlot() +{ + QVector degrees; + QVector rmse; + QVector meanError; + QVector standardDeviation; + QCPGraph *graph; + + errorPlot->clearGraphs(); + + for(int i = 0; i < fits.size(); i++) + { + degrees.append(fits[i].degree); + rmse.append(fits[i].rmse); + meanError.append(fits[i].meanError); + standardDeviation.append(fits[i].standardDeviation); + } + + graph = errorPlot->addGraph(); + graph->setName("RMSE"); + graph->setPen(QPen(QColor(200, 60, 20), 2)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(200, 60, 20), QColor(200, 60, 20), 6)); + graph->setData(degrees, rmse, true); + + graph = errorPlot->addGraph(); + graph->setName("Mean absolute error"); + graph->setPen(QPen(QColor(30, 110, 200), 2)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(30, 110, 200), QColor(30, 110, 200), 6)); + graph->setData(degrees, meanError, true); + + graph = errorPlot->addGraph(); + graph->setName("Standard deviation"); + graph->setPen(QPen(QColor(30, 150, 60), 2)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(30, 150, 60), QColor(30, 150, 60), 6)); + graph->setData(degrees, standardDeviation, true); + + errorPlot->rescaleAxes(); + BATTERYPARAMSPLOT_Apply(errorPlot, plotSettings); +} + +void BatteryOcvAnalysisWnd::updateSummary() +{ + double noise = noiseGet(); + int selected = -1; + + for(int i = 0; i < fits.size() && i < degreeTable->rowCount(); i++) + { + if(degreeTable->item(i, 0)->checkState() != Qt::Checked) continue; + selected = i; + break; + } + + if(selected < 0) + { + summaryLabel->setText("Tick a polynomial degree to see how the voltage noise translates into a state of charge error."); + return; + } + + double sensitivityMean = 0; + double sensitivityMax = 0; + double sensitivityMaxSoc = 0; + QVector magnitudes; + int pointNo = 0; + + for(int point = 0; point < BATTERYOCV_CURVE_POINTS; point++) + { + double soc = measuredSoc.first() + (measuredSoc.last() - measuredSoc.first()) * + (double)point / (BATTERYOCV_CURVE_POINTS - 1); + double slope = polynomialSlope(fits[selected].coefficients, soc); + double sensitivity; + + if(fabs(slope) < 1e-9) continue; + + sensitivity = fabs(1.0 / slope); + + magnitudes.append(sensitivity); + sensitivityMean += sensitivity; + pointNo++; + + if(sensitivity <= sensitivityMax) continue; + + sensitivityMax = sensitivity; + sensitivityMaxSoc = soc; + } + + if(pointNo == 0) + { + summaryLabel->setText("Sensitivity could not be evaluated for the selected degree."); + return; + } + + /*Average is taken over the bulk of the curve for the same reason the plot + limits its range: a single turning point would otherwise dominate it*/ + std::sort(magnitudes.begin(), magnitudes.end()); + sensitivityMean = magnitudes[magnitudes.size() / 2]; + + summaryLabel->setText(QString("Degree %1 used for the numbers below.\n\n" + "Sensitivity dSoC/dV:\n" + " median %2 %/V\n" + " worst %3 %/V at %4 % SoC\n\n" + "With %5 mV of voltage noise:\n" + " typical SoC error %6 %\n" + " worst SoC error %7 %\n\n" + "The flatter the OCV curve, the larger the SoC error the same voltage " + "noise produces.") + .arg(fits[selected].degree) + .arg(sensitivityMean, 0, 'f', 2) + .arg(sensitivityMax, 0, 'f', 2) + .arg(sensitivityMaxSoc, 0, 'f', 1) + .arg(noise) + .arg(sensitivityMean * noise / 1000.0, 0, 'f', 2) + .arg(sensitivityMax * noise / 1000.0, 0, 'f', 2)); +} + +void BatteryOcvAnalysisWnd::setPlotSettings(batteryparams_plot_settings_t settings) +{ + plotSettings = settings; + + BATTERYPARAMSPLOT_Apply(ocvPlot, plotSettings); + BATTERYPARAMSPLOT_Apply(sensitivityPlot, plotSettings); + BATTERYPARAMSPLOT_Apply(errorPlot, plotSettings); +} + +void BatteryOcvAnalysisWnd::updateAll() +{ + updateOcvPlot(); + updateSensitivityPlot(); + updateErrorPlot(); + updateSummary(); +} + +void BatteryOcvAnalysisWnd::onDegreeRangeChanged() +{ + buildFits(); + updateAll(); +} + +void BatteryOcvAnalysisWnd::onSelectionChanged() +{ + if(tableUpdating) return; + + updateOcvPlot(); + updateSensitivityPlot(); + updateSummary(); +} + +void BatteryOcvAnalysisWnd::onNoiseChanged() +{ + updateSummary(); +} + +void BatteryOcvAnalysisWnd::onSaveImage() +{ + QCustomPlot *plot = qobject_cast(tabWidget->currentWidget()); + + if(plot == NULL) return; + + BATTERYPARAMSPLOT_Save(plot, this, + tabWidget->tabText(tabWidget->currentIndex()).toLower().replace(' ', '_').replace('/', '_')); +} diff --git a/Source/Windows/BatteryParams/batteryocvanalysiswnd.h b/Source/Windows/BatteryParams/batteryocvanalysiswnd.h new file mode 100644 index 0000000..4461248 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryocvanalysiswnd.h @@ -0,0 +1,85 @@ +#ifndef BATTERYOCVANALYSISWND_H +#define BATTERYOCVANALYSISWND_H + +#include +#include +#include +#include +#include +#include +#include + +#include "Chart/qcustomplot.h" +#include "batteryparamsplotsettings.h" + +#define BATTERYOCV_DEGREE_MIN_DEFAULT 2 +#define BATTERYOCV_DEGREE_MAX_DEFAULT 20 +#define BATTERYOCV_DEGREE_LIMIT 20 +#define BATTERYOCV_NOISE_DEFAULT 5.0 +#define BATTERYOCV_CURVE_POINTS 400 +#define BATTERYOCV_SENSITIVITY_PERCENTILE 0.95 +#define BATTERYOCV_SENSITIVITY_MARGIN 1.4 + +typedef struct +{ + int degree; + QVector coefficients; + double rmse; + double meanError; + double standardDeviation; + bool valid; +}batteryocv_fit_t; + +class BatteryOcvAnalysisWnd : public QWidget +{ + Q_OBJECT + +public: + explicit BatteryOcvAnalysisWnd(QWidget *parent = nullptr); + + void setData(QVector soc, QVector ocv); + void setPlotSettings(batteryparams_plot_settings_t settings); + +private slots: + void onDegreeRangeChanged(); + void onSelectionChanged(); + void onNoiseChanged(); + void onSaveImage(); + +private: + QTabWidget *tabWidget; + QCustomPlot *ocvPlot; + QCustomPlot *sensitivityPlot; + QCustomPlot *errorPlot; + + QLineEdit *degreeMinEdit; + QLineEdit *degreeMaxEdit; + QLineEdit *noiseEdit; + QTableWidget *degreeTable; + QCheckBox *measuredCheckBox; + QLabel *summaryLabel; + + QVector measuredSoc; + QVector measuredOcv; + QVector fits; + + batteryparams_plot_settings_t plotSettings; + double socOffset; + double socScale; + bool tableUpdating; + + bool polynomialFit(int degree, QVector *coefficients); + double polynomialValue(const QVector &coefficients, double soc); + double polynomialSlope(const QVector &coefficients, double soc); + QColor degreeColor(int index); + + void buildFits(); + void updateOcvPlot(); + void updateSensitivityPlot(); + void updateErrorPlot(); + void updateSummary(); + void updateAll(); + double noiseGet(); +}; + +#endif // BATTERYOCVANALYSISWND_H diff --git a/Source/Windows/BatteryParams/batteryparamsbarwnd.cpp b/Source/Windows/BatteryParams/batteryparamsbarwnd.cpp new file mode 100644 index 0000000..d4e668a --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamsbarwnd.cpp @@ -0,0 +1,113 @@ +#include "batteryparamsbarwnd.h" + +#include +#include +#include +#include +#include + +#define BATTERYPARAMSBAR_WIDTH 0.6 + +BatteryParamsBarWnd::BatteryParamsBarWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QHBoxLayout *bottomLayout = new QHBoxLayout(); + QPushButton *saveButton = new QPushButton("Save image", this); + + setFont(defaultFont); + setWindowTitle("Battery parameters"); + + plotSettings = BATTERYPARAMSPLOT_SettingsDefault(); + + plot = new QCustomPlot(this); + plot->setInteraction(QCP::iRangeDrag, true); + plot->setInteraction(QCP::iRangeZoom, true); + plot->legend->setVisible(false); + plot->xAxis->setLabel("Cycle"); + + bars = new QCPBars(plot->xAxis, plot->yAxis); + bars->setWidth(BATTERYPARAMSBAR_WIDTH); + + summaryLabel = new QLabel(this); + summaryLabel->setStyleSheet("color: gray;"); + + bottomLayout->addWidget(summaryLabel, 1); + bottomLayout->addWidget(saveButton); + + mainLayout->addWidget(plot, 1); + mainLayout->addLayout(bottomLayout); + + connect(saveButton, &QPushButton::clicked, this, &BatteryParamsBarWnd::onSaveImage); + + resize(1000, 550); +} + +void BatteryParamsBarWnd::setPlotSettings(batteryparams_plot_settings_t settings) +{ + plotSettings = settings; + BATTERYPARAMSPLOT_Apply(plot, plotSettings); + + /*A single series needs no legend*/ + plot->legend->setVisible(false); + plot->replot(); +} + +void BatteryParamsBarWnd::setData(QString title, QString yLabel, QColor color, QVector aBars) +{ + QVector keys; + QVector values; + QSharedPointer ticker(new QCPAxisTickerFixed); + double sum = 0; + double minimum = 0; + double maximum = 0; + + setWindowTitle(title); + plot->yAxis->setLabel(yLabel); + + bars->setPen(QPen(color.darker(130))); + bars->setBrush(QBrush(color)); + + for(int i = 0; i < aBars.size(); i++) + { + keys.append(aBars[i].index); + values.append(aBars[i].value); + + sum += aBars[i].value; + if((i == 0) || (aBars[i].value < minimum)) minimum = aBars[i].value; + if((i == 0) || (aBars[i].value > maximum)) maximum = aBars[i].value; + } + + bars->setData(keys, values, true); + + ticker->setTickStep(1.0); + ticker->setScaleStrategy(QCPAxisTickerFixed::ssNone); + plot->xAxis->setTicker(ticker); + + plot->rescaleAxes(); + plot->yAxis->setRangeLower(0); + plot->xAxis->setRange(plot->xAxis->range().lower - 0.5, plot->xAxis->range().upper + 0.5); + + if(aBars.isEmpty()) + { + summaryLabel->setText("Nothing to show"); + } + else + { + summaryLabel->setText(QString("%1 cycles, average %2, shortest %3, longest %4") + .arg(aBars.size()) + .arg(sum / aBars.size(), 0, 'f', 1) + .arg(minimum, 0, 'f', 1) + .arg(maximum, 0, 'f', 1)); + } + + BATTERYPARAMSPLOT_Apply(plot, plotSettings); + plot->legend->setVisible(false); + plot->replot(); +} + +void BatteryParamsBarWnd::onSaveImage() +{ + BATTERYPARAMSPLOT_Save(plot, this, windowTitle().toLower().replace(' ', '_')); +} diff --git a/Source/Windows/BatteryParams/batteryparamsbarwnd.h b/Source/Windows/BatteryParams/batteryparamsbarwnd.h new file mode 100644 index 0000000..2aa4f56 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamsbarwnd.h @@ -0,0 +1,38 @@ +#ifndef BATTERYPARAMSBARWND_H +#define BATTERYPARAMSBARWND_H + +#include +#include +#include + +#include "Chart/qcustomplot.h" +#include "batteryparamsplotsettings.h" + +typedef struct +{ + unsigned int index; + double value; +}batteryparams_bar_t; + +class BatteryParamsBarWnd : public QWidget +{ + Q_OBJECT + +public: + explicit BatteryParamsBarWnd(QWidget *parent = nullptr); + + void setData(QString title, QString yLabel, QColor color, QVector bars); + void setPlotSettings(batteryparams_plot_settings_t settings); + +private slots: + void onSaveImage(); + +private: + QCustomPlot *plot; + QCPBars *bars; + QLabel *summaryLabel; + + batteryparams_plot_settings_t plotSettings; +}; + +#endif // BATTERYPARAMSBARWND_H diff --git a/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp b/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp new file mode 100644 index 0000000..534af8f --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp @@ -0,0 +1,116 @@ +#include "batteryparamsplotsettings.h" + +#include +#include +#include + +batteryparams_plot_settings_t BATTERYPARAMSPLOT_SettingsDefault() +{ + batteryparams_plot_settings_t defaults; + + defaults.fontFamily = BATTERYPARAMSPLOT_FONT_DEFAULT; + defaults.legendFontFamily = BATTERYPARAMSPLOT_FONT_DEFAULT; + defaults.labelFontSize = BATTERYPARAMSPLOT_LABEL_SIZE_DEFAULT; + defaults.tickFontSize = BATTERYPARAMSPLOT_TICK_SIZE_DEFAULT; + defaults.legendFontSize = BATTERYPARAMSPLOT_LEGEND_SIZE_DEFAULT; + defaults.lineWidth = BATTERYPARAMSPLOT_LINE_WIDTH_DEFAULT; + defaults.markerSize = BATTERYPARAMSPLOT_MARKER_SIZE_DEFAULT; + defaults.gridVisible = true; + defaults.minorGridVisible = false; + defaults.legendVisible = true; + defaults.legendPosition = BATTERYPARAMSPLOT_LEGEND_RIGHT; + + return defaults; +} + +/*Applied after the graphs are built, so every analysis window shares the same look*/ +void BATTERYPARAMSPLOT_Apply(QCustomPlot *plot, batteryparams_plot_settings_t settings) +{ + QFont labelFont(settings.fontFamily, settings.labelFontSize); + QFont tickFont(settings.fontFamily, settings.tickFontSize); + QFont legendFont(settings.legendFontFamily, settings.legendFontSize); + QCPAxis *axes[3]; + int axisNo = 2; + + if(plot == NULL) return; + + axes[0] = plot->xAxis; + axes[1] = plot->yAxis; + + if(plot->yAxis2->visible()) + { + axes[2] = plot->yAxis2; + axisNo = 3; + } + + for(int i = 0; i < axisNo; i++) + { + axes[i]->setLabelFont(labelFont); + axes[i]->setTickLabelFont(tickFont); + axes[i]->grid()->setVisible(settings.gridVisible && (axes[i] != plot->yAxis2)); + axes[i]->grid()->setSubGridVisible(settings.minorGridVisible && (axes[i] != plot->yAxis2)); + axes[i]->setSubTicks(settings.minorGridVisible); + } + + plot->legend->setFont(legendFont); + plot->legend->setVisible(settings.legendVisible); + plot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignTop | + ((settings.legendPosition == BATTERYPARAMSPLOT_LEGEND_LEFT) ? Qt::AlignLeft : Qt::AlignRight)); + + for(int i = 0; i < plot->graphCount(); i++) + { + QPen pen = plot->graph(i)->pen(); + QCPScatterStyle scatter = plot->graph(i)->scatterStyle(); + + pen.setWidth(settings.lineWidth); + plot->graph(i)->setPen(pen); + + if(scatter.shape() == QCPScatterStyle::ssNone) continue; + + scatter.setSize(settings.markerSize); + plot->graph(i)->setScatterStyle(scatter); + } + + plot->replot(); +} + +void BATTERYPARAMSPLOT_Save(QCustomPlot *plot, QWidget *parent, QString suggestedName) +{ + QString selectedFilter; + QString path; + + if(plot == NULL) return; + + path = QFileDialog::getSaveFileName(parent, "Save plot", suggestedName + ".png", + "PNG image (*.png);;SVG image (*.svg)", &selectedFilter); + + if(path.isEmpty()) return; + + if(selectedFilter.startsWith("SVG") && !path.endsWith(".svg", Qt::CaseInsensitive)) path += ".svg"; + if(selectedFilter.startsWith("PNG") && !path.endsWith(".png", Qt::CaseInsensitive)) path += ".png"; + + if(path.endsWith(".svg", Qt::CaseInsensitive)) + { + QSvgGenerator generator; + QCPPainter painter; + + generator.setFileName(path); + generator.setSize(plot->size()); + generator.setViewBox(QRect(0, 0, plot->width(), plot->height())); + generator.setTitle(parent != NULL ? parent->windowTitle() : "OpenEPT plot"); + + if(!painter.begin(&generator)) + { + QMessageBox::warning(parent, "Save plot", "Cannot save " + path); + return; + } + + painter.setMode(QCPPainter::pmVectorized); + painter.setMode(QCPPainter::pmNoCaching); + plot->toPainter(&painter, plot->width(), plot->height()); + painter.end(); + return; + } + + if(!plot->savePng(path)) QMessageBox::warning(parent, "Save plot", "Cannot save " + path); +} diff --git a/Source/Windows/BatteryParams/batteryparamsplotsettings.h b/Source/Windows/BatteryParams/batteryparamsplotsettings.h new file mode 100644 index 0000000..4c0e87d --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamsplotsettings.h @@ -0,0 +1,41 @@ +#ifndef BATTERYPARAMSPLOTSETTINGS_H +#define BATTERYPARAMSPLOTSETTINGS_H + +#include +#include + +#include "Chart/qcustomplot.h" + +#define BATTERYPARAMSPLOT_FONT_DEFAULT "Arial" +#define BATTERYPARAMSPLOT_LABEL_SIZE_DEFAULT 10 +#define BATTERYPARAMSPLOT_TICK_SIZE_DEFAULT 9 +#define BATTERYPARAMSPLOT_LEGEND_SIZE_DEFAULT 9 +#define BATTERYPARAMSPLOT_LINE_WIDTH_DEFAULT 2 +#define BATTERYPARAMSPLOT_MARKER_SIZE_DEFAULT 7 + +typedef enum +{ + BATTERYPARAMSPLOT_LEGEND_LEFT = 0, + BATTERYPARAMSPLOT_LEGEND_RIGHT +}batteryparams_legend_position_t; + +typedef struct +{ + QString fontFamily; + QString legendFontFamily; + int labelFontSize; + int tickFontSize; + int legendFontSize; + int lineWidth; + int markerSize; + bool gridVisible; + bool minorGridVisible; + bool legendVisible; + batteryparams_legend_position_t legendPosition; +}batteryparams_plot_settings_t; + +batteryparams_plot_settings_t BATTERYPARAMSPLOT_SettingsDefault(); +void BATTERYPARAMSPLOT_Apply(QCustomPlot *plot, batteryparams_plot_settings_t settings); +void BATTERYPARAMSPLOT_Save(QCustomPlot *plot, QWidget *parent, QString suggestedName); + +#endif // BATTERYPARAMSPLOTSETTINGS_H diff --git a/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp b/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp new file mode 100644 index 0000000..a0655ab --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp @@ -0,0 +1,209 @@ +#include "batteryparamssettingsdlg.h" + +#include +#include +#include +#include +#include +#include + +#define BATTERYPARAMSSETTINGS_EDIT_WIDTH 110 +#define BATTERYPARAMSSETTINGS_ROW_HEIGHT 28 + +BatteryParamsSettingsDlg::BatteryParamsSettingsDlg(QWidget *parent) : + QDialog(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QGroupBox *batteryGroup = new QGroupBox("Battery", this); + QGroupBox *relaxationGroup = new QGroupBox("Relaxation", this); + QGroupBox *extractionGroup = new QGroupBox("Extraction", this); + QGridLayout *batteryLayout = new QGridLayout(batteryGroup); + QGridLayout *relaxationLayout = new QGridLayout(relaxationGroup); + QGridLayout *extractionLayout = new QGridLayout(extractionGroup); + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + setFont(defaultFont); + setWindowTitle("Battery parameters settings"); + + capacityEdit = createEntry(batteryLayout, 0, "Battery capacity", "mAh", + "Nominal capacity, used to convert the extracted charge into state of charge"); + initialSocEdit = createEntry(batteryLayout, 1, "Initial SoC", "%", + "State of charge the battery starts the measurement with"); + + relaxationThresholdEdit = createEntry(relaxationLayout, 0, "Relaxation threshold", "mV", + "Battery counts as relaxed once the voltage stays inside this band for the whole window"); + relaxationWindowEdit = createEntry(relaxationLayout, 1, "Relaxation window", "s", + "Time the voltage has to stay inside the band"); + + QLabel *modelLabel = new QLabel("Model", extractionGroup); + modelCombo = new QComboBox(extractionGroup); + modelCombo->addItem("First order (R1 C1)"); + modelCombo->addItem("Second order (R1 C1 R2 C2)"); + modelCombo->setFixedHeight(BATTERYPARAMSSETTINGS_ROW_HEIGHT); + modelCombo->setToolTip("Equivalent circuit fitted to the relaxation curve"); + extractionLayout->addWidget(modelLabel, 0, 0); + extractionLayout->addWidget(modelCombo, 0, 1, 1, 2); + + QLabel *fitEndLabel = new QLabel("Fit interval", extractionGroup); + fitEndCombo = new QComboBox(extractionGroup); + fitEndCombo->addItem("Pause start to relaxation point"); + fitEndCombo->addItem("Pause start to pause end"); + fitEndCombo->setFixedHeight(BATTERYPARAMSSETTINGS_ROW_HEIGHT); + fitEndCombo->setToolTip("Part of the pause the RC model is fitted over.\n" + "Up to the relaxation point leaves out the tail that is still moving,\n" + "up to the end of the pause uses every sample"); + extractionLayout->addWidget(fitEndLabel, 1, 0); + extractionLayout->addWidget(fitEndCombo, 1, 1, 1, 2); + + tauGridPointsEdit = createEntry(extractionLayout, 2, "Tau search points", "", + "Number of candidate time constants tried per decade of the search range.\nMore points give a finer fit and take longer"); + tauRefinementEdit = createEntry(extractionLayout, 3, "Tau refinement steps", "", + "How many times the search range is narrowed around the best result"); + tauSeparationEdit = createEntry(extractionLayout, 4, "Tau separation", "x", + "Smallest allowed ratio between the slow and the fast time constant.\nBranches closer than this describe the same effect"); + + QTabWidget *tabWidget = new QTabWidget(this); + QWidget *analysisTab = new QWidget(this); + QVBoxLayout *analysisLayout = new QVBoxLayout(analysisTab); + QWidget *plotTab = new QWidget(this); + QGridLayout *plotLayout = new QGridLayout(plotTab); + + analysisLayout->addWidget(batteryGroup); + analysisLayout->addWidget(relaxationGroup); + analysisLayout->addWidget(extractionGroup); + analysisLayout->addStretch(); + tabWidget->addTab(analysisTab, "Analysis"); + + plotFontCombo = new QFontComboBox(plotTab); + plotFontCombo->setCurrentFont(QFont(BATTERYPARAMSPLOT_FONT_DEFAULT)); + plotFontCombo->setFixedHeight(BATTERYPARAMSSETTINGS_ROW_HEIGHT); + plotFontCombo->setToolTip("Font used for the axis labels, the ticks and the legend of the analysis plots"); + plotLayout->addWidget(new QLabel("Label font", plotTab), 0, 0); + plotLayout->addWidget(plotFontCombo, 0, 1, 1, 2); + + plotLegendFontCombo = new QFontComboBox(plotTab); + plotLegendFontCombo->setFixedHeight(BATTERYPARAMSSETTINGS_ROW_HEIGHT); + plotLegendFontCombo->setToolTip("Font used for the legend entries"); + + plotLegendPositionCombo = new QComboBox(plotTab); + plotLegendPositionCombo->addItem("Left"); + plotLegendPositionCombo->addItem("Right"); + plotLegendPositionCombo->setFixedHeight(BATTERYPARAMSSETTINGS_ROW_HEIGHT); + plotLegendPositionCombo->setToolTip("Corner the legend sits in"); + + plotLabelSizeEdit = createEntry(plotLayout, 1, "Label font size", "pt", "Size of the axis label text"); + plotTickSizeEdit = createEntry(plotLayout, 2, "Tick font size", "pt", "Size of the numbers along the axes"); + plotLegendSizeEdit = createEntry(plotLayout, 3, "Legend font size", "pt", "Size of the legend text"); + plotLineWidthEdit = createEntry(plotLayout, 4, "Line width", "px", "Thickness of the drawn curves"); + plotMarkerSizeEdit = createEntry(plotLayout, 5, "Marker size", "px", "Size of the point markers"); + + plotLayout->addWidget(new QLabel("Legend font", plotTab), 6, 0); + plotLayout->addWidget(plotLegendFontCombo, 6, 1, 1, 2); + plotLayout->addWidget(new QLabel("Legend position", plotTab), 7, 0); + plotLayout->addWidget(plotLegendPositionCombo, 7, 1, 1, 2); + + plotGridCheckBox = new QCheckBox("Show grid", plotTab); + plotMinorGridCheckBox = new QCheckBox("Show minor grid", plotTab); + plotLegendCheckBox = new QCheckBox("Show legend", plotTab); + plotLayout->addWidget(plotGridCheckBox, 8, 0, 1, 3); + plotLayout->addWidget(plotMinorGridCheckBox, 9, 0, 1, 3); + plotLayout->addWidget(plotLegendCheckBox, 10, 0, 1, 3); + plotLayout->setRowStretch(11, 1); + + tabWidget->addTab(plotTab, "Plots"); + + mainLayout->addWidget(tabWidget); + mainLayout->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &BatteryParamsSettingsDlg::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &BatteryParamsSettingsDlg::reject); + + setSettings(BatteryParamsExtraction::settingsDefault()); + setPlotSettings(BATTERYPARAMSPLOT_SettingsDefault()); +} + +QLineEdit* BatteryParamsSettingsDlg::createEntry(QGridLayout *layout, int row, QString name, QString unit, QString tooltip) +{ + QLabel *label = new QLabel(name, this); + QLineEdit *edit = new QLineEdit(this); + QLabel *unitLabel = new QLabel(unit.isEmpty() ? "" : "[" + unit + "]", this); + + edit->setFixedSize(BATTERYPARAMSSETTINGS_EDIT_WIDTH, BATTERYPARAMSSETTINGS_ROW_HEIGHT); + edit->setToolTip(tooltip); + label->setToolTip(tooltip); + + layout->addWidget(label, row, 0); + layout->addWidget(edit, row, 1); + layout->addWidget(unitLabel, row, 2); + + return edit; +} + +void BatteryParamsSettingsDlg::setSettings(batteryparams_settings_t settings) +{ + capacityEdit->setText(QString::number(settings.capacity)); + initialSocEdit->setText(QString::number(settings.initialSoc)); + relaxationThresholdEdit->setText(QString::number(settings.relaxationThreshold)); + relaxationWindowEdit->setText(QString::number(settings.relaxationWindow)); + modelCombo->setCurrentIndex(settings.model == BATTERYPARAMS_MODEL_FIRST_ORDER ? 0 : 1); + fitEndCombo->setCurrentIndex(settings.fitEnd == BATTERYPARAMS_FIT_END_RELAXATION ? 0 : 1); + tauGridPointsEdit->setText(QString::number(settings.tauGridPoints)); + tauRefinementEdit->setText(QString::number(settings.tauRefinementNo)); + tauSeparationEdit->setText(QString::number(settings.tauSeparation)); +} + +void BatteryParamsSettingsDlg::setPlotSettings(batteryparams_plot_settings_t settings) +{ + plotFontCombo->setCurrentFont(QFont(settings.fontFamily)); + plotLabelSizeEdit->setText(QString::number(settings.labelFontSize)); + plotTickSizeEdit->setText(QString::number(settings.tickFontSize)); + plotLegendSizeEdit->setText(QString::number(settings.legendFontSize)); + plotLineWidthEdit->setText(QString::number(settings.lineWidth)); + plotMarkerSizeEdit->setText(QString::number(settings.markerSize)); + plotLegendFontCombo->setCurrentFont(QFont(settings.legendFontFamily)); + plotLegendPositionCombo->setCurrentIndex(settings.legendPosition == BATTERYPARAMSPLOT_LEGEND_LEFT ? 0 : 1); + plotGridCheckBox->setChecked(settings.gridVisible); + plotMinorGridCheckBox->setChecked(settings.minorGridVisible); + plotLegendCheckBox->setChecked(settings.legendVisible); +} + +batteryparams_plot_settings_t BatteryParamsSettingsDlg::getPlotSettings() +{ + batteryparams_plot_settings_t settings = BATTERYPARAMSPLOT_SettingsDefault(); + + settings.fontFamily = plotFontCombo->currentFont().family(); + + if(plotLabelSizeEdit->text().toInt() > 0) settings.labelFontSize = plotLabelSizeEdit->text().toInt(); + if(plotTickSizeEdit->text().toInt() > 0) settings.tickFontSize = plotTickSizeEdit->text().toInt(); + if(plotLegendSizeEdit->text().toInt() > 0) settings.legendFontSize = plotLegendSizeEdit->text().toInt(); + if(plotLineWidthEdit->text().toInt() > 0) settings.lineWidth = plotLineWidthEdit->text().toInt(); + if(plotMarkerSizeEdit->text().toInt() > 0) settings.markerSize = plotMarkerSizeEdit->text().toInt(); + + settings.legendFontFamily = plotLegendFontCombo->currentFont().family(); + settings.legendPosition = (plotLegendPositionCombo->currentIndex() == 0) ? + BATTERYPARAMSPLOT_LEGEND_LEFT : BATTERYPARAMSPLOT_LEGEND_RIGHT; + settings.gridVisible = plotGridCheckBox->isChecked(); + settings.minorGridVisible = plotMinorGridCheckBox->isChecked(); + settings.legendVisible = plotLegendCheckBox->isChecked(); + + return settings; +} + +batteryparams_settings_t BatteryParamsSettingsDlg::getSettings() +{ + batteryparams_settings_t settings = BatteryParamsExtraction::settingsDefault(); + + if(capacityEdit->text().toDouble() > 0) settings.capacity = capacityEdit->text().toDouble(); + if(initialSocEdit->text().toDouble() > 0) settings.initialSoc = initialSocEdit->text().toDouble(); + if(relaxationThresholdEdit->text().toDouble() > 0) settings.relaxationThreshold = relaxationThresholdEdit->text().toDouble(); + if(relaxationWindowEdit->text().toDouble() > 0) settings.relaxationWindow = relaxationWindowEdit->text().toDouble(); + if(tauGridPointsEdit->text().toInt() > 1) settings.tauGridPoints = tauGridPointsEdit->text().toInt(); + if(tauRefinementEdit->text().toInt() > 0) settings.tauRefinementNo = tauRefinementEdit->text().toInt(); + if(tauSeparationEdit->text().toDouble() > 1) settings.tauSeparation = tauSeparationEdit->text().toDouble(); + + settings.model = (modelCombo->currentIndex() == 0) ? BATTERYPARAMS_MODEL_FIRST_ORDER : BATTERYPARAMS_MODEL_SECOND_ORDER; + settings.fitEnd = (fitEndCombo->currentIndex() == 0) ? BATTERYPARAMS_FIT_END_RELAXATION : BATTERYPARAMS_FIT_END_PAUSE; + + return settings; +} diff --git a/Source/Windows/BatteryParams/batteryparamssettingsdlg.h b/Source/Windows/BatteryParams/batteryparamssettingsdlg.h new file mode 100644 index 0000000..9d93930 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamssettingsdlg.h @@ -0,0 +1,54 @@ +#ifndef BATTERYPARAMSSETTINGSDLG_H +#define BATTERYPARAMSSETTINGSDLG_H + +#include +#include +#include +#include +#include + +#include + +#include "Processing/batteryparamsextraction.h" +#include "batteryparamsplotsettings.h" + +class BatteryParamsSettingsDlg : public QDialog +{ + Q_OBJECT + +public: + explicit BatteryParamsSettingsDlg(QWidget *parent = nullptr); + + void setSettings(batteryparams_settings_t settings); + batteryparams_settings_t getSettings(); + + void setPlotSettings(batteryparams_plot_settings_t settings); + batteryparams_plot_settings_t getPlotSettings(); + +private: + QLineEdit *capacityEdit; + QLineEdit *initialSocEdit; + QLineEdit *relaxationThresholdEdit; + QLineEdit *relaxationWindowEdit; + QComboBox *modelCombo; + QComboBox *fitEndCombo; + QLineEdit *tauGridPointsEdit; + QLineEdit *tauRefinementEdit; + QLineEdit *tauSeparationEdit; + + QFontComboBox *plotFontCombo; + QLineEdit *plotLabelSizeEdit; + QLineEdit *plotTickSizeEdit; + QLineEdit *plotLegendSizeEdit; + QLineEdit *plotLineWidthEdit; + QLineEdit *plotMarkerSizeEdit; + QFontComboBox *plotLegendFontCombo; + QComboBox *plotLegendPositionCombo; + QCheckBox *plotGridCheckBox; + QCheckBox *plotMinorGridCheckBox; + QCheckBox *plotLegendCheckBox; + + QLineEdit* createEntry(QGridLayout *layout, int row, QString name, QString unit, QString tooltip); +}; + +#endif // BATTERYPARAMSSETTINGSDLG_H diff --git a/Source/Windows/BatteryParams/batteryparamstrendwnd.cpp b/Source/Windows/BatteryParams/batteryparamstrendwnd.cpp new file mode 100644 index 0000000..3cc5648 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamstrendwnd.cpp @@ -0,0 +1,124 @@ +#include "batteryparamstrendwnd.h" + +#include +#include +#include +#include +#include + +#define BATTERYPARAMSTREND_MARKER_SIZE 7 + +BatteryParamsTrendWnd::BatteryParamsTrendWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QHBoxLayout *controlLayout = new QHBoxLayout(); + QPushButton *saveButton = new QPushButton("Save image", this); + + setFont(defaultFont); + setWindowTitle("Battery parameters trend"); + + seriesLayout = new QHBoxLayout(); + controlLayout->addLayout(seriesLayout); + controlLayout->addStretch(); + controlLayout->addWidget(saveButton); + mainLayout->addLayout(controlLayout); + + plot = new QCustomPlot(this); + plot->setInteraction(QCP::iRangeDrag, true); + plot->setInteraction(QCP::iRangeZoom, true); + plot->legend->setVisible(true); + plot->legend->setFont(defaultFont); + plot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignTop | Qt::AlignRight); + plot->xAxis->setLabel("SoC [%]"); + plot->xAxis->setRangeReversed(true); + mainLayout->addWidget(plot, 1); + + connect(saveButton, &QPushButton::clicked, this, &BatteryParamsTrendWnd::onSaveImage); + + plotSettings = BATTERYPARAMSPLOT_SettingsDefault(); + + resize(900, 550); +} + +void BatteryParamsTrendWnd::setTrends(QString title, QString yLabel, QVector trends) +{ + setWindowTitle(title); + + for(int i = 0; i < seriesCheckBoxes.size(); i++) + { + seriesLayout->removeWidget(seriesCheckBoxes[i]); + seriesCheckBoxes[i]->deleteLater(); + } + seriesCheckBoxes.clear(); + plot->clearGraphs(); + + plot->yAxis->setLabel(yLabel); + + for(int i = 0; i < trends.size(); i++) + { + QCPGraph *graph = plot->addGraph(); + QCheckBox *checkBox = new QCheckBox(trends[i].name, this); + + graph->setName(trends[i].name); + graph->setPen(QPen(trends[i].color, 2)); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, trends[i].color, + trends[i].color, BATTERYPARAMSTREND_MARKER_SIZE)); + graph->setData(trends[i].soc, trends[i].value, true); + + checkBox->setChecked(true); + checkBox->setStyleSheet("color: " + trends[i].color.name() + ";"); + seriesLayout->addWidget(checkBox); + seriesCheckBoxes.append(checkBox); + + connect(checkBox, &QCheckBox::toggled, this, &BatteryParamsTrendWnd::onSeriesToggled); + } + + rescale(); +} + +void BatteryParamsTrendWnd::setPlotSettings(batteryparams_plot_settings_t settings) +{ + plotSettings = settings; + BATTERYPARAMSPLOT_Apply(plot, plotSettings); +} + +void BatteryParamsTrendWnd::onSeriesToggled() +{ + for(int i = 0; i < seriesCheckBoxes.size() && i < plot->graphCount(); i++) + { + plot->graph(i)->setVisible(seriesCheckBoxes[i]->isChecked()); + plot->graph(i)->removeFromLegend(); + if(seriesCheckBoxes[i]->isChecked()) plot->graph(i)->addToLegend(); + } + + rescale(); +} + +void BatteryParamsTrendWnd::rescale() +{ + bool firstVisible = true; + + plot->xAxis->rescale(); + + for(int i = 0; i < plot->graphCount(); i++) + { + if(!plot->graph(i)->visible()) continue; + plot->graph(i)->rescaleValueAxis(!firstVisible); + firstVisible = false; + } + + if(!firstVisible) + { + QCPRange range = plot->yAxis->range(); + plot->yAxis->setRange(range.lower - range.size() * 0.1, range.upper + range.size() * 0.1); + } + + plot->replot(); +} + +void BatteryParamsTrendWnd::onSaveImage() +{ + BATTERYPARAMSPLOT_Save(plot, this, windowTitle().toLower().replace(' ', '_')); +} diff --git a/Source/Windows/BatteryParams/batteryparamstrendwnd.h b/Source/Windows/BatteryParams/batteryparamstrendwnd.h new file mode 100644 index 0000000..a306e3a --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamstrendwnd.h @@ -0,0 +1,44 @@ +#ifndef BATTERYPARAMSTRENDWND_H +#define BATTERYPARAMSTRENDWND_H + +#include +#include +#include +#include +#include + +#include "Chart/qcustomplot.h" +#include "batteryparamsplotsettings.h" + +typedef struct +{ + QString name; + QColor color; + QVector soc; + QVector value; +}batteryparams_trend_t; + +class BatteryParamsTrendWnd : public QWidget +{ + Q_OBJECT + +public: + explicit BatteryParamsTrendWnd(QWidget *parent = nullptr); + + void setTrends(QString title, QString yLabel, QVector trends); + void setPlotSettings(batteryparams_plot_settings_t settings); + +private slots: + void onSeriesToggled(); + void onSaveImage(); + +private: + QCustomPlot *plot; + QHBoxLayout *seriesLayout; + QVector seriesCheckBoxes; + batteryparams_plot_settings_t plotSettings; + + void rescale(); +}; + +#endif // BATTERYPARAMSTRENDWND_H diff --git a/Source/Windows/BatteryParams/batteryparamswnd.cpp b/Source/Windows/BatteryParams/batteryparamswnd.cpp new file mode 100644 index 0000000..3b5d2ed --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamswnd.cpp @@ -0,0 +1,1060 @@ +#include "batteryparamswnd.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BATTERYPARAMS_COLUMN_NO 21 +#define BATTERYPARAMS_SETTINGS_EDIT_WIDTH 80 +#define BATTERYPARAMS_SETTINGS_ROW_HEIGHT 28 +#define BATTERYPARAMS_AUTO_RESIZE_ROW_NO 10 + +BatteryParamsWnd::BatteryParamsWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QStringList header; + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QHBoxLayout *topLayout = new QHBoxLayout(); + QHBoxLayout *bottomLayout = new QHBoxLayout(); + + setFont(defaultFont); + setWindowTitle("Battery parameters"); + + statusLabel = new QLabel("Waiting for the first cycle...", this); + topLayout->addWidget(statusLabel); + topLayout->addStretch(); + mainLayout->addLayout(topLayout); + + QHBoxLayout *settingsLayout = new QHBoxLayout(); + + settings = BatteryParamsExtraction::settingsDefault(); + parallelAnswer = -1; + settingsDlg = NULL; + resistanceTrendWnd = NULL; + capacitanceTrendWnd = NULL; + ocvTrendWnd = NULL; + ocvAnalysisWnd = NULL; + fitQualityWnd = NULL; + relaxationTimeWnd = NULL; + fitIntervalWnd = NULL; + connectedToFitIntervalWnd = false; + plotSettings = BATTERYPARAMSPLOT_SettingsDefault(); + + settingsButton = new QPushButton("Settings", this); + settingsButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + settingsButton->setToolTip("Battery capacity, initial state of charge, relaxation and extraction settings"); + + resistanceTrendButton = new QPushButton("R vs SoC", this); + resistanceTrendButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + resistanceTrendButton->setToolTip("Show R0, R1 and R2 against the state of charge"); + + capacitanceTrendButton = new QPushButton("C vs SoC", this); + capacitanceTrendButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + capacitanceTrendButton->setToolTip("Show C1 and C2 against the state of charge"); + + ocvTrendButton = new QPushButton("OCV vs SoC", this); + ocvTrendButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + ocvTrendButton->setToolTip("Show the open circuit voltage against the state of charge"); + + ocvAnalysisButton = new QPushButton("OCV analysis", this); + ocvAnalysisButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + ocvAnalysisButton->setToolTip("State of charge against the open circuit voltage, its sensitivity and the effect of measurement noise"); + + fitQualityButton = new QPushButton("Estimation quality", this); + fitQualityButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + fitQualityButton->setToolTip("Compare how well the first and the second order model describe every cycle"); + + relaxationTimeButton = new QPushButton("Relaxation time", this); + relaxationTimeButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + relaxationTimeButton->setToolTip("Time every cycle needed from the beginning of the pause until the battery relaxed"); + + fitIntervalButton = new QPushButton("Fit interval", this); + fitIntervalButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + fitIntervalButton->setToolTip("Compare the parameters and the errors obtained by fitting up to the relaxation point and up to the end of the pause"); + + settingsLabel = new QLabel(this); + settingsLabel->setStyleSheet("color: gray;"); + + settingsLayout->addWidget(settingsButton); + settingsLayout->addSpacing(15); + settingsLayout->addWidget(resistanceTrendButton); + settingsLayout->addWidget(capacitanceTrendButton); + settingsLayout->addWidget(ocvTrendButton); + settingsLayout->addWidget(ocvAnalysisButton); + settingsLayout->addWidget(fitQualityButton); + settingsLayout->addWidget(relaxationTimeButton); + settingsLayout->addWidget(fitIntervalButton); + settingsLayout->addSpacing(15); + settingsLayout->addWidget(settingsLabel, 1); + mainLayout->addLayout(settingsLayout); + + connect(settingsButton, &QPushButton::clicked, this, &BatteryParamsWnd::onSettings); + connect(resistanceTrendButton, &QPushButton::clicked, this, &BatteryParamsWnd::onResistanceTrend); + connect(capacitanceTrendButton, &QPushButton::clicked, this, &BatteryParamsWnd::onCapacitanceTrend); + connect(ocvTrendButton, &QPushButton::clicked, this, &BatteryParamsWnd::onOcvTrend); + connect(ocvAnalysisButton, &QPushButton::clicked, this, &BatteryParamsWnd::onOcvAnalysis); + connect(fitQualityButton, &QPushButton::clicked, this, &BatteryParamsWnd::onFitQuality); + connect(relaxationTimeButton, &QPushButton::clicked, this, &BatteryParamsWnd::onRelaxationTime); + connect(fitIntervalButton, &QPushButton::clicked, this, &BatteryParamsWnd::onFitInterval); + + updateSettingsLabel(); + + header << "Cycle" << "Start [ms]" << "End [ms]" + << "Q [mAh]" << "Q total [mAh]" << "SoC used [%]" << "SoC [%]" << "OCV [V]" + << "V pulse end [V]" << "V pause start [V]" << "dV [mV]" + << "I pulse end [mA]" << "I pause start [mA]" << "dI [mA]" + << "R0 [mOhm]" + << "R1 [mOhm]" << "C1 [F]" << "R2 [mOhm]" << "C2 [F]" + << "RMSE [mV]" << "Max err [mV]"; + + cyclesTable = new QTableWidget(0, BATTERYPARAMS_COLUMN_NO, this); + cyclesTable->setHorizontalHeaderLabels(header); + cyclesTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + cyclesTable->horizontalHeader()->setSectionsMovable(true); + cyclesTable->horizontalHeader()->setStretchLastSection(true); + cyclesTable->horizontalHeader()->setMinimumSectionSize(40); + cyclesTable->resizeColumnsToContents(); + cyclesTable->verticalHeader()->setVisible(false); + cyclesTable->verticalHeader()->setDefaultSectionSize(22); + cyclesTable->setSelectionBehavior(QAbstractItemView::SelectRows); + cyclesTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + cyclesTable->setMinimumHeight(200); + mainLayout->addWidget(cyclesTable, 1); + + summaryLabel = new QLabel(this); + summaryLabel->setStyleSheet("color: gray;"); + bottomLayout->addWidget(summaryLabel, 1); + + exportButton = new QPushButton("Export CSV", this); + clearButton = new QPushButton("Clear", this); + bottomLayout->addWidget(exportButton); + bottomLayout->addWidget(clearButton); + mainLayout->addLayout(bottomLayout); + + cyclesTable->setToolTip("Click a cycle to show its waveform.\nColumns can be resized and reordered by dragging the header"); + + cycleWnd = NULL; + connectedToCycleWnd = false; + + connect(cyclesTable, &QTableWidget::cellClicked, this, &BatteryParamsWnd::onCycleSelected); + connect(exportButton, &QPushButton::clicked, this, &BatteryParamsWnd::onExport); + connect(clearButton, &QPushButton::clicked, this, &BatteryParamsWnd::onClear); + + resize(1000, 400); + updateSummary(); +} + +void BatteryParamsWnd::setProfileName(QString name) +{ + profileName = name; + setWindowTitle(name.isEmpty() ? "Battery parameters" : "Battery parameters - " + name); +} + +QTableWidgetItem* BatteryParamsWnd::createItem(QString text) +{ + QTableWidgetItem *item = new QTableWidgetItem(text); + item->setTextAlignment(Qt::AlignCenter); + return item; +} + +void BatteryParamsWnd::fillCycleRow(int row, batteryparams_cycle_t cycle) +{ + cyclesTable->setItem(row, 0, createItem(QString::number(cycle.index))); + cyclesTable->setItem(row, 1, createItem(QString::number(cycle.pulseStartKey, 'f', 3))); + cyclesTable->setItem(row, 2, createItem(QString::number(cycle.pauseEndKey, 'f', 3))); + cyclesTable->setItem(row, 3, createItem(QString::number(cycle.charge, 'f', 3))); + cyclesTable->setItem(row, 4, createItem(QString::number(cycle.chargeTotal, 'f', 3))); + cyclesTable->setItem(row, 5, createItem(cycle.socValid ? QString::number(cycle.socUsed, 'f', 2) : "-")); + cyclesTable->setItem(row, 6, createItem(cycle.socValid ? QString::number(cycle.soc, 'f', 2) : "-")); + cyclesTable->setItem(row, 7, createItem(cycle.relaxationReached ? QString::number(cycle.ocv, 'f', 4) : "-")); + cyclesTable->setItem(row, 8, createItem(QString::number(cycle.pulseEndVoltage, 'f', 4))); + cyclesTable->setItem(row, 9, createItem(QString::number(cycle.pauseStartVoltage, 'f', 4))); + cyclesTable->setItem(row, 10, createItem(QString::number(cycle.deltaVoltage * 1000.0, 'f', 2))); + cyclesTable->setItem(row, 11, createItem(QString::number(cycle.pulseEndCurrent, 'f', 2))); + cyclesTable->setItem(row, 12, createItem(QString::number(cycle.pauseStartCurrent, 'f', 2))); + cyclesTable->setItem(row, 13, createItem(QString::number(cycle.deltaCurrent, 'f', 2))); + cyclesTable->setItem(row, 14, createItem(cycle.resistanceValid ? QString::number(cycle.resistance * 1000.0, 'f', 2) : "-")); + cyclesTable->setItem(row, 15, createItem(cycle.modelValid ? QString::number(cycle.r1 * 1000.0, 'f', 2) : "-")); + cyclesTable->setItem(row, 16, createItem(cycle.modelValid ? QString::number(cycle.c1, 'f', 1) : "-")); + cyclesTable->setItem(row, 17, createItem((cycle.modelValid && (cycle.model == BATTERYPARAMS_MODEL_SECOND_ORDER)) ? QString::number(cycle.r2 * 1000.0, 'f', 2) : "-")); + cyclesTable->setItem(row, 18, createItem((cycle.modelValid && (cycle.model == BATTERYPARAMS_MODEL_SECOND_ORDER)) ? QString::number(cycle.c2, 'f', 1) : "-")); + cyclesTable->setItem(row, 19, createItem(cycle.modelValid ? QString::number(cycle.fitRmse * 1000.0, 'f', 3) : "-")); + cyclesTable->setItem(row, 20, createItem(cycle.modelValid ? QString::number(cycle.fitMaxError * 1000.0, 'f', 3) : "-")); + + /*Green marks a cycle where the cell relaxed and the model could be fitted, + yellow one where it did not*/ + QColor rowColor = cycle.relaxationReached ? QColor(200, 240, 200) : QColor(255, 245, 190); + + for(int column = 0; column < BATTERYPARAMS_COLUMN_NO; column++) + { + if(cyclesTable->item(row, column) == NULL) continue; + cyclesTable->item(row, column)->setBackground(rowColor); + } +} + +void BatteryParamsWnd::addCycleRow(batteryparams_cycle_t cycle) +{ + int row = cyclesTable->rowCount(); + + cyclesTable->insertRow(row); + fillCycleRow(row, cycle); + + /*Column widths follow the content while the first cycles arrive, after that + they stay as the user left them*/ + if(row < BATTERYPARAMS_AUTO_RESIZE_ROW_NO) + { + cyclesTable->resizeColumnsToContents(); + } + cyclesTable->scrollToBottom(); +} + +void BatteryParamsWnd::updateSummary() +{ + double sum = 0; + int validNo = 0; + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].resistanceValid) continue; + sum += cycles[i].resistance; + validNo++; + } + + if(validNo == 0) + { + summaryLabel->setText(QString::number(cycles.size()) + " cycles, no resistance calculated yet"); + return; + } + + summaryLabel->setText(QString::number(cycles.size()) + " cycles, " + + QString::number(cycles.last().chargeTotal, 'f', 3) + " mAh extracted" + + (cycles.last().socValid ? ", " + QString::number(cycles.last().socUsed, 'f', 2) + " % SoC used" : "") + + ", average resistance " + QString::number(sum / validNo * 1000.0, 'f', 2) + " mOhm"); +} + +/*Used by the offline analysis, where the cycles are detected first and fitted + afterwards, optionally on all cores*/ +void BatteryParamsWnd::addCycles(QVector newCycles, QProgressDialog *progressDialog) +{ + analyzeCycles(&newCycles, progressDialog, parallelGet(newCycles.size())); + + for(int i = 0; i < newCycles.size(); i++) + { + onCycleFinished(newCycles[i]); + } +} + +void BatteryParamsWnd::onCycleStarted(unsigned int index, double key) +{ + statusLabel->setText("Cycle " + QString::number(index) + " ongoing (started at " + + QString::number(key, 'f', 3) + " ms)"); +} + +void BatteryParamsWnd::onCycleFinished(batteryparams_cycle_t cycle) +{ + cycles.append(cycle); + addCycleRow(cycle); + updateSummary(); + statusLabel->setText("Cycle " + QString::number(cycle.index) + " done, waiting for the next one..."); +} + +void BatteryParamsWnd::onCycleSelected(int row, int column) +{ + Q_UNUSED(column); + + if(row < 0 || row >= cycles.size()) return; + + if(cycleWnd == NULL) + { + cycleWnd = new BatteryCycleWnd(); + cycleWnd->setAttribute(Qt::WA_QuitOnClose, false); + } + + if(!connectedToCycleWnd) + { + connect(cycleWnd, &BatteryCycleWnd::sigCycleUpdated, this, &BatteryParamsWnd::onCycleUpdated); + connectedToCycleWnd = true; + } + + cycleWnd->setPlotSettings(plotSettings); + cycleWnd->setCycle(cycles[row]); + cycleWnd->show(); + cycleWnd->raise(); + cycleWnd->activateWindow(); +} + +void BatteryParamsWnd::onCycleUpdated(batteryparams_cycle_t cycle) +{ + for(int i = 0; i < cycles.size(); i++) + { + if(cycles[i].index != cycle.index) continue; + + cycles[i] = cycle; + fillCycleRow(i, cycle); + updateSummary(); + statusLabel->setText("Cycle " + QString::number(cycle.index) + " markers adjusted manually"); + return; + } +} + +void BatteryParamsWnd::onClear() +{ + cycles.clear(); + cyclesTable->setRowCount(0); + statusLabel->setText("Waiting for the first cycle..."); + updateSummary(); +} + +void BatteryParamsWnd::setSettings(batteryparams_settings_t aSettings) +{ + settings = aSettings; + updateSettingsLabel(); + if(settingsDlg != NULL) settingsDlg->setSettings(settings); +} + +void BatteryParamsWnd::updateSettingsLabel() +{ + settingsLabel->setText(QString(settings.model == BATTERYPARAMS_MODEL_SECOND_ORDER ? "2nd order" : "1st order") + + " | relaxation " + QString::number(settings.relaxationThreshold) + " mV / " + + QString::number(settings.relaxationWindow) + " s" + + " | capacity " + (settings.capacity > 0 ? QString::number(settings.capacity) + " mAh" : "unknown") + + " | initial SoC " + QString::number(settings.initialSoc) + " %" + + " | fit to " + QString(settings.fitEnd == BATTERYPARAMS_FIT_END_RELAXATION ? + "relaxation point" : "pause end")); +} + +void BatteryParamsWnd::onSettings() +{ + if(settingsDlg == NULL) + { + settingsDlg = new BatteryParamsSettingsDlg(this); + } + + settingsDlg->setSettings(settings); + settingsDlg->setPlotSettings(plotSettings); + + if(settingsDlg->exec() != QDialog::Accepted) return; + + settings = settingsDlg->getSettings(); + plotSettings = settingsDlg->getPlotSettings(); + + if(resistanceTrendWnd != NULL) resistanceTrendWnd->setPlotSettings(plotSettings); + if(capacitanceTrendWnd != NULL) capacitanceTrendWnd->setPlotSettings(plotSettings); + if(ocvTrendWnd != NULL) ocvTrendWnd->setPlotSettings(plotSettings); + if(relaxationTimeWnd != NULL) relaxationTimeWnd->setPlotSettings(plotSettings); + if(ocvAnalysisWnd != NULL) ocvAnalysisWnd->setPlotSettings(plotSettings); + if(fitQualityWnd != NULL) fitQualityWnd->setPlotSettings(plotSettings); + if(fitIntervalWnd != NULL) fitIntervalWnd->setPlotSettings(plotSettings); + if(cycleWnd != NULL) cycleWnd->setPlotSettings(plotSettings); + + updateSettingsLabel(); + recalculate(); + + emit sigSettingsChanged(settings); +} + +/*Fitting one cycle does not depend on any other cycle, so the work can be spread + over all cores. The question is asked once and the answer kept for the rest of + the session*/ +bool BatteryParamsWnd::parallelGet(int cycleNo) +{ + int coreNo = QThread::idealThreadCount(); + + if(cycleNo < 2) return false; + if(coreNo < 2) return false; + if(parallelAnswer >= 0) return (parallelAnswer == 1); + + parallelAnswer = (QMessageBox::question(this, "Battery parameters", + "Analysis of " + QString::number(cycleNo) + " cycles is about to start.\n\n" + "Use all " + QString::number(coreNo) + " processor cores?\n" + "The analysis finishes faster, but the machine is busy while it runs.", + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes) == QMessageBox::Yes) ? 1 : 0; + + return (parallelAnswer == 1); +} + +void BatteryParamsWnd::analyzeCycles(QVector *target, QProgressDialog *progressDialog, bool parallel) +{ + analyzeCycles(target, progressDialog, parallel, settings); +} + +void BatteryParamsWnd::analyzeCycles(QVector *target, QProgressDialog *progressDialog, bool parallel, batteryparams_settings_t usedSettings) +{ + int cycleNo = target->size(); + + if(!parallel) + { + for(int i = 0; i < cycleNo; i++) + { + if(progressDialog != NULL) + { + progressDialog->setLabelText(QString("%1 cycles detected\nProcessing cycle %2 of %1...") + .arg(cycleNo).arg(i + 1)); + progressDialog->setValue(i); + QApplication::processEvents(); + } + + BatteryParamsExtraction::analyzeCycle(&(*target)[i], usedSettings); + } + + if(progressDialog != NULL) progressDialog->setValue(cycleNo); + return; + } + + QFutureWatcher watcher; + QFuture future = QtConcurrent::map(*target, [usedSettings](batteryparams_cycle_t &cycle) + { + BatteryParamsExtraction::analyzeCycle(&cycle, usedSettings); + }); + + watcher.setFuture(future); + + while(!watcher.isFinished()) + { + if(progressDialog != NULL) + { + progressDialog->setLabelText(QString("%1 cycles detected\nProcessing on %2 cores, %3 of %1 done...") + .arg(cycleNo) + .arg(QThread::idealThreadCount()) + .arg(watcher.progressValue())); + progressDialog->setValue(watcher.progressValue()); + } + QApplication::processEvents(); + QThread::msleep(20); + } + + if(progressDialog != NULL) progressDialog->setValue(cycleNo); +} + +void BatteryParamsWnd::recalculate() +{ + /*Already collected cycles keep their samples, so changed settings are + applied to them as well instead of only to the cycles still to come. + Fitting takes a moment per cycle, so the progress is shown*/ + if(cycles.isEmpty()) return; + + QProgressDialog progressDialog(QString("%1 cycles detected\nRecalculating battery parameters...").arg(cycles.size()), + QString(), 0, cycles.size(), this); + + progressDialog.setWindowModality(Qt::WindowModal); + progressDialog.setMinimumDuration(0); + progressDialog.setAutoClose(false); + progressDialog.setAutoReset(false); + progressDialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + progressDialog.setWindowTitle("Battery parameters"); + progressDialog.setMinimumWidth(320); + progressDialog.setValue(0); + QApplication::processEvents(); + + for(int i = 0; i < cycles.size(); i++) + { + if(settings.capacity <= 0) continue; + + cycles[i].socUsed = cycles[i].chargeTotal / settings.capacity * 100.0; + cycles[i].soc = settings.initialSoc - cycles[i].socUsed; + cycles[i].socValid = true; + } + + analyzeCycles(&cycles, &progressDialog, parallelGet(cycles.size())); + + for(int i = 0; i < cycles.size(); i++) + { + fillCycleRow(i, cycles[i]); + } + + progressDialog.close(); + + updateSummary(); +} + +batteryparams_trend_t BatteryParamsWnd::trendCreate(QString name, QColor color) +{ + batteryparams_trend_t trend; + + trend.name = name; + trend.color = color; + + return trend; +} + +void BatteryParamsWnd::trendShow(BatteryParamsTrendWnd **window, QString title, QString yLabel, QVector trends) +{ + bool empty = true; + + for(int i = 0; i < trends.size(); i++) + { + if(trends[i].soc.isEmpty()) continue; + empty = false; + break; + } + + if(empty) + { + QMessageBox::warning(this, title, "No relaxed cycle with a known state of charge yet.\n" + "Enter the battery capacity in Settings and wait for a cycle where the battery relaxes."); + return; + } + + if(*window == NULL) + { + *window = new BatteryParamsTrendWnd(); + (*window)->setAttribute(Qt::WA_QuitOnClose, false); + } + + (*window)->setPlotSettings(plotSettings); + (*window)->setTrends(title, yLabel, trends); + (*window)->show(); + (*window)->raise(); + (*window)->activateWindow(); +} + +void BatteryParamsWnd::onResistanceTrend() +{ + QVector trends; + batteryparams_trend_t r0 = trendCreate("R0", QColor(200, 60, 20)); + batteryparams_trend_t r1 = trendCreate("R1", QColor(30, 110, 200)); + batteryparams_trend_t r2 = trendCreate("R2", QColor(30, 150, 60)); + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].socValid) continue; + + if(cycles[i].resistanceValid) + { + r0.soc.append(cycles[i].soc); + r0.value.append(cycles[i].resistance * 1000.0); + } + + if(!cycles[i].modelValid) continue; + + r1.soc.append(cycles[i].soc); + r1.value.append(cycles[i].r1 * 1000.0); + + if(cycles[i].model != BATTERYPARAMS_MODEL_SECOND_ORDER) continue; + + r2.soc.append(cycles[i].soc); + r2.value.append(cycles[i].r2 * 1000.0); + } + + trends << r0 << r1 << r2; + trendShow(&resistanceTrendWnd, "Resistance vs SoC", "R [mOhm]", trends); +} + +void BatteryParamsWnd::onCapacitanceTrend() +{ + QVector trends; + batteryparams_trend_t c1 = trendCreate("C1", QColor(30, 110, 200)); + batteryparams_trend_t c2 = trendCreate("C2", QColor(30, 150, 60)); + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].socValid) continue; + if(!cycles[i].modelValid) continue; + + c1.soc.append(cycles[i].soc); + c1.value.append(cycles[i].c1); + + if(cycles[i].model != BATTERYPARAMS_MODEL_SECOND_ORDER) continue; + + c2.soc.append(cycles[i].soc); + c2.value.append(cycles[i].c2); + } + + trends << c1 << c2; + trendShow(&capacitanceTrendWnd, "Capacitance vs SoC", "C [F]", trends); +} + +void BatteryParamsWnd::onOcvTrend() +{ + QVector trends; + batteryparams_trend_t ocv = trendCreate("OCV", QColor(120, 60, 180)); + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].socValid) continue; + if(!cycles[i].relaxationReached) continue; + + ocv.soc.append(cycles[i].soc); + ocv.value.append(cycles[i].ocv); + } + + trends << ocv; + trendShow(&ocvTrendWnd, "OCV vs SoC", "OCV [V]", trends); +} + +void BatteryParamsWnd::onOcvAnalysis() +{ + QVector ocv; + QVector soc; + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].socValid) continue; + if(!cycles[i].relaxationReached) continue; + + ocv.append(cycles[i].ocv); + soc.append(cycles[i].soc); + } + + if(ocv.size() < 3) + { + QMessageBox::warning(this, "OCV analysis", + "At least three relaxed cycles with a known state of charge are needed.\n" + "Enter the battery capacity in Settings and wait for the cycles where the battery relaxes."); + return; + } + + if(ocvAnalysisWnd == NULL) + { + ocvAnalysisWnd = new BatteryOcvAnalysisWnd(); + ocvAnalysisWnd->setAttribute(Qt::WA_QuitOnClose, false); + } + + ocvAnalysisWnd->setPlotSettings(plotSettings); + ocvAnalysisWnd->setData(soc, ocv); + ocvAnalysisWnd->show(); + ocvAnalysisWnd->raise(); + ocvAnalysisWnd->activateWindow(); +} + +void BatteryParamsWnd::onFitQuality() +{ + QVector firstOrder; + QVector secondOrder; + QVector quality; + batteryparams_settings_t firstSettings = settings; + batteryparams_settings_t secondSettings = settings; + bool parallel; + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].relaxationReached) continue; + firstOrder.append(cycles[i]); + secondOrder.append(cycles[i]); + } + + if(firstOrder.isEmpty()) + { + QMessageBox::warning(this, "Estimation quality", + "No cycle where the battery relaxed, so no model could be fitted."); + return; + } + + firstSettings.model = BATTERYPARAMS_MODEL_FIRST_ORDER; + secondSettings.model = BATTERYPARAMS_MODEL_SECOND_ORDER; + + parallel = parallelGet(firstOrder.size() * 2); + + QProgressDialog progressDialog(QString("%1 cycles detected\nFitting the first order model...").arg(firstOrder.size()), + QString(), 0, firstOrder.size(), this); + + progressDialog.setWindowModality(Qt::WindowModal); + progressDialog.setMinimumDuration(0); + progressDialog.setAutoClose(false); + progressDialog.setAutoReset(false); + progressDialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + progressDialog.setWindowTitle("Estimation quality"); + progressDialog.setMinimumWidth(320); + progressDialog.setValue(0); + QApplication::processEvents(); + + /*Both models are fitted over the same cycles, so their errors can be put + side by side*/ + analyzeCycles(&firstOrder, &progressDialog, parallel, firstSettings); + + progressDialog.setLabelText(QString("%1 cycles detected\nFitting the second order model...").arg(secondOrder.size())); + progressDialog.setValue(0); + QApplication::processEvents(); + + analyzeCycles(&secondOrder, &progressDialog, parallel, secondSettings); + + progressDialog.close(); + + for(int i = 0; i < firstOrder.size(); i++) + { + batteryfitquality_cycle_t entry; + + entry.index = firstOrder[i].index; + entry.firstOrderValid = firstOrder[i].modelValid; + entry.firstOrderRmse = firstOrder[i].fitRmse * 1000.0; + entry.firstOrderMaxError = firstOrder[i].fitMaxError * 1000.0; + entry.secondOrderValid = secondOrder[i].modelValid && (secondOrder[i].model == BATTERYPARAMS_MODEL_SECOND_ORDER); + entry.secondOrderRmse = secondOrder[i].fitRmse * 1000.0; + entry.secondOrderMaxError = secondOrder[i].fitMaxError * 1000.0; + + quality.append(entry); + } + + if(fitQualityWnd == NULL) + { + fitQualityWnd = new BatteryFitQualityWnd(); + fitQualityWnd->setAttribute(Qt::WA_QuitOnClose, false); + } + + fitQualityWnd->setPlotSettings(plotSettings); + fitQualityWnd->setData(quality); + fitQualityWnd->show(); + fitQualityWnd->raise(); + fitQualityWnd->activateWindow(); +} + +void BatteryParamsWnd::onRelaxationTime() +{ + QVector bars; + + for(int i = 0; i < cycles.size(); i++) + { + batteryparams_bar_t bar; + + if(!cycles[i].relaxationReached) continue; + + bar.index = cycles[i].index; + bar.value = (cycles[i].relaxationKey - cycles[i].pauseStartKey) / 1000.0; + + bars.append(bar); + } + + if(bars.isEmpty()) + { + QMessageBox::warning(this, "Relaxation time", "No cycle where the battery relaxed."); + return; + } + + if(relaxationTimeWnd == NULL) + { + relaxationTimeWnd = new BatteryParamsBarWnd(); + relaxationTimeWnd->setAttribute(Qt::WA_QuitOnClose, false); + } + + relaxationTimeWnd->setPlotSettings(plotSettings); + relaxationTimeWnd->setData("Relaxation time", "Time to relaxation [s]", QColor(30, 150, 60), bars); + relaxationTimeWnd->show(); + relaxationTimeWnd->raise(); + relaxationTimeWnd->activateWindow(); +} + +void BatteryParamsWnd::onFitInterval() +{ + QVector relaxationFit; + QVector pauseFit; + QVector comparison; + batteryparams_settings_t relaxationSettings = settings; + batteryparams_settings_t pauseSettings = settings; + bool parallel; + + for(int i = 0; i < cycles.size(); i++) + { + if(!cycles[i].relaxationReached) continue; + relaxationFit.append(cycles[i]); + pauseFit.append(cycles[i]); + } + + if(relaxationFit.isEmpty()) + { + QMessageBox::warning(this, "Fit interval", "No cycle where the battery relaxed, so no model could be fitted."); + return; + } + + relaxationSettings.fitEnd = BATTERYPARAMS_FIT_END_RELAXATION; + pauseSettings.fitEnd = BATTERYPARAMS_FIT_END_PAUSE; + + parallel = parallelGet(relaxationFit.size() * 2); + + QProgressDialog progressDialog(QString("%1 cycles detected\nFitting up to the relaxation point...").arg(relaxationFit.size()), + QString(), 0, relaxationFit.size(), this); + + progressDialog.setWindowModality(Qt::WindowModal); + progressDialog.setMinimumDuration(0); + progressDialog.setAutoClose(false); + progressDialog.setAutoReset(false); + progressDialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + progressDialog.setWindowTitle("Fit interval"); + progressDialog.setMinimumWidth(320); + progressDialog.setValue(0); + QApplication::processEvents(); + + analyzeCycles(&relaxationFit, &progressDialog, parallel, relaxationSettings); + + progressDialog.setLabelText(QString("%1 cycles detected\nFitting up to the end of the pause...").arg(pauseFit.size())); + progressDialog.setValue(0); + QApplication::processEvents(); + + analyzeCycles(&pauseFit, &progressDialog, parallel, pauseSettings); + + progressDialog.close(); + + for(int i = 0; i < relaxationFit.size(); i++) + { + batteryfitinterval_cycle_t entry; + + entry.index = relaxationFit[i].index; + + entry.relaxationValid = relaxationFit[i].modelValid; + entry.relaxationR1 = relaxationFit[i].r1; + entry.relaxationC1 = relaxationFit[i].c1; + entry.relaxationR2 = relaxationFit[i].r2; + entry.relaxationC2 = relaxationFit[i].c2; + entry.relaxationRmse = relaxationFit[i].fitRmse * 1000.0; + entry.relaxationMaxError = relaxationFit[i].fitMaxError * 1000.0; + + entry.pauseValid = pauseFit[i].modelValid; + entry.pauseR1 = pauseFit[i].r1; + entry.pauseC1 = pauseFit[i].c1; + entry.pauseR2 = pauseFit[i].r2; + entry.pauseC2 = pauseFit[i].c2; + entry.pauseRmse = pauseFit[i].fitRmse * 1000.0; + entry.pauseMaxError = pauseFit[i].fitMaxError * 1000.0; + + comparison.append(entry); + } + + if(fitIntervalWnd == NULL) + { + fitIntervalWnd = new BatteryFitIntervalWnd(); + fitIntervalWnd->setAttribute(Qt::WA_QuitOnClose, false); + } + + if(!connectedToFitIntervalWnd) + { + connect(fitIntervalWnd, &BatteryFitIntervalWnd::sigSweepRequested, this, &BatteryParamsWnd::onSweepRequested); + connectedToFitIntervalWnd = true; + } + + fitIntervalWnd->setPlotSettings(plotSettings); + fitIntervalWnd->setData(comparison); + fitIntervalWnd->show(); + fitIntervalWnd->raise(); + fitIntervalWnd->activateWindow(); +} + +/*Every sweep point is a complete analysis of all cycles with a different + relaxation criterion, so the work goes through the same parallel path as the + other analyses*/ +void BatteryParamsWnd::onSweepRequested(batteryfitinterval_sweep_t mode, double fixedValue, + double from, double to, int steps, bool compare) +{ + QVector source; + QVector sweepValues; + QVector rmseValues; + QVector maxErrorValues; + QVector relaxedValues; + QVector cycleIndexes; + QVector> cycleRmse; + QVector> cycleMaxError; + QVector relaxationTimeValues; + QVector> cycleRelaxationTime; + double referenceRmse = 0; + double referenceMaxError = 0; + bool parallel; + + for(int i = 0; i < cycles.size(); i++) + { + source.append(cycles[i]); + } + + if(source.isEmpty()) return; + + for(int i = 0; i < source.size(); i++) + { + cycleIndexes.append(source[i].index); + cycleRmse.append(QVector()); + cycleMaxError.append(QVector()); + cycleRelaxationTime.append(QVector()); + } + if(steps < 2) steps = 2; + if(to <= from) to = from + 1; + + parallel = parallelGet(source.size() * steps); + + QProgressDialog progressDialog(QString("Sweeping the relaxation criterion over %1 points...").arg(steps), + QString(), 0, steps + (compare ? 1 : 0), this); + + progressDialog.setWindowModality(Qt::WindowModal); + progressDialog.setMinimumDuration(0); + progressDialog.setAutoClose(false); + progressDialog.setAutoReset(false); + progressDialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + progressDialog.setWindowTitle("Relaxation sweep"); + progressDialog.setMinimumWidth(340); + progressDialog.setValue(0); + QApplication::processEvents(); + + for(int step = 0; step < steps; step++) + { + QVector target = source; + batteryparams_settings_t stepSettings = settings; + double value = from + (to - from) * (double)step / (steps - 1); + double rmseSum = 0; + double maxErrorSum = 0; + double relaxationTimeSum = 0; + int validNo = 0; + + stepSettings.fitEnd = BATTERYPARAMS_FIT_END_RELAXATION; + + if(mode == BATTERYFITINTERVAL_SWEEP_THRESHOLD) + { + stepSettings.relaxationThreshold = value; + stepSettings.relaxationWindow = fixedValue; + } + else + { + stepSettings.relaxationThreshold = fixedValue; + stepSettings.relaxationWindow = value; + } + + progressDialog.setLabelText(QString("Sweep point %1 of %2\n%3 %4 with %5 %6 held") + .arg(step + 1).arg(steps) + .arg(value, 0, 'f', 2) + .arg(mode == BATTERYFITINTERVAL_SWEEP_THRESHOLD ? "mV" : "s") + .arg(fixedValue, 0, 'f', 2) + .arg(mode == BATTERYFITINTERVAL_SWEEP_THRESHOLD ? "s" : "mV")); + QApplication::processEvents(); + + analyzeCycles(&target, NULL, parallel, stepSettings); + + for(int i = 0; i < target.size(); i++) + { + if(!target[i].modelValid) + { + /*Negative marks a cycle that could not be fitted with this criterion*/ + cycleRmse[i].append(-1); + cycleMaxError[i].append(-1); + cycleRelaxationTime[i].append(-1); + continue; + } + + cycleRmse[i].append(target[i].fitRmse * 1000.0); + cycleMaxError[i].append(target[i].fitMaxError * 1000.0); + cycleRelaxationTime[i].append((target[i].relaxationKey - target[i].pauseStartKey) / 1000.0); + + rmseSum += target[i].fitRmse * 1000.0; + maxErrorSum += target[i].fitMaxError * 1000.0; + relaxationTimeSum += (target[i].relaxationKey - target[i].pauseStartKey) / 1000.0; + validNo++; + } + + /*A criterion so tight that no cycle relaxes has no error to report, so the + point is left out instead of being drawn as zero*/ + if(validNo > 0) + { + sweepValues.append(value); + rmseValues.append(rmseSum / validNo); + maxErrorValues.append(maxErrorSum / validNo); + relaxationTimeValues.append(relaxationTimeSum / validNo); + relaxedValues.append(validNo); + } + else + { + /*Point is dropped, so the per cycle rows stay aligned with it*/ + for(int i = 0; i < cycleRmse.size(); i++) + { + cycleRmse[i].removeLast(); + cycleMaxError[i].removeLast(); + cycleRelaxationTime[i].removeLast(); + } + } + + progressDialog.setValue(step + 1); + QApplication::processEvents(); + } + + if(compare) + { + QVector target = source; + batteryparams_settings_t referenceSettings = settings; + double rmseSum = 0; + double maxErrorSum = 0; + int validNo = 0; + + referenceSettings.fitEnd = BATTERYPARAMS_FIT_END_PAUSE; + + progressDialog.setLabelText("Fitting the reference over the whole pause..."); + QApplication::processEvents(); + + analyzeCycles(&target, NULL, parallel, referenceSettings); + + for(int i = 0; i < target.size(); i++) + { + if(!target[i].modelValid) continue; + + rmseSum += target[i].fitRmse * 1000.0; + maxErrorSum += target[i].fitMaxError * 1000.0; + validNo++; + } + + if(validNo > 0) + { + referenceRmse = rmseSum / validNo; + referenceMaxError = maxErrorSum / validNo; + } + + progressDialog.setValue(steps + 1); + } + + progressDialog.close(); + + fitIntervalWnd->setSweep(sweepValues, rmseValues, maxErrorValues, relaxedValues, + referenceRmse, referenceMaxError, cycleIndexes, cycleRmse, cycleMaxError, + relaxationTimeValues, cycleRelaxationTime); +} + +void BatteryParamsWnd::onExport() +{ + QString suggested = profileName.isEmpty() ? "battery_params.csv" : profileName + "_battery_params.csv"; + QString path; + QFile file; + + if(cycles.isEmpty()) + { + QMessageBox::warning(this, "Export", "No finished cycles to export"); + return; + } + + path = QFileDialog::getSaveFileName(this, "Export battery parameters", suggested, "CSV files (*.csv);;All files (*)"); + if(path.isEmpty()) return; + + file.setFileName(path); + if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::warning(this, "Export", "Cannot open " + path); + return; + } + + QTextStream stream(&file); + stream << "Cycle,Start [ms],End [ms],Q [mAh],Q total [mAh],SoC used [%],SoC [%],OCV [V],R1 [mOhm],C1 [F],R2 [mOhm],C2 [F],RMSE [mV],Max err [mV],V pulse end [V],V pause start [V],dV [mV],I pulse end [mA],I pause start [mA],dI [mA],R [mOhm]\n"; + for(int i = 0; i < cycles.size(); i++) + { + stream << cycles[i].index << "," + << QString::number(cycles[i].pulseStartKey, 'f', 3) << "," + << QString::number(cycles[i].pauseEndKey, 'f', 3) << "," + << QString::number(cycles[i].charge, 'f', 3) << "," + << QString::number(cycles[i].chargeTotal, 'f', 3) << "," + << (cycles[i].socValid ? QString::number(cycles[i].socUsed, 'f', 2) : "") << "," + << (cycles[i].socValid ? QString::number(cycles[i].soc, 'f', 2) : "") << "," + << (cycles[i].relaxationReached ? QString::number(cycles[i].ocv, 'f', 4) : "") << "," + << (cycles[i].modelValid ? QString::number(cycles[i].r1 * 1000.0, 'f', 2) : "") << "," + << (cycles[i].modelValid ? QString::number(cycles[i].c1, 'f', 1) : "") << "," + << ((cycles[i].modelValid && cycles[i].model == BATTERYPARAMS_MODEL_SECOND_ORDER) ? QString::number(cycles[i].r2 * 1000.0, 'f', 2) : "") << "," + << ((cycles[i].modelValid && cycles[i].model == BATTERYPARAMS_MODEL_SECOND_ORDER) ? QString::number(cycles[i].c2, 'f', 1) : "") << "," + << (cycles[i].modelValid ? QString::number(cycles[i].fitRmse * 1000.0, 'f', 3) : "") << "," + << (cycles[i].modelValid ? QString::number(cycles[i].fitMaxError * 1000.0, 'f', 3) : "") << "," + << QString::number(cycles[i].pulseEndVoltage, 'f', 4) << "," + << QString::number(cycles[i].pauseStartVoltage, 'f', 4) << "," + << QString::number(cycles[i].deltaVoltage * 1000.0, 'f', 2) << "," + << QString::number(cycles[i].pulseEndCurrent, 'f', 2) << "," + << QString::number(cycles[i].pauseStartCurrent, 'f', 2) << "," + << QString::number(cycles[i].deltaCurrent, 'f', 2) << "," + << (cycles[i].resistanceValid ? QString::number(cycles[i].resistance * 1000.0, 'f', 2) : "") << "\n"; + } + file.close(); +} diff --git a/Source/Windows/BatteryParams/batteryparamswnd.h b/Source/Windows/BatteryParams/batteryparamswnd.h new file mode 100644 index 0000000..f032af4 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamswnd.h @@ -0,0 +1,104 @@ +#ifndef BATTERYPARAMSWND_H +#define BATTERYPARAMSWND_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Processing/batteryparamsextraction.h" +#include "batterycyclewnd.h" +#include "batteryparamssettingsdlg.h" +#include "batteryfitintervalwnd.h" +#include "batteryfitqualitywnd.h" +#include "batteryparamsbarwnd.h" +#include "batteryocvanalysiswnd.h" +#include "batteryparamstrendwnd.h" + +class BatteryParamsWnd : public QWidget +{ + Q_OBJECT + +public: + explicit BatteryParamsWnd(QWidget *parent = nullptr); + + void setProfileName(QString name); + void setSettings(batteryparams_settings_t settings); + void addCycles(QVector newCycles, QProgressDialog *progressDialog); + +signals: + void sigSettingsChanged(batteryparams_settings_t settings); + +public slots: + void onCycleStarted(unsigned int index, double key); + void onCycleFinished(batteryparams_cycle_t cycle); + void onClear(); + +private slots: + void onExport(); + void onSettings(); + void onResistanceTrend(); + void onCapacitanceTrend(); + void onOcvTrend(); + void onOcvAnalysis(); + void onFitQuality(); + void onRelaxationTime(); + void onFitInterval(); + void onSweepRequested(batteryfitinterval_sweep_t mode, double fixedValue, + double from, double to, int steps, bool compare); + void onCycleSelected(int row, int column); + void onCycleUpdated(batteryparams_cycle_t cycle); + +private: + QLabel *statusLabel; + QPushButton *settingsButton; + QPushButton *resistanceTrendButton; + QPushButton *capacitanceTrendButton; + QPushButton *ocvTrendButton; + QPushButton *ocvAnalysisButton; + QPushButton *fitQualityButton; + QPushButton *relaxationTimeButton; + QPushButton *fitIntervalButton; + QLabel *settingsLabel; + + BatteryParamsSettingsDlg *settingsDlg; + BatteryParamsTrendWnd *resistanceTrendWnd; + BatteryParamsTrendWnd *capacitanceTrendWnd; + BatteryParamsTrendWnd *ocvTrendWnd; + BatteryOcvAnalysisWnd *ocvAnalysisWnd; + BatteryFitQualityWnd *fitQualityWnd; + BatteryParamsBarWnd *relaxationTimeWnd; + BatteryFitIntervalWnd *fitIntervalWnd; + + batteryparams_settings_t settings; + batteryparams_plot_settings_t plotSettings; + int parallelAnswer; + QLabel *summaryLabel; + QTableWidget *cyclesTable; + QPushButton *clearButton; + QPushButton *exportButton; + BatteryCycleWnd *cycleWnd; + bool connectedToCycleWnd; + bool connectedToFitIntervalWnd; + + QString profileName; + QVector cycles; + + QTableWidgetItem* createItem(QString text); + void addCycleRow(batteryparams_cycle_t cycle); + void fillCycleRow(int row, batteryparams_cycle_t cycle); + void updateSummary(); + void recalculate(); + bool parallelGet(int cycleNo); + void analyzeCycles(QVector *target, QProgressDialog *progressDialog, bool parallel); + void analyzeCycles(QVector *target, QProgressDialog *progressDialog, bool parallel, batteryparams_settings_t usedSettings); + void updateSettingsLabel(); + batteryparams_trend_t trendCreate(QString name, QColor color); + void trendShow(BatteryParamsTrendWnd **window, QString title, QString yLabel, QVector trends); +}; + +#endif // BATTERYPARAMSWND_H diff --git a/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp index e74826b..26220f5 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp +++ b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #define PLOT_MINIMUM_SIZE_HEIGHT 100 #define PLOT_MINIMUM_SIZE_WIDTH 500 @@ -36,6 +38,9 @@ DataAnalyzerProfile::DataAnalyzerProfile(QString aWsDirPath, QString aProfileNam QAction *genStatisticsAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/analysis.png")), "Gen statistics"); genStatisticsAction->setToolTip("Generate consumption statistics for segments between \" Start\" and \" Stop\" markers"); connect(genStatisticsAction, SIGNAL(triggered(bool)), this, SLOT(onGenerateStatistics())); + QAction *batteryAnalyzerAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/loadProfiler.png")), "Battery analyzer"); + batteryAnalyzerAction->setToolTip("Extract battery parameters from \"Pulse Start / Pulse End / Pause Start / Pause End\" marker cycles"); + connect(batteryAnalyzerAction, SIGNAL(triggered(bool)), this, SLOT(onBatteryAnalyzer())); detachAction = plotsToolBar->addAction(QIcon(QPixmap(":/images/NewSet/expand.png")), "Attach"); detachAction->setToolTip("Show this profile back as a tab of the Data Analyzer"); detachAction->setVisible(false); @@ -43,6 +48,7 @@ DataAnalyzerProfile::DataAnalyzerProfile(QString aWsDirPath, QString aProfileNam mainLayout->addWidget(plotsToolBar); statisticsWnd = new DataAnalyzerStatisticsWnd(); + batteryParamsWnd = NULL; statisticsThread = new QThread(this); statisticsWorker = new DataAnalyzerStatisticsWorker(); statisticsWorker->moveToThread(statisticsThread); @@ -307,6 +313,137 @@ void DataAnalyzerProfile::onGenerateStatistics() emit sigComputeStatistics(loadedVoltage, loadedVoltageKeys, loadedCurrent, loadedCurrentKeys, loadedMarkers); } +void DataAnalyzerProfile::onBatteryAnalyzer() +{ + BatteryParamsExtraction extraction; + QStringList requiredMarkers; + int cycleNo = 0; + + if(!graphLoad) + { + QMessageBox::warning(this, "Battery analyzer", "No consumption profile loaded"); + return; + } + + requiredMarkers << BATTERYPARAMS_DEFAULT_PULSE_START_MARKER + << BATTERYPARAMS_DEFAULT_PULSE_END_MARKER + << BATTERYPARAMS_DEFAULT_PAUSE_START_MARKER + << BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER; + + for(int i = 0; i < requiredMarkers.size(); i++) + { + bool found = false; + + for(int j = 0; j < loadedMarkers.size(); j++) + { + if(loadedMarkers[j].first.trimmed() != requiredMarkers[i]) continue; + found = true; + break; + } + + if(found) continue; + + QMessageBox::warning(this, "Battery analyzer", + "Loaded profile has no \"" + requiredMarkers[i] + "\" marker.\n" + "Battery parameters can be extracted only from a profile recorded with " + "the \"Bat Param Extraction\" load mode."); + return; + } + + /*Capacity is stored with the profile, so the offline analysis can show the + used state of charge the same way the live one does*/ + { + QVector> summaryInfo = parseSummaryFile(wsDirPath + "/" + profileName + "/OpenEPT.txt"); + double capacity = getValueForKey(summaryInfo, "Battery capacity [mAh]").toDouble(); + batteryparams_settings_t settings = extraction.getSettings(); + + if(capacity > 0) + { + settings.capacity = capacity; + extraction.setSettings(settings); + } + } + + if(batteryParamsWnd == NULL) + { + batteryParamsWnd = new BatteryParamsWnd(); + batteryParamsWnd->setAttribute(Qt::WA_QuitOnClose, false); + } + + batteryParamsWnd->setProfileName(profileName); + batteryParamsWnd->setSettings(extraction.getSettings()); + batteryParamsWnd->onClear(); + + /*Cycles are detected first and fitted afterwards, so the fitting can be + spread over all cores. The dialog is stepped manually because everything + runs from this thread*/ + extraction.setAnalysisDeferred(true); + + int detectedCycleNo = 0; + + for(int i = 0; i < loadedMarkers.size(); i++) + { + if(loadedMarkers[i].first.trimmed() != BATTERYPARAMS_DEFAULT_PULSE_START_MARKER) continue; + detectedCycleNo++; + } + + QProgressDialog progressDialog(QString("%1 cycles detected\nReading samples...").arg(detectedCycleNo), + QString(), 0, loadedMarkers.size() + 1, this); + + progressDialog.setWindowModality(Qt::WindowModal); + progressDialog.setMinimumDuration(0); + progressDialog.setAutoClose(false); + progressDialog.setAutoReset(false); + progressDialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + progressDialog.setWindowTitle("Battery analyzer"); + progressDialog.setMinimumWidth(320); + progressDialog.setValue(0); + QApplication::processEvents(); + + extraction.onNewSamplesReceived(loadedVoltage, loadedCurrent, loadedVoltageKeys, loadedCurrentKeys); + + progressDialog.setValue(1); + QApplication::processEvents(); + + for(int i = 0; i < loadedMarkers.size(); i++) + { + QString marker = loadedMarkers[i].first.trimmed(); + + if(marker == BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER) + { + progressDialog.setLabelText(QString("%1 cycles detected\nSplitting cycle %2 of %1...") + .arg(detectedCycleNo) + .arg(extraction.getCycles().size() + 1)); + QApplication::processEvents(); + } + + extraction.onNewMarkerReceived(0, loadedMarkers[i].second, marker); + + progressDialog.setValue(i + 2); + QApplication::processEvents(); + } + + cycleNo = extraction.getCycles().size(); + + if(cycleNo == 0) + { + progressDialog.close(); + QMessageBox::warning(this, "Battery analyzer", "No complete pulse/pause cycle found in the loaded profile"); + return; + } + + progressDialog.setRange(0, cycleNo); + progressDialog.setValue(0); + + batteryParamsWnd->addCycles(extraction.getCycles(), &progressDialog); + + progressDialog.close(); + + batteryParamsWnd->show(); + batteryParamsWnd->raise(); + batteryParamsWnd->activateWindow(); +} + void DataAnalyzerProfile::onStatisticsProgress(int percentage, QString text) { if(statisticsProgressDialog == NULL) return; diff --git a/Source/Windows/DataAnalyzer/dataanalyzerprofile.h b/Source/Windows/DataAnalyzer/dataanalyzerprofile.h index 296ef22..60e67a6 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzerprofile.h +++ b/Source/Windows/DataAnalyzer/dataanalyzerprofile.h @@ -2,6 +2,8 @@ #define DATAANALYZERPROFILE_H #include + +#include "Windows/BatteryParams/batteryparamswnd.h" #include #include #include @@ -37,6 +39,7 @@ class DataAnalyzerProfile : public QWidget public slots: void onGenerateStatistics(); + void onBatteryAnalyzer(); void onStatisticsProgress(int percentage, QString text); void onStatisticsFinished(QVector stats, QVector points, dataanalyzer_segment_stat_t total, QStringList warnings); void onStatisticsSegmentSelected(QString name, int startIndex, int endIndex); @@ -68,6 +71,7 @@ public slots: QThread *statisticsThread; DataAnalyzerStatisticsWorker *statisticsWorker; DataAnalyzerStatisticsWnd *statisticsWnd; + BatteryParamsWnd *batteryParamsWnd; QThread *thread; DataAnalyzerWorker *dataProcesingClass; diff --git a/Source/Windows/Device/devicewnd.cpp b/Source/Windows/Device/devicewnd.cpp index cc6058b..c040022 100644 --- a/Source/Windows/Device/devicewnd.cpp +++ b/Source/Windows/Device/devicewnd.cpp @@ -59,6 +59,9 @@ DeviceWnd::DeviceWnd(QWidget *parent) : connect(energyControlWnd, SIGNAL(sigLoadCurrentChanged(unsigned int)), this, SLOT(onLoadCurrentChanged(unsigned int))); connect(energyControlWnd, &EnergyControlWnd::sigLoadWaveChanged, this, &DeviceWnd::onLoadWaveChanged); connect(energyControlWnd, &EnergyControlWnd::sigLoadWaveStatusChanged, this, &DeviceWnd::onLoadWaveStatusChanged); + connect(energyControlWnd, &EnergyControlWnd::sigBatParamViewRequested, this, &DeviceWnd::sigBatParamViewRequested); + connect(energyControlWnd, &EnergyControlWnd::sigBatParamCapacityChanged, this, &DeviceWnd::sigBatParamCapacityChanged); + connect(energyControlWnd, &EnergyControlWnd::sigBatParamRelaxationChanged, this, &DeviceWnd::sigBatParamRelaxationChanged); connect(energyControlWnd, &EnergyControlWnd::sigLoadWaveClear, this, &DeviceWnd::sigLoadWaveClear); connect(energyControlWnd, SIGNAL(sigChargingCurrentStatusChanged(bool)), this, SLOT(onChargingCurrentStatusChanged(bool))); connect(energyControlWnd, SIGNAL(sigChargingCurrentChanged(unsigned int)), this, SLOT(onChargingCurrentChanged(unsigned int))); diff --git a/Source/Windows/Device/devicewnd.h b/Source/Windows/Device/devicewnd.h index ba69616..8c5c48f 100644 --- a/Source/Windows/Device/devicewnd.h +++ b/Source/Windows/Device/devicewnd.h @@ -196,6 +196,9 @@ class DeviceWnd : public QWidget void sigLoadCurrentStatusChanged(bool newState); void sigLoadWaveChanged(Waveform wave); void sigLoadWaveStatusChanged(bool newState); + void sigBatParamViewRequested(); + void sigBatParamCapacityChanged(double capacity); + void sigBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes); void sigLoadWaveClear(); void sigChargingCurrentChanged(unsigned int current); void sigChargingTermCurrentChanged(unsigned int current); diff --git a/Source/Windows/Device/energycontrolwnd.cpp b/Source/Windows/Device/energycontrolwnd.cpp index 7216714..ff51c26 100644 --- a/Source/Windows/Device/energycontrolwnd.cpp +++ b/Source/Windows/Device/energycontrolwnd.cpp @@ -1,4 +1,5 @@ #include "energycontrolwnd.h" +#include #include "ui_energycontrolwnd.h" #include @@ -49,9 +50,19 @@ EnergyControlWnd::EnergyControlWnd(QWidget *parent) : QVBoxLayout *loadTopLayout = new QVBoxLayout(); // Section label with horizontal lines loadTopLayout->addLayout(createSectionHeader("Parameters")); - QStringList loadTypes = {"Static", "Standard Wave", "Custom Wave"}; - loadTopLayout->addLayout(createDropBoxEntry("Mode", loadTypes, loadComboBoxEdits)); - + QStringList loadTypes = {"Static", "Standard Wave", "Custom Wave", "Bat Param Extraction"}; + QHBoxLayout *loadModeRow = createDropBoxEntry("Mode", loadTypes, loadComboBoxEdits); + batParamViewButton = new QPushButton("Battery params", this); + batParamViewButton->setFixedHeight(BUTTON_HEIGHT); + batParamViewButton->setMinimumWidth(BUTTON_WIDTH); + batParamViewButton->setToolTip("Open real time battery parameters view.\nAvailable while the wave is running"); + batParamViewButton->setEnabled(false); + batParamViewButton->setVisible(false); + loadButtons.insert("BatParamView", batParamViewButton); + loadModeRow->addWidget(batParamViewButton); + loadTopLayout->addLayout(loadModeRow); + + connect(batParamViewButton, &QPushButton::clicked, this, &EnergyControlWnd::sigBatParamViewRequested); connect(loadComboBoxEdits["Mode"], &QComboBox::currentTextChanged,this, &EnergyControlWnd::onLoadModeChanged); // --- Static mode widget --- @@ -144,23 +155,7 @@ EnergyControlWnd::EnergyControlWnd(QWidget *parent) : customWaveButtons["Clear"] = createSmallButton("Clear", fileRow); customWaveLayout->addLayout(fileRow); - QStringList customWaveTableHeader; - customWaveTableHeader << "Value [mA]" << "Dev [mA]" << "Dur [ms]" << "Dev [ms]" << "Rep" << "Last" << "Marker" << "Pos"; - customWaveTable = new QTableWidget(0, 8, this); - customWaveTable->setHorizontalHeaderLabels(customWaveTableHeader); - customWaveTable->horizontalHeaderItem(1)->setToolTip("Value deviation [mA]"); - customWaveTable->horizontalHeaderItem(2)->setToolTip("Chunk duration [ms]"); - customWaveTable->horizontalHeaderItem(3)->setToolTip("Duration deviation [ms]"); - customWaveTable->horizontalHeaderItem(4)->setToolTip("Chunk repetitions"); - customWaveTable->horizontalHeaderItem(5)->setToolTip("Last chunk in group"); - customWaveTable->horizontalHeaderItem(6)->setToolTip("Energy debugger marker name (optional)"); - customWaveTable->horizontalHeaderItem(7)->setToolTip("Marker position: s = chunk start, e = chunk end, s,e = both (marker \"Start name, End name\")"); - customWaveTable->horizontalHeader()->setStretchLastSection(true); - customWaveTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - customWaveTable->verticalHeader()->setDefaultSectionSize(22); - customWaveTable->setSelectionBehavior(QAbstractItemView::SelectRows); - customWaveTable->setMinimumHeight(100); - customWaveTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + customWaveTable = createWaveTable(true); customWaveLayout->addWidget(customWaveTable, 1); customWaveWidget->setVisible(false); @@ -177,9 +172,77 @@ EnergyControlWnd::EnergyControlWnd(QWidget *parent) : connect(customWaveTable, &QTableWidget::itemChanged, this, &EnergyControlWnd::onCustomWaveTableChanged); connect(&WaveformLibrary::instance(), &WaveformLibrary::sigLibraryChanged, this, &EnergyControlWnd::onWaveLibraryChanged); + // --- Battery parameter extraction mode widget --- + batParamRelaxCombo = NULL; + batParamWidget = new QWidget(this); + QVBoxLayout *batParamLayout = new QVBoxLayout(batParamWidget); + batParamLayout->setContentsMargins(0, 0, 0, 0); + + batParamLayout->addLayout(createEntryRow("Battery Capacity", "mAh", loadEntryEdits)); + batParamLayout->addLayout(createEntryRow("Extraction Accuracy", "%", loadEntryEdits)); + batParamLayout->addLayout(createEntryRow("Maximum Current", "mA", loadEntryEdits)); + batParamLayout->addLayout(createEntryRow("Pause Duration", "ms", loadEntryEdits)); + batParamLayout->addLayout(createEntryRow("Marker Guard", "ms", loadEntryEdits)); + + QHBoxLayout *batParamRelaxRow = new QHBoxLayout(); + QLabel *batParamRelaxLabel = new QLabel("Relaxation", this); + batParamRelaxLabel->setFixedSize(ENTRY_LABEL_WIDTH, ENTRY_ROW_HEIGHT); + batParamRelaxCombo = new QComboBox(this); + batParamRelaxCombo->addItem("Fixed pause"); + batParamRelaxCombo->addItem("Until dV/dt"); + batParamRelaxCombo->setFixedHeight(ENTRY_ROW_HEIGHT); + batParamRelaxCombo->setToolTip("Fixed pause: every pause lasts exactly the pause duration.\nUntil dV/dt: the pause ends as soon as the voltage settles, pause duration is then the maximum"); + batParamRelaxRow->addWidget(batParamRelaxLabel); + batParamRelaxRow->addWidget(batParamRelaxCombo); + batParamRelaxRow->addStretch(); + batParamLayout->addLayout(batParamRelaxRow); + + batParamLayout->addLayout(createEntryRow("Relax Threshold", "mV", loadEntryEdits)); + batParamLayout->addLayout(createEntryRow("Relax Window", "s", loadEntryEdits)); + loadEntryEdits["Battery Capacity"]->setText("500"); + loadEntryEdits["Extraction Accuracy"]->setText("1"); + loadEntryEdits["Maximum Current"]->setText("1000"); + loadEntryEdits["Pause Duration"]->setText("1000"); + loadEntryEdits["Marker Guard"]->setText("1"); + loadEntryEdits["Relax Threshold"]->setText("5"); + loadEntryEdits["Relax Window"]->setText("60"); + loadEntryEdits["Battery Capacity"]->setToolTip("Nominal battery capacity"); + loadEntryEdits["Extraction Accuracy"]->setToolTip("Charge extracted by a single pulse, in percent of the battery capacity"); + loadEntryEdits["Maximum Current"]->setToolTip("Pulse amplitude, pause is always 0 mA"); + loadEntryEdits["Pause Duration"]->setToolTip("Relaxation time between two pulses"); + loadEntryEdits["Marker Guard"]->setToolTip("Distance between a current step and the marker that follows it.\nKeeps every marker inside a settled level instead of on the step itself.\nMust cover the load response time, a few sample periods at least"); + loadEntryEdits["Relax Threshold"]->setToolTip("Voltage span allowed inside the relaxation window, the pause ends when the battery stays within it"); + loadEntryEdits["Relax Window"]->setToolTip("Time window the voltage has to stay inside the threshold before the pause is ended"); + + batParamInfoLabel = new QLabel(this); + batParamInfoLabel->setStyleSheet("color: gray;"); + QHBoxLayout *batParamSaveRow = new QHBoxLayout(); + QPushButton *batParamSaveWaveButton = createSmallButton("Save wave", batParamSaveRow); + batParamSaveWaveButton->setToolTip("Save generated wave to library for later use"); + loadButtons.insert("SaveBatParamWave", batParamSaveWaveButton); + batParamSaveRow->addWidget(batParamInfoLabel, 1); + batParamLayout->addLayout(batParamSaveRow); + connect(batParamSaveWaveButton, &QPushButton::clicked, this, &EnergyControlWnd::onLoadWaveSave); + + batParamTable = createWaveTable(false); + batParamLayout->addWidget(batParamTable, 1); + + batParamWidget->setVisible(false); + loadTopLayout->addWidget(batParamWidget); + + connect(loadEntryEdits["Battery Capacity"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(loadEntryEdits["Extraction Accuracy"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(loadEntryEdits["Maximum Current"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(loadEntryEdits["Pause Duration"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(loadEntryEdits["Marker Guard"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(loadEntryEdits["Relax Threshold"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(loadEntryEdits["Relax Window"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(batParamRelaxCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onBatParamChanged())); + refreshCustomWaveLibraryCombo(); updateStdWaveInfo(); updateCustomWaveInfo(); + updateBatParamInfo(); /*IThis should always be visible*/ loadTopLayout->addLayout(createButtonRow("Set", "Set", loadButtons)); @@ -1184,6 +1247,19 @@ void EnergyControlWnd::chdischCycleCompleted() } } +void EnergyControlWnd::loadStopRequest() +{ + /*Static mode drives the load current directly, every other mode runs a wave + and has to be stopped through the wave state*/ + if(loadModeGet() == LoadModeStatic) + { + emit sigLoadCurrentStatusChanged(false); + return; + } + + emit sigLoadWaveStatusChanged(false); +} + void EnergyControlWnd::underVoltageStatusSet(bool enabled) { QString color = enabled ? "rgba(255, 0, 0, 255)" : "rgba(128, 128, 128, 180)"; @@ -1197,7 +1273,7 @@ void EnergyControlWnd::underVoltageStatusSet(bool enabled) QMessageBox *msgBox = new QMessageBox(this); msgBox->setIcon(QMessageBox::Warning); msgBox->setWindowTitle("Warning"); - emit sigLoadCurrentStatusChanged(false); + loadStopRequest(); msgBox->setText("Load disabled due to under voltage protection"); msgBox->setStandardButtons(QMessageBox::Ok); msgBox->show(); @@ -1244,7 +1320,7 @@ void EnergyControlWnd::overVoltageStatusSet(bool enabled) msgBox->setWindowTitle("Warning"); if(loadStartStopStatus) { - emit sigLoadCurrentStatusChanged(false); + loadStopRequest(); msgBox->setText("Load disabled due to over voltage protection"); } if(chargingStartStopStatus) @@ -1271,7 +1347,7 @@ void EnergyControlWnd::overCurrentStatusSet(bool enabled) msgBox->setWindowTitle("Warning"); if(loadStartStopStatus) { - emit sigLoadCurrentStatusChanged(false); + loadStopRequest(); msgBox->setText("Load disabled due to over current protection"); } if(chargingStartStopStatus) @@ -1290,7 +1366,7 @@ void EnergyControlWnd::loadStatusSet(bool enabled) statusControlLabels["LoadDisable"]->setText("Load " + QString(enabled ? "Enabled" : "Disabled")); if(!loadStatus && loadStartStopStatus) { - emit sigLoadCurrentStatusChanged(false); + loadStopRequest(); } // Red background when disabled @@ -1356,6 +1432,8 @@ bool EnergyControlWnd::loadCurrentStatusSet(bool status) loadComboBoxEdits["Mode"]->setEnabled(false); standardWaveWidget->setEnabled(false); customWaveWidget->setEnabled(false); + batParamWidget->setEnabled(false); + batParamViewButton->setEnabled(loadModeGet() == LoadModeBatParamExtraction); statusControlButtons["LoadDisable"]->setEnabled(false); statusControlButtons["PPathDisable"]->setEnabled(false); @@ -1379,6 +1457,8 @@ bool EnergyControlWnd::loadCurrentStatusSet(bool status) loadComboBoxEdits["Mode"]->setEnabled(true); standardWaveWidget->setEnabled(true); customWaveWidget->setEnabled(true); + batParamWidget->setEnabled(true); + batParamViewButton->setEnabled(false); statusControlButtons["LoadDisable"]->setEnabled(true); statusControlButtons["PPathDisable"]->setEnabled(true); @@ -1801,6 +1881,8 @@ void EnergyControlWnd::onLoadModeChanged(const QString &mode) loadCurrentWidget->setVisible(mode == "Static"); standardWaveWidget->setVisible(mode == "Standard Wave"); customWaveWidget->setVisible(mode == "Custom Wave"); + batParamWidget->setVisible(mode == "Bat Param Extraction"); + batParamViewButton->setVisible(mode == "Bat Param Extraction"); } LoadMode EnergyControlWnd::loadModeGet() @@ -1808,6 +1890,7 @@ LoadMode EnergyControlWnd::loadModeGet() QString mode = loadComboBoxEdits["Mode"]->currentText(); if(mode == "Standard Wave") return LoadModeStandardWave; if(mode == "Custom Wave") return LoadModeCustomWave; + if(mode == "Bat Param Extraction") return LoadModeBatParamExtraction; return LoadModeStatic; } @@ -1913,28 +1996,71 @@ QString EnergyControlWnd::getCustomWaveTableText(int row, int column) return item->text(); } -void EnergyControlWnd::addCustomWaveTableRow(waveform_chunk_t chunk, int row) +QTableWidget* EnergyControlWnd::createWaveTable(bool editable) +{ + QStringList header; + QTableWidget *table; + + header << "Value [mA]" << "Dev [mA]" << "Dur [ms]" << "Dev [ms]" << "Rep" << "Last" << "Marker" << "Pos"; + table = new QTableWidget(0, 8, this); + table->setHorizontalHeaderLabels(header); + table->horizontalHeaderItem(1)->setToolTip("Value deviation [mA]"); + table->horizontalHeaderItem(2)->setToolTip("Chunk duration [ms]"); + table->horizontalHeaderItem(3)->setToolTip("Duration deviation [ms]"); + table->horizontalHeaderItem(4)->setToolTip("Chunk repetitions"); + table->horizontalHeaderItem(5)->setToolTip("Last chunk in group"); + table->horizontalHeaderItem(6)->setToolTip("Energy debugger marker name (optional)"); + table->horizontalHeaderItem(7)->setToolTip("Marker position: s = chunk start, e = chunk end, s,e = both (marker \"Start name, End name\")"); + table->horizontalHeader()->setStretchLastSection(true); + table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + table->verticalHeader()->setDefaultSectionSize(22); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setMinimumHeight(100); + table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + if(!editable) + { + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + } + + return table; +} + +void EnergyControlWnd::addWaveTableRow(QTableWidget *table, waveform_chunk_t chunk, int row) { QTableWidgetItem *lastInGroupItem; if(row < 0) { - row = customWaveTable->rowCount(); + row = table->rowCount(); } - customWaveTable->insertRow(row); - customWaveTable->setItem(row, 0, createCustomWaveTableItem(QString::number(chunk.value))); - customWaveTable->setItem(row, 1, createCustomWaveTableItem(QString::number(chunk.valueDev))); - customWaveTable->setItem(row, 2, createCustomWaveTableItem(QString::number(chunk.duration))); - customWaveTable->setItem(row, 3, createCustomWaveTableItem(QString::number(chunk.durationDev))); - customWaveTable->setItem(row, 4, createCustomWaveTableItem(QString::number(chunk.repetitions))); + table->insertRow(row); + table->setItem(row, 0, createCustomWaveTableItem(QString::number(chunk.value))); + table->setItem(row, 1, createCustomWaveTableItem(QString::number(chunk.valueDev))); + table->setItem(row, 2, createCustomWaveTableItem(QString::number(chunk.duration))); + table->setItem(row, 3, createCustomWaveTableItem(QString::number(chunk.durationDev))); + table->setItem(row, 4, createCustomWaveTableItem(QString::number(chunk.repetitions))); lastInGroupItem = new QTableWidgetItem(); lastInGroupItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable); lastInGroupItem->setCheckState(chunk.lastInGroup ? Qt::Checked : Qt::Unchecked); lastInGroupItem->setTextAlignment(Qt::AlignCenter); - customWaveTable->setItem(row, 5, lastInGroupItem); - customWaveTable->setItem(row, 6, createCustomWaveTableItem(chunk.marker)); - customWaveTable->setItem(row, 7, createCustomWaveTableItem(Waveform::chunkHasMarker(chunk) ? chunk.markerPos : "")); + table->setItem(row, 5, lastInGroupItem); + table->setItem(row, 6, createCustomWaveTableItem(chunk.marker)); + table->setItem(row, 7, createCustomWaveTableItem(Waveform::chunkHasMarker(chunk) ? chunk.markerPos : "")); +} + +void EnergyControlWnd::fillWaveTable(QTableWidget *table, Waveform wave) +{ + table->setRowCount(0); + for(int i = 0; i < wave.chunks.size(); i++) + { + addWaveTableRow(table, wave.chunks[i], -1); + } +} + +void EnergyControlWnd::addCustomWaveTableRow(waveform_chunk_t chunk, int row) +{ + addWaveTableRow(customWaveTable, chunk, row); } void EnergyControlWnd::fillCustomWaveTable(Waveform wave) @@ -1999,6 +2125,181 @@ void EnergyControlWnd::updateCustomWaveInfo() QString::number(WAVEFORM_CHUNK_MAX_NO) + " chunks)"); } +/* -------------------- Battery parameter extraction --------------------- */ +bool EnergyControlWnd::batParamCompute(unsigned int *pulseDuration, int *repetitions, QString *error) +{ + bool capacityOk; + bool accuracyOk; + bool currentOk; + double capacity = loadEntryEdits["Battery Capacity"]->text().toDouble(&capacityOk); + double accuracy = loadEntryEdits["Extraction Accuracy"]->text().toDouble(&accuracyOk); + double current = loadEntryEdits["Maximum Current"]->text().toDouble(¤tOk); + unsigned int pause = loadEntryEdits["Pause Duration"]->text().toUInt(); + unsigned int guard = loadEntryEdits["Marker Guard"]->text().toUInt(); + double duration; + + if(!capacityOk || capacity <= 0) + { + if(error != NULL) *error = "Battery capacity must be greater than 0 mAh"; + return false; + } + if(!accuracyOk || accuracy <= 0 || accuracy > 100) + { + if(error != NULL) *error = "Extraction accuracy must be between 0 and 100 %"; + return false; + } + if(!currentOk || current <= 0) + { + if(error != NULL) *error = "Maximum current must be greater than 0 mA"; + return false; + } + + /*Charge extracted by a single pulse [mAh] divided by pulse current [mA] gives pulse duration in hours*/ + duration = (capacity * accuracy / 100.0) / current * 3600000.0; + + if(duration < 1.0) + { + if(error != NULL) *error = "Calculated pulse is shorter than 1 ms, decrease maximum current or increase accuracy"; + return false; + } + if(duration > (double)UINT_MAX) + { + if(error != NULL) *error = "Calculated pulse is too long, increase maximum current"; + return false; + } + + if(guard < 1) + { + if(error != NULL) *error = "Marker guard must be at least 1 ms"; + return false; + } + if(duration <= (double)guard) + { + if(error != NULL) *error = "Calculated pulse is shorter than the marker guard, decrease maximum current or increase accuracy"; + return false; + } + if(pause <= guard) + { + if(error != NULL) *error = "Pause duration must be longer than the marker guard"; + return false; + } + + if(pulseDuration != NULL) *pulseDuration = (unsigned int)qRound(duration); + if(repetitions != NULL) *repetitions = qRound(100.0 / accuracy); + + return true; +} + +Waveform EnergyControlWnd::buildBatParamWave() +{ + Waveform wave; + waveform_chunk_t pulseHead = Waveform::chunkDefault(); + waveform_chunk_t pulse = Waveform::chunkDefault(); + waveform_chunk_t pauseHead = Waveform::chunkDefault(); + waveform_chunk_t pause = Waveform::chunkDefault(); + unsigned int pulseDuration; + unsigned int pauseDuration; + unsigned int guard; + int repetitions; + + if(!batParamCompute(&pulseDuration, &repetitions, NULL)) return wave; + + pauseDuration = loadEntryEdits["Pause Duration"]->text().toUInt(); + guard = loadEntryEdits["Marker Guard"]->text().toUInt(); + + wave.name = "BatParam " + loadEntryEdits["Battery Capacity"]->text() + "mAh " + + loadEntryEdits["Extraction Accuracy"]->text() + "%"; + wave.type = WAVEFORM_TYPE_CUSTOM; + wave.origin = WAVEFORM_ORIGIN_USER; + + /*With the adaptive relaxation the device runs one pulse and one pause per pass, + the application ends the pause as soon as the voltage settles and starts the + next pass, so the repetitions are counted on the application side*/ + wave.repetitionCounter = batParamRelaxationEnabled() ? 1 : repetitions; + + /*Every level starts with an unmarked guard chunk, so all four markers land + inside a settled level instead of on the current step itself. The load + needs a few samples to reach the new level, and a marker placed on the + step would still read the previous level*/ + pulseHead.value = loadEntryEdits["Maximum Current"]->text().toUInt(); + pulseHead.duration = guard; + pulseHead.repetitions = 1; + pulseHead.lastInGroup = false; + + pulse.value = pulseHead.value; + pulse.duration = pulseDuration - guard; + pulse.repetitions = 1; + pulse.lastInGroup = false; + pulse.marker = "Pulse Start, Pulse End"; + pulse.markerPos = "s,e"; + + pauseHead.value = 0; + pauseHead.duration = guard; + pauseHead.repetitions = 1; + pauseHead.lastInGroup = false; + + pause.value = 0; + pause.duration = pauseDuration - guard; + pause.repetitions = 1; + pause.lastInGroup = true; + pause.marker = "Pause Start, Pause End"; + pause.markerPos = "s,e"; + + wave.chunks.append(pulseHead); + wave.chunks.append(pulse); + wave.chunks.append(pauseHead); + wave.chunks.append(pause); + + return wave; +} + +void EnergyControlWnd::updateBatParamInfo() +{ + QString error; + Waveform wave; + unsigned int pulseDuration; + int repetitions; + + if(!batParamCompute(&pulseDuration, &repetitions, &error)) + { + batParamInfoLabel->setText(error); + batParamTable->setRowCount(0); + return; + } + + wave = buildBatParamWave(); + fillWaveTable(batParamTable, wave); + + QString info = QString::number(pulseDuration) + " ms pulse, " + + QString::number(wave.getTotalDuration()) + " ms per pass, " + + QString::number(repetitions) + " passes, " + + QString::number((double)repetitions * wave.getTotalDuration() / 60000.0, 'f', 1) + " min total"; + + if(batParamRelaxationEnabled()) + { + info += " (maximum, pause ends on dV/dt below " + loadEntryEdits["Relax Threshold"]->text() + + " mV in " + loadEntryEdits["Relax Window"]->text() + " s)"; + } + + batParamInfoLabel->setText(info); +} + +bool EnergyControlWnd::batParamRelaxationEnabled() +{ + if(batParamRelaxCombo == NULL) return false; + return batParamRelaxCombo->currentIndex() == 1; +} + +void EnergyControlWnd::onBatParamChanged() +{ + bool adaptive = batParamRelaxationEnabled(); + + loadEntryEdits["Relax Threshold"]->setEnabled(adaptive); + loadEntryEdits["Relax Window"]->setEnabled(adaptive); + + updateBatParamInfo(); +} + void EnergyControlWnd::onCustomWaveTableChanged() { if(customWaveTableUpdating) return; @@ -2132,6 +2433,17 @@ void EnergyControlWnd::onLoadWaveSave() return; } break; + case LoadModeBatParamExtraction: + { + QString error; + if(!batParamCompute(NULL, NULL, &error)) + { + QMessageBox::warning(this, "Save wave", error); + return; + } + } + wave = buildBatParamWave(); + break; case LoadModeStatic: default: return; @@ -2215,6 +2527,29 @@ void EnergyControlWnd::onLoadSet() loadActiveWave = wave; emit sigLoadWaveChanged(wave); break; + case LoadModeBatParamExtraction: + { + QString error; + if(!batParamCompute(NULL, NULL, &error)) + { + QMessageBox::warning(this, "Warning", error); + return; + } + } + wave = buildBatParamWave(); + if(!waveMarkersConfirm(wave)) return; + loadActiveWave = wave; + emit sigBatParamCapacityChanged(loadEntryEdits["Battery Capacity"]->text().toDouble()); + { + int repetitions = 1; + batParamCompute(NULL, &repetitions, NULL); + emit sigBatParamRelaxationChanged(batParamRelaxationEnabled(), + loadEntryEdits["Relax Threshold"]->text().toDouble(), + loadEntryEdits["Relax Window"]->text().toDouble(), + repetitions); + } + emit sigLoadWaveChanged(wave); + break; case LoadModeCustomWave: wave = buildCustomWave(); if(!wave.isValid()) diff --git a/Source/Windows/Device/energycontrolwnd.h b/Source/Windows/Device/energycontrolwnd.h index 9ba5aa6..e095673 100644 --- a/Source/Windows/Device/energycontrolwnd.h +++ b/Source/Windows/Device/energycontrolwnd.h @@ -34,7 +34,8 @@ enum LoadMode { LoadModeStatic = 0, LoadModeStandardWave, - LoadModeCustomWave + LoadModeCustomWave, + LoadModeBatParamExtraction }; enum Mode { @@ -145,6 +146,9 @@ class EnergyControlWnd : public QWidget void sigLoadWaveChanged(Waveform wave); void sigLoadWaveStatusChanged(bool start); void sigLoadWaveClear(); + void sigBatParamViewRequested(); + void sigBatParamCapacityChanged(double capacity); + void sigBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes); void sigChargingCurrentChanged(unsigned int newCurrent); void sigChargingTermVoltageChanged(float newVoltage); @@ -168,6 +172,7 @@ private slots: void onLoadWaveSave(); void onCustomWaveDeleteFromLibrary(); void onCustomWaveTableChanged(); + void onBatParamChanged(); void onLoadStatusChanged(); void onPPathStatusChanged(); @@ -238,11 +243,16 @@ private slots: QWidget *loadCurrentWidget; // for Static mode QWidget *standardWaveWidget; // for Standard wave mode QWidget *customWaveWidget; // for Custom wave mode + QWidget *batParamWidget; // for Battery parameter extraction mode QTableWidget *customWaveTable; QComboBox *customWaveLibraryCombo; QLineEdit *customWaveNameEdit; QLabel *stdWaveInfoLabel; QLabel *customWaveInfoLabel; + QTableWidget *batParamTable; + QPushButton *batParamViewButton; + QComboBox *batParamRelaxCombo; + QLabel *batParamInfoLabel; QMap customWaveButtons; Waveform loadActiveWave; bool epEnabled; @@ -287,9 +297,17 @@ private slots: QWidget* chdischTab; bool waveMarkersConfirm(Waveform wave); + void loadStopRequest(); Waveform buildStandardWave(); Waveform buildCustomWave(); + Waveform buildBatParamWave(); + bool batParamCompute(unsigned int *pulseDuration, int *repetitions, QString *error); + bool batParamRelaxationEnabled(); + void updateBatParamInfo(); void fillCustomWaveTable(Waveform wave); + QTableWidget* createWaveTable(bool editable); + void fillWaveTable(QTableWidget *table, Waveform wave); + void addWaveTableRow(QTableWidget *table, waveform_chunk_t chunk, int row); void addCustomWaveTableRow(waveform_chunk_t chunk, int row); QTableWidgetItem* createCustomWaveTableItem(QString text); QString getCustomWaveTableText(int row, int column); diff --git a/Source/Windows/Plot/plot.cpp b/Source/Windows/Plot/plot.cpp index 241524a..5229d37 100644 --- a/Source/Windows/Plot/plot.cpp +++ b/Source/Windows/Plot/plot.cpp @@ -24,6 +24,8 @@ Plot::Plot(int mw, int mh, bool aEnableTracking, QWidget *parent) plot->addGraph(); // blue line plot->graph(0)->setPen(QPen(QColor(40, 110, 255))); + overlayGraph = NULL; + keyMarkerGraph = NULL; plot->setInteraction(QCP::iSelectPlottables, true); plot->setInteraction(QCP::iRangeDrag, true); plot->setInteraction(QCP::iRangeZoom, true); @@ -301,6 +303,123 @@ void Plot::scatterReplotDataWithName() epDataName.removeAt(i); } } +void Plot::overlaySetData(QVector data, QVector keys, QColor color) +{ + if(overlayGraph == NULL) + { + overlayGraph = plot->addGraph(); + overlayGraph->setPen(QPen(color, 2, Qt::DashLine)); + } + else + { + overlayGraph->setPen(QPen(color, 2, Qt::DashLine)); + } + + overlayGraph->setData(keys, data, true); + plot->replot(); +} + +void Plot::overlayClear() +{ + if(overlayGraph == NULL) return; + + overlayGraph->data()->clear(); + plot->replot(); +} + +void Plot::markerAddAtKey(double key, double value, QString name, QColor color) +{ + QCPItemText *textLabel; + + if(keyMarkerGraph == NULL) + { + keyMarkerGraph = plot->addGraph(); + keyMarkerGraph->setLineStyle(QCPGraph::lsNone); + keyMarkerGraph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, color, 10)); + } + + keyMarkerGraph->addData(key, value); + + textLabel = new QCPItemText(plot); + textLabel->setPositionAlignment(Qt::AlignHCenter | Qt::AlignTop); + textLabel->position->setType(QCPItemPosition::ptPlotCoords); + textLabel->position->setCoords(key, value); + textLabel->setText(name); + textLabel->setFont(*scatterFont); + textLabel->setColor(color.darker(150)); + textLabel->setClipToAxisRect(false); + keyMarkerText.push_back(textLabel); + + plot->replot(); +} + +void Plot::applyStyle(QString fontFamily, int labelFontSize, int tickFontSize, + int lineWidth, bool gridVisible, bool minorGridVisible) +{ + QFont labelFont(fontFamily, labelFontSize); + QFont tickFont(fontFamily, tickFontSize); + QCPAxis *axes[2]; + + axes[0] = plot->xAxis; + axes[1] = plot->yAxis; + + for(int i = 0; i < 2; i++) + { + axes[i]->setLabelFont(labelFont); + axes[i]->setTickLabelFont(tickFont); + axes[i]->grid()->setVisible(gridVisible); + axes[i]->grid()->setSubGridVisible(minorGridVisible); + axes[i]->setSubTicks(minorGridVisible); + } + + for(int i = 0; i < plot->graphCount(); i++) + { + QPen pen = plot->graph(i)->pen(); + + if(plot->graph(i)->lineStyle() == QCPGraph::lsNone) continue; + + pen.setWidth(lineWidth); + plot->graph(i)->setPen(pen); + } + + plot->replot(); +} + +bool Plot::saveImageToSvg(QString path) +{ + QSvgGenerator generator; + QCPPainter painter; + + generator.setFileName(path); + generator.setSize(plot->size()); + generator.setViewBox(QRect(0, 0, plot->width(), plot->height())); + generator.setTitle(getTitle()); + + if(!painter.begin(&generator)) return false; + + painter.setMode(QCPPainter::pmVectorized); + painter.setMode(QCPPainter::pmNoCaching); + plot->toPainter(&painter, plot->width(), plot->height()); + painter.end(); + + return true; +} + +void Plot::markersAtKeyClear() +{ + if(keyMarkerGraph != NULL) + { + keyMarkerGraph->data()->clear(); + } + + for(int i = 0; i < keyMarkerText.size(); i++) + { + plot->removeItem(keyMarkerText[i]); + } + keyMarkerText.clear(); + plot->replot(); +} + void Plot::setData(QVector data, QVector keys) { xData = keys; diff --git a/Source/Windows/Plot/plot.h b/Source/Windows/Plot/plot.h index 7cc5a2b..8a86e1b 100644 --- a/Source/Windows/Plot/plot.h +++ b/Source/Windows/Plot/plot.h @@ -19,6 +19,13 @@ class Plot : public QWidget void scatterAddDataWithName(double value, double keys, QString name); void scatterReplotDataWithName(); void setData(QVector data, QVector keys); + void overlaySetData(QVector data, QVector keys, QColor color = Qt::red); + void overlayClear(); + void markerAddAtKey(double key, double value, QString name, QColor color); + void markersAtKeyClear(); + void applyStyle(QString fontFamily, int labelFontSize, int tickFontSize, + int lineWidth, bool gridVisible, bool minorGridVisible); + bool saveImageToSvg(QString path); void appendData(QVector data, QVector keys); void setYRange(double min, double max); void setYLabel(QString label); @@ -72,6 +79,9 @@ private slots: QVector epDataKey; QVector epDataName; QVector textData; + QCPGraph *overlayGraph; + QCPGraph *keyMarkerGraph; + QVector keyMarkerText; bool enableTracking; bool replotActive; diff --git a/Source/devicecontainer.cpp b/Source/devicecontainer.cpp index a137743..78bf4d9 100644 --- a/Source/devicecontainer.cpp +++ b/Source/devicecontainer.cpp @@ -1,4 +1,5 @@ #include +#include "Processing/batteryparamsextraction.h" #include #include "devicecontainer.h" @@ -13,6 +14,18 @@ DeviceContainer::DeviceContainer(QObject *parent, device = aDevice; log = new Log(); fileProcessing = new FileProcessing(); + batteryParamsExtraction = new BatteryParamsExtraction(this); + samplingPeriodMs = 0; + batParamRelaxEnabled = false; + batParamRelaxThreshold = 0; + batParamRelaxWindow = 0; + batParamRelaxPasses = 0; + batParamRelaxPassesDone = 0; + batParamRelaxPauseActive = false; + batParamRelaxRunning = false; + batParamRelaxRestarting = false; + batteryParamsWnd = NULL; + qRegisterMetaType("batteryparams_cycle_t"); log->assignLogWidget(deviceWnd->getLogWidget()); connect(deviceWnd->getLogDock(), SIGNAL(sigFilterChanged(int)), log, SLOT(setFilter(int))); connect(deviceWnd->getLogDock(), SIGNAL(sigClearRequested()), log, SLOT(clear())); @@ -59,6 +72,9 @@ DeviceContainer::DeviceContainer(QObject *parent, connect(deviceWnd, SIGNAL(sigLoadCurrentChanged(unsigned int)), this, SLOT(onDeviceWndLoadCurrentSetValue(unsigned int))); connect(deviceWnd, &DeviceWnd::sigLoadWaveChanged, this, &DeviceContainer::onDeviceWndLoadWaveSet); connect(deviceWnd, &DeviceWnd::sigLoadWaveStatusChanged, this, &DeviceContainer::onDeviceWndLoadWaveSetStatus); + connect(deviceWnd, &DeviceWnd::sigBatParamViewRequested, this, &DeviceContainer::onDeviceWndBatParamViewRequested); + connect(deviceWnd, &DeviceWnd::sigBatParamCapacityChanged, this, &DeviceContainer::onDeviceWndBatParamCapacityChanged); + connect(deviceWnd, &DeviceWnd::sigBatParamRelaxationChanged, this, &DeviceContainer::onDeviceWndBatParamRelaxationChanged); connect(deviceWnd, &DeviceWnd::sigLoadWaveClear, this, &DeviceContainer::onDeviceWndLoadWaveClear); connect(deviceWnd, SIGNAL(sigChargingCurrentStatusChanged(bool)), this, SLOT(onDeviceWndChargingCurrentSetStatus(bool))); connect(deviceWnd, SIGNAL(sigChargingCurrentChanged(unsigned int)), this, SLOT(onDeviceWndChargingCurrentSetValue(unsigned int))); @@ -473,6 +489,13 @@ DeviceContainer::~DeviceContainer() deviceWnd->deleteLater(); deviceWnd = nullptr; } + + if(batteryParamsWnd) + { + batteryParamsWnd->close(); + batteryParamsWnd->deleteLater(); + batteryParamsWnd = nullptr; + } } bool DeviceContainer::getDeviceName(QString *name) @@ -804,6 +827,10 @@ void DeviceContainer::onDeviceSamplingPeriodObtained(QString stime) { bool ok = deviceWnd->setSamplingPeriod(stime); + /*Device reports the sampling period in microseconds*/ + samplingPeriodMs = stime.toDouble() / 1000.0; + batteryParamsExtraction->setSamplingPeriod(samplingPeriodMs); + logResult(ok, "Sampling time successfully obtained and presented", "Unable to present sampling time"); @@ -1032,6 +1059,7 @@ void DeviceContainer::onDeviceAcquisitonStarted() { elapsedTime = 0; timer->start(1000); + batteryParamsExtraction->onReset(); } @@ -1052,6 +1080,8 @@ void DeviceContainer::onDeviceNewVoltageCurrentSamplesReceived(QVector v { deviceWnd->plotVoltageValues(voltage, voltageKeys); deviceWnd->plotCurrentValues(current, currentKeys); + batteryParamsExtraction->onNewSamplesReceived(voltage, current, voltageKeys, currentKeys); + batParamRelaxationSamplesProcess(voltage, voltageKeys); if(writeSamplesToFileEnabled) { fileProcessing->appendSampleDataQueued(voltage, voltageKeys, current, currentKeys); @@ -1095,6 +1125,7 @@ void DeviceContainer::onDeviceNewEBP(QVector ebpValues, QVector void DeviceContainer::onDeviceNewEBPFull(double value, double key, QString name) { deviceWnd->plotConsumptionEBPWithName(value, key, name); + batteryParamsExtraction->onNewMarkerReceived(value, key, name); if(writeSamplesToFileEnabled) { fileProcessing->appendEPQueued(name, key); @@ -1102,6 +1133,167 @@ void DeviceContainer::onDeviceNewEBPFull(double value, double key, QString name) log->printLogMessage(name + " (value: " + QString::number(value) + ", key: " + QString::number(key) + ")", LOG_MESSAGE_TYPE_INFO, LOG_MESSAGE_DEVICE_TYPE_DEVICE, LOG_MESSAGE_CATEGORY_ENERGY_POINT); checkAcquisitionPauseMarker(name); + batParamRelaxationMarkerProcess(name, key); +} + +void DeviceContainer::onDeviceWndBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes) +{ + batParamRelaxEnabled = enabled; + batParamRelaxThreshold = thresholdMv / 1000.0; + batParamRelaxWindow = windowS * 1000.0; + batParamRelaxPasses = passes; + + batParamRelaxationReset(); + + if(!enabled) return; + + log->printLogMessage("Battery parameters: relaxation ends on dV/dt below " + QString::number(thresholdMv) + + " mV in " + QString::number(windowS) + " s, " + QString::number(passes) + " passes", + LOG_MESSAGE_TYPE_INFO); +} + +void DeviceContainer::batParamRelaxationReset() +{ + batParamRelaxPassesDone = 0; + batParamRelaxPauseActive = false; + batParamRelaxRunning = false; + batParamRelaxRestarting = false; + batParamRelaxKeys.clear(); + batParamRelaxVoltage.clear(); +} + +void DeviceContainer::batParamRelaxationMarkerProcess(QString name, double key) +{ + Q_UNUSED(key); + + if(!batParamRelaxEnabled || !batParamRelaxRunning) return; + + if(name.trimmed().compare(BATTERYPARAMS_DEFAULT_PAUSE_START_MARKER, Qt::CaseInsensitive) == 0) + { + batParamRelaxPauseActive = true; + batParamRelaxKeys.clear(); + batParamRelaxVoltage.clear(); + return; + } + + if(name.trimmed().compare(BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER, Qt::CaseInsensitive) == 0) + { + batParamRelaxPauseActive = false; + batParamRelaxKeys.clear(); + batParamRelaxVoltage.clear(); + return; + } +} + +void DeviceContainer::batParamRelaxationSamplesProcess(QVector voltage, QVector voltageKeys) +{ + double minimum; + double maximum; + + if(!batParamRelaxEnabled || !batParamRelaxRunning || !batParamRelaxPauseActive) return; + if(batParamRelaxThreshold <= 0 || batParamRelaxWindow <= 0) return; + + for(int i = 0; i < voltage.size() && i < voltageKeys.size(); i++) + { + batParamRelaxKeys.append(voltageKeys[i]); + batParamRelaxVoltage.append(voltage[i]); + } + + if(batParamRelaxKeys.isEmpty()) return; + + while((batParamRelaxKeys.size() > 1) && + ((batParamRelaxKeys.last() - batParamRelaxKeys.first()) > batParamRelaxWindow)) + { + batParamRelaxKeys.removeFirst(); + batParamRelaxVoltage.removeFirst(); + } + + /*The pause can only be ended once the collected samples cover the whole window*/ + if((batParamRelaxKeys.last() - batParamRelaxKeys.first()) < batParamRelaxWindow) return; + + minimum = batParamRelaxVoltage.first(); + maximum = batParamRelaxVoltage.first(); + + for(int i = 1; i < batParamRelaxVoltage.size(); i++) + { + if(batParamRelaxVoltage[i] < minimum) minimum = batParamRelaxVoltage[i]; + if(batParamRelaxVoltage[i] > maximum) maximum = batParamRelaxVoltage[i]; + } + + if((maximum - minimum) > batParamRelaxThreshold) return; + + batParamRelaxationFinish(batParamRelaxKeys.last()); +} + +void DeviceContainer::batParamRelaxationFinish(double key) +{ + QString marker = BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER; + double step = samplingPeriodMs; + double index; + + /*Markers are addressed by the sample index while the collected keys are times, + so the step between two samples is needed to place the marker*/ + if((step <= 0) && (batParamRelaxKeys.size() > 1)) + { + step = batParamRelaxKeys[batParamRelaxKeys.size() - 1] - batParamRelaxKeys[batParamRelaxKeys.size() - 2]; + } + + if(step <= 0) + { + log->printLogMessage("Battery parameters: unknown sampling period, unable to place pause end marker", LOG_MESSAGE_TYPE_ERROR); + index = 0; + } + else + { + index = qRound(key / step); + } + + batParamRelaxPauseActive = false; + batParamRelaxKeys.clear(); + batParamRelaxVoltage.clear(); + + /*The device is stopped in the middle of the pause chunk, so it never sends the + pause end marker. The marker is created here instead, at the moment the voltage + settled, so that the extraction and the stored energy point file stay complete*/ + deviceWnd->plotConsumptionEBPWithName(0, index, marker); + batteryParamsExtraction->onNewMarkerReceived(0, index, marker); + if(writeSamplesToFileEnabled) fileProcessing->appendEPQueued(marker, (int)index); + + log->printLogMessage("Battery parameters: pass " + QString::number(batParamRelaxPassesDone + 1) + "/" + + QString::number(batParamRelaxPasses) + " relaxed, pause ended early", + LOG_MESSAGE_TYPE_INFO, LOG_MESSAGE_DEVICE_TYPE_DEVICE, LOG_MESSAGE_CATEGORY_ENERGY_POINT); + + batParamRelaxRestarting = true; + if(!device->setLoadWaveState(false)) + { + log->printLogMessage("Unable to stop load wave", LOG_MESSAGE_TYPE_ERROR); + batParamRelaxRestarting = false; + return; + } + + batParamRelaxationNextPass(); +} + +void DeviceContainer::batParamRelaxationNextPass() +{ + batParamRelaxRestarting = false; + batParamRelaxPassesDone++; + + if(batParamRelaxPassesDone >= batParamRelaxPasses) + { + batParamRelaxRunning = false; + deviceWnd->loadWaveStopped(); + log->printLogMessage("Battery parameters: all " + QString::number(batParamRelaxPasses) + " passes done", + LOG_MESSAGE_TYPE_INFO); + return; + } + + if(!device->setLoadWaveState(true)) + { + batParamRelaxRunning = false; + deviceWnd->loadWaveStopped(); + log->printLogMessage("Unable to start next battery parameters pass", LOG_MESSAGE_TYPE_ERROR); + } } void DeviceContainer::checkAcquisitionPauseMarker(QString name) @@ -1377,12 +1569,65 @@ void DeviceContainer::onDeviceWndLoadWaveSet(Waveform wave) "Unable to set load wave \"" + wave.name + "\""); } +void DeviceContainer::onBatParamSettingsChanged(batteryparams_settings_t settings) +{ + batteryParamsExtraction->setSettings(settings); +} + +void DeviceContainer::onDeviceWndBatParamCapacityChanged(double capacity) +{ + batteryparams_settings_t settings = batteryParamsExtraction->getSettings(); + + settings.capacity = capacity; + batteryParamsExtraction->setSettings(settings); + + if(batteryParamsWnd != NULL) batteryParamsWnd->setSettings(settings); +} + +void DeviceContainer::onDeviceWndBatParamViewRequested() +{ + if(batteryParamsWnd == NULL) + { + batteryParamsWnd = new BatteryParamsWnd(); + batteryParamsWnd->setAttribute(Qt::WA_QuitOnClose, false); + batteryParamsWnd->setProfileName(consumptionProfileName); + batteryParamsWnd->setSettings(batteryParamsExtraction->getSettings()); + + connect(batteryParamsExtraction, &BatteryParamsExtraction::sigCycleStarted, + batteryParamsWnd, &BatteryParamsWnd::onCycleStarted, Qt::QueuedConnection); + connect(batteryParamsExtraction, &BatteryParamsExtraction::sigCycleFinished, + batteryParamsWnd, &BatteryParamsWnd::onCycleFinished, Qt::QueuedConnection); + connect(batteryParamsWnd, &BatteryParamsWnd::sigSettingsChanged, + this, &DeviceContainer::onBatParamSettingsChanged); + } + + batteryParamsWnd->show(); + batteryParamsWnd->raise(); + batteryParamsWnd->activateWindow(); +} + void DeviceContainer::onDeviceWndLoadWaveSetStatus(bool status) { QString statusStr = status ? "Started" : "Stopped"; bool ok = device->setLoadWaveState(status); + if(ok) + { + batParamRelaxationReset(); + batParamRelaxRunning = (status && batParamRelaxEnabled); + } + + if(ok && status) + { + batteryParamsExtraction->onReset(); + if(batteryParamsWnd != NULL) + { + batteryParamsWnd->setProfileName(consumptionProfileName); + batteryParamsWnd->onClear(); + } + } + if(ok) deviceWnd->setLoadCurrentStatus(status); @@ -1393,7 +1638,21 @@ void DeviceContainer::onDeviceWndLoadWaveSetStatus(bool status) void DeviceContainer::onDeviceLoadWaveStopped() { - bool ok = deviceWnd->loadWaveStopped(); + bool ok; + + if(batParamRelaxRestarting) return; + + if(batParamRelaxEnabled && batParamRelaxRunning) + { + log->printLogMessage("Battery parameters: pass " + QString::number(batParamRelaxPassesDone + 1) + "/" + + QString::number(batParamRelaxPasses) + " finished on maximum pause", + LOG_MESSAGE_TYPE_INFO); + batParamRelaxPauseActive = false; + batParamRelaxationNextPass(); + return; + } + + ok = deviceWnd->loadWaveStopped(); logResult(ok, "Load wave completed", diff --git a/Source/devicecontainer.h b/Source/devicecontainer.h index 5972180..3e2dc9a 100644 --- a/Source/devicecontainer.h +++ b/Source/devicecontainer.h @@ -10,6 +10,8 @@ #include "Utility/log.h" #include "Processing/fileprocessing.h" #include "Processing/charginganalysis.h" +#include "Processing/batteryparamsextraction.h" +#include "Windows/BatteryParams/batteryparamswnd.h" #include "Processing/Parameters/applicationparameters.h" class DeviceContainer : public QObject @@ -58,6 +60,10 @@ public slots: void onDeviceWndLoadCurrentSetStatus(bool status); void onDeviceWndLoadWaveSet(Waveform wave); void onDeviceWndLoadWaveSetStatus(bool status); + void onDeviceWndBatParamViewRequested(); + void onDeviceWndBatParamCapacityChanged(double capacity); + void onDeviceWndBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes); + void onBatParamSettingsChanged(batteryparams_settings_t settings); void onDeviceWndLoadWaveClear(); void onDeviceLoadWaveStopped(); void onDeviceWndChargingCurrentSetValue(unsigned int current); @@ -140,9 +146,28 @@ private slots: Device* device; Log* log; FileProcessing* fileProcessing; + BatteryParamsExtraction* batteryParamsExtraction; + BatteryParamsWnd* batteryParamsWnd; void fillDeviceSetFunctions(); void checkAcquisitionPauseMarker(QString name); + void batParamRelaxationReset(); + void batParamRelaxationMarkerProcess(QString name, double key); + void batParamRelaxationSamplesProcess(QVector voltage, QVector voltageKeys); + void batParamRelaxationFinish(double key); + void batParamRelaxationNextPass(); + + bool batParamRelaxEnabled; + double batParamRelaxThreshold; + double batParamRelaxWindow; + int batParamRelaxPasses; + int batParamRelaxPassesDone; + bool batParamRelaxPauseActive; + bool batParamRelaxRunning; + bool batParamRelaxRestarting; + double samplingPeriodMs; + QVector batParamRelaxKeys; + QVector batParamRelaxVoltage; bool createSubDir(const QString &subDirName, QString &fullPath); void logResult(bool status, const QString& successMsg, const QString& errorMsg); From f412a3d1f5d6e600084e2f1998d63dbb0cee152f Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Sat, 3 Oct 2026 12:19:58 +0200 Subject: [PATCH 6/8] Battery extraction parameters --- .../Processing/Parameters/deviceparamdefs.cpp | 42 +++ Source/Processing/batteryparamsextraction.cpp | 265 ++++++++++++++++-- Source/Processing/batteryparamsextraction.h | 13 +- Source/Processing/calibrationdata.cpp | 7 +- Source/Processing/calibrationdata.h | 2 + .../Windows/BatteryParams/batterycyclewnd.cpp | 139 ++++++--- .../Windows/BatteryParams/batterycyclewnd.h | 12 + .../batteryparamsplotsettings.cpp | 31 ++ .../BatteryParams/batteryparamsplotsettings.h | 11 + .../batteryparamssettingsdlg.cpp | 31 +- .../BatteryParams/batteryparamssettingsdlg.h | 3 + .../BatteryParams/batteryparamswnd.cpp | 184 +++++++++++- .../Windows/BatteryParams/batteryparamswnd.h | 10 + Source/Windows/Device/calibrationwnd.cpp | 4 + Source/Windows/Device/calibrationwnd.ui | 182 ++++++++++++ Source/Windows/Device/devicewnd.cpp | 12 + Source/Windows/Device/devicewnd.h | 4 +- Source/Windows/Device/energycontrolwnd.cpp | 46 +-- Source/Windows/Device/energycontrolwnd.h | 2 +- Source/Windows/Plot/plot.cpp | 43 +++ Source/Windows/Plot/plot.h | 7 + Source/device.cpp | 18 +- Source/devicecontainer.cpp | 180 +++++++++--- Source/devicecontainer.h | 11 +- 24 files changed, 1133 insertions(+), 126 deletions(-) diff --git a/Source/Processing/Parameters/deviceparamdefs.cpp b/Source/Processing/Parameters/deviceparamdefs.cpp index 5565259..7cead10 100644 --- a/Source/Processing/Parameters/deviceparamdefs.cpp +++ b/Source/Processing/Parameters/deviceparamdefs.cpp @@ -627,6 +627,48 @@ QList defaultParams() 0, false }, + { + { + "loadDacOffset", + "Load DAC Offset", + "Offset added to the requested load current before it is converted to a DAC voltage, compensates the DAC offset circuit", + "mA", + DeviceParamDefs::DeviceConfig, + DeviceParamDefs::Calibration, + Params::Access::ReadOnly, + Params::Storage::SaveOnly, + Params::Target::Device, + {}, + {}, + {}, + {}, + true, + 20 + }, + 0, + false + }, + { + { + "loadDacCor", + "Load DAC Correction", + "Requested load current is multiplied with this factor before the DAC offset is added, compensates gain of the load hardware", + "", + DeviceParamDefs::DeviceConfig, + DeviceParamDefs::Calibration, + Params::Access::ReadOnly, + Params::Storage::SaveOnly, + Params::Target::Device, + {}, + {}, + {}, + {}, + true, + 21 + }, + 0, + false + }, { { "shuntValue", diff --git a/Source/Processing/batteryparamsextraction.cpp b/Source/Processing/batteryparamsextraction.cpp index 63bbcce..1fb0e48 100644 --- a/Source/Processing/batteryparamsextraction.cpp +++ b/Source/Processing/batteryparamsextraction.cpp @@ -39,6 +39,7 @@ batteryparams_settings_t BatteryParamsExtraction::settingsDefault() defaults.relaxationThreshold = BATTERYPARAMS_RELAX_THRESHOLD_DEFAULT; defaults.relaxationWindow = BATTERYPARAMS_RELAX_WINDOW_DEFAULT; + defaults.relaxationFilter = BATTERYPARAMS_RELAX_FILTER_DEFAULT; defaults.model = BATTERYPARAMS_MODEL_SECOND_ORDER; defaults.fitEnd = BATTERYPARAMS_FIT_END_PAUSE; defaults.tauGridPoints = BATTERYPARAMS_TAU_GRID_POINTS; @@ -46,6 +47,7 @@ batteryparams_settings_t BatteryParamsExtraction::settingsDefault() defaults.tauSeparation = BATTERYPARAMS_TAU_SEPARATION; defaults.capacity = 0; defaults.initialSoc = BATTERYPARAMS_INITIAL_SOC_DEFAULT; + defaults.plotDecimation = BATTERYPARAMS_CYCLE_PLOT_DECIMATION_DEFAULT; return defaults; } @@ -79,32 +81,33 @@ batteryparams_settings_t BatteryParamsExtraction::getSettings() static int prvBATTERYPARAMS_RelaxationPositionGet(const QVector &keys, const QVector &voltage, int start, int end, double threshold, double window) { - QVector suffixMaximum(end - start + 1); - QVector suffixMinimum(end - start + 1); + QVector maximumPositions; + QVector minimumPositions; + int first = start; if(end < start) return -1; - for(int i = end; i >= start; i--) + /*Cell counts as relaxed at the first sample whose preceding window stayed inside + the threshold. Running maximum and minimum of that window are held in two + monotonic queues, so the whole pause is scanned once*/ + for(int i = start; i <= end; i++) { - int position = i - start; + while(!maximumPositions.isEmpty() && (voltage[maximumPositions.last()] <= voltage[i])) maximumPositions.removeLast(); + maximumPositions.append(i); + + while(!minimumPositions.isEmpty() && (voltage[minimumPositions.last()] >= voltage[i])) minimumPositions.removeLast(); + minimumPositions.append(i); - if(i == end) + while((keys[i] - keys[first]) > window) { - suffixMaximum[position] = voltage[i]; - suffixMinimum[position] = voltage[i]; - continue; + if(maximumPositions.first() == first) maximumPositions.removeFirst(); + if(minimumPositions.first() == first) minimumPositions.removeFirst(); + first++; } - suffixMaximum[position] = qMax(voltage[i], suffixMaximum[position + 1]); - suffixMinimum[position] = qMin(voltage[i], suffixMinimum[position + 1]); - } - - for(int i = start; i <= end; i++) - { - if((keys[end] - keys[i]) < window) break; - if((suffixMaximum[i - start] - suffixMinimum[i - start]) > threshold) continue; + if((keys[i] - keys[start]) < window) continue; - return i; + if((voltage[maximumPositions.first()] - voltage[minimumPositions.first()]) <= threshold) return i; } return -1; @@ -233,14 +236,50 @@ bool BatteryParamsExtraction::analyzeCycle(batteryparams_cycle_t *cycle, battery if((pauseStart < 0) || (pauseEnd <= pauseStart)) return false; - relaxPosition = prvBATTERYPARAMS_RelaxationPositionGet(cycle->plotKeys, cycle->plotVoltage, - pauseStart, pauseEnd, threshold, window); + /*Relaxation is searched over an evenly spaced series of averaged samples. The + plotted waveform keeps the extremes of every bucket instead, and a time based + filter over those uneven pairs does not give the same result as the filter the + application runs on the live samples*/ + const QVector &searchKeys = cycle->relaxKeys.isEmpty() ? cycle->plotKeys : cycle->relaxKeys; + const QVector &searchVoltage = cycle->relaxKeys.isEmpty() ? cycle->plotVoltage : cycle->relaxVoltage; + QVector smoothedVoltage; + int searchStart = -1; + int searchEnd = -1; + + for(int i = 0; i < searchKeys.size(); i++) + { + if((searchStart < 0) && (searchKeys[i] >= cycle->pauseStartKey)) searchStart = i; + if(searchKeys[i] <= cycle->pauseEndKey) searchEnd = i; + } + + if((searchStart < 0) || (searchEnd <= searchStart)) return false; + + if(searchKeys.size() != searchVoltage.size()) return false; + + seriesSmooth(searchKeys, searchVoltage, settings.relaxationFilter, &smoothedVoltage); + + if(smoothedVoltage.size() != searchKeys.size()) return false; + + relaxPosition = prvBATTERYPARAMS_RelaxationPositionGet(searchKeys, smoothedVoltage, + searchStart, searchEnd, threshold, window); if(relaxPosition < 0) return false; cycle->relaxationReached = true; - cycle->relaxationKey = cycle->plotKeys[relaxPosition]; - cycle->relaxationVoltage = cycle->plotVoltage[relaxPosition]; + cycle->relaxationKey = searchKeys[relaxPosition]; + cycle->relaxationVoltage = searchVoltage[relaxPosition]; + + /*Fit works on the plotted waveform, so the relaxation point is taken back to it*/ + relaxPosition = pauseEnd; + + for(int i = pauseStart; i <= pauseEnd; i++) + { + if(cycle->plotKeys[i] >= cycle->relaxationKey) + { + relaxPosition = i; + break; + } + } /*Cell is at rest once it relaxed, so the voltage measured there is its open circuit voltage*/ @@ -476,6 +515,8 @@ void BatteryParamsExtraction::cycleReset() activeCycle.plotKeys.clear(); activeCycle.plotVoltage.clear(); activeCycle.plotCurrent.clear(); + activeCycle.relaxKeys.clear(); + activeCycle.relaxVoltage.clear(); } void BatteryParamsExtraction::storeCycleSamples() @@ -508,12 +549,41 @@ void BatteryParamsExtraction::storeCycleSamples() activeCycle.socValid = true; } - stride = samplesNo / BATTERYPARAMS_CYCLE_PLOT_POINTS_MAX; - if(stride < 1) stride = 1; + /*Relaxation search needs evenly spaced averages that are not affected by how + much the stored waveform is decimated. Buckets are kept well below the + relaxation filter, otherwise averaging averages shifts the point*/ + int relaxStride = 1; + + if((settings.relaxationFilter > 0) && (samplingPeriod > 0)) + { + relaxStride = (int)((settings.relaxationFilter / 20.0) / samplingPeriod); + } + + if(relaxStride < 1) relaxStride = 1; + + for(int i = startPosition; i <= endPosition; i += relaxStride) + { + double keySum = 0; + double voltageSum = 0; + int no = 0; + + for(int k = i; (k <= endPosition) && (k < (i + relaxStride)); k++) + { + keySum += keySamples[k]; + voltageSum += voltageSamples[k]; + no++; + } + + if(no == 0) continue; + + activeCycle.relaxKeys.append(keySum / (double)no); + activeCycle.relaxVoltage.append(voltageSum / (double)no); + } + + /*Stored waveform keeps every n-th sample, with the samples around the current + step untouched because the step is what the resistance is calculated from*/ + stride = (settings.plotDecimation > 0) ? settings.plotDecimation : 1; - /*A full cycle can hold millions of samples, so the plotted waveform is - decimated. Samples around the current step are kept untouched because the - step is what the resistance is calculated from*/ for(int i = startPosition; i <= endPosition; i++) { bool inStepWindow = (keySamples[i] >= stepWindowStart) && (keySamples[i] <= stepWindowEnd); @@ -526,6 +596,149 @@ void BatteryParamsExtraction::storeCycleSamples() } } +int BatteryParamsExtraction::relaxationPositionGet(const QVector &keys, const QVector &voltage, + int start, int end, double threshold, double window) +{ + return prvBATTERYPARAMS_RelaxationPositionGet(keys, voltage, start, end, threshold, window); +} + +/*Markers reported by the device can land on the current step itself instead of on + the settled levels around it, and the resistance is then calculated across the + step. Pulse End is moved to the last sample that still carries the pulse current + and Pause Start to the first sample that already carries the pause current*/ +bool BatteryParamsExtraction::cyclePointsFit(batteryparams_cycle_t *cycle) +{ + int startPosition = -1; + int endPosition = -1; + int stepPosition = -1; + double stepDrop = 0; + double pulseLevel = 0; + double pauseLevel = 0; + double amplitude; + double tolerance; + int pulseEndPosition; + int pauseStartPosition; + int no; + + if(cycle == NULL) return false; + if(cycle->plotKeys.size() != cycle->plotCurrent.size()) return false; + + for(int i = 0; i < cycle->plotKeys.size(); i++) + { + if((startPosition < 0) && (cycle->plotKeys[i] >= cycle->pulseStartKey)) startPosition = i; + if(cycle->plotKeys[i] <= cycle->pauseEndKey) endPosition = i; + } + + if((startPosition < 0) || (endPosition <= startPosition)) return false; + + for(int i = startPosition; i < endPosition; i++) + { + double drop = cycle->plotCurrent[i] - cycle->plotCurrent[i + 1]; + + if((stepPosition < 0) || (drop > stepDrop)) + { + stepPosition = i; + stepDrop = drop; + } + } + + if(stepPosition <= startPosition) return false; + + no = 0; + for(int i = startPosition; i <= stepPosition; i++) + { + pulseLevel += cycle->plotCurrent[i]; + no++; + } + if(no == 0) return false; + pulseLevel /= (double)no; + + no = 0; + for(int i = stepPosition + 1; i <= endPosition; i++) + { + pauseLevel += cycle->plotCurrent[i]; + no++; + } + if(no == 0) return false; + pauseLevel /= (double)no; + + amplitude = pulseLevel - pauseLevel; + + if(amplitude <= 0) return false; + + tolerance = amplitude * 0.1; + + pulseEndPosition = stepPosition; + while((pulseEndPosition > startPosition) && (cycle->plotCurrent[pulseEndPosition] < (pulseLevel - tolerance))) + { + pulseEndPosition--; + } + + pauseStartPosition = stepPosition + 1; + while((pauseStartPosition < endPosition) && (cycle->plotCurrent[pauseStartPosition] > (pauseLevel + tolerance))) + { + pauseStartPosition++; + } + + if((cycle->plotKeys[pulseEndPosition] == cycle->pulseEndKey) && + (cycle->plotKeys[pauseStartPosition] == cycle->pauseStartKey)) + { + return false; + } + + cycle->pulseEndKey = cycle->plotKeys[pulseEndPosition]; + cycle->pulseEndVoltage = cycle->plotVoltage[pulseEndPosition]; + cycle->pulseEndCurrent = cycle->plotCurrent[pulseEndPosition]; + + cycle->pauseStartKey = cycle->plotKeys[pauseStartPosition]; + cycle->pauseStartVoltage = cycle->plotVoltage[pauseStartPosition]; + cycle->pauseStartCurrent = cycle->plotCurrent[pauseStartPosition]; + + cycle->deltaVoltage = cycle->pauseStartVoltage - cycle->pulseEndVoltage; + cycle->deltaCurrent = cycle->pulseEndCurrent - cycle->pauseStartCurrent; + cycle->resistanceValid = fabs(cycle->deltaCurrent) > 0.000001; + cycle->resistance = cycle->resistanceValid ? cycle->deltaVoltage / (cycle->deltaCurrent / 1000.0) : 0; + + return true; +} + +void BatteryParamsExtraction::seriesSmooth(const QVector &keys, const QVector &values, + double window, QVector *smoothed) +{ + double sum = 0; + int first = 0; + + if(smoothed == NULL) return; + + smoothed->clear(); + + if(keys.size() != values.size()) return; + + smoothed->reserve(values.size()); + + /*Voltage noise is compared against a threshold of a few mV, so a single sample + decides nothing. Every sample is replaced with the average of the samples that + fall inside the filter window ending at it*/ + if(window <= 0) + { + *smoothed = values; + return; + } + + for(int i = 0; i < values.size(); i++) + { + sum += values[i]; + + while((first < i) && ((keys[i] - keys[first]) > window)) + { + sum -= values[first]; + first++; + } + + smoothed->append(sum / (double)(i - first + 1)); + } +} + void BatteryParamsExtraction::onReset() { voltageSamples.clear(); diff --git a/Source/Processing/batteryparamsextraction.h b/Source/Processing/batteryparamsextraction.h index 32d2ba7..98ec8b3 100644 --- a/Source/Processing/batteryparamsextraction.h +++ b/Source/Processing/batteryparamsextraction.h @@ -10,9 +10,10 @@ #define BATTERYPARAMS_DEFAULT_PAUSE_START_MARKER "Pause Start" #define BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER "Pause End" -#define BATTERYPARAMS_CYCLE_PLOT_POINTS_MAX 4000 +#define BATTERYPARAMS_CYCLE_PLOT_DECIMATION_DEFAULT 10 #define BATTERYPARAMS_RELAX_THRESHOLD_DEFAULT 5.0 #define BATTERYPARAMS_RELAX_WINDOW_DEFAULT 60.0 +#define BATTERYPARAMS_RELAX_FILTER_DEFAULT 1000.0 #define BATTERYPARAMS_RELAX_FAST_WINDOW_DIVIDER 60.0 #define BATTERYPARAMS_SETTLING_TAU_NO 5.0 #define BATTERYPARAMS_TAU_GRID_POINTS 60 @@ -38,6 +39,7 @@ typedef struct { double relaxationThreshold; double relaxationWindow; + double relaxationFilter; /*ms*/ batteryparams_model_t model; batteryparams_fit_end_t fitEnd; int tauGridPoints; @@ -45,6 +47,7 @@ typedef struct double tauSeparation; double capacity; double initialSoc; + int plotDecimation; }batteryparams_settings_t; typedef struct @@ -92,6 +95,9 @@ typedef struct QVector fitKeys; QVector fitVoltage; + QVector relaxKeys; + QVector relaxVoltage; + QVector plotKeys; QVector plotVoltage; QVector plotCurrent; @@ -113,6 +119,11 @@ class BatteryParamsExtraction : public QObject static batteryparams_settings_t settingsDefault(); static bool analyzeCycle(batteryparams_cycle_t *cycle, batteryparams_settings_t settings); + static int relaxationPositionGet(const QVector &keys, const QVector &voltage, + int start, int end, double threshold, double window); + static bool cyclePointsFit(batteryparams_cycle_t *cycle); + static void seriesSmooth(const QVector &keys, const QVector &values, + double window, QVector *smoothed); QVector getCycles(); signals: diff --git a/Source/Processing/calibrationdata.cpp b/Source/Processing/calibrationdata.cpp index cfbaaf4..103e490 100644 --- a/Source/Processing/calibrationdata.cpp +++ b/Source/Processing/calibrationdata.cpp @@ -3,7 +3,8 @@ CalibrationData::CalibrationData(QObject *parent) : QObject{parent} { - + dacOffset = 0; + dacCorrection = 1; } CalibrationData &CalibrationData::operator=(const CalibrationData &other) { @@ -15,6 +16,8 @@ CalibrationData &CalibrationData::operator=(const CalibrationData &other) currentCorrection = other.currentCorrection; currentGain = other.currentGain; currentShunt = other.currentShunt; + dacOffset = other.dacOffset; + dacCorrection = other.dacCorrection; } return *this; } @@ -28,4 +31,6 @@ CalibrationData::CalibrationData(const CalibrationData &other) currentCorrection = other.currentCorrection; currentGain = other.currentGain; currentShunt = other.currentShunt; + dacOffset = other.dacOffset; + dacCorrection = other.dacCorrection; } diff --git a/Source/Processing/calibrationdata.h b/Source/Processing/calibrationdata.h index 0c705f6..6199340 100644 --- a/Source/Processing/calibrationdata.h +++ b/Source/Processing/calibrationdata.h @@ -23,6 +23,8 @@ class CalibrationData : public QObject double currentCorrection; double currentGain; double currentShunt; + double dacOffset; + double dacCorrection; }; #endif // CALIBRATIONDATA_H diff --git a/Source/Windows/BatteryParams/batterycyclewnd.cpp b/Source/Windows/BatteryParams/batterycyclewnd.cpp index b1aa929..2fccd7e 100644 --- a/Source/Windows/BatteryParams/batterycyclewnd.cpp +++ b/Source/Windows/BatteryParams/batterycyclewnd.cpp @@ -41,6 +41,10 @@ BatteryCycleWnd::BatteryCycleWnd(QWidget *parent) : pulseEndEdit->setToolTip("Pulse End position. Resistance is calculated between this point and Pause Start"); pauseStartEdit->setToolTip("Pause Start position. Resistance is calculated between Pulse End and this point"); + fitButton = new QPushButton("Fit cycle points", this); + fitButton->setToolTip("Move Pulse End and Pause Start to the settled samples on both sides of the current step"); + fitButton->setFixedSize(BATTERYCYCLE_BUTTON_WIDTH + 60, BATTERYCYCLE_ROW_HEIGHT); + restoreButton = new QPushButton("Restore", this); restoreButton->setToolTip("Return all markers to the positions reported by the device"); saveButton = new QPushButton("Save", this); @@ -48,6 +52,7 @@ BatteryCycleWnd::BatteryCycleWnd(QWidget *parent) : restoreButton->setFixedSize(BATTERYCYCLE_BUTTON_WIDTH, BATTERYCYCLE_ROW_HEIGHT); saveButton->setFixedSize(BATTERYCYCLE_BUTTON_WIDTH, BATTERYCYCLE_ROW_HEIGHT); + buttonLayout->addWidget(fitButton); buttonLayout->addStretch(); buttonLayout->addWidget(restoreButton); buttonLayout->addWidget(saveButton); @@ -67,6 +72,7 @@ BatteryCycleWnd::BatteryCycleWnd(QWidget *parent) : currentPlot->scatterAddGraph(); mainLayout->addWidget(currentPlot, 1); + connect(fitButton, &QPushButton::clicked, this, &BatteryCycleWnd::onFitPoints); connect(restoreButton, &QPushButton::clicked, this, &BatteryCycleWnd::onRestore); connect(saveButton, &QPushButton::clicked, this, &BatteryCycleWnd::onSave); @@ -120,7 +126,12 @@ QHBoxLayout* BatteryCycleWnd::createMarkerRow(QString name, QLineEdit **edit) stepBack->setProperty("markerEdit", QVariant::fromValue((QObject*)(*edit))); stepForward->setProperty("markerEdit", QVariant::fromValue((QObject*)(*edit))); - rowLayout->addWidget(new QLabel(name + " [ms]", this)); + QLabel *nameLabel = new QLabel(name + " [ms]", this); + + markerLabels.append(nameLabel); + markerNames.append(name); + + rowLayout->addWidget(nameLabel); rowLayout->addWidget(stepBack); rowLayout->addWidget(*edit); rowLayout->addWidget(stepForward); @@ -138,7 +149,7 @@ void BatteryCycleWnd::markerStep(QLineEdit *edit, int step) if(edit == NULL) return; - position = keyPositionGet(cycle.plotKeys, edit->text().toDouble()); + position = keyPositionGet(cycle.plotKeys, edit->text().toDouble() * timeScale()); if(position < 0) return; @@ -146,7 +157,7 @@ void BatteryCycleWnd::markerStep(QLineEdit *edit, int step) if(position < 0) position = 0; if(position >= cycle.plotKeys.size()) position = cycle.plotKeys.size() - 1; - edit->setText(QString::number(cycle.plotKeys[position], 'f', 3)); + edit->setText(QString::number(cycle.plotKeys[position] / timeScale(), 'f', timeDecimals())); applyMarkers(); } @@ -166,6 +177,46 @@ void BatteryCycleWnd::onMarkersEdited() applyMarkers(); } +double BatteryCycleWnd::timeScale() +{ + return BATTERYPARAMSPLOT_TimeScale(plotSettings.timeUnit); +} + +QString BatteryCycleWnd::timeSuffix() +{ + return BATTERYPARAMSPLOT_TimeSuffix(plotSettings.timeUnit); +} + +int BatteryCycleWnd::timeDecimals() +{ + return BATTERYPARAMSPLOT_TimeDecimals(plotSettings.timeUnit); +} + +void BatteryCycleWnd::updateMarkerLabels() +{ + for(int i = 0; i < markerLabels.size() && i < markerNames.size(); i++) + { + markerLabels[i]->setText(markerNames[i] + " [" + timeSuffix() + "]"); + } +} + +void BatteryCycleWnd::refreshMarkerEdits() +{ + pulseStartEdit->setText(QString::number(cycle.pulseStartKey / timeScale(), 'f', timeDecimals())); + pulseEndEdit->setText(QString::number(cycle.pulseEndKey / timeScale(), 'f', timeDecimals())); + pauseStartEdit->setText(QString::number(cycle.pauseStartKey / timeScale(), 'f', timeDecimals())); + pauseEndEdit->setText(QString::number(cycle.pauseEndKey / timeScale(), 'f', timeDecimals())); +} + +void BatteryCycleWnd::onFitPoints() +{ + if(!BatteryParamsExtraction::cyclePointsFit(&cycle)) return; + + refreshMarkerEdits(); + updateInfo(); + updatePlots(); +} + void BatteryCycleWnd::onRestore() { setCycle(originalCycle); @@ -178,10 +229,10 @@ void BatteryCycleWnd::onSave() void BatteryCycleWnd::applyMarkers() { - int pulseStartPosition = keyPositionGet(cycle.plotKeys, pulseStartEdit->text().toDouble()); - int pulseEndPosition = keyPositionGet(cycle.plotKeys, pulseEndEdit->text().toDouble()); - int pauseStartPosition = keyPositionGet(cycle.plotKeys, pauseStartEdit->text().toDouble()); - int pauseEndPosition = keyPositionGet(cycle.plotKeys, pauseEndEdit->text().toDouble()); + int pulseStartPosition = keyPositionGet(cycle.plotKeys, pulseStartEdit->text().toDouble() * timeScale()); + int pulseEndPosition = keyPositionGet(cycle.plotKeys, pulseEndEdit->text().toDouble() * timeScale()); + int pauseStartPosition = keyPositionGet(cycle.plotKeys, pauseStartEdit->text().toDouble() * timeScale()); + int pauseEndPosition = keyPositionGet(cycle.plotKeys, pauseEndEdit->text().toDouble() * timeScale()); if(pulseStartPosition < 0 || pulseEndPosition < 0) return; if(pauseStartPosition < 0 || pauseEndPosition < 0) return; @@ -203,12 +254,7 @@ void BatteryCycleWnd::applyMarkers() cycle.resistanceValid = fabs(cycle.deltaCurrent) > 0.000001; cycle.resistance = cycle.resistanceValid ? cycle.deltaVoltage / (cycle.deltaCurrent / 1000.0) : 0; - pulseStartEdit->setText(QString::number(cycle.pulseStartKey, 'f', 3)); - pulseEndEdit->setText(QString::number(cycle.pulseEndKey, 'f', 3)); - pulseStartEdit->setText(QString::number(cycle.pulseStartKey, 'f', 3)); - pulseEndEdit->setText(QString::number(cycle.pulseEndKey, 'f', 3)); - pauseStartEdit->setText(QString::number(cycle.pauseStartKey, 'f', 3)); - pauseEndEdit->setText(QString::number(cycle.pauseEndKey, 'f', 3)); + refreshMarkerEdits(); updateInfo(); updatePlots(); @@ -225,7 +271,7 @@ void BatteryCycleWnd::updateInfo() " | " + QString::number(cycle.charge, 'f', 3) + " mAh this cycle" + " | " + QString::number(cycle.chargeTotal, 'f', 3) + " mAh total" + (cycle.socValid ? " | " + QString::number(cycle.socUsed, 'f', 2) + " % SoC used" : "") + - " | duration " + QString::number(cycle.pauseEndKey - cycle.pulseStartKey, 'f', 1) + " ms" + + " | duration " + QString::number((cycle.pauseEndKey - cycle.pulseStartKey) / timeScale(), 'f', 1) + " " + timeSuffix() + " | dV " + QString::number(cycle.deltaVoltage * 1000.0, 'f', 2) + " mV" + " | dI " + QString::number(cycle.deltaCurrent, 'f', 2) + " mA" + " | R0 " + (cycle.resistanceValid ? QString::number(cycle.resistance * 1000.0, 'f', 2) + " mOhm" : "-") + @@ -240,13 +286,13 @@ void BatteryCycleWnd::updateInfo() if(!cycle.modelValid) { - modelLabel->setText("Relaxed at " + QString::number(cycle.relaxationKey, 'f', 1) + " ms, RC model could not be fitted"); + modelLabel->setText("Relaxed at " + QString::number(cycle.relaxationKey / timeScale(), 'f', 1) + " " + timeSuffix() + ", RC model could not be fitted"); modelLabel->setStyleSheet("color: rgb(170, 130, 0);"); return; } modelLabel->setStyleSheet("color: rgb(0, 120, 0);"); - modelLabel->setText("Relaxed at " + QString::number(cycle.relaxationKey, 'f', 1) + " ms | " + + modelLabel->setText("Relaxed at " + QString::number(cycle.relaxationKey / timeScale(), 'f', 1) + " " + timeSuffix() + " | " + QString(cycle.model == BATTERYPARAMS_MODEL_SECOND_ORDER ? "2nd order" : "1st order") + " | R1 " + QString::number(cycle.r1 * 1000.0, 'f', 2) + " mOhm" + " C1 " + QString::number(cycle.c1, 'f', 1) + " F" + @@ -262,14 +308,29 @@ void BatteryCycleWnd::updateInfo() restoreButton->setEnabled(moved); } +void BatteryCycleWnd::applyPlotStyle() +{ + voltagePlot->applyStyle(plotSettings.fontFamily, plotSettings.labelFontSize, plotSettings.tickFontSize, + plotSettings.lineWidth, plotSettings.gridVisible, plotSettings.minorGridVisible); + currentPlot->applyStyle(plotSettings.fontFamily, plotSettings.labelFontSize, plotSettings.tickFontSize, + plotSettings.lineWidth, plotSettings.gridVisible, plotSettings.minorGridVisible); +} + void BatteryCycleWnd::setPlotSettings(batteryparams_plot_settings_t settings) { + bool unitChanged = (settings.timeUnit != plotSettings.timeUnit); + plotSettings = settings; - voltagePlot->applyStyle(settings.fontFamily, settings.labelFontSize, settings.tickFontSize, - settings.lineWidth, settings.gridVisible, settings.minorGridVisible); - currentPlot->applyStyle(settings.fontFamily, settings.labelFontSize, settings.tickFontSize, - settings.lineWidth, settings.gridVisible, settings.minorGridVisible); + applyPlotStyle(); + + if(unitChanged) + { + updateMarkerLabels(); + refreshMarkerEdits(); + updateInfo(); + updatePlots(); + } } void BatteryCycleWnd::updatePlots() @@ -280,8 +341,18 @@ void BatteryCycleWnd::updatePlots() currentPlot->markersAtKeyClear(); voltagePlot->overlayClear(); - voltagePlot->setData(cycle.plotVoltage, cycle.plotKeys); - currentPlot->setData(cycle.plotCurrent, cycle.plotKeys); + QVector scaledKeys; + + for(int i = 0; i < cycle.plotKeys.size(); i++) + { + scaledKeys.append(cycle.plotKeys[i] / timeScale()); + } + + voltagePlot->setXLabel("[" + timeSuffix() + "]"); + currentPlot->setXLabel("[" + timeSuffix() + "]"); + + voltagePlot->setData(cycle.plotVoltage, scaledKeys); + currentPlot->setData(cycle.plotCurrent, scaledKeys); addMarker(voltagePlot, cycle.plotVoltage, cycle.pulseStartKey, "Pulse Start"); addMarker(voltagePlot, cycle.plotVoltage, cycle.pulseEndKey, "Pulse End"); @@ -295,22 +366,29 @@ void BatteryCycleWnd::updatePlots() if(cycle.relaxationReached) { - voltagePlot->markerAddAtKey(cycle.relaxationKey, cycle.relaxationVoltage, "Relaxed", QColor(230, 180, 0)); - currentPlot->markerAddAtKey(cycle.relaxationKey, 0, "Relaxed", QColor(230, 180, 0)); + voltagePlot->markerAddAtKey(cycle.relaxationKey / timeScale(), cycle.relaxationVoltage, "Relaxed", QColor(230, 180, 0)); + currentPlot->markerAddAtKey(cycle.relaxationKey / timeScale(), 0, "Relaxed", QColor(230, 180, 0)); } if(cycle.modelValid && !cycle.fitKeys.isEmpty()) { - voltagePlot->overlaySetData(cycle.fitVoltage, cycle.fitKeys); + QVector scaledFitKeys; + + for(int i = 0; i < cycle.fitKeys.size(); i++) + { + scaledFitKeys.append(cycle.fitKeys[i] / timeScale()); + } + + voltagePlot->overlaySetData(cycle.fitVoltage, scaledFitKeys); } if(!cycle.plotKeys.isEmpty()) { - voltagePlot->zoomToKeyRange(cycle.plotKeys.first(), cycle.plotKeys.last()); - currentPlot->zoomToKeyRange(cycle.plotKeys.first(), cycle.plotKeys.last()); + voltagePlot->zoomToKeyRange(scaledKeys.first(), scaledKeys.last()); + currentPlot->zoomToKeyRange(scaledKeys.first(), scaledKeys.last()); } - setPlotSettings(plotSettings); + applyPlotStyle(); } void BatteryCycleWnd::setCycle(batteryparams_cycle_t aCycle) @@ -320,10 +398,7 @@ void BatteryCycleWnd::setCycle(batteryparams_cycle_t aCycle) setWindowTitle("Battery cycle " + QString::number(cycle.index)); - pulseStartEdit->setText(QString::number(cycle.pulseStartKey, 'f', 3)); - pulseEndEdit->setText(QString::number(cycle.pulseEndKey, 'f', 3)); - pauseStartEdit->setText(QString::number(cycle.pauseStartKey, 'f', 3)); - pauseEndEdit->setText(QString::number(cycle.pauseEndKey, 'f', 3)); + refreshMarkerEdits(); updateInfo(); updatePlots(); diff --git a/Source/Windows/BatteryParams/batterycyclewnd.h b/Source/Windows/BatteryParams/batterycyclewnd.h index 667d2f1..a490a16 100644 --- a/Source/Windows/BatteryParams/batterycyclewnd.h +++ b/Source/Windows/BatteryParams/batterycyclewnd.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include "Processing/batteryparamsextraction.h" #include "Windows/BatteryParams/batteryparamsplotsettings.h" @@ -28,6 +30,7 @@ private slots: void onMarkerStepBack(); void onMarkerStepForward(); void onMarkersEdited(); + void onFitPoints(); void onRestore(); void onSave(); @@ -43,6 +46,9 @@ private slots: QLineEdit *pauseEndEdit; QPushButton *saveButton; QPushButton *restoreButton; + QPushButton *fitButton; + QVector markerLabels; + QStringList markerNames; batteryparams_plot_settings_t plotSettings; batteryparams_cycle_t cycle; @@ -53,6 +59,12 @@ private slots: QHBoxLayout* createMarkerRow(QString name, QLineEdit **edit); void markerStep(QLineEdit *edit, int step); void applyMarkers(); + void applyPlotStyle(); + void refreshMarkerEdits(); + void updateMarkerLabels(); + double timeScale(); + QString timeSuffix(); + int timeDecimals(); void updateInfo(); void updatePlots(); }; diff --git a/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp b/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp index 534af8f..191673f 100644 --- a/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp +++ b/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp @@ -19,10 +19,41 @@ batteryparams_plot_settings_t BATTERYPARAMSPLOT_SettingsDefault() defaults.minorGridVisible = false; defaults.legendVisible = true; defaults.legendPosition = BATTERYPARAMSPLOT_LEGEND_RIGHT; + defaults.timeUnit = BATTERYPARAMSPLOT_TIME_MS; return defaults; } +double BATTERYPARAMSPLOT_TimeScale(batteryparams_time_unit_t unit) +{ + switch(unit) + { + case BATTERYPARAMSPLOT_TIME_S: return 1000.0; + case BATTERYPARAMSPLOT_TIME_MIN: return 60000.0; + default: return 1.0; + } +} + +QString BATTERYPARAMSPLOT_TimeSuffix(batteryparams_time_unit_t unit) +{ + switch(unit) + { + case BATTERYPARAMSPLOT_TIME_S: return "s"; + case BATTERYPARAMSPLOT_TIME_MIN: return "min"; + default: return "ms"; + } +} + +int BATTERYPARAMSPLOT_TimeDecimals(batteryparams_time_unit_t unit) +{ + switch(unit) + { + case BATTERYPARAMSPLOT_TIME_S: return 4; + case BATTERYPARAMSPLOT_TIME_MIN: return 6; + default: return 3; + } +} + /*Applied after the graphs are built, so every analysis window shares the same look*/ void BATTERYPARAMSPLOT_Apply(QCustomPlot *plot, batteryparams_plot_settings_t settings) { diff --git a/Source/Windows/BatteryParams/batteryparamsplotsettings.h b/Source/Windows/BatteryParams/batteryparamsplotsettings.h index 4c0e87d..d019419 100644 --- a/Source/Windows/BatteryParams/batteryparamsplotsettings.h +++ b/Source/Windows/BatteryParams/batteryparamsplotsettings.h @@ -19,6 +19,13 @@ typedef enum BATTERYPARAMSPLOT_LEGEND_RIGHT }batteryparams_legend_position_t; +typedef enum +{ + BATTERYPARAMSPLOT_TIME_MS = 0, + BATTERYPARAMSPLOT_TIME_S, + BATTERYPARAMSPLOT_TIME_MIN +}batteryparams_time_unit_t; + typedef struct { QString fontFamily; @@ -32,9 +39,13 @@ typedef struct bool minorGridVisible; bool legendVisible; batteryparams_legend_position_t legendPosition; + batteryparams_time_unit_t timeUnit; }batteryparams_plot_settings_t; batteryparams_plot_settings_t BATTERYPARAMSPLOT_SettingsDefault(); +double BATTERYPARAMSPLOT_TimeScale(batteryparams_time_unit_t unit); +QString BATTERYPARAMSPLOT_TimeSuffix(batteryparams_time_unit_t unit); +int BATTERYPARAMSPLOT_TimeDecimals(batteryparams_time_unit_t unit); void BATTERYPARAMSPLOT_Apply(QCustomPlot *plot, batteryparams_plot_settings_t settings); void BATTERYPARAMSPLOT_Save(QCustomPlot *plot, QWidget *parent, QString suggestedName); diff --git a/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp b/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp index a0655ab..2e569ec 100644 --- a/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp +++ b/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp @@ -35,6 +35,14 @@ BatteryParamsSettingsDlg::BatteryParamsSettingsDlg(QWidget *parent) : "Battery counts as relaxed once the voltage stays inside this band for the whole window"); relaxationWindowEdit = createEntry(relaxationLayout, 1, "Relaxation window", "s", "Time the voltage has to stay inside the band"); + relaxationFilterEdit = createEntry(relaxationLayout, 2, "Relaxation filter", "ms", + "Voltage is averaged over this window, in milliseconds, before it is compared with the threshold.\n" + "Without it the threshold is compared against the raw noise.\n" + "0 turns the filter off"); + plotDecimationEdit = createEntry(relaxationLayout, 3, "Stored sample step", "", + "How many samples of a cycle are skipped when the waveform is stored for the plots.\n" + "1 keeps every sample, 10 keeps every tenth one.\n" + "Relaxation search is not affected, it always works on averaged full resolution data"); QLabel *modelLabel = new QLabel("Model", extractionGroup); modelCombo = new QComboBox(extractionGroup); @@ -106,10 +114,19 @@ BatteryParamsSettingsDlg::BatteryParamsSettingsDlg(QWidget *parent) : plotGridCheckBox = new QCheckBox("Show grid", plotTab); plotMinorGridCheckBox = new QCheckBox("Show minor grid", plotTab); plotLegendCheckBox = new QCheckBox("Show legend", plotTab); - plotLayout->addWidget(plotGridCheckBox, 8, 0, 1, 3); - plotLayout->addWidget(plotMinorGridCheckBox, 9, 0, 1, 3); - plotLayout->addWidget(plotLegendCheckBox, 10, 0, 1, 3); - plotLayout->setRowStretch(11, 1); + plotTimeUnitCombo = new QComboBox(plotTab); + plotTimeUnitCombo->addItem("Milliseconds"); + plotTimeUnitCombo->addItem("Seconds"); + plotTimeUnitCombo->addItem("Minutes"); + plotTimeUnitCombo->setFixedHeight(BATTERYPARAMSSETTINGS_ROW_HEIGHT); + plotTimeUnitCombo->setToolTip("Unit the cycle points and the time axis are shown in"); + plotLayout->addWidget(new QLabel("Time unit", plotTab), 8, 0); + plotLayout->addWidget(plotTimeUnitCombo, 8, 1, 1, 2); + + plotLayout->addWidget(plotGridCheckBox, 9, 0, 1, 3); + plotLayout->addWidget(plotMinorGridCheckBox, 10, 0, 1, 3); + plotLayout->addWidget(plotLegendCheckBox, 11, 0, 1, 3); + plotLayout->setRowStretch(12, 1); tabWidget->addTab(plotTab, "Plots"); @@ -146,6 +163,8 @@ void BatteryParamsSettingsDlg::setSettings(batteryparams_settings_t settings) initialSocEdit->setText(QString::number(settings.initialSoc)); relaxationThresholdEdit->setText(QString::number(settings.relaxationThreshold)); relaxationWindowEdit->setText(QString::number(settings.relaxationWindow)); + relaxationFilterEdit->setText(QString::number(settings.relaxationFilter)); + plotDecimationEdit->setText(QString::number(settings.plotDecimation)); modelCombo->setCurrentIndex(settings.model == BATTERYPARAMS_MODEL_FIRST_ORDER ? 0 : 1); fitEndCombo->setCurrentIndex(settings.fitEnd == BATTERYPARAMS_FIT_END_RELAXATION ? 0 : 1); tauGridPointsEdit->setText(QString::number(settings.tauGridPoints)); @@ -166,6 +185,7 @@ void BatteryParamsSettingsDlg::setPlotSettings(batteryparams_plot_settings_t set plotGridCheckBox->setChecked(settings.gridVisible); plotMinorGridCheckBox->setChecked(settings.minorGridVisible); plotLegendCheckBox->setChecked(settings.legendVisible); + plotTimeUnitCombo->setCurrentIndex((int)settings.timeUnit); } batteryparams_plot_settings_t BatteryParamsSettingsDlg::getPlotSettings() @@ -183,6 +203,7 @@ batteryparams_plot_settings_t BatteryParamsSettingsDlg::getPlotSettings() settings.legendFontFamily = plotLegendFontCombo->currentFont().family(); settings.legendPosition = (plotLegendPositionCombo->currentIndex() == 0) ? BATTERYPARAMSPLOT_LEGEND_LEFT : BATTERYPARAMSPLOT_LEGEND_RIGHT; + settings.timeUnit = (batteryparams_time_unit_t)plotTimeUnitCombo->currentIndex(); settings.gridVisible = plotGridCheckBox->isChecked(); settings.minorGridVisible = plotMinorGridCheckBox->isChecked(); settings.legendVisible = plotLegendCheckBox->isChecked(); @@ -198,6 +219,8 @@ batteryparams_settings_t BatteryParamsSettingsDlg::getSettings() if(initialSocEdit->text().toDouble() > 0) settings.initialSoc = initialSocEdit->text().toDouble(); if(relaxationThresholdEdit->text().toDouble() > 0) settings.relaxationThreshold = relaxationThresholdEdit->text().toDouble(); if(relaxationWindowEdit->text().toDouble() > 0) settings.relaxationWindow = relaxationWindowEdit->text().toDouble(); + if(relaxationFilterEdit->text().toDouble() >= 0) settings.relaxationFilter = relaxationFilterEdit->text().toDouble(); + if(plotDecimationEdit->text().toInt() > 0) settings.plotDecimation = plotDecimationEdit->text().toInt(); if(tauGridPointsEdit->text().toInt() > 1) settings.tauGridPoints = tauGridPointsEdit->text().toInt(); if(tauRefinementEdit->text().toInt() > 0) settings.tauRefinementNo = tauRefinementEdit->text().toInt(); if(tauSeparationEdit->text().toDouble() > 1) settings.tauSeparation = tauSeparationEdit->text().toDouble(); diff --git a/Source/Windows/BatteryParams/batteryparamssettingsdlg.h b/Source/Windows/BatteryParams/batteryparamssettingsdlg.h index 9d93930..1697a52 100644 --- a/Source/Windows/BatteryParams/batteryparamssettingsdlg.h +++ b/Source/Windows/BatteryParams/batteryparamssettingsdlg.h @@ -30,6 +30,8 @@ class BatteryParamsSettingsDlg : public QDialog QLineEdit *initialSocEdit; QLineEdit *relaxationThresholdEdit; QLineEdit *relaxationWindowEdit; + QLineEdit *relaxationFilterEdit; + QLineEdit *plotDecimationEdit; QComboBox *modelCombo; QComboBox *fitEndCombo; QLineEdit *tauGridPointsEdit; @@ -44,6 +46,7 @@ class BatteryParamsSettingsDlg : public QDialog QLineEdit *plotMarkerSizeEdit; QFontComboBox *plotLegendFontCombo; QComboBox *plotLegendPositionCombo; + QComboBox *plotTimeUnitCombo; QCheckBox *plotGridCheckBox; QCheckBox *plotMinorGridCheckBox; QCheckBox *plotLegendCheckBox; diff --git a/Source/Windows/BatteryParams/batteryparamswnd.cpp b/Source/Windows/BatteryParams/batteryparamswnd.cpp index 3b5d2ed..316e3c5 100644 --- a/Source/Windows/BatteryParams/batteryparamswnd.cpp +++ b/Source/Windows/BatteryParams/batteryparamswnd.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #define BATTERYPARAMS_SETTINGS_EDIT_WIDTH 80 #define BATTERYPARAMS_SETTINGS_ROW_HEIGHT 28 #define BATTERYPARAMS_AUTO_RESIZE_ROW_NO 10 +#define BATTERYPARAMS_FIT_WINDOWS_MAX 5 BatteryParamsWnd::BatteryParamsWnd(QWidget *parent) : QWidget(parent) @@ -81,6 +83,18 @@ BatteryParamsWnd::BatteryParamsWnd(QWidget *parent) : fitIntervalButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); fitIntervalButton->setToolTip("Compare the parameters and the errors obtained by fitting up to the relaxation point and up to the end of the pause"); + fitPointsButton = new QPushButton("Fit cycle points", this); + fitPointsButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + fitPointsButton->setToolTip("Check every cycle and move Pulse End and Pause Start onto the settled levels around the current step.\nCycles that had to be corrected are opened for review"); + + fitPointsSaveButton = new QPushButton("Save new points", this); + fitPointsSaveButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + fitPointsSaveButton->setToolTip("Keep the fitted points on every corrected cycle and clear the marks"); + + fitPointsRestoreButton = new QPushButton("Restore old points", this); + fitPointsRestoreButton->setFixedHeight(BATTERYPARAMS_SETTINGS_ROW_HEIGHT); + fitPointsRestoreButton->setToolTip("Return every corrected cycle to the points reported by the device"); + settingsLabel = new QLabel(this); settingsLabel->setStyleSheet("color: gray;"); @@ -94,6 +108,10 @@ BatteryParamsWnd::BatteryParamsWnd(QWidget *parent) : settingsLayout->addWidget(relaxationTimeButton); settingsLayout->addWidget(fitIntervalButton); settingsLayout->addSpacing(15); + settingsLayout->addWidget(fitPointsButton); + settingsLayout->addWidget(fitPointsSaveButton); + settingsLayout->addWidget(fitPointsRestoreButton); + settingsLayout->addSpacing(15); settingsLayout->addWidget(settingsLabel, 1); mainLayout->addLayout(settingsLayout); @@ -105,8 +123,12 @@ BatteryParamsWnd::BatteryParamsWnd(QWidget *parent) : connect(fitQualityButton, &QPushButton::clicked, this, &BatteryParamsWnd::onFitQuality); connect(relaxationTimeButton, &QPushButton::clicked, this, &BatteryParamsWnd::onRelaxationTime); connect(fitIntervalButton, &QPushButton::clicked, this, &BatteryParamsWnd::onFitInterval); + connect(fitPointsButton, &QPushButton::clicked, this, &BatteryParamsWnd::onFitCyclePoints); + connect(fitPointsSaveButton, &QPushButton::clicked, this, &BatteryParamsWnd::onFitPointsSave); + connect(fitPointsRestoreButton, &QPushButton::clicked, this, &BatteryParamsWnd::onFitPointsRestore); updateSettingsLabel(); + updateFitPointsButtons(); header << "Cycle" << "Start [ms]" << "End [ms]" << "Q [mAh]" << "Q total [mAh]" << "SoC used [%]" << "SoC [%]" << "OCV [V]" @@ -143,6 +165,8 @@ BatteryParamsWnd::BatteryParamsWnd(QWidget *parent) : cyclesTable->setToolTip("Click a cycle to show its waveform.\nColumns can be resized and reordered by dragging the header"); cycleWnd = NULL; + fittedPositions.clear(); + fittedOriginals.clear(); connectedToCycleWnd = false; connect(cyclesTable, &QTableWidget::cellClicked, this, &BatteryParamsWnd::onCycleSelected); @@ -168,7 +192,7 @@ QTableWidgetItem* BatteryParamsWnd::createItem(QString text) void BatteryParamsWnd::fillCycleRow(int row, batteryparams_cycle_t cycle) { - cyclesTable->setItem(row, 0, createItem(QString::number(cycle.index))); + cyclesTable->setItem(row, 0, createItem(QString::number(cycle.index) + (fittedPositions.contains(row) ? " *" : ""))); cyclesTable->setItem(row, 1, createItem(QString::number(cycle.pulseStartKey, 'f', 3))); cyclesTable->setItem(row, 2, createItem(QString::number(cycle.pauseEndKey, 'f', 3))); cyclesTable->setItem(row, 3, createItem(QString::number(cycle.charge, 'f', 3))); @@ -292,6 +316,160 @@ void BatteryParamsWnd::onCycleSelected(int row, int column) cycleWnd->activateWindow(); } +void BatteryParamsWnd::updateFitPointsButtons() +{ + fitPointsSaveButton->setEnabled(!fittedPositions.isEmpty()); + fitPointsRestoreButton->setEnabled(!fittedPositions.isEmpty()); +} + +void BatteryParamsWnd::onFitCyclePoints() +{ + QVector working = cycles; + QVector changed; + QProgressDialog progressDialog("Fitting cycle points...", QString(), 0, cycles.size(), this); + batteryparams_settings_t usedSettings = settings; + int cycleNo = cycles.size(); + int fittedNo = 0; + + if(cycles.isEmpty()) return; + + changed.fill(0, cycleNo); + + progressDialog.setWindowModality(Qt::WindowModal); + progressDialog.setWindowTitle("Fit cycle points"); + progressDialog.setMinimumDuration(0); + progressDialog.setAutoClose(false); + progressDialog.setAutoReset(false); + progressDialog.setValue(0); + + if(!parallelGet(cycleNo)) + { + for(int i = 0; i < cycleNo; i++) + { + progressDialog.setLabelText(QString("Fitting cycle %1 of %2...").arg(i + 1).arg(cycleNo)); + progressDialog.setValue(i); + QApplication::processEvents(); + + if(!BatteryParamsExtraction::cyclePointsFit(&working[i])) continue; + + BatteryParamsExtraction::analyzeCycle(&working[i], usedSettings); + changed[i] = 1; + } + } + else + { + QVector positions; + QFutureWatcher watcher; + + /*Containers are detached here, in this thread, so that the tasks only write + through plain pointers and never share a Qt container between threads*/ + batteryparams_cycle_t *workingData = working.data(); + int *changedData = changed.data(); + + for(int i = 0; i < cycleNo; i++) positions.append(i); + + /*Every cycle is fitted and analysed on its own entry of the working copy, so + the tasks never touch the same data*/ + QFuture future = QtConcurrent::map(positions, [workingData, changedData, usedSettings](int position) + { + if(!BatteryParamsExtraction::cyclePointsFit(&workingData[position])) return; + + BatteryParamsExtraction::analyzeCycle(&workingData[position], usedSettings); + changedData[position] = 1; + }); + + watcher.setFuture(future); + + while(!watcher.isFinished()) + { + progressDialog.setLabelText(QString("Fitting %1 cycles on %2 cores, %3 done...") + .arg(cycleNo) + .arg(QThread::idealThreadCount()) + .arg(watcher.progressValue())); + progressDialog.setValue(watcher.progressValue()); + QApplication::processEvents(); + QThread::msleep(20); + } + } + + progressDialog.setValue(cycleNo); + progressDialog.close(); + + for(int i = 0; i < cycleNo; i++) + { + if(changed[i] == 0) continue; + + /*Points reported by the device are kept until the correction is confirmed*/ + if(!fittedPositions.contains(i)) + { + fittedPositions.append(i); + fittedOriginals.append(cycles[i]); + } + + cycles[i] = working[i]; + fillCycleRow(i, cycles[i]); + fittedNo++; + } + + updateSummary(); + updateFitPointsButtons(); + + if(fittedNo == 0) + { + statusLabel->setText("Cycle points checked, every cycle already sits on the settled levels"); + return; + } + + statusLabel->setText("Cycle points fitted on " + QString::number(fittedNo) + " of " + + QString::number(cycleNo) + " cycles, marked with * and not confirmed yet"); +} + +void BatteryParamsWnd::onFitPointsSave() +{ + int no = fittedPositions.size(); + + if(no == 0) return; + + fittedPositions.clear(); + fittedOriginals.clear(); + + for(int i = 0; i < cycles.size(); i++) + { + fillCycleRow(i, cycles[i]); + } + + updateFitPointsButtons(); + statusLabel->setText("Fitted points kept on " + QString::number(no) + " cycles"); +} + +void BatteryParamsWnd::onFitPointsRestore() +{ + int no = fittedPositions.size(); + + if(no == 0) return; + + for(int i = 0; i < fittedPositions.size() && i < fittedOriginals.size(); i++) + { + int position = fittedPositions[i]; + + if(position < 0 || position >= cycles.size()) continue; + + cycles[position] = fittedOriginals[i]; + } + + fittedPositions.clear(); + fittedOriginals.clear(); + + for(int i = 0; i < cycles.size(); i++) + { + fillCycleRow(i, cycles[i]); + } + + updateSummary(); + updateFitPointsButtons(); + statusLabel->setText("Points reported by the device restored on " + QString::number(no) + " cycles"); +} + void BatteryParamsWnd::onCycleUpdated(batteryparams_cycle_t cycle) { for(int i = 0; i < cycles.size(); i++) @@ -308,6 +486,10 @@ void BatteryParamsWnd::onCycleUpdated(batteryparams_cycle_t cycle) void BatteryParamsWnd::onClear() { + fittedPositions.clear(); + fittedOriginals.clear(); + updateFitPointsButtons(); + cycles.clear(); cyclesTable->setRowCount(0); statusLabel->setText("Waiting for the first cycle..."); diff --git a/Source/Windows/BatteryParams/batteryparamswnd.h b/Source/Windows/BatteryParams/batteryparamswnd.h index f032af4..d0826cc 100644 --- a/Source/Windows/BatteryParams/batteryparamswnd.h +++ b/Source/Windows/BatteryParams/batteryparamswnd.h @@ -48,6 +48,9 @@ private slots: void onFitQuality(); void onRelaxationTime(); void onFitInterval(); + void onFitCyclePoints(); + void onFitPointsSave(); + void onFitPointsRestore(); void onSweepRequested(batteryfitinterval_sweep_t mode, double fixedValue, double from, double to, int steps, bool compare); void onCycleSelected(int row, int column); @@ -63,6 +66,9 @@ private slots: QPushButton *fitQualityButton; QPushButton *relaxationTimeButton; QPushButton *fitIntervalButton; + QPushButton *fitPointsButton; + QPushButton *fitPointsSaveButton; + QPushButton *fitPointsRestoreButton; QLabel *settingsLabel; BatteryParamsSettingsDlg *settingsDlg; @@ -82,6 +88,10 @@ private slots: QPushButton *clearButton; QPushButton *exportButton; BatteryCycleWnd *cycleWnd; + QVector fittedPositions; + QVector fittedOriginals; + + void updateFitPointsButtons(); bool connectedToCycleWnd; bool connectedToFitIntervalWnd; diff --git a/Source/Windows/Device/calibrationwnd.cpp b/Source/Windows/Device/calibrationwnd.cpp index 1110b4c..b4a487d 100644 --- a/Source/Windows/Device/calibrationwnd.cpp +++ b/Source/Windows/Device/calibrationwnd.cpp @@ -27,6 +27,8 @@ void CalibrationWnd::showWnd() ui->volOffLine->setText(QString::number(calData->voltageOff)); ui->volCorrLine->setText(QString::number(calData->voltageCorr)); ui->volCOffLine->setText(QString::number(calData->voltageCurrOffset)); + ui->dacOffLine->setText(QString::number(calData->dacOffset)); + ui->dacCorLine->setText(QString::number(calData->dacCorrection)); show(); } @@ -44,6 +46,8 @@ void CalibrationWnd::onSubmitPressed(bool pressed) calData->voltageOff = ui->volOffLine->text().toDouble(); calData->voltageCorr = ui->volCorrLine->text().toDouble(); calData->voltageCurrOffset = ui->volCOffLine->text().toDouble(); + calData->dacOffset = ui->dacOffLine->text().toDouble(); + calData->dacCorrection = ui->dacCorLine->text().toDouble(); emit sigCalibrationDataUpdated(); } diff --git a/Source/Windows/Device/calibrationwnd.ui b/Source/Windows/Device/calibrationwnd.ui index 3269b2f..ba3566d 100644 --- a/Source/Windows/Device/calibrationwnd.ui +++ b/Source/Windows/Device/calibrationwnd.ui @@ -671,6 +671,188 @@ + + + + + + + 0 + 0 + + + + + 150 + 30 + + + + + 150 + 30 + + + + + Arial + 10 + + + + Load DAC Offset + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 60 + 30 + + + + + 60 + 30 + + + + + Arial + 10 + + + + 0 + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + + Arial + 10 + + + + mA + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + 150 + 30 + + + + + 150 + 30 + + + + + Arial + 10 + + + + Load DAC Correction + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 60 + 30 + + + + + 60 + 30 + + + + + Arial + 10 + + + + 1 + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + + Arial + 10 + + + + + + + Qt::AlignCenter + + + + + diff --git a/Source/Windows/Device/devicewnd.cpp b/Source/Windows/Device/devicewnd.cpp index c040022..676f475 100644 --- a/Source/Windows/Device/devicewnd.cpp +++ b/Source/Windows/Device/devicewnd.cpp @@ -1144,6 +1144,18 @@ bool DeviceWnd::plotVoltageValues(QVector values, QVector keys) return true; } +bool DeviceWnd::plotVoltageAverageValues(QVector values, QVector keys) +{ + voltageChart->appendAverageData(values, keys); + return true; +} + +void DeviceWnd::enableVoltageAveragePlot(bool enable) +{ + if(enable) voltageChart->averageAddGraph(); + else voltageChart->clearAverageData(); +} + bool DeviceWnd::plotCurrentValues(QVector values, QVector keys) { currentChart->appendData(values, keys); diff --git a/Source/Windows/Device/devicewnd.h b/Source/Windows/Device/devicewnd.h index 8c5c48f..c201dc9 100644 --- a/Source/Windows/Device/devicewnd.h +++ b/Source/Windows/Device/devicewnd.h @@ -147,6 +147,8 @@ class DeviceWnd : public QWidget void setConfigurationChargerBDProgressStatus(int percentage, QString status); bool plotVoltageValues(QVector values, QVector keys); + bool plotVoltageAverageValues(QVector values, QVector keys); + void enableVoltageAveragePlot(bool enable); bool plotCurrentValues(QVector values, QVector keys); bool plotConsumptionValues(QVector values, QVector keys); bool plotConsumptionEBP(QVector values, QVector keys); @@ -198,7 +200,7 @@ class DeviceWnd : public QWidget void sigLoadWaveStatusChanged(bool newState); void sigBatParamViewRequested(); void sigBatParamCapacityChanged(double capacity); - void sigBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes); + void sigBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, double filterMs); void sigLoadWaveClear(); void sigChargingCurrentChanged(unsigned int current); void sigChargingTermCurrentChanged(unsigned int current); diff --git a/Source/Windows/Device/energycontrolwnd.cpp b/Source/Windows/Device/energycontrolwnd.cpp index ff51c26..9efd0f7 100644 --- a/Source/Windows/Device/energycontrolwnd.cpp +++ b/Source/Windows/Device/energycontrolwnd.cpp @@ -199,6 +199,7 @@ EnergyControlWnd::EnergyControlWnd(QWidget *parent) : batParamLayout->addLayout(createEntryRow("Relax Threshold", "mV", loadEntryEdits)); batParamLayout->addLayout(createEntryRow("Relax Window", "s", loadEntryEdits)); + batParamLayout->addLayout(createEntryRow("Relax Filter", "ms", loadEntryEdits)); loadEntryEdits["Battery Capacity"]->setText("500"); loadEntryEdits["Extraction Accuracy"]->setText("1"); loadEntryEdits["Maximum Current"]->setText("1000"); @@ -206,6 +207,7 @@ EnergyControlWnd::EnergyControlWnd(QWidget *parent) : loadEntryEdits["Marker Guard"]->setText("1"); loadEntryEdits["Relax Threshold"]->setText("5"); loadEntryEdits["Relax Window"]->setText("60"); + loadEntryEdits["Relax Filter"]->setText("1000"); loadEntryEdits["Battery Capacity"]->setToolTip("Nominal battery capacity"); loadEntryEdits["Extraction Accuracy"]->setToolTip("Charge extracted by a single pulse, in percent of the battery capacity"); loadEntryEdits["Maximum Current"]->setToolTip("Pulse amplitude, pause is always 0 mA"); @@ -213,6 +215,7 @@ EnergyControlWnd::EnergyControlWnd(QWidget *parent) : loadEntryEdits["Marker Guard"]->setToolTip("Distance between a current step and the marker that follows it.\nKeeps every marker inside a settled level instead of on the step itself.\nMust cover the load response time, a few sample periods at least"); loadEntryEdits["Relax Threshold"]->setToolTip("Voltage span allowed inside the relaxation window, the pause ends when the battery stays within it"); loadEntryEdits["Relax Window"]->setToolTip("Time window the voltage has to stay inside the threshold before the pause is ended"); + loadEntryEdits["Relax Filter"]->setToolTip("Voltage is averaged over this window, in milliseconds, before it is compared with the threshold.\nThe averaged voltage is drawn over the live voltage plot.\n0 turns the filter off"); batParamInfoLabel = new QLabel(this); batParamInfoLabel->setStyleSheet("color: gray;"); @@ -237,6 +240,7 @@ EnergyControlWnd::EnergyControlWnd(QWidget *parent) : connect(loadEntryEdits["Marker Guard"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); connect(loadEntryEdits["Relax Threshold"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); connect(loadEntryEdits["Relax Window"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); + connect(loadEntryEdits["Relax Filter"], &QLineEdit::textChanged, this, &EnergyControlWnd::onBatParamChanged); connect(batParamRelaxCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onBatParamChanged())); refreshCustomWaveLibraryCombo(); @@ -2011,8 +2015,14 @@ QTableWidget* EnergyControlWnd::createWaveTable(bool editable) table->horizontalHeaderItem(5)->setToolTip("Last chunk in group"); table->horizontalHeaderItem(6)->setToolTip("Energy debugger marker name (optional)"); table->horizontalHeaderItem(7)->setToolTip("Marker position: s = chunk start, e = chunk end, s,e = both (marker \"Start name, End name\")"); - table->horizontalHeader()->setStretchLastSection(true); + /*Sections are stretched to the viewport, so the horizontal scroll bar is never + needed. It has to stay off: with both scroll bars on demand the scroll area + oscillates between showing and hiding them and the window locks up*/ + table->horizontalHeader()->setStretchLastSection(false); + table->horizontalHeader()->setMinimumSectionSize(40); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + table->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); table->verticalHeader()->setDefaultSectionSize(22); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setMinimumHeight(100); @@ -2200,9 +2210,8 @@ Waveform EnergyControlWnd::buildBatParamWave() unsigned int pulseDuration; unsigned int pauseDuration; unsigned int guard; - int repetitions; - if(!batParamCompute(&pulseDuration, &repetitions, NULL)) return wave; + if(!batParamCompute(&pulseDuration, NULL, NULL)) return wave; pauseDuration = loadEntryEdits["Pause Duration"]->text().toUInt(); guard = loadEntryEdits["Marker Guard"]->text().toUInt(); @@ -2215,7 +2224,10 @@ Waveform EnergyControlWnd::buildBatParamWave() /*With the adaptive relaxation the device runs one pulse and one pause per pass, the application ends the pause as soon as the voltage settles and starts the next pass, so the repetitions are counted on the application side*/ - wave.repetitionCounter = batParamRelaxationEnabled() ? 1 : repetitions; + /*Procedure runs until the battery reaches the under voltage protection, the + number of passes cannot be known in advance because the real capacity differs + from the one written on the cell*/ + wave.repetitionCounter = batParamRelaxationEnabled() ? 1 : -1; /*Every level starts with an unmarked guard chunk, so all four markers land inside a settled level instead of on the current step itself. The load @@ -2258,9 +2270,8 @@ void EnergyControlWnd::updateBatParamInfo() QString error; Waveform wave; unsigned int pulseDuration; - int repetitions; - if(!batParamCompute(&pulseDuration, &repetitions, &error)) + if(!batParamCompute(&pulseDuration, NULL, &error)) { batParamInfoLabel->setText(error); batParamTable->setRowCount(0); @@ -2271,16 +2282,16 @@ void EnergyControlWnd::updateBatParamInfo() fillWaveTable(batParamTable, wave); QString info = QString::number(pulseDuration) + " ms pulse, " + - QString::number(wave.getTotalDuration()) + " ms per pass, " + - QString::number(repetitions) + " passes, " + - QString::number((double)repetitions * wave.getTotalDuration() / 60000.0, 'f', 1) + " min total"; + QString::number(wave.getTotalDuration()) + " ms per pass"; if(batParamRelaxationEnabled()) { - info += " (maximum, pause ends on dV/dt below " + loadEntryEdits["Relax Threshold"]->text() + - " mV in " + loadEntryEdits["Relax Window"]->text() + " s)"; + info += " at most, pause ends on " + loadEntryEdits["Relax Threshold"]->text() + + " mV in " + loadEntryEdits["Relax Window"]->text() + " s"; } + info += ", runs until under voltage protection"; + batParamInfoLabel->setText(info); } @@ -2296,6 +2307,7 @@ void EnergyControlWnd::onBatParamChanged() loadEntryEdits["Relax Threshold"]->setEnabled(adaptive); loadEntryEdits["Relax Window"]->setEnabled(adaptive); + loadEntryEdits["Relax Filter"]->setEnabled(adaptive); updateBatParamInfo(); } @@ -2540,14 +2552,10 @@ void EnergyControlWnd::onLoadSet() if(!waveMarkersConfirm(wave)) return; loadActiveWave = wave; emit sigBatParamCapacityChanged(loadEntryEdits["Battery Capacity"]->text().toDouble()); - { - int repetitions = 1; - batParamCompute(NULL, &repetitions, NULL); - emit sigBatParamRelaxationChanged(batParamRelaxationEnabled(), - loadEntryEdits["Relax Threshold"]->text().toDouble(), - loadEntryEdits["Relax Window"]->text().toDouble(), - repetitions); - } + emit sigBatParamRelaxationChanged(batParamRelaxationEnabled(), + loadEntryEdits["Relax Threshold"]->text().toDouble(), + loadEntryEdits["Relax Window"]->text().toDouble(), + loadEntryEdits["Relax Filter"]->text().toDouble()); emit sigLoadWaveChanged(wave); break; case LoadModeCustomWave: diff --git a/Source/Windows/Device/energycontrolwnd.h b/Source/Windows/Device/energycontrolwnd.h index e095673..f6df914 100644 --- a/Source/Windows/Device/energycontrolwnd.h +++ b/Source/Windows/Device/energycontrolwnd.h @@ -148,7 +148,7 @@ class EnergyControlWnd : public QWidget void sigLoadWaveClear(); void sigBatParamViewRequested(); void sigBatParamCapacityChanged(double capacity); - void sigBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes); + void sigBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, double filterMs); void sigChargingCurrentChanged(unsigned int newCurrent); void sigChargingTermVoltageChanged(float newVoltage); diff --git a/Source/Windows/Plot/plot.cpp b/Source/Windows/Plot/plot.cpp index 5229d37..2aee8ea 100644 --- a/Source/Windows/Plot/plot.cpp +++ b/Source/Windows/Plot/plot.cpp @@ -127,6 +127,8 @@ Plot::Plot(int mw, int mh, bool aEnableTracking, QWidget *parent) enableTracking = aEnableTracking; replotActive = true; scatterGraphAdded = false; + averageGraphAdded = false; + averageGraphIndex = -1; axisLocked = false; xRangeSyncInProgress = false; @@ -156,6 +158,46 @@ void Plot::scatterAddGraph() } +void Plot::averageAddGraph() +{ + if(averageGraphAdded) return; + + averageGraphAdded = true; + plot->addGraph(); + averageGraphIndex = plot->graphCount() - 1; + plot->graph(averageGraphIndex)->setPen(QPen(QColor(230, 180, 0), 1.5)); + plot->graph(averageGraphIndex)->setName("Averaged"); +} + +void Plot::appendAverageData(QVector data, QVector keys) +{ + if(!averageGraphAdded) return; + if(keys.isEmpty() || keys.size() != data.size()) return; + + averageXData.append(keys); + averageYData.append(data); + + /*Averaged trace follows the same window as the plotted samples*/ + while(!plotXData.isEmpty() && !averageXData.isEmpty() && (averageXData.first() < plotXData.first())) + { + averageXData.removeFirst(); + averageYData.removeFirst(); + } + + if(replotActive) + { + plot->graph(averageGraphIndex)->setData(averageXData, averageYData, true); + plot->replot(); + } +} + +void Plot::clearAverageData() +{ + averageXData.clear(); + averageYData.clear(); + if(averageGraphAdded && (averageGraphIndex >= 0)) plot->graph(averageGraphIndex)->data()->clear(); +} + void Plot::scatterAddData(QVector data, QVector keys) { plot->graph(1)->setData(keys, data); @@ -499,6 +541,7 @@ void Plot::clear() } xData.clear(); yData.clear(); + clearAverageData(); plot->replot(); epDataKey.clear(); epDataName.clear(); diff --git a/Source/Windows/Plot/plot.h b/Source/Windows/Plot/plot.h index 8a86e1b..b8fcf5f 100644 --- a/Source/Windows/Plot/plot.h +++ b/Source/Windows/Plot/plot.h @@ -16,6 +16,9 @@ class Plot : public QWidget void scatterAddData(QVector data, QVector keys); void scatterAddAllDataWithName(QVector> data); void scatterClearMarkers(); + void averageAddGraph(); + void appendAverageData(QVector data, QVector keys); + void clearAverageData(); void scatterAddDataWithName(double value, double keys, QString name); void scatterReplotDataWithName(); void setData(QVector data, QVector keys); @@ -76,6 +79,10 @@ private slots: QVector yData; QVector plotXData; QVector plotYData; + QVector averageXData; + QVector averageYData; + bool averageGraphAdded; + int averageGraphIndex; QVector epDataKey; QVector epDataName; QVector textData; diff --git a/Source/device.cpp b/Source/device.cpp index 4a0de3d..13d7553 100644 --- a/Source/device.cpp +++ b/Source/device.cpp @@ -744,6 +744,8 @@ bool Device::getSamplingPeriod(QString *time) bool Device::getCalParam() { float vref, voff, vcor,coff, ccor; + float dacoff = dataProcessing->getCalibrationData()->dacOffset; + float daccor = dataProcessing->getCalibrationData()->dacCorrection; QString response; QString command = "device param cal get"; @@ -758,6 +760,8 @@ bool Device::getCalParam() paramMap["VCOR"] = &vcor; paramMap["COFF"] = &coff; paramMap["CCOR"] = &ccor; + paramMap["DACOFF"] = &dacoff; + paramMap["DACCOR"] = &daccor; for(const QString &token : tokens) { @@ -784,6 +788,10 @@ bool Device::getCalParam() // Provera da li su svi parametri popunjeni (opciono ali preporučeno) dataProcessing->setCalibrationData(vref, voff, vcor, coff, ccor); + dataProcessing->getCalibrationData()->dacOffset = dacoff; + dataProcessing->getCalibrationData()->dacCorrection = daccor; + m_params->setParamValue("loadDacOffset", QString::number(dacoff)); + m_params->setParamValue("loadDacCor", QString::number(daccor)); m_params->setParamValue("adcVRef", QString::number(vref)); m_params->setParamValue("adcVOff", QString::number(voff)); m_params->setParamValue("adcVCor", QString::number(vcor)); @@ -801,13 +809,17 @@ bool Device::setCalParam() float vcor = dataProcessing->getCalibrationData()->voltageCorr; float coff = dataProcessing->getCalibrationData()->voltageCurrOffset; float ccor = dataProcessing->getCalibrationData()->currentCorrection; + float dacoff = dataProcessing->getCalibrationData()->dacOffset; + float daccor = dataProcessing->getCalibrationData()->dacCorrection; - QString command = QString("device param cal set -vref=%1 -voff=%2 -vcor=%3 -coff=%4 -ccor=%5") + QString command = QString("device param cal set -vref=%1 -voff=%2 -vcor=%3 -coff=%4 -ccor=%5 -dacoff=%6 -daccor=%7") .arg(vref, 0, 'f', 4) .arg(voff, 0, 'f', 4) .arg(vcor, 0, 'f', 4) .arg(coff, 0, 'f', 4) - .arg(ccor, 0, 'f', 4); + .arg(ccor, 0, 'f', 4) + .arg(dacoff, 0, 'f', 4) + .arg(daccor, 0, 'f', 4); if(!controlLink->executeCommand(command, &response, 1000)) return false; @@ -820,6 +832,8 @@ bool Device::setCalParam() m_params->setParamValue("adcVCor", QString::number(vcor)); m_params->setParamValue("adcVCOffset", QString::number(coff)); m_params->setParamValue("adcCCor", QString::number(ccor)); + m_params->setParamValue("loadDacOffset", QString::number(dacoff)); + m_params->setParamValue("loadDacCor", QString::number(daccor)); return true; } diff --git a/Source/devicecontainer.cpp b/Source/devicecontainer.cpp index 78bf4d9..1d7e6ee 100644 --- a/Source/devicecontainer.cpp +++ b/Source/devicecontainer.cpp @@ -1,5 +1,8 @@ #include #include "Processing/batteryparamsextraction.h" + +#define DEVICECONTAINER_RELAX_SAMPLES_MAX 2000000 +#define DEVICECONTAINER_RELAX_CHECK_PERIOD 500.0 #include #include "devicecontainer.h" @@ -15,11 +18,15 @@ DeviceContainer::DeviceContainer(QObject *parent, log = new Log(); fileProcessing = new FileProcessing(); batteryParamsExtraction = new BatteryParamsExtraction(this); + connect(batteryParamsExtraction, &BatteryParamsExtraction::sigCycleFinished, + this, &DeviceContainer::onBatParamCycleFinished); samplingPeriodMs = 0; batParamRelaxEnabled = false; batParamRelaxThreshold = 0; batParamRelaxWindow = 0; - batParamRelaxPasses = 0; + batParamRelaxFilter = 0; + batParamAverageSum = 0; + batParamAverageFirst = 0; batParamRelaxPassesDone = 0; batParamRelaxPauseActive = false; batParamRelaxRunning = false; @@ -694,6 +701,10 @@ void DeviceContainer::onDeviceWndSamplingPeriodChanged(QString time) { bool ok = device->setSamplingPeriod(time); + /*Everything that maps a marker to a sample works with the sampling period, so it + is read back from the device after every change*/ + if(ok) device->getSamplingPeriod(NULL); + logResult(ok, "Sampling time successfully set: " + time, "Unable to set sampling time"); @@ -831,6 +842,9 @@ void DeviceContainer::onDeviceSamplingPeriodObtained(QString stime) samplingPeriodMs = stime.toDouble() / 1000.0; batteryParamsExtraction->setSamplingPeriod(samplingPeriodMs); + log->printLogMessage("Battery parameters: sampling period " + QString::number(samplingPeriodMs, 'f', 4) + " ms", + LOG_MESSAGE_TYPE_INFO); + logResult(ok, "Sampling time successfully obtained and presented", "Unable to present sampling time"); @@ -1081,7 +1095,21 @@ void DeviceContainer::onDeviceNewVoltageCurrentSamplesReceived(QVector v deviceWnd->plotVoltageValues(voltage, voltageKeys); deviceWnd->plotCurrentValues(current, currentKeys); batteryParamsExtraction->onNewSamplesReceived(voltage, current, voltageKeys, currentKeys); - batParamRelaxationSamplesProcess(voltage, voltageKeys); + + /*Averaged voltage belongs to a running battery parameters wave, outside of it + there is nothing to relax and the trace would only clutter the plot*/ + if(batParamRelaxRunning && (batParamRelaxFilter > 0)) + { + QVector averaged; + + batParamAverageProcess(voltage, voltageKeys, &averaged); + deviceWnd->plotVoltageAverageValues(averaged, voltageKeys); + batParamRelaxationSamplesProcess(averaged, voltageKeys); + } + else + { + batParamRelaxationSamplesProcess(voltage, voltageKeys); + } if(writeSamplesToFileEnabled) { fileProcessing->appendSampleDataQueued(voltage, voltageKeys, current, currentKeys); @@ -1136,24 +1164,84 @@ void DeviceContainer::onDeviceNewEBPFull(double value, double key, QString name) batParamRelaxationMarkerProcess(name, key); } -void DeviceContainer::onDeviceWndBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes) +void DeviceContainer::onDeviceWndBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, double filterMs) { batParamRelaxEnabled = enabled; batParamRelaxThreshold = thresholdMv / 1000.0; batParamRelaxWindow = windowS * 1000.0; - batParamRelaxPasses = passes; + batParamRelaxFilter = filterMs; batParamRelaxationReset(); + deviceWnd->enableVoltageAveragePlot(false); + + /*Load tab drives the procedure, so the analysis works with the same relaxation + parameters the pause was cut with*/ + batteryparams_settings_t settings = batteryParamsExtraction->getSettings(); + + settings.relaxationThreshold = thresholdMv; + settings.relaxationWindow = windowS; + settings.relaxationFilter = filterMs; + + batteryParamsExtraction->setSettings(settings); + + if(batteryParamsWnd != NULL) batteryParamsWnd->setSettings(settings); if(!enabled) return; - log->printLogMessage("Battery parameters: relaxation ends on dV/dt below " + QString::number(thresholdMv) + - " mV in " + QString::number(windowS) + " s, " + QString::number(passes) + " passes", + log->printLogMessage("Battery parameters: relaxation ends on " + QString::number(thresholdMv) + + " mV in " + QString::number(windowS) + " s, voltage averaged over " + QString::number(filterMs) + + " ms, runs until under voltage protection", + LOG_MESSAGE_TYPE_INFO); +} + +void DeviceContainer::batParamAverageProcess(QVector voltage, QVector voltageKeys, QVector *averaged) +{ + if(averaged != NULL) averaged->clear(); + + for(int i = 0; i < voltage.size() && i < voltageKeys.size(); i++) + { + batParamAverageKeys.append(voltageKeys[i]); + batParamAverageVoltage.append(voltage[i]); + batParamAverageSum += voltage[i]; + + while((batParamAverageFirst < (batParamAverageKeys.size() - 1)) && + ((batParamAverageKeys.last() - batParamAverageKeys[batParamAverageFirst]) > batParamRelaxFilter)) + { + batParamAverageSum -= batParamAverageVoltage[batParamAverageFirst]; + batParamAverageFirst++; + } + + if(averaged != NULL) + { + averaged->append(batParamAverageSum / (double)(batParamAverageKeys.size() - batParamAverageFirst)); + } + } + + /*Only the samples inside the filter window matter, the rest is dropped so that a + long measurement does not keep growing the buffer*/ + if(batParamAverageFirst > 0) + { + batParamAverageKeys.remove(0, batParamAverageFirst); + batParamAverageVoltage.remove(0, batParamAverageFirst); + batParamAverageFirst = 0; + } +} + +void DeviceContainer::onBatParamCycleFinished(batteryparams_cycle_t cycle) +{ + log->printLogMessage("Battery parameters: cycle " + QString::number(cycle.index) + " extracted, R = " + + (cycle.resistanceValid ? QString::number(cycle.resistance * 1000.0, 'f', 1) + " mOhm" : QString("n/a")) + + ", relaxed " + (cycle.relaxationReached ? QString("yes") : QString("no")), LOG_MESSAGE_TYPE_INFO); } void DeviceContainer::batParamRelaxationReset() { + batParamRelaxCheckKey = 0; + batParamAverageSum = 0; + batParamAverageFirst = 0; + batParamAverageKeys.clear(); + batParamAverageVoltage.clear(); batParamRelaxPassesDone = 0; batParamRelaxPauseActive = false; batParamRelaxRunning = false; @@ -1173,6 +1261,7 @@ void DeviceContainer::batParamRelaxationMarkerProcess(QString name, double key) batParamRelaxPauseActive = true; batParamRelaxKeys.clear(); batParamRelaxVoltage.clear(); + batParamRelaxCheckKey = 0; return; } @@ -1181,14 +1270,14 @@ void DeviceContainer::batParamRelaxationMarkerProcess(QString name, double key) batParamRelaxPauseActive = false; batParamRelaxKeys.clear(); batParamRelaxVoltage.clear(); + batParamRelaxCheckKey = 0; return; } } void DeviceContainer::batParamRelaxationSamplesProcess(QVector voltage, QVector voltageKeys) { - double minimum; - double maximum; + int relaxPosition; if(!batParamRelaxEnabled || !batParamRelaxRunning || !batParamRelaxPauseActive) return; if(batParamRelaxThreshold <= 0 || batParamRelaxWindow <= 0) return; @@ -1199,28 +1288,33 @@ void DeviceContainer::batParamRelaxationSamplesProcess(QVector voltage, batParamRelaxVoltage.append(voltage[i]); } - if(batParamRelaxKeys.isEmpty()) return; + if(batParamRelaxKeys.size() < 2) return; - while((batParamRelaxKeys.size() > 1) && - ((batParamRelaxKeys.last() - batParamRelaxKeys.first()) > batParamRelaxWindow)) + /*Whole pause is kept, the relaxation point can lie far behind the newest sample. + A long pause is still bounded so that a stuck measurement cannot eat the memory*/ + if(batParamRelaxKeys.size() > DEVICECONTAINER_RELAX_SAMPLES_MAX) { - batParamRelaxKeys.removeFirst(); - batParamRelaxVoltage.removeFirst(); + int excess = batParamRelaxKeys.size() - DEVICECONTAINER_RELAX_SAMPLES_MAX; + batParamRelaxKeys.remove(0, excess); + batParamRelaxVoltage.remove(0, excess); } - /*The pause can only be ended once the collected samples cover the whole window*/ - if((batParamRelaxKeys.last() - batParamRelaxKeys.first()) < batParamRelaxWindow) return; + /*Evaluating the whole pause on every packet would be wasted work, the pause is + minutes long and the search is done over a growing array*/ + if((batParamRelaxKeys.last() - batParamRelaxCheckKey) < DEVICECONTAINER_RELAX_CHECK_PERIOD) return; - minimum = batParamRelaxVoltage.first(); - maximum = batParamRelaxVoltage.first(); + batParamRelaxCheckKey = batParamRelaxKeys.last(); - for(int i = 1; i < batParamRelaxVoltage.size(); i++) - { - if(batParamRelaxVoltage[i] < minimum) minimum = batParamRelaxVoltage[i]; - if(batParamRelaxVoltage[i] > maximum) maximum = batParamRelaxVoltage[i]; - } + /*Same search the analysis uses, so the relaxation point reported in the battery + parameters window is the one the pause was ended on*/ + relaxPosition = BatteryParamsExtraction::relaxationPositionGet(batParamRelaxKeys, batParamRelaxVoltage, + 0, batParamRelaxKeys.size() - 1, + batParamRelaxThreshold, batParamRelaxWindow); - if((maximum - minimum) > batParamRelaxThreshold) return; + if(relaxPosition < 0) return; + + log->printLogMessage("Battery parameters: relaxed at " + QString::number(batParamRelaxKeys[relaxPosition] / 1000.0, 'f', 3) + " s", + LOG_MESSAGE_TYPE_INFO); batParamRelaxationFinish(batParamRelaxKeys.last()); } @@ -1251,6 +1345,7 @@ void DeviceContainer::batParamRelaxationFinish(double key) batParamRelaxPauseActive = false; batParamRelaxKeys.clear(); batParamRelaxVoltage.clear(); + batParamRelaxCheckKey = 0; /*The device is stopped in the middle of the pause chunk, so it never sends the pause end marker. The marker is created here instead, at the moment the voltage @@ -1259,8 +1354,7 @@ void DeviceContainer::batParamRelaxationFinish(double key) batteryParamsExtraction->onNewMarkerReceived(0, index, marker); if(writeSamplesToFileEnabled) fileProcessing->appendEPQueued(marker, (int)index); - log->printLogMessage("Battery parameters: pass " + QString::number(batParamRelaxPassesDone + 1) + "/" + - QString::number(batParamRelaxPasses) + " relaxed, pause ended early", + log->printLogMessage("Battery parameters: pass " + QString::number(batParamRelaxPassesDone + 1) + " relaxed, pause ended early", LOG_MESSAGE_TYPE_INFO, LOG_MESSAGE_DEVICE_TYPE_DEVICE, LOG_MESSAGE_CATEGORY_ENERGY_POINT); batParamRelaxRestarting = true; @@ -1279,18 +1373,12 @@ void DeviceContainer::batParamRelaxationNextPass() batParamRelaxRestarting = false; batParamRelaxPassesDone++; - if(batParamRelaxPassesDone >= batParamRelaxPasses) - { - batParamRelaxRunning = false; - deviceWnd->loadWaveStopped(); - log->printLogMessage("Battery parameters: all " + QString::number(batParamRelaxPasses) + " passes done", - LOG_MESSAGE_TYPE_INFO); - return; - } - + /*Procedure has no pass count, it runs until the battery hits the under voltage + protection and the protection stops the wave*/ if(!device->setLoadWaveState(true)) { batParamRelaxRunning = false; + deviceWnd->enableVoltageAveragePlot(false); deviceWnd->loadWaveStopped(); log->printLogMessage("Unable to start next battery parameters pass", LOG_MESSAGE_TYPE_ERROR); } @@ -1347,7 +1435,18 @@ void DeviceContainer::onDeviceMeasurementEnergyFlowStatusChanged(charginganalysi void DeviceContainer::onDeviceWndCalibrationUpdated() { + bool ok; + device->calibrationUpdated(); + + /*Voltage and current calibration parameters are used by the application itself, + but the load DAC offset is applied on the device, so every update has to be + sent there as well*/ + ok = device->setCalParam(); + + logResult(ok, + "Calibration parameters successfully applied on device", + "Unable to apply calibration parameters on device"); } void DeviceContainer::onDeviceWndCalibrationStoreRequest() @@ -1599,6 +1698,15 @@ void DeviceContainer::onDeviceWndBatParamViewRequested() batteryParamsWnd, &BatteryParamsWnd::onCycleFinished, Qt::QueuedConnection); connect(batteryParamsWnd, &BatteryParamsWnd::sigSettingsChanged, this, &DeviceContainer::onBatParamSettingsChanged); + + /*Window is usually opened while the procedure already runs, so the cycles + extracted before it existed are handed over*/ + QVector collected = batteryParamsExtraction->getCycles(); + + for(int i = 0; i < collected.size(); i++) + { + batteryParamsWnd->onCycleFinished(collected[i]); + } } batteryParamsWnd->show(); @@ -1616,6 +1724,7 @@ void DeviceContainer::onDeviceWndLoadWaveSetStatus(bool status) { batParamRelaxationReset(); batParamRelaxRunning = (status && batParamRelaxEnabled); + deviceWnd->enableVoltageAveragePlot(batParamRelaxRunning && (batParamRelaxFilter > 0)); } if(ok && status) @@ -1644,8 +1753,7 @@ void DeviceContainer::onDeviceLoadWaveStopped() if(batParamRelaxEnabled && batParamRelaxRunning) { - log->printLogMessage("Battery parameters: pass " + QString::number(batParamRelaxPassesDone + 1) + "/" + - QString::number(batParamRelaxPasses) + " finished on maximum pause", + log->printLogMessage("Battery parameters: pass " + QString::number(batParamRelaxPassesDone + 1) + " finished on maximum pause", LOG_MESSAGE_TYPE_INFO); batParamRelaxPauseActive = false; batParamRelaxationNextPass(); diff --git a/Source/devicecontainer.h b/Source/devicecontainer.h index 3e2dc9a..20f57dc 100644 --- a/Source/devicecontainer.h +++ b/Source/devicecontainer.h @@ -62,7 +62,8 @@ public slots: void onDeviceWndLoadWaveSetStatus(bool status); void onDeviceWndBatParamViewRequested(); void onDeviceWndBatParamCapacityChanged(double capacity); - void onDeviceWndBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, int passes); + void onDeviceWndBatParamRelaxationChanged(bool enabled, double thresholdMv, double windowS, double filterMs); + void onBatParamCycleFinished(batteryparams_cycle_t cycle); void onBatParamSettingsChanged(batteryparams_settings_t settings); void onDeviceWndLoadWaveClear(); void onDeviceLoadWaveStopped(); @@ -154,18 +155,24 @@ private slots: void batParamRelaxationReset(); void batParamRelaxationMarkerProcess(QString name, double key); void batParamRelaxationSamplesProcess(QVector voltage, QVector voltageKeys); + void batParamAverageProcess(QVector voltage, QVector voltageKeys, QVector *averaged); void batParamRelaxationFinish(double key); void batParamRelaxationNextPass(); bool batParamRelaxEnabled; double batParamRelaxThreshold; double batParamRelaxWindow; - int batParamRelaxPasses; + double batParamRelaxFilter; + double batParamAverageSum; + int batParamAverageFirst; + QVector batParamAverageKeys; + QVector batParamAverageVoltage; int batParamRelaxPassesDone; bool batParamRelaxPauseActive; bool batParamRelaxRunning; bool batParamRelaxRestarting; double samplingPeriodMs; + double batParamRelaxCheckKey; QVector batParamRelaxKeys; QVector batParamRelaxVoltage; bool createSubDir(const QString &subDirName, QString &fullPath); From 423d39640fe32b2aa8bc0e5583df02e990fea481 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Sat, 3 Oct 2026 16:10:53 +0200 Subject: [PATCH 7/8] Auto Calibration implemented --- Source/OpenEPT.pro | 2 + Source/Windows/Device/autocalibrationwnd.cpp | 870 ++++++++++++++++++ Source/Windows/Device/autocalibrationwnd.h | 112 +++ Source/Windows/Device/calibrationwnd.cpp | 136 ++- Source/Windows/Device/calibrationwnd.h | 23 +- Source/Windows/Device/calibrationwnd.ui | 908 +------------------ Source/Windows/Device/devicewnd.cpp | 39 + Source/Windows/Device/devicewnd.h | 12 + Source/devicecontainer.cpp | 72 ++ Source/devicecontainer.h | 7 + 10 files changed, 1254 insertions(+), 927 deletions(-) create mode 100644 Source/Windows/Device/autocalibrationwnd.cpp create mode 100644 Source/Windows/Device/autocalibrationwnd.h diff --git a/Source/OpenEPT.pro b/Source/OpenEPT.pro index b57e9aa..23dc59c 100644 --- a/Source/OpenEPT.pro +++ b/Source/OpenEPT.pro @@ -62,6 +62,7 @@ SOURCES += \ Windows/DataAnalyzer/dataanalyzerprofile.cpp \ Windows/DataAnalyzer/dataanalyzerstatistics.cpp \ Windows/Device/calibrationwnd.cpp \ + Windows/Device/autocalibrationwnd.cpp \ Windows/Device/configurationwnd.cpp \ Windows/Device/datastatistics.cpp \ Windows/Device/devicewnd.cpp \ @@ -114,6 +115,7 @@ HEADERS += \ Windows/DataAnalyzer/dataanalyzerworker.h \ Windows/DataAnalyzer/dataanalyzerstatistics.h \ Windows/Device/calibrationwnd.h \ + Windows/Device/autocalibrationwnd.h \ Windows/Device/configurationwnd.h \ Windows/Device/datastatistics.h \ Windows/Device/devicewnd.h \ diff --git a/Source/Windows/Device/autocalibrationwnd.cpp b/Source/Windows/Device/autocalibrationwnd.cpp new file mode 100644 index 0000000..20e5050 --- /dev/null +++ b/Source/Windows/Device/autocalibrationwnd.cpp @@ -0,0 +1,870 @@ +#include "autocalibrationwnd.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define AUTOCAL_VOLTAGE_REFERENCE 2.049 +#define AUTOCAL_FLOAT_VOLTAGE_MIN 5.000 +#define AUTOCAL_ZERO_VOLTAGE_LIMIT 0.100 +#define AUTOCAL_CONNECTED_VOLTAGE_MIN 0.300 +#define AUTOCAL_CONNECTED_VOLTAGE_MAX 4.600 +#define AUTOCAL_VOLTAGE_SPREAD_LIMIT 0.010 +#define AUTOCAL_CURRENT_SPREAD_LIMIT 2.000 +#define AUTOCAL_VOLTAGE_TOLERANCE 0.005 +#define AUTOCAL_CURRENT_TOLERANCE 0.500 +#define AUTOCAL_HISTORY_DEPTH 6 +#define AUTOCAL_LOAD_SETTLE_TICKS 8 +#define AUTOCAL_LOAD_MIN_CURRENT 100.0 +#define AUTOCAL_LOAD_POINTS_MIN 3 +#define AUTOCAL_LOAD_POINTS_MAX 8 +#define AUTOCAL_CURRENT_SPAN_TARGET 200.0 + +AutoCalibrationWnd::AutoCalibrationWnd(QWidget *parent) : + QWidget(parent) +{ + QFont defaultFont("Arial", 10); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QHBoxLayout *buttonLayout = new QHBoxLayout(); + + setFont(defaultFont); + setWindowTitle("Automatic calibration"); + resize(520, 520); + + calData = NULL; + step = AUTOCAL_STEP_IDLE; + acquisitionActive = false; + loadDisabled = true; + referenceVoltage = AUTOCAL_VOLTAGE_REFERENCE; + externalReference = false; + voltageAvg = 0; + currentAvg = 0; + statsValid = false; + loadPhase = 0; + loadSettleTicks = 0; + snapshotValid = false; + + titleLabel = new QLabel(this); + titleLabel->setStyleSheet("font-weight: bold; font-size: 14px;"); + stepLabel = new QLabel(this); + stepLabel->setStyleSheet("color: gray;"); + instructionLabel = new QLabel(this); + instructionLabel->setWordWrap(true); + instructionLabel->setMinimumHeight(70); + imageLabel = new QLabel(this); + imageLabel->setAlignment(Qt::AlignCenter); + imageLabel->setMinimumHeight(160); + liveLabel = new QLabel(this); + liveLabel->setStyleSheet("font-size: 13px;"); + resultLabel = new QLabel(this); + resultLabel->setWordWrap(true); + resultLabel->setStyleSheet("font-weight: bold;"); + + logView = new QPlainTextEdit(this); + logView->setReadOnly(true); + logView->setMaximumBlockCount(500); + logView->setMinimumHeight(110); + logView->setStyleSheet("font-family: monospace; font-size: 11px;"); + + backButton = new QPushButton("Back", this); + cancelButton = new QPushButton("Cancel", this); + readyButton = new QPushButton("I am ready, continue", this); + readyButton->setToolTip("Continue to the next step without waiting for the automatic detection"); + nextButton = new QPushButton("Next", this); + + buttonLayout->addWidget(cancelButton); + buttonLayout->addStretch(); + buttonLayout->addWidget(backButton); + buttonLayout->addWidget(readyButton); + buttonLayout->addWidget(nextButton); + + mainLayout->addWidget(titleLabel); + mainLayout->addWidget(stepLabel); + mainLayout->addWidget(instructionLabel); + mainLayout->addWidget(imageLabel, 1); + mainLayout->addWidget(liveLabel); + mainLayout->addWidget(resultLabel); + mainLayout->addWidget(logView, 1); + mainLayout->addLayout(buttonLayout); + + loadTimer = new QTimer(this); + loadTimer->setInterval(500); + + connect(nextButton, &QPushButton::clicked, this, &AutoCalibrationWnd::onNextClicked); + connect(backButton, &QPushButton::clicked, this, &AutoCalibrationWnd::onBackClicked); + connect(cancelButton, &QPushButton::clicked, this, &AutoCalibrationWnd::onCancelClicked); + connect(readyButton, &QPushButton::clicked, this, &AutoCalibrationWnd::onReadyClicked); + connect(loadTimer, &QTimer::timeout, this, &AutoCalibrationWnd::onLoadStepTick); +} + +void AutoCalibrationWnd::setCalibrationData(CalibrationData *aCalData) +{ + calData = aCalData; +} + +void AutoCalibrationWnd::setAcquisitionActive(bool active) +{ + acquisitionActive = active; +} + +void AutoCalibrationWnd::setLoadDisabled(bool disabled) +{ + loadDisabled = disabled; +} + +double AutoCalibrationWnd::maxLoadCurrentMa() +{ + if(calData == NULL || calData->currentShunt <= 0) return 1000.0; + + /*Shunt 0.045 ohm corresponds to a 3 A maximum, so the full scale current scales + inversely with the shunt value*/ + return 135.0 / calData->currentShunt; +} + +int AutoCalibrationWnd::loadPointCount() +{ + /*At least three points, more for a wider current range so the line is well + constrained*/ + int points = (int)(maxLoadCurrentMa() / 500.0) + AUTOCAL_LOAD_POINTS_MIN; + + if(points < AUTOCAL_LOAD_POINTS_MIN) points = AUTOCAL_LOAD_POINTS_MIN; + if(points > AUTOCAL_LOAD_POINTS_MAX) points = AUTOCAL_LOAD_POINTS_MAX; + + return points; +} + +void AutoCalibrationWnd::setImage(QString fileName) +{ + QString path = ":/images/Calibration/" + fileName; + + if(QFile::exists(path)) + { + imageLabel->setPixmap(QPixmap(path).scaledToHeight(150, Qt::SmoothTransformation)); + } + else + { + imageLabel->setPixmap(QPixmap()); + } +} + +void AutoCalibrationWnd::pushHistory(double v, double c) +{ + voltageHistory.append(v); + currentHistory.append(c); + + while(voltageHistory.size() > AUTOCAL_HISTORY_DEPTH) voltageHistory.removeFirst(); + while(currentHistory.size() > AUTOCAL_HISTORY_DEPTH) currentHistory.removeFirst(); +} + +bool AutoCalibrationWnd::voltageStable(double *mean, double spreadLimit) +{ + double minimum; + double maximum; + double sum = 0; + + if(voltageHistory.size() < AUTOCAL_HISTORY_DEPTH) return false; + + minimum = voltageHistory.first(); + maximum = voltageHistory.first(); + + for(int i = 0; i < voltageHistory.size(); i++) + { + if(voltageHistory[i] < minimum) minimum = voltageHistory[i]; + if(voltageHistory[i] > maximum) maximum = voltageHistory[i]; + sum += voltageHistory[i]; + } + + if(mean != NULL) *mean = sum / voltageHistory.size(); + + return (maximum - minimum) <= spreadLimit; +} + +bool AutoCalibrationWnd::currentStable(double *mean, double spreadLimit) +{ + double minimum; + double maximum; + double sum = 0; + + if(currentHistory.size() < AUTOCAL_HISTORY_DEPTH) return false; + + minimum = currentHistory.first(); + maximum = currentHistory.first(); + + for(int i = 0; i < currentHistory.size(); i++) + { + if(currentHistory[i] < minimum) minimum = currentHistory[i]; + if(currentHistory[i] > maximum) maximum = currentHistory[i]; + sum += currentHistory[i]; + } + + if(mean != NULL) *mean = sum / currentHistory.size(); + + return (maximum - minimum) <= spreadLimit; +} + +void AutoCalibrationWnd::startCalibration() +{ + if(!acquisitionActive) + { + QMessageBox::warning(this, "Automatic calibration", + "Start the acquisition before the calibration.\n\n" + "The calibration uses the real time averaged voltage and current."); + return; + } + + QMessageBox box(this); + box.setWindowTitle("Automatic calibration"); + box.setText("Which voltage reference is used for the voltage channel?"); + QPushButton *internalBtn = box.addButton("Internal 2.049 V", QMessageBox::AcceptRole); + QPushButton *externalBtn = box.addButton("External value", QMessageBox::AcceptRole); + box.addButton(QMessageBox::Cancel); + box.exec(); + + if(box.clickedButton() == externalBtn) + { + bool ok = false; + double value = QInputDialog::getDouble(this, "External reference", + "External reference voltage held on the input [V]:", + 2.049, 0.05, 20.0, 4, &ok); + if(!ok) return; + + externalReference = true; + referenceVoltage = value; + } + else if(box.clickedButton() == internalBtn) + { + externalReference = false; + referenceVoltage = AUTOCAL_VOLTAGE_REFERENCE; + } + else + { + return; + } + + { + bool ok = false; + int defaultPoints = loadPointCount(); + + currentSpanPoints = QInputDialog::getInt(this, "Current span points", + "Number of current points, you enter the reference current at each (min 3):", + qMax(3, defaultPoints), 3, 20, 1, &ok); + if(!ok) return; + + loadPoints = QInputDialog::getInt(this, "Load calibration points", + "Number of load points (no input needed, measured automatically):", + defaultPoints, 2, 20, 1, &ok); + if(!ok) return; + } + + voltageHistory.clear(); + currentHistory.clear(); + logView->clear(); + appendLog("Calibration started"); + appendLog("Current span points: " + QString::number(currentSpanPoints) + + ", load points: " + QString::number(loadPoints)); + appendLog(externalReference ? ("External reference " + QString::number(referenceVoltage, 'f', 4) + + " V, jumper steps skipped") + : "Internal reference 2.049 V"); + + if(calData != NULL) + { + startSnapshot = *calData; + snapshotValid = true; + } + goToStep(externalReference ? AUTOCAL_STEP_VOLTAGE_TUNE : AUTOCAL_STEP_DISCONNECT_INPUT); + show(); + raise(); + activateWindow(); +} + +void AutoCalibrationWnd::appendLog(QString message) +{ + logView->appendPlainText(QTime::currentTime().toString("HH:mm:ss") + " " + message); +} + +QString AutoCalibrationWnd::buildReport() +{ + QString report; + int changed = 0; + + if(!snapshotValid || calData == NULL) return "No calibration data."; + + struct { const char *name; double before; double after; int decimals; } rows[] = { + { "ADC voltage reference", startSnapshot.adcVoltageRef, calData->adcVoltageRef, 4 }, + { "Voltage offset", startSnapshot.voltageOff, calData->voltageOff, 5 }, + { "Voltage correction", startSnapshot.voltageCorr, calData->voltageCorr, 5 }, + { "Voltage C. offset", startSnapshot.voltageCurrOffset, calData->voltageCurrOffset, 5 }, + { "Current correction", startSnapshot.currentCorrection, calData->currentCorrection, 5 }, + { "Current gain", startSnapshot.currentGain, calData->currentGain, 4 }, + { "Current shunt", startSnapshot.currentShunt, calData->currentShunt, 4 }, + { "Load DAC offset", startSnapshot.dacOffset, calData->dacOffset, 2 }, + { "Load DAC correction", startSnapshot.dacCorrection, calData->dacCorrection, 4 } + }; + + report = "Changed calibration coefficients:\n"; + + for(unsigned int i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) + { + if(fabs(rows[i].after - rows[i].before) < 1e-9) continue; + + report += QString(" %1: %2 -> %3\n") + .arg(rows[i].name) + .arg(rows[i].before, 0, 'f', rows[i].decimals) + .arg(rows[i].after, 0, 'f', rows[i].decimals); + changed++; + } + + if(changed == 0) report += " none\n"; + + return report; +} + +autocal_step_t AutoCalibrationWnd::modeStartStep(autocal_step_t s) +{ + /*Keep the already calibrated channels, restart only the channel that failed*/ + if(s <= AUTOCAL_STEP_VOLTAGE_TUNE) return externalReference ? AUTOCAL_STEP_VOLTAGE_TUNE : AUTOCAL_STEP_DISCONNECT_INPUT; + if(s == AUTOCAL_STEP_CURRENT_OFFSET) return AUTOCAL_STEP_CURRENT_OFFSET; + if(s == AUTOCAL_STEP_CURRENT_SPAN) return AUTOCAL_STEP_CURRENT_SPAN; + if(s == AUTOCAL_STEP_CURRENT_ZERO2) return AUTOCAL_STEP_CURRENT_ZERO2; + return externalReference ? AUTOCAL_STEP_CONNECT_SOURCE : AUTOCAL_STEP_JUMPER_BATTERY; +} + +void AutoCalibrationWnd::goToStep(autocal_step_t newStep) +{ + step = newStep; + voltageHistory.clear(); + currentHistory.clear(); + loadTimer->stop(); + resultLabel->clear(); + updateView(); + appendLog("Step: " + stepLabel->text()); + + if(step == AUTOCAL_STEP_CURRENT_SPAN) + { + int points = currentSpanPoints; + double maxMa = maxLoadCurrentMa() * 0.8; + double first = AUTOCAL_LOAD_MIN_CURRENT; + + if(points < 2) points = 2; + if(maxMa < first + 1.0) maxMa = first + 1.0; + + loadRequested.clear(); + for(int i = 0; i < points; i++) + { + loadRequested.append(first + (maxMa - first) * i / (double)(points - 1)); + } + + loadPhase = 0; + loadSettleTicks = 0; + spanSumMT = 0; + spanSumMM = 0; + + appendLog("Current span: " + QString::number(points) + " points from " + + QString::number((int)first) + " mA to " + QString::number((int)maxMa) + + " mA, you enter the reference at each"); + + loadDisabled = false; + emit sigResetProtection(); + emit sigSetLoadCurrent((int)loadRequested.first()); + emit sigSetLoadEnabled(true); + loadTimer->start(); + } + + if(step == AUTOCAL_STEP_LOAD_TUNE) + { + { + int points = loadPoints; + double maxMa = maxLoadCurrentMa() * 0.8; + double first = AUTOCAL_LOAD_MIN_CURRENT; + + if(maxMa < first + 1.0) maxMa = first + 1.0; + + loadRequested.clear(); + loadMeasured.clear(); + + for(int i = 0; i < points; i++) + { + loadRequested.append(first + (maxMa - first) * i / (double)(points - 1)); + } + + loadPhase = 0; + loadSettleTicks = 0; + + appendLog("Load tuning: " + QString::number(points) + " points from " + + QString::number((int)first) + " mA to " + QString::number((int)maxMa) + + " mA (max " + QString::number((int)maxLoadCurrentMa()) + " mA)"); + + loadDisabled = false; + emit sigResetProtection(); + emit sigSetLoadCurrent((int)loadRequested.first()); + emit sigSetLoadEnabled(true); + loadTimer->start(); + } + } +} + +void AutoCalibrationWnd::updateView() +{ + backButton->setEnabled(step > AUTOCAL_STEP_DISCONNECT_INPUT && step < AUTOCAL_STEP_DONE); + nextButton->setText(step == AUTOCAL_STEP_DONE ? "Finish" : "Next"); + nextButton->setEnabled(step == AUTOCAL_STEP_DONE); + readyButton->setEnabled(step == AUTOCAL_STEP_DISCONNECT_INPUT || + step == AUTOCAL_STEP_SET_REFERENCE || + step == AUTOCAL_STEP_JUMPER_BATTERY || + step == AUTOCAL_STEP_CONNECT_SOURCE); + + switch(step) + { + case AUTOCAL_STEP_DISCONNECT_INPUT: + titleLabel->setText("Voltage channel calibration"); + stepLabel->setText("Step 1 of 3 - disconnect the input"); + instructionLabel->setText("Disconnect everything from the Power Input connector.\n" + "The calibration continues on its own once the input reading drops to zero " + "or floats above " + QString::number(AUTOCAL_FLOAT_VOLTAGE_MIN, 'f', 1) + + " V, or press \"I am ready, continue\"."); + setImage("input_disconnect.png"); + break; + case AUTOCAL_STEP_SET_REFERENCE: + titleLabel->setText("Voltage channel calibration"); + stepLabel->setText("Step 1 of 3 - select the voltage reference"); + instructionLabel->setText("Move the ADC IN Selection jumper to the voltage reference position.\n" + "The voltage channel is tuned automatically once the reference is detected, " + "or press \"I am ready, continue\"."); + setImage("adc_in_selection.png"); + break; + case AUTOCAL_STEP_VOLTAGE_TUNE: + titleLabel->setText("Voltage channel calibration"); + stepLabel->setText("Step 1 of 3 - tuning"); + instructionLabel->setText("Tuning the voltage correction so the measured voltage reaches " + + QString::number(referenceVoltage, 'f', 3) + " V." + + QString(externalReference ? "\nKeep the external reference connected to the input." : "")); + break; + case AUTOCAL_STEP_CURRENT_OFFSET: + titleLabel->setText("Current channel calibration"); + stepLabel->setText("Step 2 of 3 - zero current"); + instructionLabel->setText("Make sure the Load is disabled.\n" + "The current offset is tuned automatically so the measured current gets as " + "close to zero as possible (the measurement is bidirectional)."); + setImage(""); + break; + case AUTOCAL_STEP_CURRENT_ZERO2: + titleLabel->setText("Current channel calibration"); + stepLabel->setText("Step 2 of 3 - zero re-check"); + instructionLabel->setText("Make sure the Load is disabled.\n" + "The current offset is checked again after the span before the load calibration."); + setImage(""); + break; + case AUTOCAL_STEP_JUMPER_BATTERY: + titleLabel->setText("Load calibration"); + stepLabel->setText("Step 3 of 3 - select the battery"); + instructionLabel->setText("Move the ADC IN Selection jumper back to the Battery position.\n" + "Wait until the input reading drops to zero or floats above " + + QString::number(AUTOCAL_FLOAT_VOLTAGE_MIN, 'f', 1) + + " V, or press \"I am ready, continue\"."); + setImage("adc_in_selection.png"); + break; + case AUTOCAL_STEP_CONNECT_SOURCE: + titleLabel->setText("Load calibration"); + stepLabel->setText("Step 3 of 3 - connect the source"); + instructionLabel->setText("Connect a battery or a generator to the Power Input connector.\n" + "The calibration continues once the input voltage rises, " + "or press \"I am ready, continue\"."); + setImage("input_disconnect.png"); + break; + case AUTOCAL_STEP_CURRENT_SPAN: + titleLabel->setText("Current channel calibration"); + stepLabel->setText("Step 2 of 3 - span"); + instructionLabel->setText("Driving a known current through the load.\n" + "Read the true current on your reference meter, the wizard will ask for it."); + setImage(""); + break; + case AUTOCAL_STEP_LOAD_TUNE: + titleLabel->setText("Load calibration"); + stepLabel->setText("Step 3 of 3 - tuning"); + instructionLabel->setText("Driving two load currents and tuning the load DAC so the requested " + "current matches the measured current.\nKeep the source connected."); + setImage(""); + break; + case AUTOCAL_STEP_DONE: + { + QString report = buildReport(); + + titleLabel->setText("Automatic calibration finished"); + stepLabel->setText("All channels calibrated"); + instructionLabel->setText(report + + "\nThe parameters are kept in the application. Open the Calibration " + "window and press Store to keep them on the device."); + liveLabel->clear(); + setImage(""); + appendLog(report); + break; + } + default: + break; + } +} + +void AutoCalibrationWnd::failAndRestart(QString reason) +{ + autocal_step_t restart = modeStartStep(step); + + resultLabel->setStyleSheet("font-weight: bold; color: rgb(170, 0, 0);"); + resultLabel->setText(reason); + appendLog("FAILED: " + reason + " - restarting this channel"); + QMessageBox::warning(this, "Automatic calibration", + reason + "\n\nThe calibration restarts this channel, the already calibrated channels are kept."); + goToStep(restart); +} + +void AutoCalibrationWnd::onNewStatistics(double aVoltageAvg, double aCurrentAvg) +{ + double mean; + + voltageAvg = aVoltageAvg; + currentAvg = aCurrentAvg; + statsValid = true; + + if(!isVisible() || calData == NULL) return; + + pushHistory(voltageAvg, currentAvg); + + liveLabel->setText("Measured - voltage " + QString::number(voltageAvg, 'f', 4) + " V | current " + + QString::number(currentAvg, 'f', 2) + " mA"); + + switch(step) + { + case AUTOCAL_STEP_DISCONNECT_INPUT: + /*Depending on the wiring a free input either floats high above the range or + sits near zero, so both count as disconnected*/ + if(voltageStable(&mean, AUTOCAL_VOLTAGE_SPREAD_LIMIT) && + ((mean > AUTOCAL_FLOAT_VOLTAGE_MIN) || (fabs(mean) < AUTOCAL_ZERO_VOLTAGE_LIMIT))) + { + appendLog("Input free detected at " + QString::number(mean, 'f', 3) + " V"); + goToStep(AUTOCAL_STEP_SET_REFERENCE); + } + break; + + case AUTOCAL_STEP_SET_REFERENCE: + if(voltageStable(&mean, AUTOCAL_VOLTAGE_SPREAD_LIMIT) && + (mean > AUTOCAL_CONNECTED_VOLTAGE_MIN) && (mean < qMax(AUTOCAL_CONNECTED_VOLTAGE_MAX, referenceVoltage * 1.5))) + { + appendLog("Reference detected at " + QString::number(mean, 'f', 3) + " V"); + goToStep(AUTOCAL_STEP_VOLTAGE_TUNE); + } + break; + + case AUTOCAL_STEP_VOLTAGE_TUNE: + if(voltageStable(&mean, AUTOCAL_VOLTAGE_SPREAD_LIMIT)) + { + if((mean > qMax(AUTOCAL_CONNECTED_VOLTAGE_MAX, referenceVoltage * 1.5)) || (mean < AUTOCAL_CONNECTED_VOLTAGE_MIN)) + { + failAndRestart("Voltage reference lost during tuning."); + break; + } + + if(fabs(mean - referenceVoltage) <= AUTOCAL_VOLTAGE_TOLERANCE) + { + resultLabel->setStyleSheet("font-weight: bold; color: rgb(0, 130, 0);"); + resultLabel->setText("Voltage channel calibrated, correction " + + QString::number(calData->voltageCorr, 'f', 5)); + appendLog("Voltage channel OK, measured " + QString::number(mean, 'f', 4) + " V"); + goToStep(AUTOCAL_STEP_CURRENT_OFFSET); + break; + } + + /*Measured voltage scales linearly with the correction, so the correction + that reaches the reference is found in one step*/ + appendLog("Voltage " + QString::number(mean, 'f', 4) + " V, correction -> " + + QString::number(calData->voltageCorr * referenceVoltage / mean, 'f', 5)); + calData->voltageCorr = calData->voltageCorr * referenceVoltage / mean; + emit sigApplyCalibration(); + voltageHistory.clear(); + } + break; + + case AUTOCAL_STEP_CURRENT_OFFSET: + if(!loadDisabled) + { + resultLabel->setStyleSheet("font-weight: bold; color: rgb(170, 0, 0);"); + resultLabel->setText("Disable the Load to calibrate the current offset."); + break; + } + + if(currentStable(&mean, AUTOCAL_CURRENT_SPREAD_LIMIT)) + { + if(fabs(mean) <= AUTOCAL_CURRENT_TOLERANCE) + { + resultLabel->setStyleSheet("font-weight: bold; color: rgb(0, 130, 0);"); + resultLabel->setText("Current channel calibrated, offset " + + QString::number(calData->voltageCurrOffset, 'f', 5)); + appendLog("Current zero OK, measured " + QString::number(mean, 'f', 2) + " mA"); + goToStep(externalReference ? AUTOCAL_STEP_CONNECT_SOURCE : AUTOCAL_STEP_JUMPER_BATTERY); + break; + } + + /*Current reads zero when the offset cancels the raw average, so the offset + is shifted by the measured current expressed back in volts*/ + if((calData->currentShunt > 0) && (calData->currentGain > 0) && (fabs(calData->currentCorrection) > 1e-9)) + { + appendLog("Current " + QString::number(mean, 'f', 2) + " mA, offset -> adjusting"); + calData->voltageCurrOffset += mean * (calData->currentShunt * calData->currentGain) + / (1000.0 * calData->currentCorrection); + emit sigApplyCalibration(); + currentHistory.clear(); + } + } + break; + + case AUTOCAL_STEP_CURRENT_ZERO2: + if(!loadDisabled) + { + resultLabel->setStyleSheet("font-weight: bold; color: rgb(170, 0, 0);"); + resultLabel->setText("Disable the Load to calibrate the current offset."); + break; + } + + if(currentStable(&mean, AUTOCAL_CURRENT_SPREAD_LIMIT)) + { + if(fabs(mean) <= AUTOCAL_CURRENT_TOLERANCE) + { + resultLabel->setStyleSheet("font-weight: bold; color: rgb(0, 130, 0);"); + resultLabel->setText("Current channel calibrated, offset " + + QString::number(calData->voltageCurrOffset, 'f', 5)); + appendLog("Current zero re-check OK, measured " + QString::number(mean, 'f', 2) + " mA"); + goToStep(AUTOCAL_STEP_LOAD_TUNE); + break; + } + + /*Current reads zero when the offset cancels the raw average, so the offset + is shifted by the measured current expressed back in volts*/ + if((calData->currentShunt > 0) && (calData->currentGain > 0) && (fabs(calData->currentCorrection) > 1e-9)) + { + appendLog("Current " + QString::number(mean, 'f', 2) + " mA, offset -> adjusting"); + calData->voltageCurrOffset += mean * (calData->currentShunt * calData->currentGain) + / (1000.0 * calData->currentCorrection); + emit sigApplyCalibration(); + currentHistory.clear(); + } + } + break; + + case AUTOCAL_STEP_JUMPER_BATTERY: + /*Input is still free, so switching the jumper back to the battery path makes + the reading leave the connected band again, floating high or dropping to zero*/ + if(voltageStable(&mean, AUTOCAL_VOLTAGE_SPREAD_LIMIT) && + ((mean > AUTOCAL_FLOAT_VOLTAGE_MIN) || (fabs(mean) < AUTOCAL_ZERO_VOLTAGE_LIMIT))) + { + appendLog("Battery path selected, input free at " + QString::number(mean, 'f', 3) + " V"); + goToStep(AUTOCAL_STEP_CONNECT_SOURCE); + } + break; + + case AUTOCAL_STEP_CONNECT_SOURCE: + if(voltageStable(&mean, AUTOCAL_VOLTAGE_SPREAD_LIMIT) && + (mean > AUTOCAL_CONNECTED_VOLTAGE_MIN) && (mean < AUTOCAL_CONNECTED_VOLTAGE_MAX)) + { + appendLog("Source detected at " + QString::number(mean, 'f', 3) + " V"); + goToStep(AUTOCAL_STEP_CURRENT_SPAN); + } + break; + + default: + break; + } +} + +void AutoCalibrationWnd::onLoadEnableResult(bool ok) +{ + if(step != AUTOCAL_STEP_LOAD_TUNE) return; + if(ok) return; + + loadTimer->stop(); + failAndRestart("Unable to enable the load. Check that the protections are reset and the Load is allowed."); +} + +void AutoCalibrationWnd::onLoadStepTick() +{ + double mean; + + if((step != AUTOCAL_STEP_LOAD_TUNE) && (step != AUTOCAL_STEP_CURRENT_SPAN)) { loadTimer->stop(); return; } + + loadSettleTicks++; + + if(loadSettleTicks < AUTOCAL_LOAD_SETTLE_TICKS) return; + if(!currentStable(&mean, AUTOCAL_CURRENT_SPREAD_LIMIT * 4.0)) return; + + if(step == AUTOCAL_STEP_CURRENT_SPAN) + { + loadTimer->stop(); + + bool ok = false; + double trueCurrent = QInputDialog::getDouble(this, "Current span calibration", + "Point " + QString::number(loadPhase + 1) + " of " + QString::number(loadRequested.size()) + + ".\nThe wizard measures " + QString::number(mean, 'f', 1) + " mA.\n" + "Enter the true current read on your reference meter [mA]:", + mean, 0.0, 100000.0, 2, &ok); + + if(!ok) + { + emit sigSetLoadEnabled(false); + emit sigSetLoadCurrent(0); + failAndRestart("Current span cancelled."); + return; + } + + spanSumMT += mean * trueCurrent; + spanSumMM += mean * mean; + appendLog("Span point " + QString::number(loadPhase + 1) + ": measured " + + QString::number(mean, 'f', 2) + " mA, reference " + QString::number(trueCurrent, 'f', 2) + " mA"); + + loadPhase++; + loadSettleTicks = 0; + currentHistory.clear(); + + if(loadPhase < loadRequested.size()) + { + emit sigSetLoadCurrent((int)loadRequested[loadPhase]); + loadTimer->start(); + return; + } + + emit sigSetLoadEnabled(false); + emit sigSetLoadCurrent(0); + + if(spanSumMM < 1e-9) + { + failAndRestart("No current measured for the span calibration."); + return; + } + + /*Current measurement is a pure scale once the offset is zeroed, so the scale + that maps every measured value onto the reference reading is the least + squares slope through the origin*/ + double scale = spanSumMT / spanSumMM; + + appendLog("Current correction -> " + QString::number(calData->currentCorrection * scale, 'f', 5)); + calData->currentCorrection = calData->currentCorrection * scale; + emit sigApplyCalibration(); + + resultLabel->setStyleSheet("font-weight: bold; color: rgb(0, 130, 0);"); + resultLabel->setText("Current channel span calibrated, correction " + + QString::number(calData->currentCorrection, 'f', 5)); + appendLog("Current span OK over " + QString::number(loadRequested.size()) + " points, scale " + + QString::number(scale, 'f', 5)); + + loadDisabled = true; + goToStep(AUTOCAL_STEP_CURRENT_ZERO2); + return; + } + + loadMeasured.append(mean); + appendLog("Load point " + QString::number(loadMeasured.size()) + ": requested " + + QString::number((int)loadRequested[loadPhase]) + " mA, measured " + + QString::number(mean, 'f', 1) + " mA"); + + loadPhase++; + loadSettleTicks = 0; + currentHistory.clear(); + + if(loadPhase < loadRequested.size()) + { + emit sigSetLoadCurrent((int)loadRequested[loadPhase]); + return; + } + + loadTimer->stop(); + emit sigSetLoadEnabled(false); + emit sigSetLoadCurrent(0); + + /*Least squares line through all measured points, measured = slope*requested + b*/ + int n = loadRequested.size(); + double sx = 0, sy = 0, sxx = 0, sxy = 0; + + for(int i = 0; i < n; i++) + { + sx += loadRequested[i]; + sy += loadMeasured[i]; + sxx += loadRequested[i] * loadRequested[i]; + sxy += loadRequested[i] * loadMeasured[i]; + } + + double denom = n * sxx - sx * sx; + + if(fabs(denom) < 1e-9) + { + failAndRestart("Load calibration could not fit the measured points."); + return; + } + + double slope = (n * sxy - sx * sy) / denom; + double intercept = (sy - slope * sx) / n; + + if(slope <= 0.0001) + { + failAndRestart("Load did not respond to the requested current."); + return; + } + + double oldCor = (calData->dacCorrection > 0) ? calData->dacCorrection : 1.0; + double oldOff = calData->dacOffset; + + /*Firmware drives the DAC with requested*cor+off, so the correction and offset + that make the measured current follow the requested one are derived from the + measured slope and intercept and the parameters already in the device*/ + calData->dacCorrection = oldCor / slope; + calData->dacOffset = oldOff - intercept * oldCor / slope; + + appendLog("Load fit over " + QString::number(n) + " points: slope " + + QString::number(slope, 'f', 4) + ", intercept " + QString::number(intercept, 'f', 2) + " mA"); + resultLabel->setStyleSheet("font-weight: bold; color: rgb(0, 130, 0);"); + resultLabel->setText("Load calibrated, correction " + QString::number(calData->dacCorrection, 'f', 4) + + ", offset " + QString::number(calData->dacOffset, 'f', 2) + " mA"); + appendLog("Load channel OK, correction " + QString::number(calData->dacCorrection, 'f', 4) + + ", offset " + QString::number(calData->dacOffset, 'f', 2) + " mA"); + + goToStep(AUTOCAL_STEP_DONE); +} + +void AutoCalibrationWnd::onNextClicked() +{ + if(step == AUTOCAL_STEP_DONE) + { + emit sigCalibrationFinished(true); + close(); + } +} + +void AutoCalibrationWnd::onBackClicked() +{ + if(step <= AUTOCAL_STEP_DISCONNECT_INPUT) return; + goToStep((autocal_step_t)(step - 1)); +} + +void AutoCalibrationWnd::onReadyClicked() +{ + appendLog("User confirmed the step manually"); + + switch(step) + { + case AUTOCAL_STEP_DISCONNECT_INPUT: goToStep(AUTOCAL_STEP_SET_REFERENCE); break; + case AUTOCAL_STEP_SET_REFERENCE: goToStep(AUTOCAL_STEP_VOLTAGE_TUNE); break; + case AUTOCAL_STEP_JUMPER_BATTERY: goToStep(AUTOCAL_STEP_CONNECT_SOURCE); break; + case AUTOCAL_STEP_CONNECT_SOURCE: goToStep(AUTOCAL_STEP_LOAD_TUNE); break; + default: break; + } +} + +void AutoCalibrationWnd::onCancelClicked() +{ + loadTimer->stop(); + emit sigSetLoadEnabled(false); + emit sigSetLoadCurrent(0); + step = AUTOCAL_STEP_IDLE; + emit sigCalibrationFinished(false); + close(); +} diff --git a/Source/Windows/Device/autocalibrationwnd.h b/Source/Windows/Device/autocalibrationwnd.h new file mode 100644 index 0000000..08d1c65 --- /dev/null +++ b/Source/Windows/Device/autocalibrationwnd.h @@ -0,0 +1,112 @@ +#ifndef AUTOCALIBRATIONWND_H +#define AUTOCALIBRATIONWND_H + +#include +#include +#include +#include +#include +#include + +#include "Processing/calibrationdata.h" + +typedef enum +{ + AUTOCAL_STEP_IDLE = 0, + AUTOCAL_STEP_DISCONNECT_INPUT, + AUTOCAL_STEP_SET_REFERENCE, + AUTOCAL_STEP_VOLTAGE_TUNE, + AUTOCAL_STEP_CURRENT_OFFSET, + AUTOCAL_STEP_JUMPER_BATTERY, + AUTOCAL_STEP_CONNECT_SOURCE, + AUTOCAL_STEP_CURRENT_SPAN, + AUTOCAL_STEP_CURRENT_ZERO2, + AUTOCAL_STEP_LOAD_TUNE, + AUTOCAL_STEP_DONE +}autocal_step_t; + +class AutoCalibrationWnd : public QWidget +{ + Q_OBJECT + +public: + explicit AutoCalibrationWnd(QWidget *parent = nullptr); + + void setCalibrationData(CalibrationData *aCalData); + void setAcquisitionActive(bool active); + void setLoadDisabled(bool disabled); + void startCalibration(); + +signals: + void sigApplyCalibration(); + void sigSetLoadCurrent(int mA); + void sigSetLoadEnabled(bool enabled); + void sigResetProtection(); + void sigCalibrationFinished(bool success); + +public slots: + void onNewStatistics(double voltageAvg, double currentAvg); + void onLoadEnableResult(bool ok); + void appendLog(QString message); + +private slots: + void onNextClicked(); + void onBackClicked(); + void onCancelClicked(); + void onReadyClicked(); + void onLoadStepTick(); + +private: + QLabel *titleLabel; + QLabel *stepLabel; + QLabel *instructionLabel; + QLabel *imageLabel; + QLabel *liveLabel; + QLabel *resultLabel; + QPlainTextEdit *logView; + QPushButton *nextButton; + QPushButton *backButton; + QPushButton *cancelButton; + QPushButton *readyButton; + + CalibrationData *calData; + autocal_step_t step; + bool acquisitionActive; + bool loadDisabled; + + CalibrationData startSnapshot; + bool snapshotValid; + + double referenceVoltage; + bool externalReference; + double voltageAvg; + double currentAvg; + bool statsValid; + + QVector voltageHistory; + QVector currentHistory; + + QTimer *loadTimer; + int loadPhase; + QVector loadRequested; + QVector loadMeasured; + int loadSettleTicks; + double spanSumMT; + double spanSumMM; + int currentSpanPoints; + int loadPoints; + + void goToStep(autocal_step_t newStep); + void updateView(); + void pushHistory(double v, double c); + bool voltageStable(double *mean, double spreadLimit); + bool currentStable(double *mean, double spreadLimit); + double maxLoadCurrentMa(); + int loadPointCount(); + void failAndRestart(QString reason); + autocal_step_t modeStartStep(autocal_step_t s); + void setImage(QString fileName); + QString buildReport(); +}; + +#endif // AUTOCALIBRATIONWND_H diff --git a/Source/Windows/Device/calibrationwnd.cpp b/Source/Windows/Device/calibrationwnd.cpp index b4a487d..a6c1e3c 100644 --- a/Source/Windows/Device/calibrationwnd.cpp +++ b/Source/Windows/Device/calibrationwnd.cpp @@ -1,6 +1,16 @@ #include "calibrationwnd.h" #include "ui_calibrationwnd.h" +#include +#include +#include +#include +#include + +#define CALIBRATION_FIELD_WIDTH 90 +#define CALIBRATION_UNIT_WIDTH 40 +#define CALIBRATION_ROW_HEIGHT 26 + CalibrationWnd::CalibrationWnd(QWidget *parent) : QWidget(parent) , ui(new Ui::CalibrationWnd) @@ -9,8 +19,11 @@ CalibrationWnd::CalibrationWnd(QWidget *parent) calData = NULL; - connect(ui->submitPusb, SIGNAL(clicked(bool)), this, SLOT(onSubmitPressed(bool))); - connect(ui->storePusb, SIGNAL(clicked(bool)), this, SLOT(onStorePressed(bool))); + buildUi(); + + connect(submitPusb, SIGNAL(clicked(bool)), this, SLOT(onSubmitPressed(bool))); + connect(storePusb, SIGNAL(clicked(bool)), this, SLOT(onStorePressed(bool))); + connect(autoPusb, &QPushButton::clicked, this, [this](){ emit sigStartAutoCalibration(); }); } CalibrationWnd::~CalibrationWnd() @@ -18,17 +31,100 @@ CalibrationWnd::~CalibrationWnd() delete ui; } +QLineEdit* CalibrationWnd::createField(const QString &label, const QString &unit, QGridLayout *grid, int row) +{ + QLabel *nameLabel = new QLabel(label, this); + QLineEdit *edit = new QLineEdit(this); + QLabel *unitLabel = new QLabel(unit, this); + + edit->setFixedSize(CALIBRATION_FIELD_WIDTH, CALIBRATION_ROW_HEIGHT); + unitLabel->setFixedWidth(CALIBRATION_UNIT_WIDTH); + + grid->addWidget(nameLabel, row, 0); + grid->addWidget(edit, row, 1); + grid->addWidget(unitLabel, row, 2); + + return edit; +} + +void CalibrationWnd::buildUi() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + QGroupBox *adcGroup = new QGroupBox("General ADC configuration", this); + QGridLayout *adcGrid = new QGridLayout(adcGroup); + adcVolRefLine = createField("ADC Voltage Ref", "V", adcGrid, 0); + adcGrid->setColumnStretch(3, 1); + adcGrid->setVerticalSpacing(8); + + QGroupBox *voltageGroup = new QGroupBox("Voltage channel calibration", this); + QGridLayout *voltageGrid = new QGridLayout(voltageGroup); + volOffLine = createField("Voltage Offset", "V", voltageGrid, 0); + volCorrLine = createField("Voltage Correction", "", voltageGrid, 1); + voltageGrid->setColumnStretch(3, 1); + voltageGrid->setVerticalSpacing(8); + voltageGrid->setRowStretch(4, 1); + + QGroupBox *currentGroup = new QGroupBox("Current channel calibration", this); + QGridLayout *currentGrid = new QGridLayout(currentGroup); + volCOffLine = createField("Voltage C. Offset", "V", currentGrid, 0); + currCorrLine = createField("Current Correction", "", currentGrid, 1); + currGainLine = createField("Current Gain", "", currentGrid, 2); + currShuntLine = createField("Current Shunt", "ohm", currentGrid, 3); + currentGrid->setColumnStretch(3, 1); + currentGrid->setVerticalSpacing(8); + currentGrid->setRowStretch(4, 1); + + QGroupBox *loadGroup = new QGroupBox("Load calibration", this); + QGridLayout *loadGrid = new QGridLayout(loadGroup); + dacOffLine = createField("Load DAC Offset", "mA", loadGrid, 0); + dacCorLine = createField("Load DAC Correction", "", loadGrid, 1); + loadGrid->setColumnStretch(3, 1); + loadGrid->setVerticalSpacing(8); + + QHBoxLayout *channelsLayout = new QHBoxLayout(); + channelsLayout->addWidget(voltageGroup, 0, Qt::AlignTop); + channelsLayout->addWidget(currentGroup, 0, Qt::AlignTop); + + QHBoxLayout *bottomLayout = new QHBoxLayout(); + bottomLayout->addWidget(loadGroup, 1); + + autoPusb = new QPushButton("Start auto calibration", this); + autoPusb->setToolTip("Guided calibration of the voltage, current and load channels.\nThe acquisition has to be running"); + submitPusb = new QPushButton("Submit", this); + storePusb = new QPushButton("Store", this); + submitPusb->setToolTip("Apply the calibration parameters on the device"); + storePusb->setToolTip("Apply and permanently store the calibration parameters on the device"); + + QVBoxLayout *buttonLayout = new QVBoxLayout(); + buttonLayout->addStretch(); + buttonLayout->addWidget(submitPusb); + buttonLayout->addWidget(storePusb); + + bottomLayout->addLayout(buttonLayout); + + mainLayout->addWidget(adcGroup); + mainLayout->addLayout(channelsLayout); + mainLayout->addLayout(bottomLayout); + mainLayout->addStretch(); + + QHBoxLayout *autoLayout = new QHBoxLayout(); + autoLayout->addWidget(autoPusb); + autoLayout->addStretch(); + mainLayout->addLayout(autoLayout); +} + void CalibrationWnd::showWnd() { - ui->adcVolRefLine->setText(QString::number(calData->adcVoltageRef)); - ui->currCorrLine->setText(QString::number(calData->currentCorrection)); - ui->currGainLine->setText(QString::number(calData->currentGain)); - ui->currShuntLine->setText(QString::number(calData->currentShunt)); - ui->volOffLine->setText(QString::number(calData->voltageOff)); - ui->volCorrLine->setText(QString::number(calData->voltageCorr)); - ui->volCOffLine->setText(QString::number(calData->voltageCurrOffset)); - ui->dacOffLine->setText(QString::number(calData->dacOffset)); - ui->dacCorLine->setText(QString::number(calData->dacCorrection)); + adcVolRefLine->setText(QString::number(calData->adcVoltageRef)); + currCorrLine->setText(QString::number(calData->currentCorrection)); + currGainLine->setText(QString::number(calData->currentGain)); + currShuntLine->setText(QString::number(calData->currentShunt)); + volOffLine->setText(QString::number(calData->voltageOff)); + volCorrLine->setText(QString::number(calData->voltageCorr)); + volCOffLine->setText(QString::number(calData->voltageCurrOffset)); + dacOffLine->setText(QString::number(calData->dacOffset)); + dacCorLine->setText(QString::number(calData->dacCorrection)); show(); } @@ -39,15 +135,15 @@ void CalibrationWnd::setCalibrationData(CalibrationData *aCalData) void CalibrationWnd::onSubmitPressed(bool pressed) { - calData->adcVoltageRef = ui->adcVolRefLine->text().toDouble(); - calData->currentCorrection = ui->currCorrLine->text().toDouble(); - calData->currentGain = ui->currGainLine->text().toDouble(); - calData->currentShunt = ui->currShuntLine->text().toDouble(); - calData->voltageOff = ui->volOffLine->text().toDouble(); - calData->voltageCorr = ui->volCorrLine->text().toDouble(); - calData->voltageCurrOffset = ui->volCOffLine->text().toDouble(); - calData->dacOffset = ui->dacOffLine->text().toDouble(); - calData->dacCorrection = ui->dacCorLine->text().toDouble(); + calData->adcVoltageRef = adcVolRefLine->text().toDouble(); + calData->currentCorrection = currCorrLine->text().toDouble(); + calData->currentGain = currGainLine->text().toDouble(); + calData->currentShunt = currShuntLine->text().toDouble(); + calData->voltageOff = volOffLine->text().toDouble(); + calData->voltageCorr = volCorrLine->text().toDouble(); + calData->voltageCurrOffset = volCOffLine->text().toDouble(); + calData->dacOffset = dacOffLine->text().toDouble(); + calData->dacCorrection = dacCorLine->text().toDouble(); emit sigCalibrationDataUpdated(); } diff --git a/Source/Windows/Device/calibrationwnd.h b/Source/Windows/Device/calibrationwnd.h index ab1e2cc..177f135 100644 --- a/Source/Windows/Device/calibrationwnd.h +++ b/Source/Windows/Device/calibrationwnd.h @@ -2,10 +2,11 @@ #define CALIBRATIONWND_H #include +#include +#include #include "Processing/calibrationdata.h" - - +class QGridLayout; namespace Ui { class CalibrationWnd; @@ -26,6 +27,7 @@ class CalibrationWnd : public QWidget signals: void sigCalibrationDataUpdated(); void sigCalibrationStoreRequest(); + void sigStartAutoCalibration(); private slots: void onSubmitPressed(bool pressed); @@ -34,6 +36,23 @@ private slots: private: Ui::CalibrationWnd *ui; CalibrationData* calData; + + QLineEdit *adcVolRefLine; + QLineEdit *volOffLine; + QLineEdit *volCorrLine; + QLineEdit *volCOffLine; + QLineEdit *currCorrLine; + QLineEdit *currGainLine; + QLineEdit *currShuntLine; + QLineEdit *dacOffLine; + QLineEdit *dacCorLine; + + QPushButton *submitPusb; + QPushButton *storePusb; + QPushButton *autoPusb; + + QLineEdit* createField(const QString &label, const QString &unit, QGridLayout *grid, int row); + void buildUi(); }; #endif // CALIBRATIONWND_H diff --git a/Source/Windows/Device/calibrationwnd.ui b/Source/Windows/Device/calibrationwnd.ui index ba3566d..db2428e 100644 --- a/Source/Windows/Device/calibrationwnd.ui +++ b/Source/Windows/Device/calibrationwnd.ui @@ -2,920 +2,18 @@ CalibrationWnd - - Qt::WindowModal - 0 0 - 536 - 249 + 520 + 360 - - - 0 - 0 - - - - - 662 - 388 - - - Calibration Data + Calibration - - - QLayout::SetFixedSize - - - - - - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - ADC Voltage Ref - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - V - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Voltage C. Offset - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - V - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Voltage Correction - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Voltage Offset - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - V - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Current Gain - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Current Shunt - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - ohm - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Current Correction - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Load DAC Offset - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 60 - 30 - - - - - 60 - 30 - - - - - Arial - 10 - - - - 0 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - mA - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - - 150 - 30 - - - - - 150 - 30 - - - - - Arial - 10 - - - - Load DAC Correction - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 60 - 30 - - - - - 60 - 30 - - - - - Arial - 10 - - - - 1 - - - - - - - - 40 - 30 - - - - - 40 - 30 - - - - - Arial - 10 - - - - - - - Qt::AlignCenter - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 20 - 40 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Submit - - - - - - - Store - - - - - - - - - - adcVolRefLine - volOffLine - currCorrLine - volCorrLine - currGainLine - volCOffLine - currShuntLine - submitPusb - diff --git a/Source/Windows/Device/devicewnd.cpp b/Source/Windows/Device/devicewnd.cpp index 676f475..4e18c74 100644 --- a/Source/Windows/Device/devicewnd.cpp +++ b/Source/Windows/Device/devicewnd.cpp @@ -114,6 +114,7 @@ DeviceWnd::DeviceWnd(QWidget *parent) : consoleWnd = new ConsoleWnd(); calibrationWnd = new CalibrationWnd(); + autoCalibrationWnd = new AutoCalibrationWnd(); createPlotsArea(); @@ -139,6 +140,12 @@ DeviceWnd::DeviceWnd(QWidget *parent) : connect(calibrationWnd, SIGNAL(sigCalibrationDataUpdated()), this, SLOT(onCalibrationUpdated())); connect(calibrationWnd, SIGNAL(sigCalibrationStoreRequest()), this, SLOT(onCalibrationStoreRequest())); + connect(calibrationWnd, &CalibrationWnd::sigStartAutoCalibration, this, &DeviceWnd::onStartAutoCalibration); + connect(autoCalibrationWnd, &AutoCalibrationWnd::sigApplyCalibration, this, &DeviceWnd::sigAutoCalApplyCalibration); + connect(autoCalibrationWnd, &AutoCalibrationWnd::sigSetLoadCurrent, this, &DeviceWnd::sigAutoCalSetLoadCurrent); + connect(autoCalibrationWnd, &AutoCalibrationWnd::sigSetLoadEnabled, this, &DeviceWnd::sigAutoCalSetLoadEnabled); + connect(autoCalibrationWnd, &AutoCalibrationWnd::sigResetProtection, this, &DeviceWnd::sigAutoCalResetProtection); + connect(autoCalibrationWnd, &AutoCalibrationWnd::sigCalibrationFinished, this, [this](bool){ calibrationWnd->showWnd(); }); connect(ui->dischargeControlPusb1, SIGNAL(clicked(bool)), this, SLOT(onCalibrationButtonPressed(bool))); connect(ui->dischargeControlPusb2, SIGNAL(clicked(bool)), this, SLOT(onEnenergyControlButtonPressed(bool))); @@ -318,6 +325,7 @@ void DeviceWnd::closeSubWindows() if(consoleWnd) consoleWnd->close(); if(energyControlWnd) energyControlWnd->close(); if(calibrationWnd) calibrationWnd->close(); + if(autoCalibrationWnd) autoCalibrationWnd->close(); if(dataAnalyzer) dataAnalyzer->close(); } @@ -332,6 +340,34 @@ void DeviceWnd::onCalibrationButtonPressed(bool pressed) showSubWindow(calibrationWnd); } +void DeviceWnd::onStartAutoCalibration() +{ + /*startCalibration asks for the reference and shows the window itself, so it is + not forced open when the user cancels the reference prompt*/ + autoCalibrationWnd->setAcquisitionActive(acqState == DEVICE_ACQ_ACTIVE); + autoCalibrationWnd->startCalibration(); +} + +void DeviceWnd::forwardCalibrationStatistics(double voltageAvg, double currentAvg) +{ + autoCalibrationWnd->onNewStatistics(voltageAvg, currentAvg); +} + +void DeviceWnd::setAutoCalibrationLoadDisabled(bool disabled) +{ + autoCalibrationWnd->setLoadDisabled(disabled); +} + +void DeviceWnd::autoCalibrationLoadEnableResult(bool ok) +{ + autoCalibrationWnd->onLoadEnableResult(ok); +} + +void DeviceWnd::autoCalibrationLog(QString message) +{ + autoCalibrationWnd->appendLog(message); +} + void DeviceWnd::onEnenergyControlButtonPressed(bool pressed) { showSubWindow(energyControlWnd); @@ -883,6 +919,7 @@ void DeviceWnd::setChargerConnectionStatus(bool status) void DeviceWnd::setLoadCurrentStatus(bool state) { + autoCalibrationWnd->setLoadDisabled(!state); energyControlWnd->loadCurrentStatusSet(state); } @@ -1187,6 +1224,7 @@ bool DeviceWnd::plotConsumptionEBPWithName(double value, double key, QString nam bool DeviceWnd::showStatistic(device_stat_info statInfo) { + autoCalibrationWnd->onNewStatistics(statInfo.voltageAvg, statInfo.currentAvg); dataAnalyzer->setVoltageStatisticInfo(statInfo.voltageAvg, statInfo.voltageMax, statInfo.voltageMin); dataAnalyzer->setCurrentStatisticInfo(statInfo.currentAvg, statInfo.currentMax, statInfo.currentMin); dataAnalyzer->setConsumptionStatisticInfo(statInfo.consumptionAvg, statInfo.consumptionMax, statInfo.consumptionMin); @@ -1203,4 +1241,5 @@ bool DeviceWnd::setWorkingSpaceDir(QString aWsPath) void DeviceWnd::setCalibrationData(CalibrationData *data) { calibrationWnd->setCalibrationData(data); + autoCalibrationWnd->setCalibrationData(data); } diff --git a/Source/Windows/Device/devicewnd.h b/Source/Windows/Device/devicewnd.h index c201dc9..c5b3b6c 100644 --- a/Source/Windows/Device/devicewnd.h +++ b/Source/Windows/Device/devicewnd.h @@ -13,6 +13,7 @@ #include "Windows/Device/advcofigurationdata.h" #include "Windows/Device/datastatistics.h" #include "Windows/Device/calibrationwnd.h" +#include "Windows/Device/autocalibrationwnd.h" #include "Windows/Device/energycontrolwnd.h" #include "Windows/Device/configurationwnd.h" #include "Processing/Parameters/deviceparameters.h" @@ -92,6 +93,10 @@ class DeviceWnd : public QWidget void setParameters(DeviceParameters* params); void setDeviceNetworkState(device_state_t aDeviceState); void setDeviceAcqState(device_acq_mode_t aAcqState); + void forwardCalibrationStatistics(double voltageAvg, double currentAvg); + void setAutoCalibrationLoadDisabled(bool disabled); + void autoCalibrationLoadEnableResult(bool ok); + void autoCalibrationLog(QString message); void printConsoleMsg(QString msg, bool exeStatus); void setDeviceInterfaceSelectionState(device_interface_selection_state_t selectionState=DEVICE_INTERFACE_SELECTION_STATE_UNDEFINED); bool setSamplingPeriod(QString stime); @@ -221,6 +226,10 @@ class DeviceWnd : public QWidget void sigCalibrationUpdated(); void sigCalibrationStoreRequest(); + void sigAutoCalApplyCalibration(); + void sigAutoCalSetLoadCurrent(int mA); + void sigAutoCalSetLoadEnabled(bool enabled); + void sigAutoCalResetProtection(); void sigDeviceConfigSet(QMap changedFields); void sigDeviceConfigGet(); void sigDeviceConfigStore(); @@ -292,6 +301,8 @@ public slots: void onPlotScatterNameAndKey(QString name, double key); private slots: + void onStartAutoCalibration(); + void onCalibrationUpdated(); void onCalibrationStoreRequest(); void onPlotDockMaximizeToggled(PlotDockWidget* dock, bool maximized); @@ -308,6 +319,7 @@ private slots: ConsoleWnd *consoleWnd; DataStatistics *dataAnalyzer; CalibrationWnd *calibrationWnd; + AutoCalibrationWnd *autoCalibrationWnd; EnergyControlWnd *energyControlWnd; Plot *voltageChart; Plot *currentChart; diff --git a/Source/devicecontainer.cpp b/Source/devicecontainer.cpp index 1d7e6ee..0a504b6 100644 --- a/Source/devicecontainer.cpp +++ b/Source/devicecontainer.cpp @@ -21,6 +21,9 @@ DeviceContainer::DeviceContainer(QObject *parent, connect(batteryParamsExtraction, &BatteryParamsExtraction::sigCycleFinished, this, &DeviceContainer::onBatParamCycleFinished); samplingPeriodMs = 0; + uVoltageActive = false; + oVoltageActive = false; + oCurrentActive = false; batParamRelaxEnabled = false; batParamRelaxThreshold = 0; batParamRelaxWindow = 0; @@ -70,6 +73,10 @@ DeviceContainer::DeviceContainer(QObject *parent, connect(deviceWnd, SIGNAL(sigConsumptionProfileNameChanged(QString)), this, SLOT(onDeviceWndConsumptionProfileNameChanged(QString))); connect(deviceWnd, SIGNAL(sigCalibrationUpdated()), this, SLOT(onDeviceWndCalibrationUpdated())); connect(deviceWnd, SIGNAL(sigCalibrationStoreRequest()), this, SLOT(onDeviceWndCalibrationStoreRequest())); + connect(deviceWnd, SIGNAL(sigAutoCalApplyCalibration()), this, SLOT(onDeviceWndAutoCalApply())); + connect(deviceWnd, SIGNAL(sigAutoCalSetLoadCurrent(int)), this, SLOT(onDeviceWndAutoCalSetLoadCurrent(int))); + connect(deviceWnd, SIGNAL(sigAutoCalSetLoadEnabled(bool)), this, SLOT(onDeviceWndAutoCalSetLoadEnabled(bool))); + connect(deviceWnd, SIGNAL(sigAutoCalResetProtection()), this, SLOT(onDeviceWndAutoCalResetProtection())); connect(deviceWnd, SIGNAL(sigLoadStatusChanged(bool)), this, SLOT(onDeviceWndLoadStatusChanged(bool))); connect(deviceWnd, SIGNAL(sigPPathStatusChanged(bool)), this, SLOT(onDeviceWndPPathStatusChanged(bool))); @@ -1449,6 +1456,65 @@ void DeviceContainer::onDeviceWndCalibrationUpdated() "Unable to apply calibration parameters on device"); } +void DeviceContainer::onDeviceWndAutoCalApply() +{ + /*Recompute the increments so the live measurement reflects the parameter the + wizard just changed*/ + device->calibrationUpdated(); +} + +void DeviceContainer::onDeviceWndAutoCalSetLoadCurrent(int mA) +{ + device->setLoadCurrent(mA); +} + +void DeviceContainer::onDeviceWndAutoCalSetLoadEnabled(bool enabled) +{ + bool ok; + + if(enabled) + { + /*A protection can latch while the source is being connected, so it is reset + before the load is enabled instead of refusing the calibration*/ + if(uVoltageActive || oVoltageActive || oCurrentActive) + { + device->latchTrigger(); + uVoltageActive = false; + oVoltageActive = false; + oCurrentActive = false; + deviceWnd->autoCalibrationLog("Protection was active, latch reset"); + } + } + + /*Load output is driven directly here, not through the energy control window, so + the calibration does not depend on the energy control working mode. The load + stage is off by default, so it is enabled together with the DAC*/ + if(enabled) + { + bool loadOk = device->setLoadStatus(true); + bool dacOk = device->setDACStatus(true); + + ok = loadOk && dacOk; + if(ok) deviceWnd->autoCalibrationLog("Load and DAC enabled"); + deviceWnd->autoCalibrationLoadEnableResult(ok); + } + else + { + device->setDACStatus(false); + device->setLoadStatus(false); + ok = true; + } +} + +void DeviceContainer::onDeviceWndAutoCalResetProtection() +{ + device->latchTrigger(); + uVoltageActive = false; + oVoltageActive = false; + oCurrentActive = false; + deviceWnd->autoCalibrationLog("Protection latch reset requested"); +} + void DeviceContainer::onDeviceWndCalibrationStoreRequest() { bool ok = device->setCalParam(); @@ -1515,6 +1581,8 @@ void DeviceContainer::onDeviceLoadCurrentObtained(int current) void DeviceContainer::onDeviceUVoltageObtained(bool state) { + uVoltageActive = state; + if(state) deviceWnd->autoCalibrationLog("Under voltage protection tripped"); bool ok = deviceWnd->setUVoltageIndication(state); logResult(ok, @@ -1546,6 +1614,8 @@ void DeviceContainer::onDeviceChargerConnectionStatusOntained(bool state) void DeviceContainer::onDeviceOVoltageObtained(bool state) { + oVoltageActive = state; + if(state) deviceWnd->autoCalibrationLog("Over voltage protection tripped"); bool ok = deviceWnd->setOVoltageIndication(state); logResult(ok, @@ -1555,6 +1625,8 @@ void DeviceContainer::onDeviceOVoltageObtained(bool state) void DeviceContainer::onDeviceOCurrentObtained(bool state) { + oCurrentActive = state; + if(state) deviceWnd->autoCalibrationLog("Over current protection tripped"); bool ok = deviceWnd->setOCurrentIndication(state); logResult(ok, diff --git a/Source/devicecontainer.h b/Source/devicecontainer.h index 20f57dc..d60a1e7 100644 --- a/Source/devicecontainer.h +++ b/Source/devicecontainer.h @@ -139,6 +139,10 @@ public slots: private slots: void onDeviceWndCalibrationUpdated(); void onDeviceWndCalibrationStoreRequest(); + void onDeviceWndAutoCalApply(); + void onDeviceWndAutoCalSetLoadCurrent(int mA); + void onDeviceWndAutoCalSetLoadEnabled(bool enabled); + void onDeviceWndAutoCalResetProtection(); @@ -172,6 +176,9 @@ private slots: bool batParamRelaxRunning; bool batParamRelaxRestarting; double samplingPeriodMs; + bool uVoltageActive; + bool oVoltageActive; + bool oCurrentActive; double batParamRelaxCheckKey; QVector batParamRelaxKeys; QVector batParamRelaxVoltage; From 1748d6a289d7467c5b28c076e61be38509131031 Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Sat, 3 Oct 2026 16:18:05 +0200 Subject: [PATCH 8/8] Bump version to 2.1.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e9307ca..7ec1d6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.2 +2.1.0