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..23dc59c 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. @@ -28,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 \ @@ -35,12 +45,24 @@ SOURCES += \ Processing/fileprocessing.cpp \ 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 \ Windows/DataAnalyzer/dataanalyzer.cpp \ + 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 \ @@ -66,6 +88,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 \ @@ -74,12 +97,25 @@ HEADERS += \ Processing/fileprocessing.h \ 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 \ Windows/DataAnalyzer/dataanalyzer.h \ + Windows/DataAnalyzer/dataanalyzerprofile.h \ + 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/Processing/Parameters/deviceparamdefs.cpp b/Source/Processing/Parameters/deviceparamdefs.cpp index 6e87762..7cead10 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 + }, }; } @@ -615,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", @@ -1078,6 +1132,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/Processing/batteryparamsextraction.cpp b/Source/Processing/batteryparamsextraction.cpp new file mode 100644 index 0000000..1fb0e48 --- /dev/null +++ b/Source/Processing/batteryparamsextraction.cpp @@ -0,0 +1,929 @@ +#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.relaxationFilter = BATTERYPARAMS_RELAX_FILTER_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; + defaults.plotDecimation = BATTERYPARAMS_CYCLE_PLOT_DECIMATION_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 maximumPositions; + QVector minimumPositions; + int first = start; + + if(end < start) return -1; + + /*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++) + { + 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); + + while((keys[i] - keys[first]) > window) + { + if(maximumPositions.first() == first) maximumPositions.removeFirst(); + if(minimumPositions.first() == first) minimumPositions.removeFirst(); + first++; + } + + if((keys[i] - keys[start]) < window) continue; + + if((voltage[maximumPositions.first()] - voltage[minimumPositions.first()]) <= threshold) 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; + + /*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 = 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*/ + 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(); + activeCycle.relaxKeys.clear(); + activeCycle.relaxVoltage.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; + } + + /*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; + + 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]); + } +} + +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(); + 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..98ec8b3 --- /dev/null +++ b/Source/Processing/batteryparamsextraction.h @@ -0,0 +1,173 @@ +#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_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 +#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; + double relaxationFilter; /*ms*/ + batteryparams_model_t model; + batteryparams_fit_end_t fitEnd; + int tauGridPoints; + int tauRefinementNo; + double tauSeparation; + double capacity; + double initialSoc; + int plotDecimation; +}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 relaxKeys; + QVector relaxVoltage; + + 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); + 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: + 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/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/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/BatteryParams/batterycyclewnd.cpp b/Source/Windows/BatteryParams/batterycyclewnd.cpp new file mode 100644 index 0000000..2fccd7e --- /dev/null +++ b/Source/Windows/BatteryParams/batterycyclewnd.cpp @@ -0,0 +1,405 @@ +#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"); + + 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); + 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->addWidget(fitButton); + 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(fitButton, &QPushButton::clicked, this, &BatteryCycleWnd::onFitPoints); + 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))); + + 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); + + 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() * timeScale()); + + 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] / timeScale(), 'f', timeDecimals())); + + 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(); +} + +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); +} + +void BatteryCycleWnd::onSave() +{ + emit sigCycleUpdated(cycle); +} + +void BatteryCycleWnd::applyMarkers() +{ + 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; + + 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; + + refreshMarkerEdits(); + + 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) / 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" : "-") + + (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 / 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 / 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" + + " 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::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; + + applyPlotStyle(); + + if(unitChanged) + { + updateMarkerLabels(); + refreshMarkerEdits(); + updateInfo(); + updatePlots(); + } +} + +void BatteryCycleWnd::updatePlots() +{ + voltagePlot->scatterClearMarkers(); + currentPlot->scatterClearMarkers(); + voltagePlot->markersAtKeyClear(); + currentPlot->markersAtKeyClear(); + voltagePlot->overlayClear(); + + 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"); + 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 / 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()) + { + 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(scaledKeys.first(), scaledKeys.last()); + currentPlot->zoomToKeyRange(scaledKeys.first(), scaledKeys.last()); + } + + applyPlotStyle(); +} + +void BatteryCycleWnd::setCycle(batteryparams_cycle_t aCycle) +{ + cycle = aCycle; + originalCycle = aCycle; + + setWindowTitle("Battery cycle " + QString::number(cycle.index)); + + refreshMarkerEdits(); + + updateInfo(); + updatePlots(); +} diff --git a/Source/Windows/BatteryParams/batterycyclewnd.h b/Source/Windows/BatteryParams/batterycyclewnd.h new file mode 100644 index 0000000..a490a16 --- /dev/null +++ b/Source/Windows/BatteryParams/batterycyclewnd.h @@ -0,0 +1,72 @@ +#ifndef BATTERYCYCLEWND_H +#define BATTERYCYCLEWND_H + +#include +#include +#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 onFitPoints(); + 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; + QPushButton *fitButton; + QVector markerLabels; + QStringList markerNames; + + 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 applyPlotStyle(); + void refreshMarkerEdits(); + void updateMarkerLabels(); + double timeScale(); + QString timeSuffix(); + int timeDecimals(); + 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..191673f --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamsplotsettings.cpp @@ -0,0 +1,147 @@ +#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; + 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) +{ + 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..d019419 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamsplotsettings.h @@ -0,0 +1,52 @@ +#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 enum +{ + BATTERYPARAMSPLOT_TIME_MS = 0, + BATTERYPARAMSPLOT_TIME_S, + BATTERYPARAMSPLOT_TIME_MIN +}batteryparams_time_unit_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_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); + +#endif // BATTERYPARAMSPLOTSETTINGS_H diff --git a/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp b/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp new file mode 100644 index 0000000..2e569ec --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamssettingsdlg.cpp @@ -0,0 +1,232 @@ +#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"); + 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); + 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); + 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"); + + 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)); + 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)); + 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); + plotTimeUnitCombo->setCurrentIndex((int)settings.timeUnit); +} + +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.timeUnit = (batteryparams_time_unit_t)plotTimeUnitCombo->currentIndex(); + 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(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(); + + 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..1697a52 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamssettingsdlg.h @@ -0,0 +1,57 @@ +#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; + QLineEdit *relaxationFilterEdit; + QLineEdit *plotDecimationEdit; + 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; + QComboBox *plotTimeUnitCombo; + 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..316e3c5 --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamswnd.cpp @@ -0,0 +1,1242 @@ +#include "batteryparamswnd.h" + +#include +#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 +#define BATTERYPARAMS_FIT_WINDOWS_MAX 5 + +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"); + + 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;"); + + 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(fitPointsButton); + settingsLayout->addWidget(fitPointsSaveButton); + settingsLayout->addWidget(fitPointsRestoreButton); + 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); + 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]" + << "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; + fittedPositions.clear(); + fittedOriginals.clear(); + 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) + (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))); + 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::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++) + { + 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() +{ + fittedPositions.clear(); + fittedOriginals.clear(); + updateFitPointsButtons(); + + 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..d0826cc --- /dev/null +++ b/Source/Windows/BatteryParams/batteryparamswnd.h @@ -0,0 +1,114 @@ +#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 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); + 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; + QPushButton *fitPointsButton; + QPushButton *fitPointsSaveButton; + QPushButton *fitPointsRestoreButton; + 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; + QVector fittedPositions; + QVector fittedOriginals; + + void updateFitPointsButtons(); + 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/dataanalyzer.cpp b/Source/Windows/DataAnalyzer/dataanalyzer.cpp index 8fd939d..720aab0 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzer.cpp +++ b/Source/Windows/DataAnalyzer/dataanalyzer.cpp @@ -4,33 +4,30 @@ #include #include #include - - -#define PLOT_MINIMUM_SIZE_HEIGHT 100 -#define PLOT_MINIMUM_SIZE_WIDTH 500 +#include +#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,228 +39,96 @@ 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->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())); - // Add the button and line edit to the horizontal layout topLayout->addWidget(detectedProfilesLabe); - topLayout->addWidget(consumptionProfilesCB); topLayout->addWidget(reloadProfileNamesPushb); + topLayout->addWidget(consumptionProfilesCB); topLayout->addWidget(processFilePushb); - topLayout->addSpacing(20); topLayout->addWidget(deleteProfilePushb); topLayout->addStretch(); 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); - + mdiArea = new QMdiArea(this); + mdiArea->setViewMode(QMdiArea::TabbedView); + mdiArea->setTabsClosable(true); + mdiArea->setTabsMovable(true); + mdiArea->setDocumentMode(true); + mdiArea->setTabPosition(QTabWidget::North); + mainLayout->addWidget(mdiArea); - connect(dataProcesingClass, SIGNAL(progressUpdated(int)), - this, SLOT(updateProgress(int)), Qt::QueuedConnection); - connect(dataProcesingClass, SIGNAL(updateProgressText(QString)), - this, SLOT(updateProgressText(QString)), Qt::QueuedConnection); + 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())); - thread->start(); + mdiArea->installEventFilter(this); } -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 +140,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 +166,157 @@ void DataAnalyzer::realoadConsumptionProfiles() } } -DataAnalyzer::~DataAnalyzer() +void DataAnalyzer::onRealoadConsumptionProfiles() { - statisticsThread->quit(); - statisticsThread->wait(); - delete statisticsWorker; - delete statisticsWnd; - delete ui; + realoadConsumptionProfiles(); } -void DataAnalyzer::showEvent(QShowEvent *event) +void DataAnalyzer::onConsumptionProfileChanged(int index) { - QWidget::showEvent(event); - realoadConsumptionProfiles(); + selectedConsumptionProfile = consumptionProfilesCB->itemText(index); } -void DataAnalyzer::setWorkspacePath(QString aWsDirPath) +void DataAnalyzer::attachProfile(DataAnalyzerProfile *profile, QString title) { - wsDirPath = aWsDirPath; - realoadConsumptionProfiles(); + QMdiSubWindow *subWindow = mdiArea->addSubWindow(profile); + + subWindow->setWindowTitle(title); + subWindow->setAttribute(Qt::WA_DeleteOnClose); + subWindow->showMaximized(); + mdiArea->setActiveSubWindow(subWindow); + profile->setDetached(false); + profile->show(); + updateMaximizeButton(); } -void DataAnalyzer::onPlotXRangeChanged(QCPRange range) +void DataAnalyzer::onLoadConsumptionProfileData() { - Plot *source = qobject_cast(sender()); - Plot *plots[3] = {voltageChart, currentChart, consumptionChart}; + DataAnalyzerProfile *profile; + QString title; + int counter; - for(int i = 0; i < 3; i++) + if(selectedConsumptionProfile.isEmpty() || !consumptionProfilesCB->isEnabled()) { - if(plots[i] == source) continue; - plots[i]->setXRangeSynced(range); + QMessageBox::warning(this, "Load profile", "No consumption profile selected"); + return; } + + counter = profileCounters.value(selectedConsumptionProfile, 0) + 1; + profileCounters[selectedConsumptionProfile] = counter; + title = selectedConsumptionProfile.section('/', -1); + if(counter > 1) title += " (" + QString::number(counter) + ")"; + + profile = new DataAnalyzerProfile(wsDirPath, selectedConsumptionProfile); + profiles.append(profile); + + connect(profile, SIGNAL(sigDockStateToggleRequested()), this, SLOT(onProfileDockStateToggleRequested())); + connect(profile, SIGNAL(destroyed(QObject*)), this, SLOT(onProfileDestroyed(QObject*))); + + attachProfile(profile, title); + + profile->startLoading(); } -void DataAnalyzer::onGenerateStatistics() +QMdiSubWindow* DataAnalyzer::subWindowFor(DataAnalyzerProfile *profile) { - if(!graphLoad) + QList subWindows = mdiArea->subWindowList(); + + for(int i = 0; i < subWindows.size(); i++) { - QMessageBox::warning(this, "Statistics", "No consumption profile loaded"); - return; + if(subWindows[i]->widget() == profile) return subWindows[i]; } - if(loadedMarkers.isEmpty()) + + return NULL; +} + +bool DataAnalyzer::eventFilter(QObject *object, QEvent *event) +{ + if(object == mdiArea && (event->type() == QEvent::Resize || + event->type() == QEvent::Show || + event->type() == QEvent::LayoutRequest)) { - QMessageBox::warning(this, "Statistics", "Loaded profile has no energy point markers (\" Start\" / \" Stop\" pairs are required)"); - return; + updateMaximizeButton(); } - emit sigComputeStatistics(loadedVoltage, loadedVoltageKeys, loadedCurrent, loadedCurrentKeys, loadedMarkers); + + return QWidget::eventFilter(object, event); } -void DataAnalyzer::onStatisticsFinished(QVector stats, QVector points, dataanalyzer_segment_stat_t total, QStringList warnings) +void DataAnalyzer::updateMaximizeButton() { - if(stats.isEmpty() && points.isEmpty() && warnings.isEmpty()) + QTabBar *tabBar = mdiArea->findChild(); + + if(tabBar == NULL || mdiArea->subWindowList().isEmpty()) { - QMessageBox::information(this, "Statistics", "No \" Start\" / \" Stop\" marker pairs found"); + maximizeProfilePushb->hide(); return; } - statisticsWnd->setStatistics(selectedConsumptionProfile, stats, points, total, warnings); - statisticsWnd->show(); - statisticsWnd->raise(); - statisticsWnd->activateWindow(); + + 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::onStatisticsSegmentSelected(QString name, int startIndex, int endIndex) +void DataAnalyzer::onMaximizeProfile() { - double min; - double max; + QMdiSubWindow *activeSubWindow = mdiArea->activeSubWindow(); - if(!graphLoad) return; - if(startIndex < 0 || endIndex >= loadedVoltageKeys.size() || endIndex <= startIndex) return; + if(activeSubWindow == NULL) return; - min = loadedVoltageKeys[startIndex]; - max = loadedVoltageKeys[endIndex]; - voltageChart->zoomToKeyRange(min, max); - currentChart->zoomToKeyRange(min, max); - consumptionChart->zoomToKeyRange(min, max); + toggleProfileDockState(qobject_cast(activeSubWindow->widget())); +} - raise(); - activateWindow(); +void DataAnalyzer::onProfileDockStateToggleRequested() +{ + toggleProfileDockState(qobject_cast(sender())); } -void DataAnalyzer::onSaveAllPlots() +void DataAnalyzer::toggleProfileDockState(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()) - { - defaultDir = wsDirPath + "/" + selectedConsumptionProfile; - } + QMdiSubWindow *subWindow; - dir = QFileDialog::getExistingDirectory(this, "Select folder for plot images", defaultDir); - if(dir.isEmpty()) return; + if(profile == NULL) return; - prefix = selectedConsumptionProfile.isEmpty() ? "profile" : selectedConsumptionProfile.section('/', -1).toLower().replace(' ', '_'); + subWindow = subWindowFor(profile); - for(int i = 0; i < 3; i++) + 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(); + updateMaximizeButton(); + return; } - if(!failed.isEmpty()) + profile->setAttribute(Qt::WA_DeleteOnClose, false); + profile->setWindowFlags(Qt::Widget); + attachProfile(profile, profile->windowTitle().section(" - ", -1)); + updateMaximizeButton(); +} + +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 +347,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 +364,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..8e06aa5 100644 --- a/Source/Windows/DataAnalyzer/dataanalyzer.h +++ b/Source/Windows/DataAnalyzer/dataanalyzer.h @@ -5,138 +5,68 @@ #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 +#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(); + bool eventFilter(QObject *object, QEvent *event) override; 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 onMaximizeProfile(); + 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; + QToolButton *maximizeProfilePushb; 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); + void toggleProfileDockState(DataAnalyzerProfile *profile); + void updateMaximizeButton(); }; #endif // DATAANALYZER_H diff --git a/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp new file mode 100644 index 0000000..26220f5 --- /dev/null +++ b/Source/Windows/DataAnalyzer/dataanalyzerprofile.cpp @@ -0,0 +1,617 @@ +#include "dataanalyzerprofile.h" + +#include +#include +#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(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); + connect(detachAction, SIGNAL(triggered(bool)), this, SLOT(onDetachClicked())); + mainLayout->addWidget(plotsToolBar); + + statisticsWnd = new DataAnalyzerStatisticsWnd(); + batteryParamsWnd = NULL; + 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) +{ + detachAction->setVisible(detached); +} + +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::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; + 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..60e67a6 --- /dev/null +++ b/Source/Windows/DataAnalyzer/dataanalyzerprofile.h @@ -0,0 +1,95 @@ +#ifndef DATAANALYZERPROFILE_H +#define DATAANALYZERPROFILE_H + +#include + +#include "Windows/BatteryParams/batteryparamswnd.h" +#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 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); + 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; + BatteryParamsWnd *batteryParamsWnd; + + 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/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 1110b4c..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,15 +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)); + 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(); } @@ -37,13 +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->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 3269b2f..db2428e 100644 --- a/Source/Windows/Device/calibrationwnd.ui +++ b/Source/Windows/Device/calibrationwnd.ui @@ -2,738 +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 - - - - - - - - - - - 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/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/devicewnd.cpp b/Source/Windows/Device/devicewnd.cpp index cc6058b..4e18c74 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))); @@ -111,6 +114,7 @@ DeviceWnd::DeviceWnd(QWidget *parent) : consoleWnd = new ConsoleWnd(); calibrationWnd = new CalibrationWnd(); + autoCalibrationWnd = new AutoCalibrationWnd(); createPlotsArea(); @@ -136,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))); @@ -315,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(); } @@ -329,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); @@ -880,6 +919,7 @@ void DeviceWnd::setChargerConnectionStatus(bool status) void DeviceWnd::setLoadCurrentStatus(bool state) { + autoCalibrationWnd->setLoadDisabled(!state); energyControlWnd->loadCurrentStatusSet(state); } @@ -1141,6 +1181,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); @@ -1172,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); @@ -1188,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 ba69616..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); @@ -147,6 +152,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); @@ -196,6 +203,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, double filterMs); void sigLoadWaveClear(); void sigChargingCurrentChanged(unsigned int current); void sigChargingTermCurrentChanged(unsigned int current); @@ -216,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(); @@ -287,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); @@ -303,6 +319,7 @@ private slots: ConsoleWnd *consoleWnd; DataStatistics *dataAnalyzer; CalibrationWnd *calibrationWnd; + AutoCalibrationWnd *autoCalibrationWnd; EnergyControlWnd *energyControlWnd; Plot *voltageChart; Plot *currentChart; diff --git a/Source/Windows/Device/energycontrolwnd.cpp b/Source/Windows/Device/energycontrolwnd.cpp index 7216714..9efd0f7 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,81 @@ 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)); + batParamLayout->addLayout(createEntryRow("Relax Filter", "ms", 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["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"); + 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"); + 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;"); + 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(loadEntryEdits["Relax Filter"], &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 +1251,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 +1277,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 +1324,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 +1351,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 +1370,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 +1436,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 +1461,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 +1885,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 +1894,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 +2000,77 @@ 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\")"); + /*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); + 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 +2135,183 @@ 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; + + if(!batParamCompute(&pulseDuration, NULL, 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*/ + /*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 + 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; + + if(!batParamCompute(&pulseDuration, NULL, &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"; + + if(batParamRelaxationEnabled()) + { + 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); +} + +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); + loadEntryEdits["Relax Filter"]->setEnabled(adaptive); + + updateBatParamInfo(); +} + void EnergyControlWnd::onCustomWaveTableChanged() { if(customWaveTableUpdating) return; @@ -2132,6 +2445,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 +2539,25 @@ 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()); + emit sigBatParamRelaxationChanged(batParamRelaxationEnabled(), + loadEntryEdits["Relax Threshold"]->text().toDouble(), + loadEntryEdits["Relax Window"]->text().toDouble(), + loadEntryEdits["Relax Filter"]->text().toDouble()); + 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..f6df914 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, double filterMs); 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/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..2aee8ea 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); @@ -125,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; @@ -154,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); @@ -202,6 +246,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); @@ -289,6 +345,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; @@ -368,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 82052fe..b8fcf5f 100644 --- a/Source/Windows/Plot/plot.h +++ b/Source/Windows/Plot/plot.h @@ -15,9 +15,20 @@ class Plot : public QWidget void scatterAddGraph(); 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); + 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); @@ -68,9 +79,16 @@ private slots: QVector yData; QVector plotXData; QVector plotYData; + QVector averageXData; + QVector averageYData; + bool averageGraphAdded; + int averageGraphIndex; QVector epDataKey; QVector epDataName; QVector textData; + QCPGraph *overlayGraph; + QCPGraph *keyMarkerGraph; + QVector keyMarkerText; bool enableTracking; bool replotActive; diff --git a/Source/device.cpp b/Source/device.cpp index dec6417..13d7553 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; @@ -727,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"; @@ -741,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) { @@ -767,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)); @@ -784,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; @@ -803,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/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); diff --git a/Source/devicecontainer.cpp b/Source/devicecontainer.cpp index 21e3412..0a504b6 100644 --- a/Source/devicecontainer.cpp +++ b/Source/devicecontainer.cpp @@ -1,4 +1,8 @@ #include +#include "Processing/batteryparamsextraction.h" + +#define DEVICECONTAINER_RELAX_SAMPLES_MAX 2000000 +#define DEVICECONTAINER_RELAX_CHECK_PERIOD 500.0 #include #include "devicecontainer.h" @@ -13,9 +17,30 @@ DeviceContainer::DeviceContainer(QObject *parent, device = aDevice; log = new Log(); fileProcessing = new FileProcessing(); + batteryParamsExtraction = new BatteryParamsExtraction(this); + connect(batteryParamsExtraction, &BatteryParamsExtraction::sigCycleFinished, + this, &DeviceContainer::onBatParamCycleFinished); + samplingPeriodMs = 0; + uVoltageActive = false; + oVoltageActive = false; + oCurrentActive = false; + batParamRelaxEnabled = false; + batParamRelaxThreshold = 0; + batParamRelaxWindow = 0; + batParamRelaxFilter = 0; + batParamAverageSum = 0; + batParamAverageFirst = 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())); + connect(deviceWnd->getLogDock(), SIGNAL(sigFollowOutputChanged(bool)), log, SLOT(setFollowOutput(bool))); + log->setFollowOutput(deviceWnd->getLogDock()->getFollowOutput()); m_AppParamsRef = appParam; consumptionProfileName = ""; consumptionProfileNameSet = false; @@ -48,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))); @@ -57,6 +86,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))); @@ -471,6 +503,13 @@ DeviceContainer::~DeviceContainer() deviceWnd->deleteLater(); deviceWnd = nullptr; } + + if(batteryParamsWnd) + { + batteryParamsWnd->close(); + batteryParamsWnd->deleteLater(); + batteryParamsWnd = nullptr; + } } bool DeviceContainer::getDeviceName(QString *name) @@ -669,6 +708,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"); @@ -802,6 +845,13 @@ 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); + + 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"); @@ -1030,6 +1080,7 @@ void DeviceContainer::onDeviceAcquisitonStarted() { elapsedTime = 0; timer->start(1000); + batteryParamsExtraction->onReset(); } @@ -1050,6 +1101,22 @@ void DeviceContainer::onDeviceNewVoltageCurrentSamplesReceived(QVector v { deviceWnd->plotVoltageValues(voltage, voltageKeys); deviceWnd->plotCurrentValues(current, currentKeys); + batteryParamsExtraction->onNewSamplesReceived(voltage, current, voltageKeys, currentKeys); + + /*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); @@ -1093,11 +1160,256 @@ 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); } 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, double filterMs) +{ + batParamRelaxEnabled = enabled; + batParamRelaxThreshold = thresholdMv / 1000.0; + batParamRelaxWindow = windowS * 1000.0; + 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 " + 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; + 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(); + batParamRelaxCheckKey = 0; + return; + } + + if(name.trimmed().compare(BATTERYPARAMS_DEFAULT_PAUSE_END_MARKER, Qt::CaseInsensitive) == 0) + { + batParamRelaxPauseActive = false; + batParamRelaxKeys.clear(); + batParamRelaxVoltage.clear(); + batParamRelaxCheckKey = 0; + return; + } +} + +void DeviceContainer::batParamRelaxationSamplesProcess(QVector voltage, QVector voltageKeys) +{ + int relaxPosition; + + 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.size() < 2) return; + + /*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) + { + int excess = batParamRelaxKeys.size() - DEVICECONTAINER_RELAX_SAMPLES_MAX; + batParamRelaxKeys.remove(0, excess); + batParamRelaxVoltage.remove(0, excess); + } + + /*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; + + batParamRelaxCheckKey = batParamRelaxKeys.last(); + + /*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(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()); +} + +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(); + 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 + 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) + " 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++; + + /*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); + } +} + +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) @@ -1130,9 +1442,79 @@ 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::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(); @@ -1199,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, @@ -1230,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, @@ -1239,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, @@ -1352,12 +1740,75 @@ 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); + + /*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(); + 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); + deviceWnd->enableVoltageAveragePlot(batParamRelaxRunning && (batParamRelaxFilter > 0)); + } + + if(ok && status) + { + batteryParamsExtraction->onReset(); + if(batteryParamsWnd != NULL) + { + batteryParamsWnd->setProfileName(consumptionProfileName); + batteryParamsWnd->onClear(); + } + } + if(ok) deviceWnd->setLoadCurrentStatus(status); @@ -1368,7 +1819,20 @@ 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) + " 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 22002c5..d60a1e7 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,11 @@ 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, double filterMs); + void onBatParamCycleFinished(batteryparams_cycle_t cycle); + void onBatParamSettingsChanged(batteryparams_settings_t settings); void onDeviceWndLoadWaveClear(); void onDeviceLoadWaveStopped(); void onDeviceWndChargingCurrentSetValue(unsigned int current); @@ -132,6 +139,10 @@ public slots: private slots: void onDeviceWndCalibrationUpdated(); void onDeviceWndCalibrationStoreRequest(); + void onDeviceWndAutoCalApply(); + void onDeviceWndAutoCalSetLoadCurrent(int mA); + void onDeviceWndAutoCalSetLoadEnabled(bool enabled); + void onDeviceWndAutoCalResetProtection(); @@ -140,8 +151,37 @@ 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 batParamAverageProcess(QVector voltage, QVector voltageKeys, QVector *averaged); + void batParamRelaxationFinish(double key); + void batParamRelaxationNextPass(); + + bool batParamRelaxEnabled; + double batParamRelaxThreshold; + double batParamRelaxWindow; + double batParamRelaxFilter; + double batParamAverageSum; + int batParamAverageFirst; + QVector batParamAverageKeys; + QVector batParamAverageVoltage; + int batParamRelaxPassesDone; + bool batParamRelaxPauseActive; + bool batParamRelaxRunning; + bool batParamRelaxRestarting; + double samplingPeriodMs; + bool uVoltageActive; + bool oVoltageActive; + bool oCurrentActive; + double batParamRelaxCheckKey; + QVector batParamRelaxKeys; + QVector batParamRelaxVoltage; bool createSubDir(const QString &subDirName, QString &fullPath); void logResult(bool status, const QString& successMsg, const QString& errorMsg); diff --git a/Source/images/NewSet/delete.png b/Source/images/NewSet/delete.png new file mode 100644 index 0000000..c8401db Binary files /dev/null and b/Source/images/NewSet/delete.png differ 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/Source/resource.qrc b/Source/resource.qrc index dde90d6..c0d85d4 100644 --- a/Source/resource.qrc +++ b/Source/resource.qrc @@ -48,5 +48,6 @@ images/NewSet/filter.png images/NewSet/export.png images/NewSet/lock.png + images/NewSet/delete.png diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..7ec1d6d --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.1.0 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}" 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}"