From e8bf26a5e80fb30d07583a8dd0d23a6b1708b2fd Mon Sep 17 00:00:00 2001 From: hturkmanovic Date: Tue, 6 Oct 2026 12:45:03 +0200 Subject: [PATCH] Update and calibration finished --- .github/workflows/linux-build.yml | 3 + .github/workflows/windows-build.yml | 3 + Source/OpenEPT.pro | 2 + Source/Windows/About/aboutwnd.cpp | 11 ++ Source/Windows/About/aboutwnd.h | 5 + Source/Windows/About/updatechecker.cpp | 159 +++++++++++++++++++ Source/Windows/About/updatechecker.h | 38 +++++ Source/Windows/Device/autocalibrationwnd.cpp | 124 ++++++++++----- Source/Windows/Device/autocalibrationwnd.h | 2 + Source/openept.cpp | 4 + Source/openept.h | 4 + deploy/linux/build_linux.sh | 33 ++++ deploy/windows/OpenEPT.iss | 46 ++++++ 13 files changed, 392 insertions(+), 42 deletions(-) create mode 100644 Source/Windows/About/updatechecker.cpp create mode 100644 Source/Windows/About/updatechecker.h diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 90d1d01..6ff6b89 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -7,6 +7,9 @@ on: pull_request: workflow_dispatch: +permissions: + contents: write + jobs: linux: runs-on: ubuntu-22.04 diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 63896d1..3290545 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -7,6 +7,9 @@ on: pull_request: workflow_dispatch: +permissions: + contents: write + jobs: windows: runs-on: windows-2022 diff --git a/Source/OpenEPT.pro b/Source/OpenEPT.pro index 23dc59c..5c15dc4 100644 --- a/Source/OpenEPT.pro +++ b/Source/OpenEPT.pro @@ -46,6 +46,7 @@ SOURCES += \ Processing/waveform.cpp \ Utility/log.cpp \ Windows/About/aboutwnd.cpp \ + Windows/About/updatechecker.cpp \ Windows/BatteryParams/batterycyclewnd.cpp \ Windows/BatteryParams/batteryfitintervalwnd.cpp \ Windows/BatteryParams/batteryfitqualitywnd.cpp \ @@ -98,6 +99,7 @@ HEADERS += \ Processing/waveform.h \ Utility/log.h \ Windows/About/aboutwnd.h \ + Windows/About/updatechecker.h \ Windows/BatteryParams/batterycyclewnd.h \ Windows/BatteryParams/batteryfitintervalwnd.h \ Windows/BatteryParams/batteryfitqualitywnd.h \ diff --git a/Source/Windows/About/aboutwnd.cpp b/Source/Windows/About/aboutwnd.cpp index 0df46ea..b02f202 100644 --- a/Source/Windows/About/aboutwnd.cpp +++ b/Source/Windows/About/aboutwnd.cpp @@ -1,4 +1,5 @@ #include "aboutwnd.h" +#include "updatechecker.h" #include #include @@ -70,9 +71,19 @@ AboutWnd::AboutWnd(QWidget *parent) : QLabel *copyrightLabel = new QLabel("Copyright © OpenEPT Team", this); + updateChecker = new UpdateChecker(this); + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this); buttonBox->button(QDialogButtonBox::Close)->setFixedSize(90, 30); + QPushButton *checkUpdatesButton = buttonBox->addButton("Check for Updates", QDialogButtonBox::ActionRole); + checkUpdatesButton->setFixedHeight(30); + + connect(checkUpdatesButton, &QPushButton::clicked, this, [this]() + { + updateChecker->checkForUpdates(false); + }); + connect(buttonBox, &QDialogButtonBox::rejected, this, &AboutWnd::reject); mainVL->addLayout(headerHB); diff --git a/Source/Windows/About/aboutwnd.h b/Source/Windows/About/aboutwnd.h index 44d9051..2495b97 100644 --- a/Source/Windows/About/aboutwnd.h +++ b/Source/Windows/About/aboutwnd.h @@ -7,12 +7,17 @@ #define APP_VERSION "0.0.0-dev" #endif +class UpdateChecker; + class AboutWnd : public QDialog { Q_OBJECT public: explicit AboutWnd(QWidget *parent = nullptr); + +private: + UpdateChecker *updateChecker; }; #endif // ABOUTWND_H diff --git a/Source/Windows/About/updatechecker.cpp b/Source/Windows/About/updatechecker.cpp new file mode 100644 index 0000000..977766c --- /dev/null +++ b/Source/Windows/About/updatechecker.cpp @@ -0,0 +1,159 @@ +#include "updatechecker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +UpdateChecker::UpdateChecker(QWidget *aParent) : + QObject(aParent) +{ + parentWidget = aParent; + silentMode = false; + + connect(&networkManager, &QNetworkAccessManager::finished, + this, &UpdateChecker::onManifestFinished); +} + +void UpdateChecker::checkForUpdates(bool silent) +{ + silentMode = silent; + + QNetworkRequest request((QUrl(UPDATE_MANIFEST_URL))); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + request.setHeader(QNetworkRequest::UserAgentHeader, QString("OpenEPT/%1").arg(APP_VERSION)); + + networkManager.get(request); +} + +QString UpdateChecker::normalizeVersion(const QString &version) +{ + QString value = version.trimmed(); + + if(value.startsWith('v') || value.startsWith('V')) value.remove(0, 1); + + int cut = value.indexOf('-'); + if(cut >= 0) value = value.left(cut); + + cut = value.indexOf('+'); + if(cut >= 0) value = value.left(cut); + + return value; +} + +int UpdateChecker::compareVersions(const QString &a, const QString &b) +{ + QStringList partsA = normalizeVersion(a).split('.'); + QStringList partsB = normalizeVersion(b).split('.'); + int count = qMax(partsA.size(), partsB.size()); + + for(int i = 0; i < count; i++) + { + int valueA = i < partsA.size() ? partsA[i].toInt() : 0; + int valueB = i < partsB.size() ? partsB[i].toInt() : 0; + + if(valueA != valueB) return valueA < valueB ? -1 : 1; + } + + return 0; +} + +void UpdateChecker::onManifestFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + + if(reply->error() != QNetworkReply::NoError) + { + if(!silentMode) + QMessageBox::warning(parentWidget, "Check for Updates", + "Could not reach the update server.\n\n" + reply->errorString()); + return; + } + + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(reply->readAll(), &parseError); + + if(parseError.error != QJsonParseError::NoError || !document.isObject()) + { + if(!silentMode) + QMessageBox::warning(parentWidget, "Check for Updates", + "The update information could not be read."); + return; + } + + QJsonArray components = document.object().value("components").toArray(); + + bool updateAvailable = false; + QString selfUrl; + QString firstUrl; + QString listText; + + for(const QJsonValue &value : components) + { + QJsonObject component = value.toObject(); + QString name = component.value("name").toString(); + QString latest = component.value("latest").toString(); + QString url = component.value("url").toString(); + bool isSelf = component.value("self").toBool(); + + if(name.isEmpty() || latest.isEmpty()) continue; + + QString status; + + if(isSelf) + { + selfUrl = url; + + if(compareVersions(APP_VERSION, latest) < 0) + { + updateAvailable = true; + status = " (update available, you have " + QString(APP_VERSION) + ")"; + } + else + { + status = " (up to date)"; + } + } + + listText += " " + name + " " + latest + status + "\n"; + + if(firstUrl.isEmpty() && !url.isEmpty()) firstUrl = url; + } + + if(listText.isEmpty()) + { + if(!silentMode) + QMessageBox::information(parentWidget, "Check for Updates", + "No update information is available."); + return; + } + + if(silentMode && !updateAvailable) return; + + QString downloadUrl = !selfUrl.isEmpty() ? selfUrl : firstUrl; + QString header = updateAvailable + ? "A newer OpenEPT version is available." + : "You are running the latest OpenEPT version."; + + QMessageBox box(parentWidget); + box.setWindowTitle("Check for Updates"); + box.setIcon(QMessageBox::Information); + box.setText(header); + box.setInformativeText("Latest available versions:\n\n" + listText); + + QPushButton *downloadButton = box.addButton("Go to Downloads", QMessageBox::AcceptRole); + box.addButton(QMessageBox::Close); + if(updateAvailable) box.setDefaultButton(downloadButton); + + box.exec(); + + if(box.clickedButton() == downloadButton && !downloadUrl.isEmpty()) + QDesktopServices::openUrl(QUrl(downloadUrl)); +} diff --git a/Source/Windows/About/updatechecker.h b/Source/Windows/About/updatechecker.h new file mode 100644 index 0000000..58a77c4 --- /dev/null +++ b/Source/Windows/About/updatechecker.h @@ -0,0 +1,38 @@ +#ifndef UPDATECHECKER_H +#define UPDATECHECKER_H + +#include +#include + +QT_FORWARD_DECLARE_CLASS(QNetworkReply) + +#ifndef APP_VERSION +#define APP_VERSION "0.0.0-dev" +#endif + +#ifndef UPDATE_MANIFEST_URL +#define UPDATE_MANIFEST_URL "https://www.openept.net/downloads/manifest.json" +#endif + +class UpdateChecker : public QObject +{ + Q_OBJECT + +public: + explicit UpdateChecker(QWidget *aParent = nullptr); + + void checkForUpdates(bool silent); + +private slots: + void onManifestFinished(QNetworkReply *reply); + +private: + QNetworkAccessManager networkManager; + QWidget *parentWidget; + bool silentMode; + + static QString normalizeVersion(const QString &version); + static int compareVersions(const QString &a, const QString &b); +}; + +#endif // UPDATECHECKER_H diff --git a/Source/Windows/Device/autocalibrationwnd.cpp b/Source/Windows/Device/autocalibrationwnd.cpp index 20e5050..feb4484 100644 --- a/Source/Windows/Device/autocalibrationwnd.cpp +++ b/Source/Windows/Device/autocalibrationwnd.cpp @@ -2,12 +2,18 @@ #include #include +#include #include #include #include #include #include #include +#include +#include +#include +#include +#include #define AUTOCAL_VOLTAGE_REFERENCE 2.049 #define AUTOCAL_FLOAT_VOLTAGE_MIN 5.000 @@ -48,6 +54,10 @@ AutoCalibrationWnd::AutoCalibrationWnd(QWidget *parent) : loadPhase = 0; loadSettleTicks = 0; snapshotValid = false; + currentSpanPoints = AUTOCAL_LOAD_POINTS_MIN; + loadPoints = AUTOCAL_LOAD_POINTS_MIN; + currentSpanMaxMa = AUTOCAL_CURRENT_SPAN_TARGET; + loadMaxMa = AUTOCAL_CURRENT_SPAN_TARGET; titleLabel = new QLabel(this); titleLabel->setStyleSheet("font-weight: bold; font-size: 14px;"); @@ -217,56 +227,84 @@ void AutoCalibrationWnd::startCalibration() 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(); + int absoluteMaxMa = (int)maxLoadCurrentMa(); + int defaultPoints = loadPointCount(); + int defaultMaxMa = (int)(maxLoadCurrentMa() * 0.8); + int minMaxMa = (int)AUTOCAL_LOAD_MIN_CURRENT + 1; - 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; + if(absoluteMaxMa < minMaxMa) absoluteMaxMa = minMaxMa; + if(defaultMaxMa < minMaxMa) defaultMaxMa = minMaxMa; + if(defaultMaxMa > absoluteMaxMa) defaultMaxMa = absoluteMaxMa; - externalReference = true; - referenceVoltage = value; - } - else if(box.clickedButton() == internalBtn) - { - externalReference = false; - referenceVoltage = AUTOCAL_VOLTAGE_REFERENCE; - } - else - { - return; - } + QDialog dialog(this); + dialog.setWindowTitle("Automatic calibration setup"); - { - bool ok = false; - int defaultPoints = loadPointCount(); + QFormLayout *form = new QFormLayout(&dialog); - 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; + QRadioButton *internalRadio = new QRadioButton("Internal 2.049 V", &dialog); + QRadioButton *externalRadio = new QRadioButton("External value", &dialog); + internalRadio->setChecked(true); - loadPoints = QInputDialog::getInt(this, "Load calibration points", - "Number of load points (no input needed, measured automatically):", - defaultPoints, 2, 20, 1, &ok); - if(!ok) return; - } + QHBoxLayout *referenceLayout = new QHBoxLayout(); + referenceLayout->addWidget(internalRadio); + referenceLayout->addWidget(externalRadio); + + QDoubleSpinBox *externalValueSpin = new QDoubleSpinBox(&dialog); + externalValueSpin->setDecimals(4); + externalValueSpin->setRange(0.05, 20.0); + externalValueSpin->setValue(AUTOCAL_VOLTAGE_REFERENCE); + externalValueSpin->setSuffix(" V"); + externalValueSpin->setEnabled(false); + + QSpinBox *spanPointsSpin = new QSpinBox(&dialog); + spanPointsSpin->setRange(3, 20); + spanPointsSpin->setValue(qMax(3, defaultPoints)); + + QSpinBox *spanMaxSpin = new QSpinBox(&dialog); + spanMaxSpin->setRange(minMaxMa, absoluteMaxMa); + spanMaxSpin->setValue(defaultMaxMa); + spanMaxSpin->setSuffix(" mA"); + + QSpinBox *loadPointsSpin = new QSpinBox(&dialog); + loadPointsSpin->setRange(2, 20); + loadPointsSpin->setValue(defaultPoints); + + QSpinBox *loadMaxSpin = new QSpinBox(&dialog); + loadMaxSpin->setRange(minMaxMa, absoluteMaxMa); + loadMaxSpin->setValue(defaultMaxMa); + loadMaxSpin->setSuffix(" mA"); + + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + + form->addRow("Voltage reference:", referenceLayout); + form->addRow("External reference:", externalValueSpin); + form->addRow("Current span points:", spanPointsSpin); + form->addRow("Current span max current:", spanMaxSpin); + form->addRow("Load points:", loadPointsSpin); + form->addRow("Load max current:", loadMaxSpin); + form->addRow(buttons); + + connect(externalRadio, &QRadioButton::toggled, externalValueSpin, &QDoubleSpinBox::setEnabled); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if(dialog.exec() != QDialog::Accepted) return; + + externalReference = externalRadio->isChecked(); + referenceVoltage = externalReference ? externalValueSpin->value() : AUTOCAL_VOLTAGE_REFERENCE; + currentSpanPoints = spanPointsSpin->value(); + currentSpanMaxMa = spanMaxSpin->value(); + loadPoints = loadPointsSpin->value(); + loadMaxMa = loadMaxSpin->value(); voltageHistory.clear(); currentHistory.clear(); logView->clear(); appendLog("Calibration started"); - appendLog("Current span points: " + QString::number(currentSpanPoints) - + ", load points: " + QString::number(loadPoints)); + appendLog("Current span: " + QString::number(currentSpanPoints) + " points, max " + + QString::number((int)currentSpanMaxMa) + " mA"); + appendLog("Load: " + QString::number(loadPoints) + " points, max " + + QString::number((int)loadMaxMa) + " mA"); appendLog(externalReference ? ("External reference " + QString::number(referenceVoltage, 'f', 4) + " V, jumper steps skipped") : "Internal reference 2.049 V"); @@ -347,10 +385,11 @@ void AutoCalibrationWnd::goToStep(autocal_step_t newStep) if(step == AUTOCAL_STEP_CURRENT_SPAN) { int points = currentSpanPoints; - double maxMa = maxLoadCurrentMa() * 0.8; + double maxMa = currentSpanMaxMa; double first = AUTOCAL_LOAD_MIN_CURRENT; if(points < 2) points = 2; + if(maxMa > maxLoadCurrentMa()) maxMa = maxLoadCurrentMa(); if(maxMa < first + 1.0) maxMa = first + 1.0; loadRequested.clear(); @@ -379,9 +418,10 @@ void AutoCalibrationWnd::goToStep(autocal_step_t newStep) { { int points = loadPoints; - double maxMa = maxLoadCurrentMa() * 0.8; + double maxMa = loadMaxMa; double first = AUTOCAL_LOAD_MIN_CURRENT; + if(maxMa > maxLoadCurrentMa()) maxMa = maxLoadCurrentMa(); if(maxMa < first + 1.0) maxMa = first + 1.0; loadRequested.clear(); diff --git a/Source/Windows/Device/autocalibrationwnd.h b/Source/Windows/Device/autocalibrationwnd.h index 08d1c65..db320a6 100644 --- a/Source/Windows/Device/autocalibrationwnd.h +++ b/Source/Windows/Device/autocalibrationwnd.h @@ -95,6 +95,8 @@ private slots: double spanSumMM; int currentSpanPoints; int loadPoints; + double currentSpanMaxMa; + double loadMaxMa; void goToStep(autocal_step_t newStep); void updateView(); diff --git a/Source/openept.cpp b/Source/openept.cpp index 1723fb6..d50e4b2 100644 --- a/Source/openept.cpp +++ b/Source/openept.cpp @@ -3,6 +3,7 @@ #include #include "openept.h" #include "Windows/About/aboutwnd.h" +#include "Windows/About/updatechecker.h" #include "Windows/Device/devicewnd.h" #include "ui_openept.h" #include "Links/controllink.h" @@ -73,6 +74,9 @@ OpenEPT::OpenEPT(QString aWorkspacePath, QWidget *parent) aboutWnd = new AboutWnd(this); aboutWnd->setWindowModality(Qt::WindowModal); + updateChecker = new UpdateChecker(this); + updateChecker->checkForUpdates(true); + setWindowTitle(QString("Open EPT - MCU Energy profiler (v%1)").arg(APP_VERSION)); connectedDeviceNumber = 0; diff --git a/Source/openept.h b/Source/openept.h index 17380d0..469ad27 100644 --- a/Source/openept.h +++ b/Source/openept.h @@ -17,6 +17,8 @@ QT_BEGIN_NAMESPACE namespace Ui { class OpenEPT; } QT_END_NAMESPACE +class UpdateChecker; + class OpenEPT : public QMainWindow { Q_OBJECT @@ -48,6 +50,8 @@ private slots: /**/ AboutWnd *aboutWnd; /**/ + UpdateChecker *updateChecker; + /**/ QList deviceList; /**/ QMenu *connectedDevicesMenu; diff --git a/deploy/linux/build_linux.sh b/deploy/linux/build_linux.sh index ac118a8..0cebd6f 100755 --- a/deploy/linux/build_linux.sh +++ b/deploy/linux/build_linux.sh @@ -157,6 +157,39 @@ Description: OpenEPT Energy Profiling Tool CONTROL chmod 644 "${DEBDIR}/DEBIAN/control" +cat > "${DEBDIR}/DEBIAN/postinst" << 'POSTINST' +#!/bin/sh +set -e + +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true +fi + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true +fi + +exit 0 +POSTINST +chmod 755 "${DEBDIR}/DEBIAN/postinst" + +cat > "${DEBDIR}/DEBIAN/postrm" << 'POSTRM' +#!/bin/sh +set -e + +if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true + fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true + fi +fi + +exit 0 +POSTRM +chmod 755 "${DEBDIR}/DEBIAN/postrm" + dpkg-deb --build "${DEBDIR}" "${OUTPUT_DIR}/${DEB_NAME}" info "Validating generated packages" diff --git a/deploy/windows/OpenEPT.iss b/deploy/windows/OpenEPT.iss index 627c0a6..38c93db 100644 --- a/deploy/windows/OpenEPT.iss +++ b/deploy/windows/OpenEPT.iss @@ -30,6 +30,9 @@ ArchitecturesInstallIn64BitMode=x64compatible PrivilegesRequired=admin +CloseApplications=yes +RestartApplications=no + UninstallDisplayName={#MyAppName} UninstallDisplayIcon={app}\{#MyAppExeName} @@ -45,3 +48,46 @@ Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: [Run] Filename: "{app}\{#MyAppExeName}"; Description: "Launch OpenEPT"; Flags: nowait postinstall skipifsilent + +[Code] +const + AppUninstallKey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A945B938-45C4-45B4-BE28-04E5159AE220}_is1'; + +function GetUninstallString(): String; +var + value: String; +begin + value := ''; + if not RegQueryStringValue(HKLM, AppUninstallKey, 'UninstallString', value) then + RegQueryStringValue(HKCU, AppUninstallKey, 'UninstallString', value); + Result := value; +end; + +function PrepareToInstall(var NeedsRestart: Boolean): String; +var + uninstaller: String; + resultCode: Integer; + waited: Integer; +begin + uninstaller := RemoveQuotes(GetUninstallString()); + + if (uninstaller <> '') and FileExists(uninstaller) then + begin + if not Exec(uninstaller, '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART', + '', SW_HIDE, ewWaitUntilTerminated, resultCode) then + begin + Result := 'Could not uninstall the previous OpenEPT version. ' + + 'Please remove it manually and run the installer again.'; + exit; + end; + + waited := 0; + while FileExists(uninstaller) and (waited < 100) do + begin + Sleep(200); + waited := waited + 1; + end; + end; + + Result := ''; +end;