From 58684cf3c4ae9895a71235dbceafc90f30790318 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 27 Jun 2023 17:25:23 +0200 Subject: [PATCH 001/239] Add new slots readyReadStandardOutput/Error and finished to get messages from included update_psa-script. --- update.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/update.h b/update.h index 08b6478..53c94fe 100644 --- a/update.h +++ b/update.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "plugins/interfaces.h" @@ -79,5 +80,11 @@ private: bool updateDeviceConf(QString jsFileToSendToDC); bool downloadJson(enum FileTypeJson type, int templateIdx, QString jsFileToSendToDC) const; + +private slots: + void readyReadStandardOutput(); + void readyReadStandardError(); + void finished(int exitCode, QProcess::ExitStatus exitStatus); + }; #endif // UPDATE_H_INCLUDED From c34944af8b27970c5f30d7204deb0f66987ff8b0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 27 Jun 2023 17:29:10 +0200 Subject: [PATCH 002/239] Add slots for catching output of underlying update_psa_script. iSetting timeout to 200000 when in maintenance-mode. --- update.cpp | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/update.cpp b/update.cpp index ba1274d..cc3810a 100644 --- a/update.cpp +++ b/update.cpp @@ -12,7 +12,6 @@ #include #include -#include #include #include #include @@ -134,19 +133,26 @@ bool Update::execUpdateScript() { QScopedPointer p(new QProcess(this)); p->setProcessChannelMode(QProcess::MergedChannels); + connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); + connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); + p->start(update_psa); + if (p->waitForStarted(1000)) { if (p->state() == QProcess::ProcessState::Running) { - int const timeout = 200000; // sometimes signal strength of modem is quite low + // maintenance_mode: update_psa script enters an infinite loop + int const timeout = (m_maintenanceMode ? 200000: -1); if (p->waitForFinished(timeout)) { QString output = p->readAllStandardOutput().toStdString().c_str(); QStringList lst = output.split('\n'); for (int i = 0; i < lst.size(); ++i) { qDebug() << lst[i]; } - qInfo() << "EXECUTED" << update_psa; - return ((p->exitStatus() == QProcess::NormalExit) - && (p->exitCode() == 0)); + if (p->exitStatus() == QProcess::NormalExit) { + qInfo() << "EXECUTED" << update_psa + << "with code" << p->exitCode(); + return (p->exitCode() == 0); + } } else { qCritical() << "update-script TIMEDOUT after" << timeout/1000 << "seconds"; @@ -531,7 +537,26 @@ QStringList Update::split(QString line, QChar sep) { return lst; } +void Update::readyReadStandardOutput() { + QProcess *p = (QProcess *)sender(); + QByteArray buf = p->readAllStandardOutput(); + qCritical() << buf; +} + +void Update::readyReadStandardError() { + QProcess *p = (QProcess *)sender(); + QByteArray buf = p->readAllStandardError(); + qCritical() << buf; +} + +void Update::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { + QProcess *p = (QProcess *)sender(); + disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardOutput())); + disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardError())); +} + bool Update::doUpdate() { + /* The file referred to by 'update_data' has the following structure for each line: @@ -626,7 +651,7 @@ bool Update::doUpdate() { } else if (name.contains("DC2C_cash", Qt::CaseInsensitive) && name.endsWith(".json", Qt::CaseInsensitive)) { res = true; -#if UPDATE_CASH_TEMPLATE +#if UPDATE_CASH_TEMPLATE == 1 if ((res = updateCashConf(name))) { qInfo() << "downloaded cash template"<< name; } @@ -634,7 +659,7 @@ bool Update::doUpdate() { } else if (name.contains("DC2C_conf", Qt::CaseInsensitive) && name.endsWith(".json", Qt::CaseInsensitive)) { res = true; -#if UPDATE_CONF_TEMPLATE +#if UPDATE_CONF_TEMPLATE == 1 if ((res= updateConfig(name))) { qInfo() << "downloaded config template"<< name; } @@ -642,7 +667,7 @@ bool Update::doUpdate() { } else if (name.contains("DC2C_device", Qt::CaseInsensitive) && name.endsWith(".json", Qt::CaseInsensitive)) { res = true; -#if UPDATE_DEVICE_TEMPLATE +#if UPDATE_DEVICE_TEMPLATE == 1 if ((res = updateDeviceConf(name))) { qInfo() << "downloaded device template"<< name; } @@ -657,7 +682,12 @@ bool Update::doUpdate() { #if UPDATE_OPKG == 1 QScopedPointer p(new QProcess(this)); p->setProcessChannelMode(QProcess::MergedChannels); + + connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); + connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); + p->start(name.trimmed()); + if (p->waitForStarted(1000)) { if (p->state() == QProcess::ProcessState::Running) { if (p->waitForFinished(100000)) { From 61afdfc3256daf74b27d01fe1ececcf3ca3c1246 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 27 Jun 2023 17:30:56 +0200 Subject: [PATCH 003/239] resetDeviceController(): use bl_rebootDC(). --- update.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/update.cpp b/update.cpp index cc3810a..a22fc87 100644 --- a/update.cpp +++ b/update.cpp @@ -335,16 +335,11 @@ void Update::closeSerial() const { bool Update::resetDeviceController() const { qDebug() << "resetting device controller..."; - //if (stopBootloader()) { // first stop a (maybe) running bootloader - // std::this_thread::sleep_for(std::chrono::milliseconds(1000)); m_hw->bl_rebootDC(); // wait maximally 3 seconds, before starting bootloader QThread::msleep(1500); qInfo() << "resetting device controller...OK"; return true; - //} - //qCritical() << "stopping bootloader...FAILED"; - //return false; } QByteArray Update::loadBinaryDCFile(QString filename) const { From c7acc2a99b7d9b135992706fb3626421545e48f7 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 27 Jun 2023 17:31:38 +0200 Subject: [PATCH 004/239] Add comment for the procedure of downloading device controller. --- update.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/update.cpp b/update.cpp index a22fc87..0d3b996 100644 --- a/update.cpp +++ b/update.cpp @@ -376,6 +376,38 @@ bool Update::downloadBinaryToDC(QString const &bFile) const { return true; } +/* + Using the DC bootloader: + 1 : bl_reboot() // send to application, want DC2 to reset (in order to start + // the bootloader) + 2 : bl_startBL(): // send within 4s after DC poewer-on, otherwise bl is left + 3 : bl_check(): // send command to verify if bl is up + 4 : bl_isUp(): // returns true if bl is up and running + + 5 : bl_sendAddress(blockNumber) + // send start address, nr of 64-byte block, start with 0 + // will be sent only for following block-numbers: + // 0, 1024, 2048, 3072 and 4096, so basically every 64kByte + // for other addresses nothing happens + + 6 : bl_wasSendingAddOK() + // return val: 0: no response by now + // 1: error + // 10: OK + + 7 : bl_sendDataBlock() + // send 64 byte from bin file + + 8 : bl_sendLastBlock() + // send this command after all data are transferred + + 9 : bl_wasSendingDataOK() + // return val: 0: no response by now + // 1: error + // 10: OK + + 10 : bl_stopBL() // leave bl and start (the new) application + */ bool Update::updateBinary(char const *fileToSendToDC) { qInfo() << "updating device controller binary" << fileToSendToDC; QFile fn(fileToSendToDC); From 528b74549a7dc9019018413c6220fd5fbd62f385 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 27 Jun 2023 17:32:13 +0200 Subject: [PATCH 005/239] Aopen serial port only when necessary, i.e. when downloading device controller or json-files. --- update.cpp | 54 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/update.cpp b/update.cpp index 0d3b996..1fe145e 100644 --- a/update.cpp +++ b/update.cpp @@ -605,22 +605,7 @@ bool Update::doUpdate() { return false; } - if (!openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) { - qCritical() << "CANNOT OPEN" << m_serialInterface << "(BAUDRATE=" - << m_baudrate << ")"; - return false; - } - - QString fwVersion = m_hw->dc_getSWversion(); - QString const hwVersion = m_hw->dc_getHWversion(); - - qInfo() << "current dc-hardware-version" << hwVersion; - qInfo() << "current dc-firmware-version" << fwVersion; - - m_hw->dc_autoRequest(false);// default: turn auto-request setting off - QThread::sleep(3); // wait to be sure that there are no more - // commands sent to dc-hardware - qDebug() << "SET AUTO-REQUEST=FALSE"; + bool serialOpened = false; QStringList linesToWorkOn = getLinesToWorkOn(); if (linesToWorkOn.size() == 0) { @@ -651,6 +636,26 @@ bool Update::doUpdate() { // QString const &result = lst[COLUMN_RESULT]; qDebug() << "request=" << request << ", name=" << name; if (request.trimmed() == "DOWNLOAD") { + if (!serialOpened) { // open serial code once + if (!openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) { + qCritical() << "CANNOT OPEN" << m_serialInterface << "(BAUDRATE=" + << m_baudrate << ")"; + return false; + } + + serialOpened = true; + + QString fwVersion = m_hw->dc_getSWversion(); + QString const hwVersion = m_hw->dc_getHWversion(); + + qInfo() << "current dc-hardware-version" << hwVersion; + qInfo() << "current dc-firmware-version" << fwVersion; + + m_hw->dc_autoRequest(false);// default: turn auto-request setting off + QThread::sleep(3); // wait to be sure that there are no more + // commands sent to dc-hardware + qDebug() << "SET AUTO-REQUEST=FALSE"; + } if (name.contains("dc2c", Qt::CaseInsensitive) && name.endsWith(".bin", Qt::CaseInsensitive)) { qInfo() << "downloading" << name.trimmed() << "to DC"; @@ -750,14 +755,19 @@ bool Update::doUpdate() { 20, 20, QDateTime::currentDateTime().toString(Qt::ISODate).toStdString().c_str(), 10, 10, (res == true) ? "SUCCESS" : "ERROR"); m_update_ctrl_file_copy.write(buf); + + qInfo() << "write" << buf << "into file" << m_update_ctrl_file_copy; + } // for (it = openLines.cbegin(); it != openLines.end(); ++it) { - closeSerial(); - m_hw->dc_autoRequest(true); - qDebug() << "SET AUTO-REQUEST=TRUE"; - - qInfo() << "current dc-hardware-version" << m_hw->dc_getHWversion(); - qInfo() << "current dc-firmware-version" << m_hw->dc_getSWversion(); + if (serialOpened) { + m_hw->dc_autoRequest(true); + qDebug() << "SET AUTO-REQUEST=TRUE"; + qInfo() << "current dc-hardware-version" << m_hw->dc_getHWversion(); + qInfo() << "current dc-firmware-version" << m_hw->dc_getSWversion(); + closeSerial(); + serialOpened = false; + } return finishUpdate(linesToWorkOn.size() > 0); } From 3034f49c964995215dbf445b0b514b0f6947ac93 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 29 Jun 2023 12:40:47 +0200 Subject: [PATCH 006/239] Minor change: add comment --- main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/main.cpp b/main.cpp index 721f742..f30e590 100644 --- a/main.cpp +++ b/main.cpp @@ -98,6 +98,7 @@ int main(int argc, char *argv[]) { // -d: only update device-controller firmware // -j: only update json-files // -o: only execute opkg-commnds + // -f: force. update_psa shall always perform a 'git pull' // Process the actual command line arguments given by the user parser.process(a); From 61c847102d09fd4af8a56384bcc0377bc7061c1e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 29 Jun 2023 12:42:16 +0200 Subject: [PATCH 007/239] Open/empty update_log.csv/.csv.copy at start. Added some debug-output --- update.cpp | 52 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/update.cpp b/update.cpp index 1fe145e..6557ce8 100644 --- a/update.cpp +++ b/update.cpp @@ -92,31 +92,55 @@ Update::Update(hwinf *hw, , m_maintenanceMode(maintenanceMode) , m_init(true) { - execUpdateScript(); - - if (!m_update_ctrl_file.exists()) { + // make sure the files are empty + if (m_update_ctrl_file.exists()) { + if (m_update_ctrl_file.open(QIODevice::ReadWrite | + QIODevice::Truncate | + QIODevice::Text)) { + m_update_ctrl_file.close(); + } + } else { qCritical() << "Update-file" << m_update_ctrl_file.fileName() << "does not exist"; m_init = false; } - if (!m_update_ctrl_file_copy.exists()) { + if (m_update_ctrl_file_copy.exists()) { + if (m_update_ctrl_file_copy.open(QIODevice::ReadWrite | + QIODevice::Truncate | + QIODevice::Text)) { + m_update_ctrl_file_copy.close(); + } + } else { qCritical() << "Update-file-copy" << m_update_ctrl_file_copy.fileName() << "does not exist"; m_init = false; } - if (!m_update_ctrl_file.open(QIODevice::ReadWrite | QIODevice::Text)) { - qCritical() << "can not open " << m_update_ctrl_file.fileName(); - m_init = false; + + // execute update_psa-script + if (m_init) { + if ((m_init = execUpdateScript()) == false) { + qCritical() << "UPDATE_SCRIPT FAILED"; + } else { + if (!m_update_ctrl_file.open(QIODevice::ReadWrite | QIODevice::Text)) { + qCritical() << "CAN NOT OPEN" << m_update_ctrl_file.fileName(); + m_init = false; + } else { + qDebug() << "OPENED" << m_update_ctrl_file.fileName(); + } + if (!m_update_ctrl_file_copy.open(QIODevice::ReadWrite | QIODevice::Text)) { + qCritical() << "CAN NOT OPEN" << m_update_ctrl_file_copy.fileName(); + m_init = false; + } else { + qDebug() << "OPENED" << m_update_ctrl_file_copy.fileName(); + } + } } - qDebug() << "Opened" << m_update_ctrl_file.fileName(); - if (!m_update_ctrl_file_copy.open(QIODevice::ReadWrite | QIODevice::Text)) { - qCritical() << "can not open " << m_update_ctrl_file_copy.fileName(); - m_init = false; - } - qDebug() << "Opened" << m_update_ctrl_file_copy.fileName(); } Update::~Update() { + // make sure the files are closed + m_update_ctrl_file.close(); + m_update_ctrl_file_copy.close(); } bool Update::execUpdateScript() { @@ -602,6 +626,7 @@ bool Update::doUpdate() { */ if (!m_init) { + qCritical() << "DO UPDATE: INITIALIZATION OR UPDATE-SCRIPT FAILED"; return false; } @@ -612,6 +637,7 @@ bool Update::doUpdate() { qCritical() << "No lines to handle in" << m_update_ctrl_file.fileName(); return true; } + qDebug() << "open lines..."; for (int i=0; i< linesToWorkOn.size(); ++i) { qDebug() << "line" << i << ":" << linesToWorkOn.at(i).trimmed(); From 027529161de485dc526f1ef382010678be69ed85 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:11:28 +0200 Subject: [PATCH 008/239] Add VERSION to be used inside application --- OnDemandUpdatePTU.pro | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index f7f11e2..dcc1223 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -5,12 +5,14 @@ QT += widgets serialport TARGET = ATBUpdateTool +VERSION=1.0.0 + INCLUDEPATH += plugins CONFIG += c++17 console # CONFIG -= app_bundle -# DEFINES+=LinuxDesktop +DEFINES+=APP_VERSION=\\\"$$VERSION\\\" QMAKE_CXXFLAGS += -Wno-deprecated-copy From 6bb46e165c5b14b025256daac41e25ebff3b8104 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:12:41 +0200 Subject: [PATCH 009/239] Add handling of VERSION. Add test-mode. --- main.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/main.cpp b/main.cpp index f30e590..fee9594 100644 --- a/main.cpp +++ b/main.cpp @@ -36,9 +36,9 @@ class hwinf; static void doWork(hwinf *hw, QString update_ctrl_file, - QString workingDir, bool maintenanceMode) { + QString workingDir, bool maintenanceMode, bool testMode) { std::this_thread::sleep_for(std::chrono::milliseconds(2000)); - Update update(hw, update_ctrl_file, workingDir, maintenanceMode); + Update update(hw, update_ctrl_file, workingDir, maintenanceMode, testMode); update.doUpdate(); std::this_thread::sleep_for(std::chrono::milliseconds(2000)); QCoreApplication::quit(); @@ -54,7 +54,7 @@ int main(int argc, char *argv[]) { QApplication a(argc, argv); QApplication::setApplicationName("ATBUpdateTool"); - QApplication::setApplicationVersion("1.0"); + QApplication::setApplicationVersion(APP_VERSION); if (!messageHandlerInstalled()) { // change internal qt-QDebug-handling atbInstallMessageHandler(atbDebugOutput); @@ -92,6 +92,12 @@ int main(int argc, char *argv[]) { QCoreApplication::translate("main", "Maintenance mode for underlying script")); parser.addOption(maintenanceOption); + // test-mode: edit the file update_log.csv and execute the commands + // contained in it. Do not call the update-script. + QCommandLineOption testOption("t", + QCoreApplication::translate("main", "Test mode for ATBUpdateTool")); + parser.addOption(testOption); + // TODO: // add some additional parameters // --dry-run @@ -106,6 +112,7 @@ int main(int argc, char *argv[]) { QString plugInName = parser.value(pluginNameOption); QString workingDir = parser.value(workingDirectoryOption); bool maintenanceMode = parser.isSet(maintenanceOption); + bool testMode = parser.isSet(testOption); QString const rtPath = QCoreApplication::applicationDirPath(); if (plugInDir == pluginDefault) { @@ -121,6 +128,7 @@ int main(int argc, char *argv[]) { qInfo() << "plugInName" << "=" << plugInName; qInfo() << "workingDir" << "=" << workingDir; qInfo() << "maintenanceMode" << "=" << maintenanceMode; + qInfo() << "testMode" << "=" << testMode; // before loading the library, delete all possible shared memory segments #if defined Q_OS_LINUX || defined Q_OS_UNIX @@ -130,10 +138,10 @@ int main(int argc, char *argv[]) { #endif hwinf *hw = Update::loadDCPlugin(QDir(plugInDir), plugInName); - hw->dc_autoRequest(false); + // hw->dc_autoRequest(false); QString const update_ctrl_file = "/opt/app/tools/atbupdate/update_log.csv"; - std::thread t(doWork, hw, update_ctrl_file, workingDir, maintenanceMode); + std::thread t(doWork, hw, update_ctrl_file, workingDir, maintenanceMode, testMode); int ret = a.exec(); t.join(); From 26557542f11a9fca5e2e992bd0dc7a29bbe26460 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:13:33 +0200 Subject: [PATCH 010/239] Added test-mode. Added aux. function isSerialOpen(). --- update.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/update.h b/update.h index 53c94fe..7037cf9 100644 --- a/update.h +++ b/update.h @@ -31,6 +31,7 @@ class Update : public QObject { QFile m_update_ctrl_file_copy; QString m_workingDir; bool m_maintenanceMode; + bool m_testMode; bool m_init; @@ -51,6 +52,7 @@ public: QString update_ctrl_file, QString workingDir = ".", bool maintenanceMode = false, + bool testMode = false, QObject *parent = nullptr, char const *serialInterface = SERIAL_PORT, char const *baudrate = "115200"); @@ -69,6 +71,7 @@ private: bool stopBootloader() const; bool openSerial(int br, QString baudrate, QString comPort) const; void closeSerial() const; + bool isSerialOpen() const; bool resetDeviceController() const; QByteArray loadBinaryDCFile(QString filename) const; bool downloadBinaryToDC(QString const &bFile) const; From f2556412d80a1abf279a3a022290fd6d509fd9c2 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:21:29 +0200 Subject: [PATCH 011/239] Comment some lines. --- update.cpp | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/update.cpp b/update.cpp index 6557ce8..ff85864 100644 --- a/update.cpp +++ b/update.cpp @@ -8,6 +8,10 @@ #include #include +//#include +//#include +//#include + #include "plugins/interfaces.h" #include @@ -238,16 +242,12 @@ Update::DownloadResult Update::sendNextDataBlock(QByteArray const &binary, memcpy(local, binary.constData() + bAddr, 64); local[64] = local[65] = 0x00; - //for (int i=0; i<4; ++i) { - // printf("%04d ", bNum); - // for (int j=0; j < 16; ++j) { - // printf("%02x ", local[i*16 + j]); - // } printf("\n"); - //} + // QByteArray b((const char *)(&local[0]), 64); + // qCritical() << "SNDB" << bNum << b.size() << b.toHex(); while (noAnswerCount <= 250) { m_hw->bl_sendDataBlock(64, local); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + QThread::msleep(10); DownloadResult const res = sendStatus(m_hw->bl_wasSendingDataOK()); if (res != DownloadResult::NOP) { if (res == DownloadResult::ERROR) { @@ -256,7 +256,7 @@ Update::DownloadResult Update::sendNextDataBlock(QByteArray const &binary, return res; } } else { - qInfo() << "data for block" << bNum << "OK"; + // qInfo() << "data for block" << bNum << "OK"; return res; } } else { @@ -803,21 +803,12 @@ bool Update::finishUpdate(bool swapCtrlFiles) { m_update_ctrl_file.close(); m_update_ctrl_file_copy.close(); - QString const &fn = m_update_ctrl_file.fileName(); - QString const &fn_tmp = m_update_ctrl_file.fileName() + ".tmp"; - QString const &fn_copy = m_update_ctrl_file_copy.fileName(); - QFile tmp(fn_tmp); + //std::ifstream source(m_update_ctrl_file_copy.fileName().toStdString().c_str(), std::ios::binary); + //std::ofstream dest(m_update_ctrl_file.fileName().toStdString().c_str(), std::ios::binary); - if (tmp.exists()) { - tmp.remove(); - } - - if (m_update_ctrl_file.rename(fn_tmp)) { - if (m_update_ctrl_file_copy.rename(fn)) { - return m_update_ctrl_file.rename(fn_copy); - } - } - return false; + //dest << source.rdbuf(); + //source.close(); + //dest.close(); } return true; } From 17eaa7858f4bd0be9c88ccd34045031663c4714f Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:23:04 +0200 Subject: [PATCH 012/239] Add handling of test-mode. --- update.cpp | 53 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/update.cpp b/update.cpp index ff85864..8b520cb 100644 --- a/update.cpp +++ b/update.cpp @@ -83,6 +83,7 @@ Update::Update(hwinf *hw, QString update_ctrl_file, QString workingDir, bool maintenanceMode, + bool testMode, QObject *parent, char const *serialInterface, char const *baudrate) @@ -94,37 +95,43 @@ Update::Update(hwinf *hw, , m_update_ctrl_file_copy(update_ctrl_file + ".copy") , m_workingDir(workingDir) , m_maintenanceMode(maintenanceMode) + , m_testMode(testMode) , m_init(true) { - // make sure the files are empty - if (m_update_ctrl_file.exists()) { - if (m_update_ctrl_file.open(QIODevice::ReadWrite | - QIODevice::Truncate | - QIODevice::Text)) { - m_update_ctrl_file.close(); + if (!m_testMode) { + // make sure the files are empty + if (m_update_ctrl_file.exists()) { + if (m_update_ctrl_file.open(QIODevice::ReadWrite | + QIODevice::Truncate | + QIODevice::Text)) { + m_update_ctrl_file.close(); + } + } else { + qCritical() << "Update-file" << m_update_ctrl_file.fileName() + << "does not exist"; + m_init = false; } - } else { - qCritical() << "Update-file" << m_update_ctrl_file.fileName() - << "does not exist"; - m_init = false; - } - if (m_update_ctrl_file_copy.exists()) { - if (m_update_ctrl_file_copy.open(QIODevice::ReadWrite | - QIODevice::Truncate | - QIODevice::Text)) { - m_update_ctrl_file_copy.close(); + if (m_update_ctrl_file_copy.exists()) { + if (m_update_ctrl_file_copy.open(QIODevice::ReadWrite | + QIODevice::Truncate | + QIODevice::Text)) { + m_update_ctrl_file_copy.close(); + } + } else { + qCritical() << "Update-file-copy" << m_update_ctrl_file_copy.fileName() + << "does not exist"; + m_init = false; } - } else { - qCritical() << "Update-file-copy" << m_update_ctrl_file_copy.fileName() - << "does not exist"; - m_init = false; } // execute update_psa-script if (m_init) { - if ((m_init = execUpdateScript()) == false) { - qCritical() << "UPDATE_SCRIPT FAILED"; - } else { + if (!m_testMode) { + if ((m_init = execUpdateScript()) == false) { + qCritical() << "UPDATE_SCRIPT FAILED"; + } + } + if (m_init) { if (!m_update_ctrl_file.open(QIODevice::ReadWrite | QIODevice::Text)) { qCritical() << "CAN NOT OPEN" << m_update_ctrl_file.fileName(); m_init = false; From a0d0de19c5ddcf56aa2d1cd49799ac28edbb40ec Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:24:19 +0200 Subject: [PATCH 013/239] Use QThread::sleep() instead of std-C++-code. --- update.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/update.cpp b/update.cpp index 8b520cb..f8cf320 100644 --- a/update.cpp +++ b/update.cpp @@ -216,7 +216,7 @@ Update::DownloadResult Update::sendNextAddress(int bNum) const { qDebug() << "addr-block" << bNum << "..."; while (noAnswerCount <= 250) { m_hw->bl_sendAddress(bNum); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QThread::msleep(100); DownloadResult const res = sendStatus(m_hw->bl_wasSendingAddOK()); if (res != DownloadResult::NOP) { if (res == DownloadResult::ERROR) { @@ -320,11 +320,11 @@ bool Update::startBootloader() const { int nTry = 5; while (--nTry >= 0) { m_hw->bl_startBL(); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + QThread::msleep(5000); m_hw->bl_checkBL(); if (m_hw->bl_isUp()) { qInfo() << "starting bootloader...OK"; - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + QThread::msleep(5000); return true; } else { qCritical() << "bootloader not up (" << nTry << ")"; @@ -339,7 +339,7 @@ bool Update::stopBootloader() const { int nTry = 5; while (--nTry >= 0) { m_hw->bl_stopBL(); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + QThread::msleep(500); if (!m_hw->bl_isUp()) { qInfo() << "stopping bootloader...OK"; return true; @@ -368,7 +368,7 @@ bool Update::resetDeviceController() const { qDebug() << "resetting device controller..."; m_hw->bl_rebootDC(); // wait maximally 3 seconds, before starting bootloader - QThread::msleep(1500); + QThread::sleep(1); qInfo() << "resetting device controller...OK"; return true; } From faae8b8d9a12dde92ab1e54480750c6f7ee05e1c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:25:36 +0200 Subject: [PATCH 014/239] Add aux. function isSerialOpen(). --- update.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/update.cpp b/update.cpp index f8cf320..98bca1f 100644 --- a/update.cpp +++ b/update.cpp @@ -361,9 +361,14 @@ bool Update::openSerial(int br, QString baudrate, QString comPort) const { } void Update::closeSerial() const { + qInfo() << "CLOSED SERIAL" << m_baudrate << m_serialInterface; m_hw->dc_closeSerial(); } +bool Update::isSerialOpen() const { + return m_hw->dc_isPortOpen(); +} + bool Update::resetDeviceController() const { qDebug() << "resetting device controller..."; m_hw->bl_rebootDC(); From 7d6b4333734111e3babf9812c6ff46c1be6cccbe Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:26:58 +0200 Subject: [PATCH 015/239] Removed obsolete code. --- update.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/update.cpp b/update.cpp index 98bca1f..d47c92e 100644 --- a/update.cpp +++ b/update.cpp @@ -302,14 +302,6 @@ Update::DownloadResult Update::dc_downloadBinary(QByteArray const &b) const { memcpy(local, startAddress, rest); } - //for (int i=0; i<4; ++i) { - // printf("*** %04d ", bNum); - // for (int j=0; j < 16; ++j) { - // printf("%02x ", local[i*16 + j]); - // } printf("\n"); - //} - - // bl_sendLastBlock(local); m_hw->bl_sendLastBlock(); qInfo() << "last result" << (int)sendStatus(m_hw->bl_wasSendingDataOK()); return res; From fa04e32266d28718a7a38698afdb82c861462416 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:27:44 +0200 Subject: [PATCH 016/239] When sending dc-data, fill the last block with 0xFF to stream-line the source code. --- update.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/update.cpp b/update.cpp index d47c92e..223d47b 100644 --- a/update.cpp +++ b/update.cpp @@ -277,29 +277,32 @@ Update::DownloadResult Update::sendNextDataBlock(QByteArray const &binary, Update::DownloadResult Update::dc_downloadBinary(QByteArray const &b) const { int const nBlocks = (((b.size())%64)==0) ? (b.size()/64) : (b.size()/64)+1; - qInfo() << "total number of bytes to send to dc" << b.size(); + // fill lst block of data to be sent with 0xFF + QByteArray ba = b.leftJustified(nBlocks*64, (char)(0xFF)); + + qInfo() << "total number of bytes to send to dc" << ba.size(); qInfo() << "total number of blocks to send to dc" << nBlocks; int bNum = 0; DownloadResult res = DownloadResult::OK; while (res != DownloadResult::ERROR && bNum < nBlocks) { if ((res = sendNextAddress(bNum)) != DownloadResult::ERROR) { - if ((res = sendNextDataBlock(b, bNum)) != DownloadResult::ERROR) { + if ((res = sendNextDataBlock(ba, bNum)) != DownloadResult::ERROR) { bNum += 1; } } } - qInfo() << "nBlocks" << nBlocks; - //if (res != DownloadResult::ERROR) { - // always send last block, even when there are no data !!! - int const rest = b.size() % 64; - int const offset = b.size() - rest; - char const *startAddress = b.constData() + offset; + int const rest = ba.size() % 64; + int const offset = ba.size() - rest; + char const *startAddress = ba.constData() + offset; - uint8_t local[66]; - memset(local, 0x00, sizeof(local)); if (rest > 0) { + // SHOULD NEVER HAPPEN !!! + uint8_t local[66]; + memset(local, 0xFF, sizeof(local)); memcpy(local, startAddress, rest); + qCritical() << "ERROR SEND REMAINING" << rest << "BYTES"; + m_hw->bl_sendDataBlock(64, local); } m_hw->bl_sendLastBlock(); From ba8dd7d0839d6f56e4571d2254fb13989f1c33a3 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:28:46 +0200 Subject: [PATCH 017/239] updateDC(): stop boot loader even when not ven able to start it. --- update.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/update.cpp b/update.cpp index 223d47b..9ab08d3 100644 --- a/update.cpp +++ b/update.cpp @@ -466,15 +466,21 @@ bool Update::updateDC(QString bFile) const { return false; } if (!startBootloader()) { + // even when start seems to fail, stopping the boot loader does not harm + stopBootloader(); return false; } + if (!downloadBinaryToDC(bFile)) { stopBootloader(); qCritical() << "updating dc: " << bFile << "...FAILED"; return false; } qInfo() << "updating dc: " << bFile << "...OK"; + stopBootloader(); + //resetDeviceController(); + QThread::sleep(3); return true; } From 85b2c1f08eeacdd513cad9ce65fb95c971b6f257 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:30:48 +0200 Subject: [PATCH 018/239] Write update_log.csv.copy in different format. --- update.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/update.cpp b/update.cpp index 9ab08d3..60b38ee 100644 --- a/update.cpp +++ b/update.cpp @@ -787,15 +787,10 @@ bool Update::doUpdate() { } else { // TODO } - char buf[128]; - memset(buf, 0x00, sizeof(buf)); - snprintf(buf, sizeof(buf)-1, "DONE, %*.*s, %*.*s, %*.*s\n", - 35, 35, name.toStdString().c_str(), - 20, 20, QDateTime::currentDateTime().toString(Qt::ISODate).toStdString().c_str(), - 10, 10, (res == true) ? "SUCCESS" : "ERROR"); - m_update_ctrl_file_copy.write(buf); - - qInfo() << "write" << buf << "into file" << m_update_ctrl_file_copy; + out << "DONE, " << name << ", " + << QDateTime::currentDateTime().toString(Qt::ISODate) << ", " + << ((res == true) ? "SUCCESS" : "ERROR") << "\n"; + out.flush(); } // for (it = openLines.cbegin(); it != openLines.end(); ++it) { From a41dc5403fc61714b0a5fd3df3d7f25a453dc45d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:31:32 +0200 Subject: [PATCH 019/239] Open serial port only if not already opened. --- update.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/update.cpp b/update.cpp index 60b38ee..2a34d8a 100644 --- a/update.cpp +++ b/update.cpp @@ -644,6 +644,7 @@ bool Update::doUpdate() { } bool serialOpened = false; + bool serialOpen = false; QStringList linesToWorkOn = getLinesToWorkOn(); if (linesToWorkOn.size() == 0) { From 06a9eba17798f52129653f4345e74b40bed31251 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:32:20 +0200 Subject: [PATCH 020/239] Turn auto-request ON in any case. --- update.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/update.cpp b/update.cpp index 2a34d8a..6838ee2 100644 --- a/update.cpp +++ b/update.cpp @@ -795,16 +795,12 @@ bool Update::doUpdate() { } // for (it = openLines.cbegin(); it != openLines.end(); ++it) { - if (serialOpened) { - m_hw->dc_autoRequest(true); - qDebug() << "SET AUTO-REQUEST=TRUE"; - qInfo() << "current dc-hardware-version" << m_hw->dc_getHWversion(); - qInfo() << "current dc-firmware-version" << m_hw->dc_getSWversion(); - closeSerial(); - serialOpened = false; - } + m_hw->dc_autoRequest(true); // ALWAYS turn autoRequest ON + qDebug() << "SET AUTO-REQUEST=TRUE"; - return finishUpdate(linesToWorkOn.size() > 0); + return true; + + // return finishUpdate(linesToWorkOn.size() > 0); } bool Update::finishUpdate(bool swapCtrlFiles) { From d89520d58e4167d8b8446cc8c98f471ad3699f9c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 14:32:57 +0200 Subject: [PATCH 021/239] Activate updating device-controller-firmware. --- update.cpp | 81 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/update.cpp b/update.cpp index 6838ee2..c3ace0f 100644 --- a/update.cpp +++ b/update.cpp @@ -28,7 +28,7 @@ #define COLUMN_RESULT (3) #define UPDATE_OPKG (1) -#define UPDATE_DC (0) +#define UPDATE_DC (1) #define UPDATE_PRINTER_TEMPLATES (0) #define UPDATE_CASH_TEMPLATE (0) #define UPDATE_CONF_TEMPLATE (0) @@ -672,39 +672,74 @@ bool Update::doUpdate() { } QString const &request = lst[COLUMN_REQUEST]; QString const &name = lst[COLUMN_NAME]; + + QTextStream out(&m_update_ctrl_file_copy); // QString const &datetime = lst[COLUMN_DATE_TIME]; // QString const &result = lst[COLUMN_RESULT]; qDebug() << "request=" << request << ", name=" << name; if (request.trimmed() == "DOWNLOAD") { - if (!serialOpened) { // open serial code once - if (!openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) { - qCritical() << "CANNOT OPEN" << m_serialInterface << "(BAUDRATE=" - << m_baudrate << ")"; - return false; + if (!serialOpen) { + if (!isSerialOpen()) { // open serial only if not already open + if ((serialOpened = openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) == false) { + qCritical() << "CANNOT OPEN" << m_serialInterface << "(BAUDRATE=" + << m_baudrate << ")"; + return false; + } } + serialOpen = true; + qCritical() << "SERIAL OPEN" << m_serialInterface << "(BAUDRATE=" << m_baudrate << ")"; + } - serialOpened = true; - - QString fwVersion = m_hw->dc_getSWversion(); - QString const hwVersion = m_hw->dc_getHWversion(); - + if (name.contains("dc2c", Qt::CaseInsensitive) && + name.endsWith(".bin", Qt::CaseInsensitive)) { + qDebug() << "sending sw/hw-requests..."; + for (int i=0; i < 3; ++i) { // send explicit reuests to get + // current SW/HW-versions + m_hw->request_DC2_SWversion(); + m_hw->request_DC2_HWversion(); + QThread::sleep(1); + } + QString const hwVersion = m_hw->dc_getHWversion().toLower(); + QString const fwVersion = m_hw->dc_getSWversion().toLower(); qInfo() << "current dc-hardware-version" << hwVersion; qInfo() << "current dc-firmware-version" << fwVersion; - m_hw->dc_autoRequest(false);// default: turn auto-request setting off - QThread::sleep(3); // wait to be sure that there are no more - // commands sent to dc-hardware - qDebug() << "SET AUTO-REQUEST=FALSE"; - } - if (name.contains("dc2c", Qt::CaseInsensitive) && - name.endsWith(".bin", Qt::CaseInsensitive)) { - qInfo() << "downloading" << name.trimmed() << "to DC"; - res = true; + QFile fn(name); + QFileInfo linkTarget(fn.symLinkTarget()); + if (!linkTarget.exists()) { // check for broken link + res = false; + } else { + if (fwVersion.startsWith(linkTarget.completeBaseName())) { + qCritical() << "current dc-firmware-version" << fwVersion + << "already installed"; + res = false; + } else { + res = true; + + qCritical() << "downloading" << name.trimmed() << "->" + << linkTarget.completeBaseName() << "to DC"; #if UPDATE_DC == 1 - if ((res = updateBinary(name.toStdString().c_str())) == true) { - qInfo() << "downloaded binary" << name; - } + m_hw->dc_autoRequest(false);// default: turn auto-request setting off + QThread::sleep(1); // wait to be sure that there are no more + // commands sent to dc-hardware + qDebug() << "SET AUTO-REQUEST=FALSE"; + + if ((res = updateBinary(name.toStdString().c_str())) == true) { + qCritical() << "downloaded binary" << name; + } + + m_hw->dc_autoRequest(true); // turn auto-request setting on + qDebug() << "SET AUTO-REQUEST=TRUE"; + qDebug() << "WAIT 10 SECS TO RECEIVE RESPONSES..."; + + QThread::sleep(10); // wait to be sure that responses + // have been received + + qCritical() << "updated dc-hardware-version" << m_hw->dc_getHWversion(); + qCritical() << "updated dc-firmware-version" << m_hw->dc_getSWversion(); #endif + } + } } else if (name.contains("DC2C_print", Qt::CaseInsensitive) && name.endsWith(".json", Qt::CaseInsensitive)) { res = true; From a67e5877695e47e96da736a33eff0cc9c56bbe2a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 15:14:52 +0200 Subject: [PATCH 022/239] Activate download of printer templates --- update.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.cpp b/update.cpp index c3ace0f..a163e97 100644 --- a/update.cpp +++ b/update.cpp @@ -29,7 +29,7 @@ #define UPDATE_OPKG (1) #define UPDATE_DC (1) -#define UPDATE_PRINTER_TEMPLATES (0) +#define UPDATE_PRINTER_TEMPLATES (1) #define UPDATE_CASH_TEMPLATE (0) #define UPDATE_CONF_TEMPLATE (0) #define UPDATE_DEVICE_TEMPLATE (0) From 3039fcc55395bb478f8ec91a79eb006586e56bf0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 16:52:32 +0200 Subject: [PATCH 023/239] remove superfluous \n --- message_handler.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/message_handler.cpp b/message_handler.cpp index f904a4a..8b655bd 100755 --- a/message_handler.cpp +++ b/message_handler.cpp @@ -63,7 +63,7 @@ void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt switch (type) { case QtDebugMsg: { if (debugLevel == QtDebugMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d DEBG %s\n", + snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d DEBG %s", function, file, context.line, datetime.time().toString(format).toStdString().c_str(), fractional_part, @@ -73,7 +73,7 @@ void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt } break; case QtInfoMsg: { if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d INFO %s\n", + snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d INFO %s", function, file, context.line, datetime.time().toString(format).toStdString().c_str(), fractional_part, @@ -83,7 +83,7 @@ void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt } break; case QtWarningMsg: { if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg || debugLevel == QtWarningMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d WARN %s\n", + snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d WARN %s", function, file, context.line, datetime.time().toString(format).toStdString().c_str(), fractional_part, @@ -94,7 +94,7 @@ void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt case QtCriticalMsg: { if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg || debugLevel == QtWarningMsg || debugLevel == QtCriticalMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d CRIT %s\n", + snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d CRIT %s", function, file, context.line, datetime.time().toString(format).toStdString().c_str(), fractional_part, @@ -106,7 +106,7 @@ void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg || debugLevel == QtWarningMsg || debugLevel == QtCriticalMsg || debugLevel == QtFatalMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d FATAL %s\n", + snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d FATAL %s", function, file, context.line, datetime.time().toString(format).toStdString().c_str(), fractional_part, From c4d09eb2ea2a76f30060b89f04e43612933f3976 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 6 Jul 2023 16:54:01 +0200 Subject: [PATCH 024/239] Add -e (execute-script-only) and -d (dry-run) options --- main.cpp | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/main.cpp b/main.cpp index fee9594..6bb03ce 100644 --- a/main.cpp +++ b/main.cpp @@ -88,16 +88,24 @@ int main(int argc, char *argv[]) { workingDirectoryOption.setDefaultValue(workingDirectoryDefault); parser.addOption(workingDirectoryOption); - QCommandLineOption maintenanceOption("m", + QCommandLineOption maintenanceOption(QStringList() << "m" << "maintenance", QCoreApplication::translate("main", "Maintenance mode for underlying script")); parser.addOption(maintenanceOption); // test-mode: edit the file update_log.csv and execute the commands // contained in it. Do not call the update-script. - QCommandLineOption testOption("t", + QCommandLineOption testOption(QStringList() << "t" << "test", QCoreApplication::translate("main", "Test mode for ATBUpdateTool")); parser.addOption(testOption); + QCommandLineOption execScriptOption(QStringList() << "e" << "execute-script-only", + QCoreApplication::translate("main", "ATBUpdateTool executes update-script only. No download of any files.")); + parser.addOption(execScriptOption); + + QCommandLineOption dryRunOption(QStringList() << "d" << "dry-run", + QCoreApplication::translate("main", "Start ATBUpdateTool in dry-run-mode. No actual actions.")); + parser.addOption(dryRunOption); + // TODO: // add some additional parameters // --dry-run @@ -113,6 +121,8 @@ int main(int argc, char *argv[]) { QString workingDir = parser.value(workingDirectoryOption); bool maintenanceMode = parser.isSet(maintenanceOption); bool testMode = parser.isSet(testOption); + bool executeScriptOnly = parser.isSet(execScriptOption); + bool dryRun = parser.isSet(dryRunOption); QString const rtPath = QCoreApplication::applicationDirPath(); if (plugInDir == pluginDefault) { @@ -123,12 +133,15 @@ int main(int argc, char *argv[]) { << "does not exists, but has to contain dc-library"; exit(-1); } - qInfo() << "pwd" << "=" << rtPath; - qInfo() << "plugInDir" << "=" << plugInDir; - qInfo() << "plugInName" << "=" << plugInName; - qInfo() << "workingDir" << "=" << workingDir; - qInfo() << "maintenanceMode" << "=" << maintenanceMode; - qInfo() << "testMode" << "=" << testMode; + + qInfo() << "pwd ..............." << rtPath; + qInfo() << "plugInDir ........." << plugInDir; + qInfo() << "plugInName ........" << plugInName; + qInfo() << "workingDir ........" << workingDir; + qInfo() << "maintenanceMode ..." << maintenanceMode; + qInfo() << "testMode .........." << testMode; + qInfo() << "execScriptOnly ...." << executeScriptOnly; + qInfo() << "dryRun ............" << dryRun; // before loading the library, delete all possible shared memory segments #if defined Q_OS_LINUX || defined Q_OS_UNIX From 0ebe274e9a9a30940affab4d3f3031361a0647a3 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 10 Jul 2023 15:56:20 +0200 Subject: [PATCH 025/239] Add files copied from ATBQT application --- apism/apism_client.cpp | 750 +++++++++++++++++++++++++++++++++++++ apism/apism_client.h | 120 ++++++ apism/apism_tcp_client.cpp | 166 ++++++++ apism/apism_tcp_client.h | 62 +++ apism/ismas_data.h | 136 +++++++ 5 files changed, 1234 insertions(+) create mode 100644 apism/apism_client.cpp create mode 100644 apism/apism_client.h create mode 100644 apism/apism_tcp_client.cpp create mode 100644 apism/apism_tcp_client.h create mode 100644 apism/ismas_data.h diff --git a/apism/apism_client.cpp b/apism/apism_client.cpp new file mode 100644 index 0000000..e10bd93 --- /dev/null +++ b/apism/apism_client.cpp @@ -0,0 +1,750 @@ +#include "apism_client.h" +//#include "support/VendingData.h" +//#include "support/PersistentData.h" +//#include "support/utils.h" +//#include "ATBHMIconfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Q_DECLARE_METATYPE(QAbstractSocket::SocketError) + +ApismClient::ApismClient(QObject *eventReceiver, ATBHMIconfig *config, PersistentData *persistentData, QObject *parent) + : QObject(parent) + , healthEventReceiver(eventReceiver) + , m_config(config) + , persistentData(persistentData) + , lastError(0) + , lastErrorDescription("") + , currentRequestUid("") + , currentRequest(ISMAS::REQUEST::NO_REQUEST) +{ + this->apismTcpSendClient = new ApismTcpClient("127.0.0.1", "7777", this); + this->apismTcpRequestResponseClient = new ApismTcpClient("127.0.0.1", "7778", this); + + connect(apismTcpRequestResponseClient, &ApismTcpClient::receivedData, + this, &ApismClient::onReceivedResponse); + connect(apismTcpRequestResponseClient, &ApismTcpClient::responseTimeout, + this, &ApismClient::onRequestResponseClientResponseTimeout); + + connect(apismTcpSendClient, &ApismTcpClient::responseTimeout, + this, &ApismClient::onSendClientResponseTimeout); + + + + // not needed as APISM closes the socket after we send data, so readyRead() + // might not even fire + // connect(&m_socket, SIGNAL(readyRead()), this, SLOT(onReadyRead())); + + // defined for Qt >= 5.15, we have 5.12 + // qRegisterMetaType(); + // connect(&m_socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), + // this, SLOT(onSocketError(&QAbstractSocket::SocketError))); +} + +ApismClient::~ApismClient() { +} + + + + + +void ApismClient::restartApism() +{ + QProcess::startDetached("/bin/systemctl", {"restart", "apism"}); +} + + + + +//void ApismClient::onReadyRead() { // parse APISM response +// QByteArray data = m_socket.readAll(); +// qCritical() << "APISM-RESPONSE = (" << endl << data << endl << ")"; +//} + +void ApismClient::sendTransaction(const VendingData *vendingData) { + + +#if 0 + QScopedPointer transferData(new ISMAS::TransferData()); + + PAYMENT_VARIANTS::TYPE paymentType = vendingData->getParameter("PaymentType").value(); + + + ////////////////////// DEVICE ////////////////////////////////// + bool deviceSet = false; + //QJsonValue tariffId("TariffInfo"); + //transferData->device.insert("TARIFID", tariffId); + //QJsonValue group("group"); + //transferData->device.insert("GROUP", group); + //QJsonValue zone("zone"); + //transferData->device.insert("ZONE", zone); + if (deviceSet) { + transferData->insert("DEVICE", transferData->device); + } + + + ////////////////////// TRANSACTION ///////////////////////////// + + + bool transactionSet = false; + + if (vendingData->hasParameter("TRANSACTION_STATE")) { + QVariant tstate = vendingData->getParameter("TRANSACTION_STATE"); + if (tstate.isValid() && tstate.type() == QVariant::Int) { + transferData->transaction.state = tstate.toInt(); + transferData->transaction.insert("STATE", transferData->transaction.state); + transactionSet = true; + } + } + if (vendingData->hasParameter("TRANSACTION_UID")) { + QVariant tuid = vendingData->getParameter("TRANSACTION_UID"); + if (tuid.isValid() && tuid.type() == QVariant::String) { + transferData->transaction.uid = tuid.toString(); + transferData->transaction.insert("UID", transferData->transaction.uid); + this->persistentData->setLastTransactionUID(tuid.toString()); + transactionSet = true; + } + } + if (vendingData->hasParameter("TRANSACTION_TIMESTAMP")) { + QVariant tstamp = vendingData->getParameter("TRANSACTION_TIMESTAMP"); + if (tstamp.isValid() && tstamp.type() == QVariant::String) { + transferData->transaction.timestamp = tstamp.toString(); + transferData->transaction.insert("TIMESTAMP", transferData->transaction.timestamp); + transactionSet = true; + } + } + if (vendingData->hasParameter("TRANSACTION_TICKET_SEQUENCE_NUMBER")) { + QVariant tsn = vendingData->getParameter("TRANSACTION_TICKET_SEQUENCE_NUMBER"); + if (tsn.isValid() && tsn.type() == QVariant::Int) { + transferData->transaction.seq_tick_number = tsn.toInt(); + transferData->transaction.insert("TICKETNU", transferData->transaction.seq_tick_number); + transactionSet = true; + } + } + if (vendingData->hasParameter("LICENSEPLATE")) { + QVariant lp = vendingData->getParameter("LICENSEPLATE"); + transferData->transaction.userText = QJsonValue::fromVariant(lp); + transferData->transaction.insert("USERTEXT", transferData->transaction.userText); + transferData->transaction.userTextType = "license plate"; + transferData->transaction.insert("USERTEXTTYPE", transferData->transaction.userTextType); + transactionSet = true; + } + + if (transactionSet) { + transferData->insert("TRANSACTION", transferData->transaction); + } + + + ////////////////////// ITEM ////////////////////////////////// + bool itemSet = false; + + if (vendingData->hasParameter("PermitType")) { + QVariant idVariant = vendingData->getParameter("PermitType"); + transferData->item.id = idVariant.toString(); + transferData->item.insert("ID", transferData->item.id); + itemSet = true; + } + if (vendingData->hasParameter("Product")) { + QVariant nameVariant = vendingData->getParameter("Product"); + transferData->item.name = nameVariant.toString(); + transferData->item.insert("NAME", transferData->item.name); + itemSet = true; + } + if (vendingData->hasParameter("PRICE_INFO_GROSS")) { + int priceUint = vendingData->getUintParameter("PRICE_INFO_GROSS"); + transferData->item.price = priceUint; + transferData->item.insert("PRICE", transferData->item.price); + itemSet = true; + } + if (vendingData->hasParameter("PERIOD_START")) { + QVariant startTimeVariant = vendingData->getParameter("PERIOD_START"); + transferData->item.startTime = utils::getISODateTimeWithMsAndOffset(startTimeVariant); + transferData->item.insert("STARTTIME", transferData->item.startTime); + itemSet = true; + } + if (vendingData->hasParameter("PERIOD_END")) { + QVariant endTimeVariant = vendingData->getParameter("PERIOD_END"); + transferData->item.endTime = utils::getISODateTimeWithMsAndOffset(endTimeVariant); + transferData->item.insert("ENDTIME", transferData->item.endTime); + itemSet = true; + } + if (vendingData->hasParameter("ITEM_PRINT_TEXT")) { + QVariant textVariant = vendingData->getParameter("ITEM_PRINT_TEXT"); + transferData->item.printText = textVariant.toString(); + transferData->item.insert("PRINTTEXT", transferData->item.printText); + itemSet = true; + } + + // set static data: + + // currency + if (itemSet) { + transferData->item.currency = this->m_config->getPaymentCurrencyISOCode(); + transferData->item.insert("CURRENCY", transferData->item.currency); + } + + if (itemSet) { + transferData->insert("ITEM", transferData->item); + } + + //////////////////////////////////////////////////////////////////////// + + + ////////////////////// PAYMENT ////////////////////////////// + bool paymentSet = false; + + /////////////////////////// PAYMENT.CASH ///////////////////////////// + if (paymentType == PAYMENT_VARIANTS::TYPE::CASH) { + bool cashSet = false; + + if (vendingData->hasParameter("PaymentCashCoins")) { + QVariant coins = vendingData->getParameter("PaymentCashCoins"); + transferData->payment.cash.coins = coins.toInt(); + transferData->payment.cash.insert("COINS", transferData->payment.cash.coins); + cashSet = true; + } + if (vendingData->hasParameter("PaymentCashChange")) { + QVariant change = vendingData->getParameter("PaymentCashChange"); + transferData->payment.cash.change = change.toInt(); + transferData->payment.cash.insert("CHANGE", transferData->payment.cash.change); + cashSet = true; + } + if (vendingData->hasParameter("PaymentCashOverpaid")) { + QVariant overpaid = vendingData->getParameter("PaymentCashOverpaid"); + transferData->payment.cash.overpaid = overpaid.toInt(); + transferData->payment.cash.insert("OVERPAID", transferData->payment.cash.overpaid); + cashSet = true; + } + + // currency + if (cashSet) { + transferData->payment.cash.currency = this->m_config->getPaymentCurrencyISOCode(); + transferData->payment.cash.insert("CURRENCY", transferData->payment.cash.currency); + } + + if (cashSet) { + transferData->payment.insert("CASH", transferData->payment.cash); + paymentSet = true; + } + } + + /////////////////////////// PAYMENT.CARD ///////////////////////////// + if (paymentType == PAYMENT_VARIANTS::TYPE::CARD) { + paymentSet = true; + bool cardSet = true; + + + transferData->payment.card.insert("CARDNU", "unknown"); + + transferData->payment.card.insert("VALUE", transferData->item.price); + + transferData->payment.card.insert("CARDTYPE", "unknown"); + + + transferData->payment.card.currency = this->m_config->getPaymentCurrencyISOCode(); + transferData->payment.card.insert("CURRENCY", transferData->payment.card.currency); + + // transferData->payment.card.insert("TERMINALID", tid); + //transferData->payment.card.insert("TERMINALRESULT", tresult); + + if (cardSet) { + transferData->payment.insert("CARD", transferData->payment.card); + paymentSet = true; + } + } + + if (paymentSet) { + transferData->insert("PAYMENT", transferData->payment); + } + + //////////////////////////////////////////////////////////////////////// + + + ///////////////////////////// RESULT ///////////////////////////////// + bool resultSet = false; + + + if (vendingData->hasParameter("RESULT_DELIVERY")) { + QVariant delVariant = vendingData->getParameter("RESULT_DELIVERY"); + transferData->result.delivery = delVariant.toJsonValue(); + transferData->result.insert("DELIVERY", transferData->result.delivery); + resultSet = true; + } + if (vendingData->hasParameter("RESULT_RESULT")) { + QVariant resVariant = vendingData->getParameter("RESULT_RESULT"); + transferData->result.result = resVariant.toJsonValue(); + transferData->result.insert("RESULT", transferData->result.result); + resultSet = true; + } + if (vendingData->hasParameter("RESULT_ERROR_CODE")) { + QVariant ecVariant = vendingData->getParameter("RESULT_ERROR_CODE"); + transferData->result.errorCode = ecVariant.toJsonValue(); + transferData->result.insert("ERRORCODE", transferData->result.errorCode); + resultSet = true; + } + if (vendingData->hasParameter("RESULT_ERROR_MESSAGE")) { + QVariant emsgVariant = vendingData->getParameter("RESULT_ERROR_MESSAGE"); + transferData->result.errorMsg = emsgVariant.toJsonValue(); + transferData->result.insert("ERRORMSG", transferData->result.errorMsg); + resultSet = true; + } + + if (resultSet) { + transferData->insert("RESULT", transferData->result); + } + + //////////////////////////////////////////////////////////////////////// + + + QJsonDocument jsonDoc(*transferData); + QByteArray data = "#M=APISM#C=CMD_TRANSACTION#J="; + data += jsonDoc.toJson(QJsonDocument::Compact); + + this->apismTcpSendClient->sendData(data); +} + + +void ApismClient::sendAccount(const QHash & accountDataHash) +{ + QScopedPointer accountData(new ISMAS::AccountData()); + + accountData->coinBox.UID = QUuid::createUuid().toString(QUuid::WithoutBraces); // .mid(0, 8) + accountData->insert("UID", accountData->coinBox.UID); + + accountData->coinBox.ChangeNumber = QJsonValue(static_cast(this->persistentData->getNewCoinboxChangeNumber())); + accountData->insert("COINBOX_CHANGE_NUMBER", accountData->coinBox.ChangeNumber); + + accountData->coinBox.Process = "COINBOX_CHANGE"; + accountData->insert("PROCESS", accountData->coinBox.Process); + + accountData->insert("StartTime", utils::getISODateTimeWithMsAndOffset(persistentData->getAccountStartTime())); + accountData->insert("EndTime", utils::getCurrentISODateTimeWithMsAndOffset()); + accountData->insert("StartHash", persistentData->getFirstTransactionUID()); + accountData->insert("EndHash", persistentData->getLastTransactionUID()); + + // coins + int numberOfCoinVariants = accountDataHash["NumberOfCoinVariants"].toInt(); + for (int i=0; i < numberOfCoinVariants;++i) { + accountData->coinBox.coin.value = accountDataHash["COIN_" + QString::number(i) + "_Value"].toInt(); + accountData->coinBox.coin.numberOfCoins = accountDataHash["COIN_" + QString::number(i) + "_Quantity"].toInt(); + accountData->coinBox.coin.insert("VALUE", accountData->coinBox.coin.value); + accountData->coinBox.coin.insert("QUANTITY", accountData->coinBox.coin.numberOfCoins); + + if (accountDataHash.contains("COIN_" + QString::number(i) + "_Currency")) { + accountData->coinBox.coin.currency = accountDataHash["COIN_" + QString::number(i) + "_Currency"].toString(); + accountData->coinBox.coin.insert("CURRENCY", accountData->coinBox.coin.numberOfCoins); + } + + accountData->insert("COIN_" + QString::number(i), accountData->coinBox.coin); + } + + QJsonDocument jsonDoc(*accountData); + QByteArray data = "#M=APISM#C=CMD_CashboxChange#J="; + data += jsonDoc.toJson(QJsonDocument::Compact); + + this->apismTcpSendClient->sendData(data); + + this->persistentData->clearForNewAccount(); + + persistentData->serializeToFile(); +} + + + +void ApismClient::sendEvent(const ATBMachineEvent* machineEvent) +{ + QScopedPointer eventData(new ISMAS::EventData()); + + eventData->machineEvent.eventID = machineEvent->eventId; + eventData->insert("EVENT_ID", eventData->machineEvent.eventID); + + eventData->machineEvent.deviceName = machineEvent->deviceName; + eventData->insert("DeviceName", eventData->machineEvent.deviceName); + + eventData->machineEvent.reason = ATBMachineEvent::getEventClassString(machineEvent->machineEventClass); + eventData->insert("Reason", eventData->machineEvent.reason); + + eventData->machineEvent.event = machineEvent->eventName; + eventData->insert("Event", eventData->machineEvent.event); + + eventData->machineEvent.eventState = machineEvent->eventState; + eventData->insert("EventState", eventData->machineEvent.eventState); + + eventData->machineEvent.timeStamp = machineEvent->timestamString; + eventData->insert("Timestamp", eventData->machineEvent.timeStamp); + + eventData->machineEvent.parameter = machineEvent->parameterString; + eventData->insert("Parameter", eventData->machineEvent.parameter); + + eventData->machineEvent.secondLevelInfo = machineEvent->secondLevelInfoString; + eventData->insert("SecondLevelInfo", eventData->machineEvent.secondLevelInfo); + + + QJsonDocument jsonDoc(*eventData); + QByteArray data = "#M=APISM#C=CMD_EVENT#J="; + data += jsonDoc.toJson(QJsonDocument::Compact); + + this->apismTcpSendClient->sendData(data); + +#endif + +} + + +void ApismClient::sendState(const QString & state, const QString & msg) +{ + qCritical() << "ApismClient::sendState(): "; + qCritical() << " state: " << state; + qCritical() << " msg: " << msg; + + + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "ApismClient::sendState() invalid json msg: Parsing failed:" << parseError.error << parseError.errorString(); + return; + } + + if (!document.isObject()) { + qCritical() << "File is not JSON object!"; + return; + } + + + QScopedPointer stateData(new ISMAS::StateData()); + + QJsonObject stateObject = document.object(); + + QJsonArray statesArray; + statesArray.append(stateObject); + + // stateData->insert("TIMESTAMP", utils::getCurrentISODateTimeWithMsAndOffset()); + // stateData->insert("HW_States", statesArray); + + + QJsonDocument jsonDoc(*stateData); + QByteArray data = "#M=APISM#C=CMD_HW_STATUS#J="; + data += jsonDoc.toJson(QJsonDocument::Compact); + + this->apismTcpSendClient->sendData(data); +} + + + + + + + +void ApismClient::sendMininformStartRequest(const VendingData* vendingData) +{ + this->currentRequest = ISMAS::REQUEST::START; + + struct MininFormTransferData : public QJsonObject { + struct : public QJsonObject { + QJsonValue uid; // MUST: uuid -> vendorId + QJsonValue posid; // terminal-id + QJsonValue authCode; // approval-code + QJsonValue stan; // MUST + QJsonValue lpn; + QJsonValue transactionTime; // MUST: Zeitstempel Verkauf + QJsonValue parkingStartTime; // MUST: Startzeit + QJsonValue preAuthAmount; + QJsonValue hourlyRate; + QJsonValue vehicleCategory; + QJsonValue langCode; + QJsonValue zoneCode; + } startAction; + }; + + QScopedPointer transferData(new MininFormTransferData()); + +#if 0 + transferData->startAction.uid = vendingData->getParameter("START_UID").toJsonValue(); + this->currentRequestUid = vendingData->getParameter("START_UID").toString(); + transferData->startAction.insert("UID", transferData->startAction.uid); + + transferData->startAction.insert("POSID", vendingData->getParameter("START_POSID").toString()); + + transferData->startAction.insert("AUTHCODE", vendingData->getParameter("START_AuthCode").toString()); + + transferData->startAction.insert("STAN", vendingData->getParameter("START_STAN").toString()); + + transferData->startAction.insert("LPN", vendingData->getParameter("LICENSEPLATE").toString()); + + transferData->startAction.insert("TRANSACTIONTIME", utils::getISODateTimeWithMsAndOffset(vendingData->getParameter("START_TRANSACTIONTIME").toString())); + + transferData->startAction.insert("STARTTIME", utils::getISODateTimeWithMsAndOffset(vendingData->getParameter("START_STARTTIME").toString())); + + transferData->startAction.insert("AMOUNT", static_cast(vendingData->getUintParameter("PRICE_INFO_GROSS"))); + + transferData->startAction.insert("RATE_H", static_cast(vendingData->getUintParameter("PRICE_INFO_RATE_H"))); + + transferData->startAction.insert("VEHICLETYPE", "01"); // Fixed value + + transferData->startAction.insert("LANGUAGE", "HUN"); // Fixed value + + transferData->startAction.insert("ZONE", vendingData->getParameter("MININFORM_ZONE").toString()); + + transferData->insert("STARTACTION", transferData->startAction); + + QJsonDocument jsonDoc(*transferData); + QByteArray data = "#M=APISM#C=REQ_START#J="; // REQ_... -> use port 7778 + data += jsonDoc.toJson(QJsonDocument::Compact); + + this->apismTcpRequestResponseClient->sendData(data); +} + +void ApismClient::sendMininformStopRequest(const VendingData* vendingData) +{ + this->currentRequest = ISMAS::REQUEST::STOP; + + struct MininFormTransferData : public QJsonObject { + struct : public QJsonObject { + QJsonObject uid; // MUST: uuid + QJsonObject posid; // terminal-id + QJsonObject stan; // MUST + QJsonObject lpn; // MUST + QJsonObject stopTime; // MUST: Stop-Zeit + QJsonObject langCode; + QJsonObject deviceId; + } stopAction; + }; + + QScopedPointer transferData(new MininFormTransferData()); + + this->currentRequestUid = QUuid::createUuid().toString(QUuid::WithoutBraces).mid(0, 8); + transferData->stopAction.insert("UID", this->currentRequestUid); + + transferData->stopAction.insert("POSID", vendingData->getParameter("STOP_POSID").toString()); + + transferData->stopAction.insert("STAN", vendingData->getParameter("STOP_STAN").toString()); + + transferData->stopAction.insert("LPN", vendingData->getParameter("LICENSEPLATE").toString()); + + transferData->stopAction.insert("STOPTIME", utils::getISODateTimeWithMsAndOffset(vendingData->getParameter("STOP_STOPTIME"))); + + transferData->stopAction.insert("LANGUAGE", "HUN"); // Fixed value + + transferData->stopAction.insert("DEVICE_ID", this->m_config->getMachineNr()); + + transferData->insert("STOPACTION", transferData->stopAction); + + QJsonDocument jsonDoc(*transferData); + QByteArray data = "#M=APISM#C=REQ_STOP#J="; // REQ_ -> use port 7778 + data += jsonDoc.toJson(QJsonDocument::Compact); + + this->apismTcpRequestResponseClient->sendData(data); +#endif +} + + +void ApismClient::sendSelfTest() +{ + this->currentRequest = ISMAS::REQUEST::SELF; + + QByteArray data = "#M=APISM#C=REQ_SELF#J={}"; + + this->apismTcpRequestResponseClient->sendData(data); +} + +void ApismClient::sendMininformPingRequest() +{ + this->currentRequest = ISMAS::REQUEST::PING; + + QByteArray data = "#M=APISM#C=REQ_Ping#J={\"281\":\"PING\"}"; + + this->apismTcpRequestResponseClient->sendData(data); +} + + + +void ApismClient::onReceivedResponse(QByteArray response) +{ + // get the root object + QJsonParseError jsonParseError; + QJsonDocument responseDoc = QJsonDocument::fromJson(response, &jsonParseError); + + if (jsonParseError.error != QJsonParseError::NoError) { + qCritical() << "ApismClient::onReceivedResponse() response is no json data:"; + qCritical() << " Error: " << jsonParseError.errorString(); + this->handleISMASResponseError(); + return; + } + + QJsonObject rootObject = responseDoc.object(); + + QStringList rootObjectKeys = rootObject.keys(); + + // DEBUG + qCritical() << "ApismClient::onReceivedResponse(): objects: " << rootObjectKeys; + // results to: + // ApismClient::onReceivedResponse(): objects: ("REQ_START#60044_Response", "Response") + + + if(rootObjectKeys.indexOf(QRegularExpression("^REQ_START.*")) >= 0) { + this->private_handleMininformStartResponse(rootObject["Response"].toObject()); + } + else + if(rootObjectKeys.indexOf(QRegularExpression("^REQ_STOP.*")) >= 0) { + this->private_handleMininformStopResponse(rootObject["Response"].toObject()); + } + else + if(rootObjectKeys.indexOf(QRegularExpression("^REQ_SELF.*")) >= 0) { + this->private_handleReqSelfResponse(rootObject["Response"].toObject()); + } + else + if(rootObjectKeys.indexOf(QRegularExpression("^REQ_PING.*")) >= 0) { + this->private_handleReqPingResponse(rootObject["PING"].toObject()); + } + else { + qCritical() << "ApismClient::onReceivedResponse() for unknown Request: "; + qCritical() << " currentRequestName: " << currentRequest; + qCritical() << " rootObject.keys(): " << rootObjectKeys; + this->handleISMASResponseError(); + return; + } + + this->currentRequest = ISMAS::REQUEST::NO_REQUEST; +} + +void ApismClient::handleISMASResponseError() +{ + switch (this->currentRequest) { + case ISMAS::REQUEST::NO_REQUEST: + qCritical() << "ApismClient::onReceivedResponse() for unknown Request: " << currentRequest; + break; + case ISMAS::REQUEST::START: + emit this->sendMininformStartResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); + break; + case ISMAS::REQUEST::STOP: + emit this->sendMininformStopResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); + break; + case ISMAS::REQUEST::PING: + emit this->sendMininformPingResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); + break; + case ISMAS::REQUEST::SELF: + emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); + break; + } + this->currentRequest = ISMAS::REQUEST::NO_REQUEST; +} + +/* +{\r\n \"REQ_START#53365_Response\": {\r\n \"Aknoledge\": \"OK\"\r\n }, + \r\n \"Response\": {\r\n \"Result\": \"ERR:Ung�ltiger Datums-String: \"\r\n }\r\n} +*/ + +/* +: ISMAS received: "{\r\n \"REQ_PING#30844_Response\": {\r\n \"Aknoledge\": \"OK\"\r\n }, + "PING": { "IsAviable": "TRUE" } + }" +*/ + +void ApismClient::onSendClientResponseTimeout() +{ + +} + +void ApismClient::onRequestResponseClientResponseTimeout() +{ + switch (this->currentRequest) { + case ISMAS::REQUEST::NO_REQUEST: + qCritical() << "ApismClient::onRequestResponseClientResponseTimeout() for unknown Request: " << currentRequest; + break; + case ISMAS::REQUEST::START: + emit this->sendMininformStartResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); + break; + case ISMAS::REQUEST::STOP: + emit this->sendMininformStopResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); + break; + case ISMAS::REQUEST::PING: + emit this->sendMininformPingResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); + break; + case ISMAS::REQUEST::SELF: + emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); + break; + } + + this->currentRequest = ISMAS::REQUEST::NO_REQUEST; +} + + +void ApismClient::private_handleMininformStartResponse(QJsonObject response) +{ + emit this->sendMininformStartResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); +} + +void ApismClient::private_handleMininformStopResponse(QJsonObject response) +{ + emit this->sendMininformStopResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); +} + +void ApismClient::private_handleReqSelfResponse(QJsonObject response) +{ + emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); +} + +void ApismClient::private_handleReqPingResponse(QJsonObject response) +{ + emit this->sendMininformPingResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); +} + + + + +/************************************************************************************************ + * operators + */ +QDebug operator<< (QDebug debug, ISMAS::REQUEST request) +{ + switch(request) { + case ISMAS::REQUEST::NO_REQUEST: + debug << QString("ISMAS::REQUEST::NO_REQUEST"); + break; + case ISMAS::REQUEST::START: + debug << QString("ISMAS::REQUEST::START"); + break; + case ISMAS::REQUEST::STOP: + debug << QString("ISMAS::REQUEST::STOP"); + break; + case ISMAS::REQUEST::PING: + debug << QString("ISMAS::REQUEST::PING"); + break; + case ISMAS::REQUEST::SELF: + debug << QString("ISMAS::REQUEST::SELF"); + break; + } + return debug; +} + +QString& operator<< (QString& str, ISMAS::REQUEST request) +{ + switch(request) { + case ISMAS::REQUEST::NO_REQUEST: + str = QString("ISMAS::REQUEST::NO_REQUEST"); + break; + case ISMAS::REQUEST::START: + str = QString("ISMAS::REQUEST::START"); + break; + case ISMAS::REQUEST::STOP: + str = QString("ISMAS::REQUEST::STOP"); + break; + case ISMAS::REQUEST::PING: + str = QString("ISMAS::REQUEST::PING"); + break; + case ISMAS::REQUEST::SELF: + str = QString("ISMAS::REQUEST::SELF"); + break; + } + return str; +} + diff --git a/apism/apism_client.h b/apism/apism_client.h new file mode 100644 index 0000000..4ef3f66 --- /dev/null +++ b/apism/apism_client.h @@ -0,0 +1,120 @@ +#ifndef APISM_CLIENT_H_INCLUDED +#define APISM_CLIENT_H_INCLUDED + + +#include "ismas_data.h" +#include "apism_tcp_client.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + + +QDebug operator<<(QDebug debug, ISMAS::REQUEST request); +QString& operator<<(QString& str, ISMAS::REQUEST request); + +namespace nsApismInterface { + + enum class RESULT_STATE : quint8 { + SUCCESS = 1, + ERROR_BACKEND = 2, // error from backand (e.g. backend replies with error) + ERROR_NETWORK = 3, // error from network (e.g. host not available) + ERROR_TIMEOUT = 4, // the operation timed out + ERROR_PROCESS = 5, // internal plugin error (e.g. bug in implementation) + ERROR_RETRY = 6, // retry operation + INFO = 7 + }; + +} + + +class VendingData; +class ATBHMIconfig; +class PersistentData; +class ATBMachineEvent; +class ApismClient : public QObject { + Q_OBJECT + +public: + explicit ApismClient(QObject *eventReceiver, ATBHMIconfig *config, PersistentData *persistentData, QObject *parent = 0); + ~ApismClient(); + + quint32 getLastError(); + const QString & getLastErrorDescription(); + +public slots: + void sendSelfTest(); + void sendTransaction(const VendingData* vendingData); + void sendAccount(const QHash &accountDataHash); + void sendEvent(const ATBMachineEvent* machineEvent); + + void sendState(const QString & state, const QString & msg); + + + void sendMininformStartRequest(const VendingData* vendingData); + void sendMininformStopRequest(const VendingData* vendingData); + void sendMininformPingRequest(); + + void restartApism(); + +#ifdef USE_SZEGED_START_STOP + +#endif + +signals: + // public signals: + void sendTransactionRespones(nsApismInterface::RESULT_STATE result); + void sendAccountResponse(nsApismInterface::RESULT_STATE result); + + + void sendMininformStartResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); + void sendMininformStopResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); + void sendMininformPingResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); + void sendReqSelfResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); + + +private slots: + // void onSocketError(QAbstractSocket::SocketError socketError); + + void onReceivedResponse(QByteArray response); + + void onSendClientResponseTimeout(); + void onRequestResponseClientResponseTimeout(); + +private: + QObject *healthEventReceiver; + ATBHMIconfig *m_config; + + PersistentData *persistentData; + + ApismTcpClient* apismTcpSendClient; + ApismTcpClient* apismTcpRequestResponseClient; + + quint32 lastError; + QString lastErrorDescription; + + QString currentRequestUid; + ISMAS::REQUEST currentRequest; + + + void private_handleMininformStartResponse(QJsonObject response); + void private_handleMininformStopResponse(QJsonObject response); + void private_handlePingResponse(QJsonObject response); + void private_handleReqSelfResponse(QJsonObject response); + void private_handleReqPingResponse(QJsonObject response); + + void handleISMASResponseError(); + +}; + +// Q_DECLARE_METATYPE(QAbstractSocket::SocketError) + +#endif // APISM_CLIENT_H_INCLUDED diff --git a/apism/apism_tcp_client.cpp b/apism/apism_tcp_client.cpp new file mode 100644 index 0000000..2fde29d --- /dev/null +++ b/apism/apism_tcp_client.cpp @@ -0,0 +1,166 @@ +#include "apism_tcp_client.h" + +#include +#include + +ApismTcpClient::ApismTcpClient(const QString & hostname, + const QString & port, + QObject *parent) + : QObject(parent) + , hostname(hostname) + , port(port) + , responseTimerTimeoutCounter(0) +{ + this->responseTimeoutTimer = new QTimer(this); + this->responseTimeoutTimer->setInterval(10000); + this->responseTimeoutTimer->setSingleShot(true); + + connect(this->responseTimeoutTimer, SIGNAL(timeout()), this, SLOT(onResponseTimeoutTimerTimeout())); + + socket = new QTcpSocket(this); + connect(socket, SIGNAL(connected()), this, SLOT(onSocketConnected())); + connect(socket, SIGNAL(disconnected()), this, SLOT(onSocketDisconnected())); + connect(socket, SIGNAL(readyRead()), this, SLOT(onSocketReadyRead())); + connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(onSocketBytesWritten(qint64))); +} + + + +void ApismTcpClient::connectToHost() +{ + int portNumber = this->port.toInt(); + this->socket->connectToHost(QHostAddress(this->hostname), portNumber); +} + +void ApismTcpClient::connectToHost(const QString & hostname, const QString & port) +{ + qCritical() << "ApismTcpClient::connectToHost(" << hostname << ", " << port << ")"; + + int portNumber = port.toInt(); + socket->connectToHost(hostname, portNumber); +} + +void ApismTcpClient::closeConnection() +{ + socket->close(); +} + +bool ApismTcpClient::isConnected() +{ + bool result = false; + QAbstractSocket::SocketState socketState = socket->state(); + + switch (socketState) { + case QAbstractSocket::UnconnectedState: + /* FALLTHRU */ + case QAbstractSocket::HostLookupState: + /* FALLTHRU */ + case QAbstractSocket::ConnectingState: + result = false; + break; + case QAbstractSocket::ConnectedState: + /* FALLTHRU */ + case QAbstractSocket::BoundState: + result = true; + break; + case QAbstractSocket::ClosingState: + /* FALLTHRU */ + case QAbstractSocket::ListeningState: + result = false; + break; + } + + return result; +} + + + + +void ApismTcpClient::sendData(const QByteArray & message) +{ + //qCritical() << "ApismTcpClient::send: " << message; + + this->sendQueue.enqueue(message); + + if (this->isConnected()) { + this->private_sendData(); + + } + else { + this->connectToHost(); + } +} + +/** + * @brief ApismTcpClient::private_sendData + * + * Precondition is that queue is not empty. + */ +void ApismTcpClient::private_sendData() +{ + // take message from queue + QByteArray ba = this->sendQueue.dequeue(); + + qCritical() << "ApismTcpClient::send: " << QString(ba); + + socket->write(ba); + socket->flush(); + + // start timeoutTimer + this->responseTimeoutTimer->start(); +} + +void ApismTcpClient::onSocketConnected() +{ + qCritical() << "ApismTcpClient: Connected!"; + + if (this->sendQueue.size() > 0) { + this->private_sendData(); + } +} + +void ApismTcpClient::onSocketDisconnected() +{ + qCritical() << "ApismTcpClient: Disconnected!"; + qCritical() << " -> SocketErrorString: " << socket->errorString(); + + if (this->sendQueue.size() > 0) { + this->connectToHost(); + } +} + +void ApismTcpClient::onSocketBytesWritten(qint64 bytes) +{ + Q_UNUSED(bytes) +} + +void ApismTcpClient::onSocketReadyRead() +{ + QByteArray readData; + + // stop timeoutTimer + this->responseTimeoutTimer->stop(); + + readData = socket->readAll(); + + qCritical() << "ISMAS received: " << QString(readData); + + emit this->receivedData(readData); + + this->socket->close(); +} + + +void ApismTcpClient::onResponseTimeoutTimerTimeout() +{ + if (this->sendQueue.size() == 0) { + return; + } + emit this->responseTimeout(); + + qCritical() << "ApismTcpClient::onResponseTimeoutTimerTimeout() --> skip this message, send next command, if available."; + + // Try next command + this->sendQueue.removeFirst(); + if (this->sendQueue.size() > 0) this->private_sendData(); +} diff --git a/apism/apism_tcp_client.h b/apism/apism_tcp_client.h new file mode 100644 index 0000000..2b2de10 --- /dev/null +++ b/apism/apism_tcp_client.h @@ -0,0 +1,62 @@ +#ifndef APISMTCPCLIENT_H +#define APISMTCPCLIENT_H + +#include + +#include +#include +#include +#include + +class QTimer; + +class ApismTcpClient : public QObject +{ + Q_OBJECT +public: + explicit ApismTcpClient(const QString & hostname, const QString & port, QObject *parent = nullptr); + bool isConnected(); + + void connectToHost(); + void connectToHost(const QString & hostname, const QString & port); + + // socket is implicitely closed by APISM + void closeConnection(); + + void sendData(const QByteArray & message); + +public slots: + // socket interface + void onSocketConnected(); + void onSocketDisconnected(); + void onSocketBytesWritten(qint64 bytes); + void onSocketReadyRead(); + +signals: + void receivedData(QByteArray response); + + void responseTimeout(); + +private: + QTcpSocket *socket; + + QQueue sendQueue; + + + QString hostname; + QString port; + + QTimer *responseTimeoutTimer; + quint8 responseTimerTimeoutCounter; + + + void private_sendData(); + + +public slots: + void onResponseTimeoutTimerTimeout(); + + +}; + +#endif // APISMTCPCLIENT_H diff --git a/apism/ismas_data.h b/apism/ismas_data.h new file mode 100644 index 0000000..22d8c7f --- /dev/null +++ b/apism/ismas_data.h @@ -0,0 +1,136 @@ +#ifndef ISMASDATA_H +#define ISMASDATA_H + +#include +#include + + + + +namespace ISMAS { + + struct TransferData : public QJsonObject { + struct : public QJsonObject { + QJsonValue tariffId; + QJsonValue group; + QJsonValue zone; + } device; + + struct : public QJsonObject { + QJsonValue state; + QJsonValue uid; + QJsonValue seq_tick_number; + QJsonValue timestamp; + QJsonValue userText; + QJsonValue userTextType; + } transaction; + + struct : public QJsonObject { + // TODO: check what is really used at the moment + QJsonValue id; // unique article id + QJsonValue name; // name + QJsonValue price; // price in cent + QJsonValue currency; // + QJsonValue startTime; // start time + QJsonValue endTime; // end time + QJsonValue userText; // additional info + QJsonValue parkingTime; + QJsonValue printText; + // QJsonValue discount; + } item; + + struct : public QJsonObject { + struct : public QJsonObject { + QJsonValue coins; // total amount of coins value + // QJsonValue notes; // total amount of notes value + QJsonValue overpaid; // in cent + QJsonValue currency; + QJsonValue change; + } cash; + + struct : public QJsonObject { + QJsonValue cardNumber; + QJsonValue value; // buchungsbetrag + QJsonValue cardType; + QJsonValue currency; + QJsonValue tid; + QJsonValue tresult; + } card; + + struct : public QJsonObject { + QJsonValue cardNumber; + QJsonValue cardType; + QJsonValue value; + QJsonValue valueOld; + QJsonValue valueNew; + QJsonValue time; + QJsonValue timeOld; + QJsonValue timeNew; + } prePaidCard; + } payment; + + struct : public QJsonObject { + QJsonValue delivery; // PRINT, OnlineTicket + QJsonValue result; // SUCCESS, ERROR + QJsonValue errorCode; // 0=OK, 1=... + QJsonValue errorMsg; + } result; + }; + + + struct AccountData : public QJsonObject { + struct : public QJsonObject { + QJsonValue UID; + QJsonValue ChangeNumber; + QJsonValue Process; // Vorgang + QJsonValue startDateTime; + QJsonValue endDateTime; + QJsonValue startHash; + QJsonValue endHash; + + struct : public QJsonObject { + QJsonValue value; // coin value + QJsonValue numberOfCoins; // number of coins + QJsonValue currency; + } coin; + + } coinBox; // Münzkasse + }; + + + struct EventData : public QJsonObject { + struct : public QJsonObject { + QJsonValue eventID; + QJsonValue deviceName; + QJsonValue reason; + QJsonValue event; + QJsonValue eventState; + QJsonValue timeStamp; + QJsonValue parameter; + QJsonValue secondLevelInfo; + } machineEvent; // + }; + + struct StateData : public QJsonObject { + QJsonValue Timestamp; + QJsonArray HW_States; + struct : public QJsonObject { + QJsonValue name; + QJsonValue value; + QJsonValue unit; + } machineState; // + }; + + + enum class REQUEST : quint8 { + NO_REQUEST, + START, + STOP, + PING, + SELF + }; +} + + + +#endif // ISMASDATA_H From cbefccd2d37a7ffa98e26615be3d6bd8b518b0d2 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 10 Jul 2023 15:57:17 +0200 Subject: [PATCH 026/239] Add first commands for using AUBUpdateTool as git-client --- git/git_client.cpp | 144 +++++++++++++++++++++++++++++++++++++++++++++ git/git_client.h | 35 +++++++++++ 2 files changed, 179 insertions(+) create mode 100644 git/git_client.cpp create mode 100644 git/git_client.h diff --git a/git/git_client.cpp b/git/git_client.cpp new file mode 100644 index 0000000..17eacd9 --- /dev/null +++ b/git/git_client.cpp @@ -0,0 +1,144 @@ +#include "git_client.h" +#include "update.h" + +#include +#include +#include + +GitClient::GitClient(QString const &workingDirectory, QString const &branchName) + : m_workingDirectory(workingDirectory) + , m_branchName(branchName) { +} + +void GitClient::setWorkingDirectory(QString const &workingDirectory) { + m_workingDirectory = workingDirectory; +} + +QString GitClient::workingDirectory() const { + return m_workingDirectory; +} + +void GitClient::setBranchName(QString const &branchName) { + m_branchName = branchName; +} + +QString GitClient::branchName() const { + return m_branchName; +} + +std::optional GitClient::gitCloneRepository(QString const &repPath) { + QString gitCommand("git clone "); + gitCommand += repPath; + Command c(gitCommand); + if (c.execute(m_workingDirectory)) { + QString result = c.getCommandResult(); + if (!result.isEmpty()) { + // Cloning into 'customer_281'...\n + static QRegularExpression re("(^\\s*Cloning\\s+into\\s+[']\\s*)(.*)(\\s*['].*$)"); + QRegularExpressionMatch match = re.match(result); + if (match.hasMatch()) { + if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches + return match.captured(2); + } + } + } + } + return std::nullopt; +} + +bool GitClient::gitCheckout(QString const &branchName) { + QString gitCommand("git checkout "); + gitCommand += branchName; + Command c(gitCommand); + return c.execute(m_workingDirectory); +} + +std::optional GitClient::gitCloneBranch(QString const &repPath, + QString const &branchName) { + if (std::optional rep = gitCloneRepository(repPath)) { + QDir wd(m_workingDirectory); + if (wd.cd(rep.value())) { + m_workingDirectory = wd.absolutePath(); + if (gitCheckout(branchName)) { + return branchName; + } + } + } + return std::nullopt; +} + +std::optional GitClient::gitDiff(QString const &commits) { + // 409f198..6c22726 + QString gitCommand("git diff --compact-summary "); + gitCommand += commits; + + Command c(gitCommand); + if (c.execute(m_workingDirectory)) { + QString s = c.getCommandResult().trimmed(); + QStringList lines = Update::split(s, '\n'); + QStringList fileNames; + // each line has the format "etc/psa_config/DC2C_print01.json | 1 + + // or the format "etc/psa_config/DC2C_print01.json (new) | 1 + + // the filenames are relativ to the repository + for (int i = 0; i < lines.size(); ++i) { + int newIndex = lines.at(i).indexOf("(new)"); + if (newIndex != -1) { + QString fileName = lines.at(i).mid(0, newIndex).trimmed(); + fileNames << fileName; + } else { + int pipeIndex = lines.at(i).indexOf('|'); + if (pipeIndex != -1) { + QString fileName = lines.at(i).mid(0, pipeIndex).trimmed(); + fileNames << fileName; + } + } + } + if (!fileNames.isEmpty()) { + return fileNames; + } + } + return std::nullopt; +} + +std::optional GitClient::gitFetch() { + Command c("git fetch"); + if (c.execute(m_workingDirectory)) { + QString s = c.getCommandResult().trimmed(); + QStringList lines = Update::split(s, '\n'); + if (!lines.empty()) { + // 409f198..6c22726 zg1/zone1 -> origin/zg1/zone1 + static QRegularExpression re("(^\\s*)([0-9A-Fa-f]+..[0-9A-Fa-f]+)(.*$)"); + QRegularExpressionMatch match = re.match(lines.last()); + if (match.hasMatch()) { + if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches + return match.captured(2); + } + } + } + } + return std::nullopt; +} + +bool GitClient::gitFetchAndDiff() { + if (gitFetch()) { + QString gitCommand("git diff --compact-summary HEAD..FETCH_HEAD"); + Command c(gitCommand); + return c.execute(m_workingDirectory); + } + return false; +} + +bool GitClient::gitPull() { + Command c("git pull"); + return c.execute(m_workingDirectory); +} + +std::optional GitClient::gitMerge() { + Command c("git merge"); + if (c.execute(m_workingDirectory)) { + QString s = c.getCommandResult(); + QStringList lst = Update::split(s, '\n'); + return lst; + } + return std::nullopt; +} diff --git a/git/git_client.h b/git/git_client.h new file mode 100644 index 0000000..63f446e --- /dev/null +++ b/git/git_client.h @@ -0,0 +1,35 @@ +#ifndef GIT_CLIENT_H_INCLUDED +#define GIT_CLIENT_H_INCLUDED + + +#include + +#include "process/command.h" + + +class GitClient { + QString m_workingDirectory; + QString m_branchName; + + std::optional gitCloneRepository(QString const &repPath); + bool gitCheckout(QString const &branchName); + + public: + explicit GitClient(QString const &workingDirectory = QCoreApplication::applicationDirPath(), + QString const &branchName = "master"); + + void setWorkingDirectory(QString const &workingDirectory); + QString workingDirectory() const; + void setBranchName(QString const &branchName); + QString branchName() const; + + std::optional gitCloneBranch(QString const &repPath, QString const &branchName); + + std::optional gitFetch(); + bool gitFetchAndDiff(); + bool gitPull(); + std::optional gitDiff(QString const &commit); + std::optional gitMerge(); +}; + +#endif // GIT_CLIENT_H_INCLUDED From b979fb5b2a28a60f920661901fc4b8c956e43e66 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 10 Jul 2023 15:57:59 +0200 Subject: [PATCH 027/239] command-class as abstraction for executing git-commands. --- process/command.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++++ process/command.h | 33 ++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 process/command.cpp create mode 100644 process/command.h diff --git a/process/command.cpp b/process/command.cpp new file mode 100644 index 0000000..bfb1580 --- /dev/null +++ b/process/command.cpp @@ -0,0 +1,73 @@ +#include "command.h" + +#include +#include +#include + +Command::Command(QString const &command, int start_timeout, int finish_timeout) + : m_command(command.trimmed()) + , m_commandResult("") + , m_waitForStartTimeout(start_timeout) + , m_waitForFinishTimeout(finish_timeout) { +} + +QString Command::getCommandResult() const { + return m_commandResult; +} + +void Command::readyReadStandardOutput() { + QProcess *p = (QProcess *)sender(); + m_commandResult += p->readAllStandardOutput(); + // qCritical() << m_commandResult; +} + +void Command::readyReadStandardError() { + QProcess *p = (QProcess *)sender(); + QByteArray buf = p->readAllStandardError(); + qCritical() << buf; +} + +void Command::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { + QProcess *p = (QProcess *)sender(); + // read all remaining data sent to the process, just in case + QString d = p->readAllStandardOutput(); + if (!d.isEmpty()) { + m_commandResult += d; + } + disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardOutput())); + disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardError())); +} + +bool Command::execute(QString workingDirectory) { + QScopedPointer p(new QProcess(this)); + p->setProcessChannelMode(QProcess::MergedChannels); + + connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); + connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); + + p->setWorkingDirectory(workingDirectory); + p->start(m_command); + + if (p->waitForStarted(m_waitForStartTimeout)) { + if (p->state() == QProcess::ProcessState::Running) { + if (p->waitForFinished(m_waitForFinishTimeout)) { + if (p->exitStatus() == QProcess::NormalExit) { + qInfo() << "EXECUTED" << m_command + << "with code" << p->exitCode(); + // qInfo() << "RESULT" << m_commandResult; + return true; + } else { + qCritical() << "PROCESS" << m_command << "CRASHED with code" + << p->exitCode(); + } + } else { + qCritical() << "PROCESS" << m_command << "DID NOT FINISH"; + } + } else { + qCritical() << "WRONG PROCESS STATE" << p->state(); + } + } else { + qCritical() << "PROCESS" << m_command << "TIMEOUT AT START"; + } + return false; +} diff --git a/process/command.h b/process/command.h new file mode 100644 index 0000000..9ab0f09 --- /dev/null +++ b/process/command.h @@ -0,0 +1,33 @@ +#ifndef COMMAND_H_INCLUDED +#define COMMAND_H_INCLUDED +#endif // COMMAND_H_INCLUDED + +#include +#include +#include +#include +#include + + +class Command : public QObject { + Q_OBJECT + + QString m_command; + QString m_commandResult; + int m_waitForStartTimeout; + int m_waitForFinishTimeout; + +public: + explicit Command(QString const &command, + int start_timeout = 100000, + int finish_timeout = 100000); + + QString getCommandResult() const; + + bool execute(QString workingDirectory = QCoreApplication::applicationDirPath()); + +private slots: + void readyReadStandardOutput(); + void readyReadStandardError(); + void finished(int exitCode, QProcess::ExitStatus exitStatus); +}; From 0bdbd39632cc18c57b5de5f6f96f0812e1d13a21 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 10 Jul 2023 15:59:28 +0200 Subject: [PATCH 028/239] Added directories for apism/command/process. --- OnDemandUpdatePTU.pro | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index dcc1223..f7ffc60 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -1,6 +1,6 @@ # QT -= gui QT += core -QT += widgets serialport +QT += widgets serialport network # QT += network TARGET = ATBUpdateTool @@ -65,12 +65,21 @@ contains( CONFIG, DesktopLinux ) { SOURCES += \ main.cpp \ update.cpp \ + git/git_client.cpp \ + apism/apism_client.cpp \ + apism/apism_tcp_client.cpp \ + process/command.cpp \ message_handler.cpp \ worker.cpp \ worker_thread.cpp HEADERS += \ update.h \ + git/git_client.h \ + apism/apism_client.h \ + apism/apism_tcp_client.h \ + apism/ismas_data.h \ + process/command.h \ message_handler.h \ worker.h \ worker_thread.h \ From 31b7bd1314cf3ff367358eff3396d7b79d774174 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 10 Jul 2023 16:00:34 +0200 Subject: [PATCH 029/239] Made split() a static member function. --- update.h | 1 + 1 file changed, 1 insertion(+) diff --git a/update.h b/update.h index 7037cf9..0eec879 100644 --- a/update.h +++ b/update.h @@ -46,6 +46,7 @@ public: enum class FileTypeJson {CONFIG=1, DEVICE=2, CASH=3, SERIAL=4, TIME=5, PRINTER=6}; static hwinf *loadDCPlugin(QDir const &plugInDir, QString const &fn); + static QStringList split(QString line, QChar sep = ','); explicit Update(hwinf *hw, From bd213b8f8cd9202e743ce1ba80899c9a6fe0372a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 11 Jul 2023 11:24:23 +0200 Subject: [PATCH 030/239] start --- ismas/ismas_client.cpp | 0 ismas/ismas_client.h | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 ismas/ismas_client.cpp create mode 100644 ismas/ismas_client.h diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp new file mode 100644 index 0000000..e69de29 diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h new file mode 100644 index 0000000..e69de29 From 26db620465a33eeac124cd1a74d79d7f2cb467d0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 11 Jul 2023 16:58:49 +0200 Subject: [PATCH 031/239] use of worker/worker-thread so we can work without using buttons (as the cannot be triggered by an automatic update process) --- worker.cpp | 75 +++++++++++++++++++++++++++++++++++++++--------------- worker.h | 40 ++++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/worker.cpp b/worker.cpp index 73c0c16..c9954c2 100644 --- a/worker.cpp +++ b/worker.cpp @@ -11,16 +11,20 @@ #include "message_handler.h" #include "plugins/interfaces.h" +#include "ismas/ismas_client.h" +#include "apism/apism_client.h" - -Worker::Worker(QString update_ctrl_file, QString workingDir) - : m_update_ctrl_file(update_ctrl_file) - , m_workingDir(workingDir) - , m_workerThread("workerThread") { - +Worker::Worker(hwinf *hw, QString update_ctrl_file, QString workingDir, + bool maintenanceMode, bool testMode, bool executeScriptOnly, + bool dryRun, QObject *parent, char const *serialInterface, + char const *baudrate) + : m_workerThread("workerThread") + , m_apismClient(0, 0, 0, this) + , m_gc("/opt/app/tools/atbupdate/customer_999", "zg1/zone1", this) { this->moveToThread(&m_workerThread); + //m_apismClient.moveToThread(&m_workerThread); + m_workerThread.start(); - QThread::usleep(100000); int cnt = 0; while (!m_workerThread.isRunning()) { @@ -31,10 +35,21 @@ Worker::Worker(QString update_ctrl_file, QString workingDir) QThread::sleep(1); } - connect(this, SIGNAL(workNow()), this, SLOT(work()), Qt::QueuedConnection); - connect(&m_timer, SIGNAL(timeout()), this, SLOT(update())); + m_update = new Update(hw, update_ctrl_file, workingDir, + maintenanceMode, testMode, executeScriptOnly, dryRun, + parent, serialInterface, baudrate); + + connect(&m_apismClient, SIGNAL(ismasResponseAvailable(QJsonObject)), this, + SLOT(onIsmasResponseReceived(QJsonObject))); + + connect(this, SIGNAL(executeOpkgCommands()), this, + SLOT(onExecuteOpkgCommands()), Qt::QueuedConnection); + + //connect(this, SIGNAL(workNow()), this, SLOT(work()), Qt::QueuedConnection); + connect(&m_timer, SIGNAL(timeout()), this, SLOT(runUpdate()), Qt::QueuedConnection); m_timer.setSingleShot(true); m_timer.start(1000); + } Worker::~Worker() { @@ -48,19 +63,39 @@ Worker::~Worker() { } } } + if (m_update) { + delete m_update; + } } -void Worker::update() { - qCritical() << __func__ << ":" << __LINE__; - emit workNow(); +void Worker::onExecuteOpkgCommands() { + qCritical() << "ON EXECUTE OPKG COMMANDS"; } -void Worker::work() { - qCritical() << __func__ << ":" << __LINE__; - //Update m_update(m_update_ctrl_file, m_workingDir); - QThread::sleep(3); - //if (m_update.doUpdate()) { - //} - m_workerThread.quit(); - QApplication::quit(); +// sollte ParameterResponse heissen +void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { + + QJsonValue v = ismasResponse.value("TRG"); + if (v.type() == QJsonValue::String) { + QString s = v.toString(); + if (s == "WAIT") { + // updates available + // git anwerfen + qCritical() << "GIT ANWERFEN"; + emit m_gc.ismasUpdatesAvailable(); + } + } + + //m_workerThread.quit(); + //QApplication::quit(); +} + +void Worker::runUpdate() { + QThread::sleep(2); + + IsmasClient is; + + QString data = is.setUpdatesAvailable(); + m_apismClient.sendUpdateInfoToIsmas(data); + m_apismClient.requestAvailableIsmasUpdates(); } diff --git a/worker.h b/worker.h index ced641e..6070972 100644 --- a/worker.h +++ b/worker.h @@ -4,27 +4,55 @@ #include #include #include +#include +#include #include "worker_thread.h" +#include "update.h" +#include "git/git_client.h" +#include "ismas/ismas_client.h" +#include "apism/apism_client.h" +#ifdef PTU5 +#define SERIAL_PORT "ttymxc2" +#else +#define SERIAL_PORT "ttyUSB0" +#endif + + +class hwinf; class Worker : public QObject { Q_OBJECT - QString m_update_ctrl_file; - QString m_workingDir; WorkerThread m_workerThread; QTimer m_timer; + Update *m_update; + ApismClient m_apismClient; + GitClient m_gc; + public: - explicit Worker(QString update_ctrl_file, QString workingDir); + explicit Worker(hwinf *hw, + QString update_ctrl_file, + QString workingDir = ".", + bool maintenanceMode = false, + bool testMode = false, + bool executeScriptOnly = false, + bool dryRun = false, + QObject *parent = nullptr, + char const *serialInterface = SERIAL_PORT, + char const *baudrate = "115200"); ~Worker(); void quit() { return m_workerThread.quit(); } signals: - void workNow(); + void executeOpkgCommands(); public slots: - void work(); - void update(); + void onIsmasResponseReceived(QJsonObject ismasResponse); + void onExecuteOpkgCommands(); + +private slots: + void runUpdate(); }; #endif // WORKER_H_INCLUDED From 1309c27f7c6d98aa892ecb36e4bce93f0a0229a5 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 11 Jul 2023 16:59:49 +0200 Subject: [PATCH 032/239] Added ismas_client.h/.cpp. Added c++17-flag. --- OnDemandUpdatePTU.pro | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index f7ffc60..9cdd8b2 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -36,7 +36,7 @@ contains( CONFIG, PTU5 ) { greaterThan(QT_MAJOR_VERSION, 4): QT += serialport CONFIG += link_pkgconfig lessThan(QT_MAJOR_VERSION, 5): PKGCONFIG += qextserialport - QMAKE_CXXFLAGS += -std=c++11 # for GCC >= 4.7 + QMAKE_CXXFLAGS += -std=c++17 # for GCC >= 4.7 QMAKE_CXXFLAGS += -Wno-deprecated-copy ARCH = PTU5 DEFINES+=PTU5 @@ -55,7 +55,7 @@ contains( CONFIG, DesktopLinux ) { lessThan(QT_MAJOR_VERSION, 5): CONFIG += extserialport # QMAKE_CC = ccache $$QMAKE_CC # QMAKE_CXX = ccache $$QMAKE_CXX - QMAKE_CXXFLAGS += -std=c++11 + QMAKE_CXXFLAGS += -std=c++17 QMAKE_CXXFLAGS += -Wno-deprecated-copy linux-clang { QMAKE_CXXFLAGS += -Qunused-arguments } ARCH = DesktopLinux @@ -68,6 +68,7 @@ SOURCES += \ git/git_client.cpp \ apism/apism_client.cpp \ apism/apism_tcp_client.cpp \ + ismas/ismas_client.cpp \ process/command.cpp \ message_handler.cpp \ worker.cpp \ @@ -79,6 +80,7 @@ HEADERS += \ apism/apism_client.h \ apism/apism_tcp_client.h \ apism/ismas_data.h \ + ismas/ismas_client.h \ process/command.h \ message_handler.h \ worker.h \ From 5149a67d4b47072dfa61d766257620f1b121a1af Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 12:56:33 +0200 Subject: [PATCH 033/239] Provide the possibility to pass parameters to a command using a string-list. Has to be improved later. --- process/command.cpp | 10 ++++++++-- process/command.h | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/process/command.cpp b/process/command.cpp index bfb1580..25efb3f 100644 --- a/process/command.cpp +++ b/process/command.cpp @@ -38,15 +38,21 @@ void Command::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardError())); } -bool Command::execute(QString workingDirectory) { +bool Command::execute(QString workingDirectory, QStringList args) { QScopedPointer p(new QProcess(this)); p->setProcessChannelMode(QProcess::MergedChannels); connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); + qCritical() << "START COMMAND" << m_command << "IN" << workingDirectory; + p->setWorkingDirectory(workingDirectory); - p->start(m_command); + if (!args.isEmpty()) { + p->start(m_command, args); + } else { + p->start(m_command); + } if (p->waitForStarted(m_waitForStartTimeout)) { if (p->state() == QProcess::ProcessState::Running) { diff --git a/process/command.h b/process/command.h index 9ab0f09..6b79543 100644 --- a/process/command.h +++ b/process/command.h @@ -24,7 +24,7 @@ public: QString getCommandResult() const; - bool execute(QString workingDirectory = QCoreApplication::applicationDirPath()); + bool execute(QString workingDirectory, QStringList args = QStringList()); private slots: void readyReadStandardOutput(); From 9df425f5f8fd997716be344b311db3db0461eb4d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 12:58:23 +0200 Subject: [PATCH 034/239] Use Qt to format debug-info. Note that the full info is visible only in debug-mode. --- message_handler.cpp | 70 +++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/message_handler.cpp b/message_handler.cpp index 8b655bd..3af5ff1 100755 --- a/message_handler.cpp +++ b/message_handler.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #define OUTPUT_LEN (512) @@ -42,51 +45,43 @@ QtMessageHandler atbInstallMessageHandler(QtMessageHandler handler) { /// #if (QT_VERSION > QT_VERSION_CHECK(5, 0, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - static constexpr const char *format = "hh:mm:ss"; // static constexpr const char *format = "dd.MM.yyyy hh:mm:ss"; QByteArray localMsg = msg.toLocal8Bit(); - const char *file = context.file ? context.file : ""; - const char *function = context.function ? context.function : ""; - const char *p = std::strstr(function, "::"); - if (p) { - function = p + 2; - } - char const* output = std::strrchr(file, '/'); - if (output) { - file = output + 1; - } - qint64 const currentMSecsSinceEpoch = QDateTime::currentMSecsSinceEpoch(); - int const fractional_part = currentMSecsSinceEpoch % 1000; + QString fileName(context.file ? context.file : "N/A"); + QString function(context.function ? context.function : "N/A"); char buf[OUTPUT_LEN]{}; memset(buf, 0x00, sizeof(buf)); - QDateTime const datetime = QDateTime::fromMSecsSinceEpoch(currentMSecsSinceEpoch); + QString const datetime = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); switch (type) { case QtDebugMsg: { if (debugLevel == QtDebugMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d DEBG %s", - function, file, context.line, - datetime.time().toString(format).toStdString().c_str(), - fractional_part, + snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", + datetime.toStdString().c_str(), + function.toStdString().c_str(), + fileName.toStdString().c_str(), + context.line, localMsg.constData()); fprintf(stderr, "%s\n", buf); } } break; case QtInfoMsg: { if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d INFO %s", - function, file, context.line, - datetime.time().toString(format).toStdString().c_str(), - fractional_part, + snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", + datetime.toStdString().c_str(), + function.toStdString().c_str(), + fileName.toStdString().c_str(), + context.line, localMsg.constData()); fprintf(stderr, "%s\n", buf); } } break; case QtWarningMsg: { if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg || debugLevel == QtWarningMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d WARN %s", - function, file, context.line, - datetime.time().toString(format).toStdString().c_str(), - fractional_part, + snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", + datetime.toStdString().c_str(), + function.toStdString().c_str(), + fileName.toStdString().c_str(), + context.line, localMsg.constData()); fprintf(stderr, "%s\n", buf); } @@ -94,10 +89,11 @@ void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt case QtCriticalMsg: { if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg || debugLevel == QtWarningMsg || debugLevel == QtCriticalMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d CRIT %s", - function, file, context.line, - datetime.time().toString(format).toStdString().c_str(), - fractional_part, + snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", + datetime.toStdString().c_str(), + function.toStdString().c_str(), + fileName.toStdString().c_str(), + context.line, localMsg.constData()); fprintf(stderr, "%s\n", buf); } @@ -106,18 +102,18 @@ void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg || debugLevel == QtWarningMsg || debugLevel == QtCriticalMsg || debugLevel == QtFatalMsg) { - snprintf(buf, sizeof(buf)-1, "%30.30s (%20.20s:%04u) %s.%03d FATAL %s", - function, file, context.line, - datetime.time().toString(format).toStdString().c_str(), - fractional_part, + snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", + datetime.toStdString().c_str(), + function.toStdString().c_str(), + fileName.toStdString().c_str(), + context.line, localMsg.constData()); fprintf(stderr, "%s\n", buf); } } break; default: { - fprintf(stderr, "%*.*s.%03d No ErrorLevel defined! %s\n", OUTPUT_LEN, OUTPUT_LEN, - datetime.time().toString(format).toStdString().c_str(), fractional_part, - msg.toStdString().c_str()); + fprintf(stderr, "%s No ErrorLevel defined! %s\n", + datetime.toStdString().c_str(), msg.toStdString().c_str()); } } } From aeae9002fed775b74a7fec2c66dbdc87a1908244 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:10:55 +0200 Subject: [PATCH 035/239] Made GitClient a Qt-Object. --- git/git_client.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/git/git_client.h b/git/git_client.h index 63f446e..cdfe2e7 100644 --- a/git/git_client.h +++ b/git/git_client.h @@ -1,15 +1,21 @@ #ifndef GIT_CLIENT_H_INCLUDED #define GIT_CLIENT_H_INCLUDED - +#include #include #include "process/command.h" +class Worker; +class GitClient : public QObject { + Q_OBJECT -class GitClient { - QString m_workingDirectory; - QString m_branchName; + Worker *m_worker; + QString const m_repositoryPath; + QString const m_customerId; + QString const m_workingDirectory; + QString const m_branchName; + QString const m_customerRepository; std::optional gitCloneRepository(QString const &repPath); bool gitCheckout(QString const &branchName); From 00f5216a9fafcf0bc844c45e41f6b9ca93035de0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:12:37 +0200 Subject: [PATCH 036/239] Added utility functions. --- git/git_client.h | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/git/git_client.h b/git/git_client.h index cdfe2e7..9781bce 100644 --- a/git/git_client.h +++ b/git/git_client.h @@ -17,19 +17,28 @@ class GitClient : public QObject { QString const m_branchName; QString const m_customerRepository; - std::optional gitCloneRepository(QString const &repPath); - bool gitCheckout(QString const &branchName); + bool copyGitConfigFromMaster(); public: - explicit GitClient(QString const &workingDirectory = QCoreApplication::applicationDirPath(), - QString const &branchName = "master"); + explicit GitClient(QString const &repositoryPath, + QString const &customerId, + QString const &workingDirectory = QCoreApplication::applicationDirPath(), + QString const &branchName = "master", + QObject *parent = 0); - void setWorkingDirectory(QString const &workingDirectory); - QString workingDirectory() const; - void setBranchName(QString const &branchName); - QString branchName() const; + bool gitCloneCustomerRepository(); + bool gitCheckoutBranch(); - std::optional gitCloneBranch(QString const &repPath, QString const &branchName); + QString const workingDirectory() const { return m_workingDirectory; } + QString workingDirectory() { return m_workingDirectory; } + + QString const branchName() const { return m_branchName; } + QString branchName() { return m_branchName; } + + QString repositoryPath() { return m_repositoryPath; } + QString const repositoryPath() const { return m_repositoryPath; } + + bool gitCloneAndCheckoutBranch(); std::optional gitFetch(); bool gitFetchAndDiff(); From d91d4261d9d524d290c20f078b852a082d1b6bd8 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:13:03 +0200 Subject: [PATCH 037/239] Add another utility function and added signal/slot ismasUpdatesAvailable/onIsmasUpdatesAvailable(). --- git/git_client.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/git/git_client.h b/git/git_client.h index 9781bce..dd7d97b 100644 --- a/git/git_client.h +++ b/git/git_client.h @@ -45,6 +45,18 @@ class GitClient : public QObject { bool gitPull(); std::optional gitDiff(QString const &commit); std::optional gitMerge(); + + QString gitLastCommit(QString fileName); + QString gitBlob(QString fileName); + QString gitCommitForBlob(QString blob); + bool gitIsFileTracked(QString file2name); + +signals: + void ismasUpdatesAvailable(); + +public slots: + void onIsmasUpdatesAvailable(); + }; #endif // GIT_CLIENT_H_INCLUDED From 3c54d8de6deb75ac548eac91e1cafd1410aaab07 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:16:01 +0200 Subject: [PATCH 038/239] Made GitClient a Qt-object. Impreoved several utility functions handling git commands. Added slot onIsmasUpdatesAvailable(). Added helpers getLastCommit(), gitBlob(), gitCommitForBlob(), gitIsFileTracked(). --- git/git_client.cpp | 321 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 248 insertions(+), 73 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index 17eacd9..d71374d 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -1,36 +1,97 @@ #include "git_client.h" #include "update.h" +#include "worker.h" #include #include #include -GitClient::GitClient(QString const &workingDirectory, QString const &branchName) - : m_workingDirectory(workingDirectory) - , m_branchName(branchName) { + +GitClient::GitClient(QString const &repositoryPath, + QString const &customerId, + QString const &workingDirectory, + QString const &branchName, + QObject *parent) + : QObject(parent) + , m_worker(qobject_cast(parent)) + , m_repositoryPath(repositoryPath) + , m_customerId(customerId) + , m_workingDirectory(workingDirectory) + , m_branchName(branchName) + , m_customerRepository(QDir::cleanPath(m_workingDirectory + + QDir::separator() + + m_customerId)) { + if (!m_worker) { + qCritical() << "ERROR CASTING PARENT TO WORKER FAILED"; + } + + connect(this, SIGNAL(ismasUpdatesAvailable()), + this, SLOT(onIsmasUpdatesAvailable()), Qt::QueuedConnection); } -void GitClient::setWorkingDirectory(QString const &workingDirectory) { - m_workingDirectory = workingDirectory; +void GitClient::onIsmasUpdatesAvailable() { + if (QDir(m_customerRepository).exists()) { + qInfo() << "FETCHING OF" << m_repositoryPath + << "INTO" << m_customerRepository; + std::optional changes = gitFetch(); + if (changes) { + std::optional changedFileNames = gitDiff(changes.value()); + if (changedFileNames) { + for (int i=0;ihandleChangedFiles(changedFileNames.value()); + } else { + qCritical() << "PULL FAILED FOR" << m_repositoryPath + << "IN " << m_customerRepository; + emit m_worker->terminateUpdateProcess(); + } + } else { + qCritical() << "NO CHANGES IN" << m_repositoryPath + << "(" << m_customerRepository << ")"; + emit m_worker->finishUpdateProcess(false); + } + } else { + qCritical() << "NO CHANGES IN" << m_repositoryPath + << "(" << m_customerRepository << ")"; + emit m_worker->finishUpdateProcess(false); + } + } else { + if (gitCloneAndCheckoutBranch()) { + qInfo() << "CLONED" << m_repositoryPath + << "AND CHECKED OUT INTO" << m_customerRepository; + if (m_worker) { + qDebug() << "WORKER EXECUTE OPKG COMMANDS"; + QStringList opkgCommands; + // To make sure that opkg upgrade does not break your system + // because of an unstable connection + // Add a line "option cache cachedir" to /etc/opkg/opkg.conf to + // avoid the --cache option on command line. + opkgCommands << "opkg update"; + //opkgCommands << "opkg --cache cachedir --download-only upgrade"; + //opkgCommands << "opkg --cache cachedir upgrade"; + emit m_worker->executeOpkgCommands(opkgCommands); + } + } else { + qCritical() << "ERROR CLONING " << m_repositoryPath + << "AND/OR CHECKING OUT INTO" << m_customerRepository; + emit m_worker->terminateUpdateProcess(); + } + } } -QString GitClient::workingDirectory() const { - return m_workingDirectory; -} - -void GitClient::setBranchName(QString const &branchName) { - m_branchName = branchName; -} - -QString GitClient::branchName() const { - return m_branchName; -} - -std::optional GitClient::gitCloneRepository(QString const &repPath) { +bool GitClient::gitCloneCustomerRepository() { QString gitCommand("git clone "); - gitCommand += repPath; + gitCommand += m_repositoryPath; Command c(gitCommand); - if (c.execute(m_workingDirectory)) { + + qInfo() << "IN CURRENT WD" << m_workingDirectory + << "CLONE" << m_repositoryPath << "..."; + + if (c.execute(m_workingDirectory)) { // execute the command in wd QString result = c.getCommandResult(); if (!result.isEmpty()) { // Cloning into 'customer_281'...\n @@ -38,83 +99,128 @@ std::optional GitClient::gitCloneRepository(QString const &repPath) { QRegularExpressionMatch match = re.match(result); if (match.hasMatch()) { if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches - return match.captured(2); + if (match.captured(2).trimmed() == m_customerId) { + qInfo() << "CLONING" << m_repositoryPath << "OK"; + return true; + } } } + qCritical() << "ERROR CLONE RESULT HAS WRONG FORMAT"; } } - return std::nullopt; + return false; } -bool GitClient::gitCheckout(QString const &branchName) { - QString gitCommand("git checkout "); - gitCommand += branchName; - Command c(gitCommand); - return c.execute(m_workingDirectory); +bool GitClient::copyGitConfigFromMaster() { // only allowed when called in + // master branch + if (QDir(m_customerRepository).exists()) { + QString const cp = QString("cp .gitconfig .git/config"); + Command c("bash"); + if (c.execute(m_customerRepository, QStringList() << "-c" << cp)) { + qInfo() << "cp .gitconfig .git/config OK"; + return true; + } + qCritical() << "ERROR cp .gitconfig .git/config"; + } + return false; } -std::optional GitClient::gitCloneBranch(QString const &repPath, - QString const &branchName) { - if (std::optional rep = gitCloneRepository(repPath)) { - QDir wd(m_workingDirectory); - if (wd.cd(rep.value())) { - m_workingDirectory = wd.absolutePath(); - if (gitCheckout(branchName)) { - return branchName; - } +bool GitClient::gitCheckoutBranch() { + if (QDir(m_customerRepository).exists()) { + QString gitCommand("git checkout "); + gitCommand += m_branchName; + Command c(gitCommand); + return c.execute(m_customerRepository); // execute command in customerRepo + } + qCritical() << "ERROR" << m_customerRepository << "DOES NOT EXIST"; + return false; +} + +bool GitClient::gitCloneAndCheckoutBranch() { + qInfo() << "CLONE" << m_repositoryPath << "AND CHECKOUT" << m_branchName; + if (gitCloneCustomerRepository()) { + if (copyGitConfigFromMaster()) { + return gitCheckoutBranch(); } } - return std::nullopt; + return false; } +/* + Zu beachten: wird eine datei neu hinzugefuegt (git add/commit) dann aber gleich + wieder geloscht, so wird sie im diff nicht angezeigt. + */ std::optional GitClient::gitDiff(QString const &commits) { - // 409f198..6c22726 - QString gitCommand("git diff --compact-summary "); - gitCommand += commits; + if (QDir(m_customerRepository).exists()) { + // 409f198..6c22726 + QString gitCommand("git diff --compact-summary "); + gitCommand += commits; - Command c(gitCommand); - if (c.execute(m_workingDirectory)) { - QString s = c.getCommandResult().trimmed(); - QStringList lines = Update::split(s, '\n'); - QStringList fileNames; - // each line has the format "etc/psa_config/DC2C_print01.json | 1 + - // or the format "etc/psa_config/DC2C_print01.json (new) | 1 + - // the filenames are relativ to the repository - for (int i = 0; i < lines.size(); ++i) { - int newIndex = lines.at(i).indexOf("(new)"); - if (newIndex != -1) { - QString fileName = lines.at(i).mid(0, newIndex).trimmed(); - fileNames << fileName; - } else { - int pipeIndex = lines.at(i).indexOf('|'); - if (pipeIndex != -1) { - QString fileName = lines.at(i).mid(0, pipeIndex).trimmed(); + Command c(gitCommand); + if (c.execute(m_customerRepository)) { // execute command in local customerRepo + QString s = c.getCommandResult().trimmed(); + QStringList lines = Update::split(s, '\n'); + QStringList fileNames; + // each line has the format "etc/psa_config/DC2C_print01.json | 1 + + // or the format "etc/psa_config/DC2C_print01.json (new) | 1 + + // the filenames are relativ to the repository + for (int i = 0; i < lines.size(); ++i) { + // TODO: koennte auch (delete) kommen ? + int newIndex = lines.at(i).indexOf("(new)"); // for new files + // int goneIndex = lines.at(i).indexOf("(gone)"); // for removed files + if (newIndex != -1) { + QString fileName = lines.at(i).mid(0, newIndex).trimmed(); fileNames << fileName; + } else { + int pipeIndex = lines.at(i).indexOf('|'); + if (pipeIndex != -1) { + QString fileName = lines.at(i).mid(0, pipeIndex).trimmed(); + fileNames << fileName; + } } } - } - if (!fileNames.isEmpty()) { - return fileNames; + if (!fileNames.isEmpty()) { + return fileNames; + } } } return std::nullopt; } +/* + Hat sich nichts geaendert, so werden auch keine Commits <>..<> angezeigt + */ std::optional GitClient::gitFetch() { - Command c("git fetch"); - if (c.execute(m_workingDirectory)) { - QString s = c.getCommandResult().trimmed(); - QStringList lines = Update::split(s, '\n'); - if (!lines.empty()) { - // 409f198..6c22726 zg1/zone1 -> origin/zg1/zone1 - static QRegularExpression re("(^\\s*)([0-9A-Fa-f]+..[0-9A-Fa-f]+)(.*$)"); - QRegularExpressionMatch match = re.match(lines.last()); - if (match.hasMatch()) { - if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches - return match.captured(2); + QString const &customerRepository + = QDir::cleanPath(m_workingDirectory + QDir::separator() + m_customerId); + if (QDir(customerRepository).exists()) { + Command c("git fetch"); + if (c.execute(customerRepository)) { + QString const s = c.getCommandResult().trimmed(); + if (!s.isEmpty()) { + QStringList lines = Update::split(s, '\n'); + if (!lines.empty()) { + // 409f198..6c22726 zg1/zone1 -> origin/zg1/zone1 + static QRegularExpression re("(^\\s*)([0-9A-Fa-f]+..[0-9A-Fa-f]+)(.*$)"); + QRegularExpressionMatch match = re.match(lines.last()); + if (match.hasMatch()) { + if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches + return match.captured(2); + } else { + qCritical() << "ERROR WRONG CAPTURE COUNT FOR 'GIT FETCH'" << re.captureCount(); + } + } else { + qCritical() << "ERROR NO MATCH OF COMMITS FOR 'GIT FETCH'"; + } + } else { + qCritical() << "ERROR WRONG FORMAT FOR RESULT FOR 'GIT FETCH'" << s; } + } else { + qCritical() << "ERROR EMPTY RESULT FROM 'GIT FETCH'"; } } + } else { + qCritical() << "ERROR" << customerRepository << "DOES NOT EXIST"; } return std::nullopt; } @@ -129,8 +235,15 @@ bool GitClient::gitFetchAndDiff() { } bool GitClient::gitPull() { - Command c("git pull"); - return c.execute(m_workingDirectory); + if (QDir(m_customerRepository).exists()) { + Command c("git pull"); + if (c.execute(m_customerRepository)) { + qInfo() << "PULLED INTO" << m_customerRepository; + return true; + } + qCritical() << "PULL INTO" << m_customerRepository << "FAILED"; + } + return false; } std::optional GitClient::gitMerge() { @@ -142,3 +255,65 @@ std::optional GitClient::gitMerge() { } return std::nullopt; } + +QString GitClient::gitLastCommit(QString fileName) { + if (QDir(m_customerRepository).exists()) { + QString const filePath + = QDir::cleanPath(m_customerRepository + QDir::separator() + fileName); + QString const gitCommand = QString("git log %1 | head -n 1").arg(fileName); + Command c("bash"); + if (c.execute(m_customerRepository, QStringList() << "-c" << gitCommand)) { + QString const r = c.getCommandResult(); + int const idx = r.indexOf("commit "); + if (idx != -1) { + return r.mid(idx + 8).trimmed(); + } + } + } + return ""; +} + +// get the blob of the file(name) passed as $1 +// note: this can be used for any file in the filesystem +QString GitClient::gitBlob(QString fileName) { + if (QDir(m_customerRepository).exists()) { + QString const filePath + = QDir::cleanPath(m_customerRepository + QDir::separator() + fileName); + QString const gitCommand = QString("git hash-object %1").arg(fileName); + Command c(gitCommand); + if (c.execute(m_customerRepository)) { + return c.getCommandResult(); + } + } + return ""; +} + +QString GitClient::gitCommitForBlob(QString blob) { + if (QDir(m_customerRepository).exists()) { + QString const gitCommand + = QString("git whatchanged --all --find-object=%1 | head -n 1").arg(blob); + Command c(gitCommand); + if (c.execute(m_customerRepository)) { + return c.getCommandResult(); + } + } + return ""; +} + +bool GitClient::gitIsFileTracked(QString fName) { + if (QDir(m_customerRepository).exists()) { + QString const gitCommand + = QString("git ls-files --error-unmatch %1").arg(fName); + Command c(gitCommand); + return c.execute(m_customerRepository); + } + return false; +} + + +//get_commit_for_blob () { +// # search for the blob in all commits for the file(name) $1 +// echo $(git log --all --pretty=format:%H -- $2 | +// xargs -I{} bash -c "git ls-tree {} -- $2 | +// grep -q $1 && echo -n {} && head -n 1") +//} From 9ca758ecd38c17138a7c87bc8bd6f63180686b67 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:21:02 +0200 Subject: [PATCH 039/239] Added ISMAS_PARAMETER for ISMAS::REQUEST --- apism/ismas_data.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apism/ismas_data.h b/apism/ismas_data.h index 22d8c7f..b8597e8 100644 --- a/apism/ismas_data.h +++ b/apism/ismas_data.h @@ -127,7 +127,8 @@ namespace ISMAS { START, STOP, PING, - SELF + SELF, + ISMAS_PARAMETER }; } From cd1c92a7db6c9da68fef182b37c938921fd73228 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:24:14 +0200 Subject: [PATCH 040/239] Copied from ATBQT app. Added slots requestAvailableIsmasUpdates(), sendCmdSendVersionToIsmas() and sendUpdateInfoToIsmas(). --- apism/apism_client.cpp | 107 ++++++++++++++++++++++++++++++++--------- apism/apism_client.h | 20 ++++---- 2 files changed, 95 insertions(+), 32 deletions(-) diff --git a/apism/apism_client.cpp b/apism/apism_client.cpp index e10bd93..3d03bbd 100644 --- a/apism/apism_client.cpp +++ b/apism/apism_client.cpp @@ -34,12 +34,9 @@ ApismClient::ApismClient(QObject *eventReceiver, ATBHMIconfig *config, Persisten this, &ApismClient::onReceivedResponse); connect(apismTcpRequestResponseClient, &ApismTcpClient::responseTimeout, this, &ApismClient::onRequestResponseClientResponseTimeout); - connect(apismTcpSendClient, &ApismTcpClient::responseTimeout, this, &ApismClient::onSendClientResponseTimeout); - - // not needed as APISM closes the socket after we send data, so readyRead() // might not even fire // connect(&m_socket, SIGNAL(readyRead()), this, SLOT(onReadyRead())); @@ -53,18 +50,10 @@ ApismClient::ApismClient(QObject *eventReceiver, ATBHMIconfig *config, Persisten ApismClient::~ApismClient() { } - - - - -void ApismClient::restartApism() -{ +void ApismClient::restartApism() { QProcess::startDetached("/bin/systemctl", {"restart", "apism"}); } - - - //void ApismClient::onReadyRead() { // parse APISM response // QByteArray data = m_socket.readAll(); // qCritical() << "APISM-RESPONSE = (" << endl << data << endl << ")"; @@ -399,6 +388,58 @@ void ApismClient::sendEvent(const ATBMachineEvent* machineEvent) } +void ApismClient::requestAvailableIsmasUpdates() { + QByteArray data = "#M=APISM #C=REQ_ISMASParameter #J={}"; + this->currentRequest = ISMAS::REQUEST::ISMAS_PARAMETER; + this->apismTcpRequestResponseClient->sendData(data); +} + +void ApismClient::sendCmdSendVersionToIsmas(QString const &msg) { + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "INVALID JSON MSG: PARSING FAILED:" + << parseError.error << parseError.errorString(); + return; + } + + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + return; + } + + QByteArray data = "#M=APISM#C=CMD_SENDVERSION#J="; + data += document.toJson(QJsonDocument::Compact); + + printf("data=%s\n", QString(data).toStdString().c_str()); + + this->currentRequest = ISMAS::REQUEST::NO_REQUEST; + this->apismTcpSendClient->sendData(data); + +} + +void ApismClient::sendUpdateInfoToIsmas(QString const &msg) { + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "INVALID JSON MSG: PARSING FAILED:" + << parseError.error << parseError.errorString(); + return; + } + + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + return; + } + + QByteArray data = "#M=APISM#C=CMD_EVENT#J="; + data += document.toJson(QJsonDocument::Compact); + + printf("data=%s\n", QString(data).toStdString().c_str()); + + this->currentRequest = ISMAS::REQUEST::NO_REQUEST; + this->apismTcpSendClient->sendData(data); +} void ApismClient::sendState(const QString & state, const QString & msg) { @@ -443,7 +484,7 @@ void ApismClient::sendState(const QString & state, const QString & msg) - +#if 0 void ApismClient::sendMininformStartRequest(const VendingData* vendingData) { this->currentRequest = ISMAS::REQUEST::START; @@ -467,7 +508,6 @@ void ApismClient::sendMininformStartRequest(const VendingData* vendingData) QScopedPointer transferData(new MininFormTransferData()); -#if 0 transferData->startAction.uid = vendingData->getParameter("START_UID").toJsonValue(); this->currentRequestUid = vendingData->getParameter("START_UID").toString(); transferData->startAction.insert("UID", transferData->startAction.uid); @@ -543,19 +583,18 @@ void ApismClient::sendMininformStopRequest(const VendingData* vendingData) data += jsonDoc.toJson(QJsonDocument::Compact); this->apismTcpRequestResponseClient->sendData(data); -#endif } +#endif -void ApismClient::sendSelfTest() -{ +void ApismClient::sendSelfTest() { + qDebug() << "SENDING APISM-SELF-TEST"; this->currentRequest = ISMAS::REQUEST::SELF; - QByteArray data = "#M=APISM#C=REQ_SELF#J={}"; - this->apismTcpRequestResponseClient->sendData(data); } +#if 0 void ApismClient::sendMininformPingRequest() { this->currentRequest = ISMAS::REQUEST::PING; @@ -564,11 +603,15 @@ void ApismClient::sendMininformPingRequest() this->apismTcpRequestResponseClient->sendData(data); } +#endif +void ApismClient::onReceivedResponse(QByteArray response) { + if (this->currentRequest == ISMAS::REQUEST::NO_REQUEST && + response == "RECORD SAVED") { // sent by APISM to indicate that record + return; // has been saved in DB + } -void ApismClient::onReceivedResponse(QByteArray response) -{ // get the root object QJsonParseError jsonParseError; QJsonDocument responseDoc = QJsonDocument::fromJson(response, &jsonParseError); @@ -581,11 +624,10 @@ void ApismClient::onReceivedResponse(QByteArray response) } QJsonObject rootObject = responseDoc.object(); - QStringList rootObjectKeys = rootObject.keys(); // DEBUG - qCritical() << "ApismClient::onReceivedResponse(): objects: " << rootObjectKeys; + qDebug() << "ApismClient::onReceivedResponse(): objects: " << rootObjectKeys; // results to: // ApismClient::onReceivedResponse(): objects: ("REQ_START#60044_Response", "Response") @@ -604,6 +646,9 @@ void ApismClient::onReceivedResponse(QByteArray response) else if(rootObjectKeys.indexOf(QRegularExpression("^REQ_PING.*")) >= 0) { this->private_handleReqPingResponse(rootObject["PING"].toObject()); + } else + if(rootObjectKeys.indexOf(QRegularExpression("^REQ_ISMASPARAMETER.*")) >= 0) { + this->private_handleIsmasParameterResponse(rootObject); } else { qCritical() << "ApismClient::onReceivedResponse() for unknown Request: "; @@ -634,6 +679,10 @@ void ApismClient::handleISMASResponseError() case ISMAS::REQUEST::SELF: emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); break; + case ISMAS::REQUEST::ISMAS_PARAMETER: + // TODO + // emit + break; } this->currentRequest = ISMAS::REQUEST::NO_REQUEST; } @@ -672,6 +721,8 @@ void ApismClient::onRequestResponseClientResponseTimeout() case ISMAS::REQUEST::SELF: emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); break; + case ISMAS::REQUEST::ISMAS_PARAMETER: + break; } this->currentRequest = ISMAS::REQUEST::NO_REQUEST; @@ -698,6 +749,10 @@ void ApismClient::private_handleReqPingResponse(QJsonObject response) emit this->sendMininformPingResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); } +void ApismClient::private_handleIsmasParameterResponse(QJsonObject response) { + emit this->ismasResponseAvailable(response); +} + @@ -722,6 +777,9 @@ QDebug operator<< (QDebug debug, ISMAS::REQUEST request) case ISMAS::REQUEST::SELF: debug << QString("ISMAS::REQUEST::SELF"); break; + case ISMAS::REQUEST::ISMAS_PARAMETER: + debug << QString("ISMAS::REQUEST::ISMASPARAMETER"); + break; } return debug; } @@ -744,6 +802,9 @@ QString& operator<< (QString& str, ISMAS::REQUEST request) case ISMAS::REQUEST::SELF: str = QString("ISMAS::REQUEST::SELF"); break; + case ISMAS::REQUEST::ISMAS_PARAMETER: + str = QString("ISMAS::REQUEST::ISMASPARAMETER"); + break; } return str; } diff --git a/apism/apism_client.h b/apism/apism_client.h index 4ef3f66..85bfb34 100644 --- a/apism/apism_client.h +++ b/apism/apism_client.h @@ -53,22 +53,21 @@ public: public slots: void sendSelfTest(); void sendTransaction(const VendingData* vendingData); - void sendAccount(const QHash &accountDataHash); - void sendEvent(const ATBMachineEvent* machineEvent); + // void sendAccount(const QHash &accountDataHash); + //void sendEvent(const ATBMachineEvent* machineEvent); void sendState(const QString & state, const QString & msg); + void sendUpdateInfoToIsmas(QString const &msg); + void sendCmdSendVersionToIsmas(QString const &msg); + void requestAvailableIsmasUpdates(); - void sendMininformStartRequest(const VendingData* vendingData); - void sendMininformStopRequest(const VendingData* vendingData); - void sendMininformPingRequest(); + //void sendMininformStartRequest(const VendingData* vendingData); + //void sendMininformStopRequest(const VendingData* vendingData); + //void sendMininformPingRequest(); void restartApism(); -#ifdef USE_SZEGED_START_STOP - -#endif - signals: // public signals: void sendTransactionRespones(nsApismInterface::RESULT_STATE result); @@ -80,6 +79,8 @@ signals: void sendMininformPingResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); void sendReqSelfResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); + void ismasResponseAvailable(QJsonObject ismasResponse); + private slots: // void onSocketError(QAbstractSocket::SocketError socketError); @@ -110,6 +111,7 @@ private: void private_handlePingResponse(QJsonObject response); void private_handleReqSelfResponse(QJsonObject response); void private_handleReqPingResponse(QJsonObject response); + void private_handleIsmasParameterResponse(QJsonObject response); void handleISMASResponseError(); From 92084bed99872689b09670aca49b012a54567b64 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:27:14 +0200 Subject: [PATCH 041/239] Added call to waitForConnected() when doing a connectToHost(). Replaced some qCritical() with qDebug()-calls. --- apism/apism_tcp_client.cpp | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/apism/apism_tcp_client.cpp b/apism/apism_tcp_client.cpp index 2fde29d..e878b87 100644 --- a/apism/apism_tcp_client.cpp +++ b/apism/apism_tcp_client.cpp @@ -2,6 +2,7 @@ #include #include +#include ApismTcpClient::ApismTcpClient(const QString & hostname, const QString & port, @@ -26,10 +27,15 @@ ApismTcpClient::ApismTcpClient(const QString & hostname, -void ApismTcpClient::connectToHost() -{ +void ApismTcpClient::connectToHost() { + qCritical() << "ApismTcpClient::connectToHost(this->" << hostname << ", " << port << ")"; int portNumber = this->port.toInt(); this->socket->connectToHost(QHostAddress(this->hostname), portNumber); + if (!socket->waitForConnected(10000)) { + qCritical() << "ERROR IN WAIT FOR CONNECTED" << socket->errorString(); + } else { + qDebug() << "connected to" << hostname << ", " << port << ")"; + } } void ApismTcpClient::connectToHost(const QString & hostname, const QString & port) @@ -76,17 +82,16 @@ bool ApismTcpClient::isConnected() -void ApismTcpClient::sendData(const QByteArray & message) -{ - //qCritical() << "ApismTcpClient::send: " << message; +void ApismTcpClient::sendData(const QByteArray & message) { + qDebug() << "ApismTcpClient::send: " << message; this->sendQueue.enqueue(message); if (this->isConnected()) { + qCritical() << "ApismTcpClient::send: connected, send" << message; this->private_sendData(); - - } - else { + } else { + qCritical() << "ApismTcpClient::send: not connected, connect"; this->connectToHost(); } } @@ -101,7 +106,7 @@ void ApismTcpClient::private_sendData() // take message from queue QByteArray ba = this->sendQueue.dequeue(); - qCritical() << "ApismTcpClient::send: " << QString(ba); + qDebug() << "ApismTcpClient::send: " << QString(ba); socket->write(ba); socket->flush(); @@ -112,7 +117,7 @@ void ApismTcpClient::private_sendData() void ApismTcpClient::onSocketConnected() { - qCritical() << "ApismTcpClient: Connected!"; + qInfo() << "ApismTcpClient: Connected!"; if (this->sendQueue.size() > 0) { this->private_sendData(); @@ -121,8 +126,8 @@ void ApismTcpClient::onSocketConnected() void ApismTcpClient::onSocketDisconnected() { - qCritical() << "ApismTcpClient: Disconnected!"; - qCritical() << " -> SocketErrorString: " << socket->errorString(); + qDebug() << "ApismTcpClient: Disconnected!"; + qDebug() << " -> SocketErrorString: " << socket->errorString(); if (this->sendQueue.size() > 0) { this->connectToHost(); @@ -143,9 +148,10 @@ void ApismTcpClient::onSocketReadyRead() readData = socket->readAll(); - qCritical() << "ISMAS received: " << QString(readData); + qDebug() << "ISMAS received: " << QString(readData); emit this->receivedData(readData); + //QCoreApplication::processEvents(); this->socket->close(); } From 58bceb5d270d0ae645581b921face121edc13bad Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:28:41 +0200 Subject: [PATCH 042/239] Started ISMASClient providing the data to be sent to ISMAS. It does not send to ISMAS itself. --- ismas/ismas_client.cpp | 212 +++++++++++++++++++++++++++++++++++++++++ ismas/ismas_client.h | 46 +++++++++ 2 files changed, 258 insertions(+) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index e69de29..8f957fc 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -0,0 +1,212 @@ +#include "ismas/ismas_client.h" + +#include +#include + +#if 0 +# $1: EVENT: U0001 update finished: 100% +# U0002 reset TRG +# U0003 error +# U0010 for update process +# $2: PERCENT : "only for ISMAS: 0-100%", +# $3: RESULTCODE : "only for ISMAS", +# 0: Success +# 1: no Update nessesary +# 2: Backup failed +# 3: Package error/ Wrong package +# 4: Install Error +# $4: STEP : "running step (only for us): update_psa...", +# $5: STEP_RESULT : "error and result text", +# $6: VERSION : "opkg and conf info; what will be updated" +# +#endif + +#include +#include + +QString IsmasClient::updateNewsToIsmas(char const *event, + int percent, + int resultCode, + char const *step, + char const *step_result, + char const *version) { + char buf[1024]; + memset(buf, 0, sizeof(buf)); + + QString const ts = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + snprintf(buf, sizeof(buf)-1, + "{" + "\"REASON\":\"SW_UP\"," + "\"TIMESTAMP\":\"%s\"," + "\"EVENT_ID\":\"0\"," + "\"EVENT\":\"%s\"," + "\"EVENTSTATE\":1," + "\"PARAMETER\": {" + "\"PERCENT\" : %d," + "\"RESULTCODE\" : %d," + "\"STEP\" : \"%s\"," + "\"STEP_RESULT\" : \"%s\"," + "\"VERSION\" : \"%s\"" + "}" + "}", ts.toStdString().c_str(), event, percent, resultCode, + step, step_result, version); + return buf; +} + +QString IsmasClient::updateOfPSASendVersion(QString const &tariffVersion, + QString const &tariffProject, + int tariffZone, + QString const &tariffInfo, + QString const &tariffLoadTime, + QString const &linuxVersion, + QString const &cpuSerial, + QString const &deviceControllerVersion, + QString const &deviceControllerGitBlob, + QString const &deviceControllerGitLastCommit, + QString const &raucVersion, + QString const &opkgVersion, + QString const &atbQTVersion, + QString const &atbQTGitDescribe, + QString const &deviceControllerPluginVersion, + QString const &ingenicoISelfCCPluginVersion, + QString const &mobilisisCalculatePricePluginVersion, + QString const &mobilisisCalculatePriceConfigUiVersion, + QString const &prmCalculatePricePluginVersion, + QString const &prmCalculatePriceConfigUiPluginVersion, + QString const &tcpZVTPluginVersion) { + char buf[4096]; + memset(buf, 0, sizeof(buf)); + + QString const ts = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + QString sendVersionHash; + + // local data="#M=APISM#C=CMD_SENDVERSION#J= + snprintf(buf, sizeof(buf)-1, + "{" + "\"VERSION_INFO\" : {" + "\"CREATED\":\"%s\"," + "\"HASH\":\"%s\"" + "}," + "\"TARIFF\" : {" + "\"VERSION\" : \"%s\"," + "\"PROJECT\" : \"%s\"," + "\"ZONE\" : %d," + "\"INFO\" : \"%s\"," + "\"LOADED\" : \"%s\"" + "}," + "\"HARDWARE\" : {" + "\"DEVICES\" : [\"PTU5\", \"DC\", \"PRINTER\", \"BNA\"]" + "}," + "\"OS\" : {" + "\"Linux\": \"%s\"" + "}," + "\"CONFIG\" : {" + "\"PTU5\" : {" + "\"CPU_SERIAL\" : \"%s\"" + "}," + "\"DC\" : {" + "\"VERSION\" : \"%s\"," + "\"GITBLOB\" : \"%s\"," + "\"GITLASTCOMMIT\" : \"%s\"" + "}," + "\"PRINTER\" : {" + "}," + "\"BNA\" : {" + "}" + "}," + "\"SOFTWARE\": {" + "\"RAUC\" : \"%s\"," + "\"OPKG\" : \"%s\"," + "\"ATBQT\" : {" + "\"VERSION\" : \"%s\"," + "\"GIT_DESCRIBE\" : \"%s\"" + "}" + "}," + "\"PLUGINS\" : {" + "\"libATBDeviceControllerPlugin.so\" : {" + "\"VERSION\" : \"%s\"" + "}," + "\"libIngenicoISelf_CCPlugin.so\" : {" + "\"VERSION\" : \"%s\"" + "}," + "\"libMOBILISIS_CalculatePricePlugin.so\" : {" + "\"VERSION\" : \"%s\"" + "}," + "\"libMOBILISIS_CalculatePricePlugin_ConfigUi.so\" : {" + "\"VERSION\" : \"%s\"" + "}," + "\"libPRM_CalculatePricePlugin.so\" : {" + "\"VERSION\" : \"%s\"" + "}," + "\"libPRM_CalculatePricePlugin_ConfigUi.so\" : {" + "\"VERSION\" : \"%s\"" + "}," + "\"libTCP_ZVT_CCPlugin.so\" : {" + "\"VERSION\" : \"%s\"" + "}" + "}" + "}", + ts.toStdString().c_str(), + sendVersionHash.toStdString().c_str(), + + tariffVersion.toStdString().c_str(), + tariffProject.toStdString().c_str(), + tariffZone, + tariffInfo.toStdString().c_str(), + tariffLoadTime.toStdString().c_str(), + + linuxVersion.toStdString().c_str(), + + cpuSerial.toStdString().c_str(), + + deviceControllerVersion.toStdString().c_str(), + deviceControllerGitBlob.toStdString().c_str(), + deviceControllerGitLastCommit.toStdString().c_str(), + + raucVersion.toStdString().c_str(), + opkgVersion.toStdString().c_str(), + atbQTVersion.toStdString().c_str(), + atbQTGitDescribe.toStdString().c_str(), + + deviceControllerPluginVersion.toStdString().c_str(), + ingenicoISelfCCPluginVersion.toStdString().c_str(), + mobilisisCalculatePricePluginVersion.toStdString().c_str(), + mobilisisCalculatePriceConfigUiVersion.toStdString().c_str(), + prmCalculatePricePluginVersion.toStdString().c_str(), + prmCalculatePriceConfigUiPluginVersion.toStdString().c_str(), + tcpZVTPluginVersion.toStdString().c_str()); + + return buf; +} + + +QString IsmasClient::updateOfPSAActivated() { + return updateNewsToIsmas("U0010", + 1, + 0, + "activated", + "detected WAIT state", + "1.0.0"); +} + +QString IsmasClient::updateOfPSASucceeded() { + return updateNewsToIsmas("U0001", + 100, + 0, + "update_succeeded", + "", + "1.0.0"); +} + +QString IsmasClient::setUpdatesAvailable() { + return updateNewsToIsmas("U0099", + 10, + 0, + "set_updates_available", + "", + ""); +} + + + +bool checkForAvailableUpdates(); diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index e69de29..12946ff 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -0,0 +1,46 @@ +#ifndef ISMAS_CLIENT_H_INCLUDED +#define ISMAS_CLIENT_H_INCLUDED + +#include +#include + +class IsmasClient : public QObject { + Q_OBJECT + +public: + QString updateNewsToIsmas(char const *event, + int percent, + int resultCode, + char const *step, + char const *step_result, + char const *version); + + QString updateOfPSAActivated(); + QString updateOfPSASucceeded(); + QString updateOfPSASendVersion(QString const &tariffVersion, + QString const &tariffProject, + int tariffZone, + QString const &tariffInfo, + QString const &tariffLoadTime, + QString const &linuxVersion, + QString const &cpuSerial, + QString const &deviceControllerVersion, + QString const &deviceControllerGitBlob, + QString const &deviceControllerGitLastCommit, + QString const &raucVersion, + QString const &opkgVersion, + QString const &atbQTVersion, + QString const &atbQTGitDescribe, + QString const &deviceControllerPluginVersion, + QString const &ingenicoISelfCCPluginVersion, + QString const &mobilisisCalculatePricePluginVersion, + QString const &mobilisisCalculatePriceConfigUiVersion, + QString const &prmCalculatePricePluginVersion, + QString const &prmCalculatePriceConfigUiPluginVersion, + QString const &tcpZVTPluginVersion); + + QString setUpdatesAvailable(); + bool checkForAvailableUpdates(); +}; + +#endif // ISMAS_CLIENT_H_INCLUDED From d6446f90fe51fe797ada0bfd2772739101ed8e18 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:29:52 +0200 Subject: [PATCH 043/239] Removed C++-thread-handling, replaced it with Qt-versions. Using explicit Worker instance instead. --- main.cpp | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/main.cpp b/main.cpp index 6bb03ce..8371607 100644 --- a/main.cpp +++ b/main.cpp @@ -12,7 +12,6 @@ #include "plugins/interfaces.h" #include -#include #include #include #include @@ -23,10 +22,13 @@ #include #include "update.h" +#include "git/git_client.h" +#include "ismas/ismas_client.h" +#include "apism/apism_client.h" #include "worker_thread.h" #include "worker.h" -#include +#include #ifdef PTU5 #define SERIAL_PORT "ttymxc2" @@ -34,23 +36,13 @@ #define SERIAL_PORT "ttyUSB0" #endif -class hwinf; -static void doWork(hwinf *hw, QString update_ctrl_file, - QString workingDir, bool maintenanceMode, bool testMode) { - std::this_thread::sleep_for(std::chrono::milliseconds(2000)); - Update update(hw, update_ctrl_file, workingDir, maintenanceMode, testMode); - update.doUpdate(); - std::this_thread::sleep_for(std::chrono::milliseconds(2000)); - QCoreApplication::quit(); -} - // argv[1]: file to send to dc int main(int argc, char *argv[]) { - - QByteArray const value = qgetenv("XDG_RUNTIME_DIR"); - if (value.size() == 0) { - qputenv("XDG_RUNTIME_DIR", "/run/user/0"); + QByteArray const value = qgetenv("LC_ALL"); + if (value != "C") { + qputenv("LC_ALL", "C"); } + // qputenv("XDG_RUNTIME_DIR", "/run/user/0"); QApplication a(argc, argv); QApplication::setApplicationName("ATBUpdateTool"); @@ -58,7 +50,7 @@ int main(int argc, char *argv[]) { if (!messageHandlerInstalled()) { // change internal qt-QDebug-handling atbInstallMessageHandler(atbDebugOutput); - setDebugLevel(QtMsgType::QtDebugMsg); + setDebugLevel(QtMsgType::QtInfoMsg); //setDebugLevel(QtMsgType::QtDebugMsg); } @@ -154,10 +146,15 @@ int main(int argc, char *argv[]) { // hw->dc_autoRequest(false); QString const update_ctrl_file = "/opt/app/tools/atbupdate/update_log.csv"; - std::thread t(doWork, hw, update_ctrl_file, workingDir, maintenanceMode, testMode); + Worker worker(hw, update_ctrl_file, + "https://git.mimbach49.de/GerhardHoffmann/customer_999.git", + "customer_999", + "zg1/zone1", + workingDir, + maintenanceMode, + testMode, + executeScriptOnly, + dryRun); - int ret = a.exec(); - t.join(); - - return ret; + return a.exec(); } From f5198efab3228e8a52ec438f75573f41f7af779c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:32:00 +0200 Subject: [PATCH 044/239] Added worker/worker-thread-pair. Worker uses event-loop of worker-thread. Worker itself is used as work-horse for the update-process. --- worker.cpp | 292 +++++++++++++++++++++++++++++++++++++++++++++++------ worker.h | 50 ++++++++- 2 files changed, 308 insertions(+), 34 deletions(-) diff --git a/worker.cpp b/worker.cpp index c9954c2..762ff17 100644 --- a/worker.cpp +++ b/worker.cpp @@ -7,20 +7,33 @@ #include #include #include +#include #include +#include +#include +#include #include "message_handler.h" #include "plugins/interfaces.h" #include "ismas/ismas_client.h" #include "apism/apism_client.h" -Worker::Worker(hwinf *hw, QString update_ctrl_file, QString workingDir, - bool maintenanceMode, bool testMode, bool executeScriptOnly, - bool dryRun, QObject *parent, char const *serialInterface, - char const *baudrate) +Worker::Worker(hwinf *hw, QString update_ctrl_file, QString repositoryPath, + QString customerId, QString branchName, QString workingDirectory, bool maintenanceMode, + bool testMode, bool executeScriptOnly, bool dryRun, QObject *parent, + char const *serialInterface, char const *baudrate) : m_workerThread("workerThread") - , m_apismClient(0, 0, 0, this) - , m_gc("/opt/app/tools/atbupdate/customer_999", "zg1/zone1", this) { + , m_apismClient(0, 0, 0, this) // TODO + , m_gc(repositoryPath, customerId, workingDirectory, branchName, this) + , m_customerId(customerId) + , m_workingDirectory(workingDirectory) + , m_branchName(branchName) + , m_customerRepository(QDir::cleanPath(m_workingDirectory + + QDir::separator() + + m_customerId)) + , m_maintenanceMode(maintenanceMode) + , m_ismasUpdateRequests(ISMAS_UPDATE_REQUESTS) + , m_waitForNewUpdates(this) { this->moveToThread(&m_workerThread); //m_apismClient.moveToThread(&m_workerThread); @@ -35,21 +48,41 @@ Worker::Worker(hwinf *hw, QString update_ctrl_file, QString workingDir, QThread::sleep(1); } - m_update = new Update(hw, update_ctrl_file, workingDir, + m_update = new Update(hw, update_ctrl_file, repositoryPath, customerId, + branchName, workingDirectory, maintenanceMode, testMode, executeScriptOnly, dryRun, parent, serialInterface, baudrate); connect(&m_apismClient, SIGNAL(ismasResponseAvailable(QJsonObject)), this, SLOT(onIsmasResponseReceived(QJsonObject))); - connect(this, SIGNAL(executeOpkgCommands()), this, - SLOT(onExecuteOpkgCommands()), Qt::QueuedConnection); + connect(this, SIGNAL(executeOpkgCommands(QStringList)), this, + SLOT(onExecuteOpkgCommands(QStringList)), Qt::QueuedConnection); + connect(this, SIGNAL(executeOpkgCommand(QString)), this, + SLOT(onExecuteOpkgCommand(QString)), Qt::QueuedConnection); + connect(this, SIGNAL(summarizeRepositoryStatus()), this, + SLOT(onSummarizeRepositoryStatus()), Qt::QueuedConnection); + connect(this, SIGNAL(sendCmdSendVersionToIsmas()), this, + SLOT(onSendCmdSendVersionToIsmas()), Qt::QueuedConnection); + connect(this, SIGNAL(summarizeUpload(QStringList)), this, + SLOT(onSummarizeUpload(QStringList)), Qt::QueuedConnection); + connect(this, SIGNAL(handleChangedFiles(QStringList)), this, + SLOT(onHandleChangedFiles(QStringList)), Qt::QueuedConnection); + connect(this, SIGNAL(finishUpdateProcess(bool)), this, + SLOT(onFinishUpdateProcess(bool)), Qt::QueuedConnection); + connect(this, SIGNAL(terminateUpdateProcess()), this, + SLOT(onTerminateUpdateProcess()), Qt::QueuedConnection); //connect(this, SIGNAL(workNow()), this, SLOT(work()), Qt::QueuedConnection); - connect(&m_timer, SIGNAL(timeout()), this, SLOT(runUpdate()), Qt::QueuedConnection); - m_timer.setSingleShot(true); - m_timer.start(1000); + connect(&m_startUpdateProcess, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); + m_startUpdateProcess.setSingleShot(true); + m_startUpdateProcess.start(1000); + connect(&m_waitForNewUpdates, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); + m_waitForNewUpdates.setSingleShot(false); + + m_machineNr = 996; + m_customerNr = 281; } Worker::~Worker() { @@ -68,34 +101,233 @@ Worker::~Worker() { } } -void Worker::onExecuteOpkgCommands() { - qCritical() << "ON EXECUTE OPKG COMMANDS"; +void Worker::onHandleChangedFiles(QStringList changedFiles) { + qCritical() << QDir::currentPath() << "ON HANDLE CHANGED FILES" << changedFiles; + if (QDir(m_customerRepository).exists()) { + if (QDir::setCurrent(m_customerRepository)) { + QString const params("--recursive " + "--progress " + "--checksum " + "--exclude=.* " + "--include=*.bin " + "--include=*.json " + "--include=opkg_commands " + "--include=*.ini"); + QStringList cmds; + cmds << QString("rsync ") + params.simplified() + " etc/ /etc"; + cmds << QString("rsync ") + params.simplified() + " opt/ /opt"; + + QString cmd; + bool error = false; + foreach (cmd, cmds) { + if (!error) { + Command c("bash"); + qInfo() << "EXCUTING CMD..." << cmd; + if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { + qDebug() << c.getCommandResult(); + } else { + qCritical() << "CMD" << cmd << "FAILED"; + error = true; + } + } + } + if (!error) { + onFinishUpdateProcess(true); + return; + } + } + } + onTerminateUpdateProcess(); +} + +void Worker::onSummarizeUpload(QStringList changedFiles) { + QDateTime const c = QDateTime::currentDateTime(); + QDate const d = c.date(); + QTime const t = c.time(); + + QString uploadHistoryFile = QString("upload_history_%1%2%3T%4%5%6.txt") + .arg(d.year()).arg(d.month()).arg(d.day()) + .arg(t.hour()).arg(t.minute()).arg(t.second()); + + QFile f(uploadHistoryFile); + if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream out(&f); + QString fName; + foreach (fName, changedFiles) { + QString lastCommit = m_gc.gitLastCommit(fName); + out << fName << ":" << lastCommit << "\n"; + } + } else { + // TODO: error an ISMAS + } +} + +void Worker::onSummarizeRepositoryStatus() { + // TODO + QString dir("/opt/app/tools/atbupdate/customer_999"); + QDirIterator it(dir, QStringList() << "*.jpg", + QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + qDebug() << it.next(); + if (m_gc.gitIsFileTracked(it.next())) { + QString lastCommit = m_gc.gitLastCommit(it.next()); + } + } + + /* + QString repoStatusHistoryFile = QString("repo_status_history_%1%2%3T%4%5%6.txt") + .arg(d.year()).arg(d.month()).arg(d.day()) + .arg(t.hour()).arg(t.minute()).arg(t.second()); + if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream out(&f); + QString fName; + foreach (fName, changedFiles) { + QString lastCommit = m_gc.gitLastCommit(fName); + out << fName << ":" << lastCommit << "\n"; + } + } else { + // TODO: error an ISMAS + } + */ +} + +void Worker::onExecuteOpkgCommands(QStringList opkgCommands) { + QString opkgCommand; + foreach (opkgCommand, opkgCommands) { + emit this->executeOpkgCommand(opkgCommand); + } +} + +void Worker::onExecuteOpkgCommand(QString opkgCommand) { + Command c(opkgCommand); + if (c.execute(m_workingDirectory)) { + QString const r = c.getCommandResult(); + qDebug() << opkgCommand << ": RESULT" << r; + } } // sollte ParameterResponse heissen void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { - QJsonValue v = ismasResponse.value("TRG"); - if (v.type() == QJsonValue::String) { - QString s = v.toString(); - if (s == "WAIT") { - // updates available - // git anwerfen - qCritical() << "GIT ANWERFEN"; - emit m_gc.ismasUpdatesAvailable(); + if (!ismasResponse.isEmpty()) { + QStringList const keys = ismasResponse.keys(); + static QRegularExpression re("^REQ_ISMASPARAMETER.*"); + if(keys.indexOf(re) >= 0) { + m_waitForNewUpdates.stop(); // stop asking ISMAS for updates + + // sanity check: cust_nr and machine_nr of PSA correct ? + if (keys.contains("Dev_ID", Qt::CaseInsensitive)) { + QJsonObject const devId = ismasResponse["Dev_ID"].toObject(); + QStringList const keys = devId.keys(); + if (keys.contains("Custom_ID") && keys.contains("Device_ID")) { + QJsonValue const c = devId.value("Custom_ID"); + QJsonValue const m = devId.value("Device_ID"); + int customerNr = c.toInt(-1); + int machineNr = m.toInt(-1); + if (customerNr != m_customerNr) { + qCritical() << "CUSTOMER-NR (" << customerNr << ") !=" + << "LOCAL CUSTOMER-NR (" << m_customerNr << ")"; + m_updateStatus = UPDATE_STATUS::ERROR_BACKEND; + return; + } + if (machineNr != m_machineNr) { + qCritical() << "MACHINE-NR (" << machineNr << ") !=" + << "LOCAL MACHINE-NR (" << m_machineNr << ")"; + m_updateStatus = UPDATE_STATUS::ERROR_BACKEND; + return; + } + } + } + if (keys.contains("Fileupload", Qt::CaseInsensitive)) { + QJsonObject fileUpload = ismasResponse["Fileupload"].toObject(); + QJsonValue v = fileUpload.value("TRG"); + if (!v.isNull() && !v.isUndefined()) { + QString const s = v.toString(""); + if (s == "WAIT") { + m_ismasUpdateRequests = ISMAS_UPDATE_REQUESTS; + qCritical() << "ISMAS UPDATES AVAILABLE"; + emit m_gc.ismasUpdatesAvailable(); + } + } + } } } - - //m_workerThread.quit(); - //QApplication::quit(); } -void Worker::runUpdate() { - QThread::sleep(2); +void Worker::onFinishUpdateProcess(bool changes) { + qCritical() << "ON FINISH UPDATE PROCESS. CHANGES=" << changes; + m_workerThread.quit(); + QApplication::quit(); +} - IsmasClient is; - QString data = is.setUpdatesAvailable(); - m_apismClient.sendUpdateInfoToIsmas(data); +void Worker::onTerminateUpdateProcess() { + qCritical() << "ON TERMINATE UPDATE PROCESS"; + m_workerThread.quit(); + QApplication::quit(); +} + +void Worker::onSendCmdSendVersionToIsmas() { + + QString const tariffVersion = "0.0.1"; + QString const tariffProject = "test_project"; + int tariffZone = 1; + QString const tariffInfo = "test_tariff_info"; + QString const tariffLoadTime = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + QString const linuxVersion = "test_linux_version"; + QString const cpuSerial = "test_cpu_serial"; + QString const deviceControllerVersion = "test_dc_version"; + QString const deviceControllerGitBlob = "test_dc_blob_2a3b4f50"; + QString const deviceControllerGitLastCommit = "test_dc_commit_12345abc"; + QString const raucVersion = "test_rauc_version"; + QString const opkgVersion = "test_opkg_version"; + QString const atbQTVersion = "test_qtbqt_version"; + QString const atbQTGitDescribe = "test_atbqt_git_describe"; + QString const deviceControllerPluginVersion = "test_CAmaster_version"; + QString const ingenicoISelfCCPluginVersion = "test_ingenico_plugin_version"; + QString const mobilisisCalculatePricePluginVersion = "test_mobilisis_plugin_version"; + QString const mobilisisCalculatePriceConfigUiVersion = "test_mobilisis_config_ui_plugin"; + QString const prmCalculatePricePluginVersion = "test_prm_calculate_price_plugin"; + QString const prmCalculatePriceConfigUiPluginVersion = "test_prm_calculate_price_config_ui_plugin"; + QString const tcpZVTPluginVersion = "test_tcp_zvt_plugin"; + + QString data = m_ismasClient.updateOfPSASendVersion(tariffVersion, + tariffProject, + tariffZone, + tariffInfo, + tariffLoadTime, + linuxVersion, + cpuSerial, + deviceControllerVersion, + deviceControllerGitBlob, + deviceControllerGitLastCommit, + raucVersion, + opkgVersion, + atbQTVersion, + atbQTGitDescribe, + deviceControllerPluginVersion, + ingenicoISelfCCPluginVersion, + mobilisisCalculatePricePluginVersion, + mobilisisCalculatePriceConfigUiVersion, + prmCalculatePricePluginVersion, + prmCalculatePriceConfigUiPluginVersion, + tcpZVTPluginVersion); + m_apismClient.sendCmdSendVersionToIsmas(data); +} + +void Worker::askIsmasForNewData() { + if (m_maintenanceMode) { + QString data = m_ismasClient.setUpdatesAvailable(); + m_apismClient.sendUpdateInfoToIsmas(data); + } m_apismClient.requestAvailableIsmasUpdates(); + + if (--m_ismasUpdateRequests > 0) { + // if the timer is already running, it will be stopped and restarted. + m_waitForNewUpdates.start(10000); + } else { + qCritical() << "REQUESTING ISMAS FOR UPDATES TIMED OUT"; + m_workerThread.quit(); + QApplication::quit(); + } } diff --git a/worker.h b/worker.h index 6070972..fca8b6b 100644 --- a/worker.h +++ b/worker.h @@ -3,9 +3,11 @@ #include #include +#include #include #include #include +#include #include "worker_thread.h" #include "update.h" @@ -25,14 +27,37 @@ class Worker : public QObject { Q_OBJECT WorkerThread m_workerThread; - QTimer m_timer; + QTimer m_startUpdateProcess; Update *m_update; ApismClient m_apismClient; GitClient m_gc; + QString const m_customerId; + QString const m_workingDirectory; + QString const m_branchName; + QString const m_customerRepository; + bool m_maintenanceMode; + int m_ismasUpdateRequests; + QTimer m_waitForNewUpdates; + IsmasClient m_ismasClient; + + int m_machineNr; // setzen + int m_customerNr; + + + enum { ISMAS_UPDATE_REQUESTS = 6 }; + + enum class UPDATE_STATUS : quint8 { + STARTED, + STOPPED, + ERROR_BACKEND + } m_updateStatus; public: explicit Worker(hwinf *hw, QString update_ctrl_file, + QString repositoryPath, + QString customerId, + QString branchName, QString workingDir = ".", bool maintenanceMode = false, bool testMode = false, @@ -45,14 +70,31 @@ public: void quit() { return m_workerThread.quit(); } signals: - void executeOpkgCommands(); + void executeOpkgCommands(QStringList); + void executeOpkgCommand(QString); + void handleChangedFiles(QStringList); + void summarizeUpload(QStringList); + void summarizeRepositoryStatus(); + void sendCmdSendVersionToIsmas(); + void finishUpdateProcess(bool changes); + void terminateUpdateProcess(); public slots: void onIsmasResponseReceived(QJsonObject ismasResponse); - void onExecuteOpkgCommands(); + void onExecuteOpkgCommands(QStringList opkgCommands); + void onExecuteOpkgCommand(QString opkgCommand); private slots: - void runUpdate(); + void askIsmasForNewData(); + void onSendCmdSendVersionToIsmas(); + void onSummarizeRepositoryStatus(); + void onFinishUpdateProcess(bool changes); + void onTerminateUpdateProcess(); + void onSummarizeUpload(QStringList); + void onHandleChangedFiles(QStringList); }; +//Q_DECLARE_METATYPE((QHash)) +//Q_DECLARE_ASSOCIATIVE_CONTAINER_METATYPE(QHash) + #endif // WORKER_H_INCLUDED From 9ed51d60e415194162bdac6686bebc78d097e87d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:33:46 +0200 Subject: [PATCH 045/239] Added helper functions. --- update.h | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/update.h b/update.h index 0eec879..9db3483 100644 --- a/update.h +++ b/update.h @@ -9,6 +9,7 @@ #include #include "plugins/interfaces.h" +#include "apism/apism_client.h" #ifdef PTU5 #define SERIAL_PORT "ttymxc2" @@ -29,15 +30,20 @@ class Update : public QObject { char const *m_baudrate; QFile m_update_ctrl_file; QFile m_update_ctrl_file_copy; + QString m_repositoryPath; + QString m_customerId; // customer_281 + QString m_branchName; QString m_workingDir; bool m_maintenanceMode; bool m_testMode; + bool m_executeScriptOnly; + bool m_dryRun; + //ApismClient m_apismClient; bool m_init; bool finishUpdate(bool finish); QStringList getLinesToWorkOn(); - QStringList split(QString line, QChar sep = ','); bool execUpdateScript(); @@ -51,15 +57,29 @@ public: explicit Update(hwinf *hw, QString update_ctrl_file, + QString repositoryPath, + QString customerId, + QString branchName, QString workingDir = ".", bool maintenanceMode = false, bool testMode = false, + bool executeScriptOnly = false, + bool dryRun = false, QObject *parent = nullptr, char const *serialInterface = SERIAL_PORT, char const *baudrate = "115200"); virtual ~Update() override; bool doUpdate(); + QString customerId() { return m_customerId; } + QString const customerId() const { return m_customerId; } + + QString branchName() { return m_branchName; } + QString const branchName() const { return m_branchName; } + + QString repositoryPath() { return m_repositoryPath; } + QString const repositoryPath() const { return m_repositoryPath; } + private: static QString jsonType(enum FileTypeJson type); @@ -85,6 +105,8 @@ private: bool downloadJson(enum FileTypeJson type, int templateIdx, QString jsFileToSendToDC) const; + bool executeProcess(QString const &cmd); + private slots: void readyReadStandardOutput(); void readyReadStandardError(); From f963b61ebc87c16fc4420e8f3f4d30547c869329 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 14 Jul 2023 13:34:48 +0200 Subject: [PATCH 046/239] Added helper functions. Update will be used heavily by the Worker-class in a later stage. --- update.cpp | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/update.cpp b/update.cpp index a163e97..7cf8527 100644 --- a/update.cpp +++ b/update.cpp @@ -81,9 +81,14 @@ hwinf *Update::loadDCPlugin(QDir const &plugInDir, QString const &fname) { Update::Update(hwinf *hw, QString update_ctrl_file, + QString repositoryPath, + QString customerId, + QString branchName, QString workingDir, bool maintenanceMode, bool testMode, + bool executeScriptOnly, + bool dryRun, QObject *parent, char const *serialInterface, char const *baudrate) @@ -93,11 +98,19 @@ Update::Update(hwinf *hw, , m_baudrate(baudrate) , m_update_ctrl_file(update_ctrl_file) , m_update_ctrl_file_copy(update_ctrl_file + ".copy") + , m_repositoryPath(repositoryPath) + , m_customerId(customerId) + , m_branchName(branchName) , m_workingDir(workingDir) , m_maintenanceMode(maintenanceMode) , m_testMode(testMode) + , m_executeScriptOnly(executeScriptOnly) + , m_dryRun(dryRun) + //, m_apismClient(nullptr, nullptr, nullptr) , m_init(true) { + // m_apismClient.sendSelfTest(); + if (!m_testMode) { // make sure the files are empty if (m_update_ctrl_file.exists()) { @@ -213,7 +226,7 @@ Update::DownloadResult Update::sendNextAddress(int bNum) const { int noAnswerCount = 0; int errorCount = 0; if ( bNum==0 || bNum==1024 || bNum==2048 || bNum==3072 || bNum==4096 ) { - qDebug() << "addr-block" << bNum << "..."; + // qDebug() << "addr-block" << bNum << "..."; while (noAnswerCount <= 250) { m_hw->bl_sendAddress(bNum); QThread::msleep(100); @@ -225,7 +238,7 @@ Update::DownloadResult Update::sendNextAddress(int bNum) const { return res; } } else { // res == DownloadResult::OK - qInfo() << "addr-block" << bNum << "...OK"; + // qInfo() << "addr-block" << bNum << "...OK"; return res; } } else { @@ -285,13 +298,20 @@ Update::DownloadResult Update::dc_downloadBinary(QByteArray const &b) const { int bNum = 0; DownloadResult res = DownloadResult::OK; + fprintf(stderr, "\n64-byte block %04d ", bNum); while (res != DownloadResult::ERROR && bNum < nBlocks) { if ((res = sendNextAddress(bNum)) != DownloadResult::ERROR) { if ((res = sendNextDataBlock(ba, bNum)) != DownloadResult::ERROR) { bNum += 1; + fprintf(stderr, "."); + if ((bNum % 80) == 0) { + fprintf(stderr, "\n64-byte block %04d ", bNum); + } } } } + fprintf(stderr, "\nlast 64-byte block %04d\n", bNum); + int const rest = ba.size() % 64; int const offset = ba.size() - rest; char const *startAddress = ba.constData() + offset; @@ -619,8 +639,15 @@ void Update::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardError())); } +// bool Update::executeProcess(QString const &cmd); + bool Update::doUpdate() { + // + // ACHTUNG !!! + // + return true; + /* The file referred to by 'update_data' has the following structure for each line: @@ -643,6 +670,10 @@ bool Update::doUpdate() { return false; } + if (m_executeScriptOnly) { // basically a test flag for executing only the + return true; // update script. + } + bool serialOpened = false; bool serialOpen = false; @@ -709,10 +740,11 @@ bool Update::doUpdate() { if (!linkTarget.exists()) { // check for broken link res = false; } else { - if (fwVersion.startsWith(linkTarget.completeBaseName())) { - qCritical() << "current dc-firmware-version" << fwVersion - << "already installed"; - res = false; + if (false) { + //if (fwVersion.startsWith(linkTarget.completeBaseName())) { + // qCritical() << "current dc-firmware-version" << fwVersion + // << "already installed"; + // res = false; } else { res = true; From 077eb803a16b3c4aa3439912b71352dda2539eda Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 16 Jul 2023 18:31:15 +0200 Subject: [PATCH 047/239] plan fuer das update --- allgemein.txt | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 allgemein.txt diff --git a/allgemein.txt b/allgemein.txt new file mode 100644 index 0000000..6b2561e --- /dev/null +++ b/allgemein.txt @@ -0,0 +1,126 @@ +0: + Zunaechst wir immer nachgesehen, ob das Repository bereits geklont wurde. + Wenn nein, dann wird geklont. Im Master-Branch befinden sich Dateinen, die + in allen anderen Branches gemeinsam genutzt werden. Das sollte wohl auf + jeden Fall fuer den DeviceController gelten. +1: + Anfrage bei ISMAS ob neue Daten da sind. Hier faengt die ganze Sache an. + Da man nicht weiss, on ISMAS ueberhaupt antworten wird braucht es einen + Timer. Man kann die Sache dann auch gleich mehrfach versuchen. + + Die Anfrage geht also per Signal an den Apism-Client. Im Signal wird der + Zustand mitgegeben: + + ISMAS_UPDATE_REQUEST_PENDING + + und im Slot dann entsprechend uebernommen. Der Timer sollte nicht im Worker + stehen, sondern im Apism-Client. Der Worker sollte damit nicht belastet + sein. Der wird dann vom Apism-Client entsprechend benachrichtigt. + + ISMAS_UPDATE_REQUEST_FAILURE + ISMAS_UPDATE_REQUEST_TIMEOUT + ISMAS_UPDATE_REQUEST_SUCCESS + + Im 1./2.Fall wird dann ein Fehlerhandling durchgefuehrt. Insbesondere + wird Apism ueber den DB-Kanal darueber informiert (U0003). Am Ende wie + dann wie immer SendCmdVersion und beenden der Applikation. + + Im Erfolgsfall muss in der Antwort der Branch enthalten sein, fuer den das + Update durchgefuhrt werden soll. Das ist auch bereits so (Location). + + Ein Sonderfall waere der -m-Modus, da hier niemand auf der ISMAS-Seite + aktiv eingreift. Hier braucht es dann einen Ubergabeparameter an + ATBUpdateTool: --branch-name + + Normalfall ist aber die Aktivierung via ISMAS. Dann muss der in der Antwort + enthaltene Branch zunaechst ausgecheckt werden. +2: + Dazu wird ein entsprechendes Signal an den GitClient gesandet. + + GIT_CHECKOUT_BRANCH_REQUEST + + Der GitClient sieht jetzt erstmal nach, ob es diesen Branch ueberhaupt + gibt. + + Falls nein: + + GIT_CHECKOUT_BRANCH_REQUEST_FAILURE + BRANCH_NOT_EXIST + + Falls ja: + der GitClient versucht nun den Branch auszuchecken. + Geht das gut? + Falls nein: + + GIT_CHECKOUT_BRANCH_REQUEST_FAILURE + BRANCH_CHECKOUT_ERROR + + Falls ja: + GIT_CHECKOUT_BRANCH_REQUEST_SUCCESS + -> eventl. koennte man den Namen des Branches mitgeben + + + Signal an den Worker. Im entsprechenden Slot wird jetzt der eigentliche + UpdateProzess angeworfen. + 3: + Mittels git fetch werden jetzt die Aenderungen ermittelt. + + GIT_FETCH_UPDATES_REQUEST + + GIT_FETCH_UPDATES_REQUEST_FAILURE + -> Worker informieren damit Fehlerbehandlung aktiviert wird + GIT_FETCH_UPDATES_REQUEST_SUCCESS + + GIT_DIFF_UPDATES + + Die Liste der geaenderten Dateien geht an den Worker. Der kann jetzt + noch Test machen, ob das Sinn macht. Evtl. eine Statusmeldung ausgeben + drueber was gleich passieren wird. +4: + Mittels git merge/pull werden nun die Dateien tatsaechlich im Branch + aktualisiert. + + GIT_PULL_UPDATES_REQUEST + GIT_PULL_UPDATES_REQUEST_FAILURE + -> Worker informieren damit Fehlerbehandlung aktiviert wird + GIT_PULL_UPDATES_REQUEST_SUCCESS + -> Worker informieren + + Sind die Daten vorhanden, dann werden sie mittels rsync ins Dateisystem + kopiert. + + RSYNC_UPDATES + RSYNC_UPDATES_FAILURE + RSYNC_UPDATES_SUCCESS + + Fuer jede kopierte Datei geht eine Nachricht raus ans ISMAS. +5: + Sind die Daten dann kopiert, so werden sie auf den DC kopiert (falls + notwendig): hier gehen dann wieder Nachrichten an ISMAS raus: + + DC_UPDATE + DC_UPDATE_FAILURE + DC_UPDATE_SUCCESS + + JSON_UPDATE + JSON_UPDATE_FAILURE + JSON_UPDATE_SUCCESS + + Hier fuer jedes Json-File. + + Schliesslich noch eine Zusammenfasung an ISMAS: + + ISMAS_UPDATE_INFO_CONFIRM + ISMAS_UPDATE_INFO_CONFIRM_FAILURE + -> Worker informieren damit Fehlerbehandlung aktiviert wird + ISMAS_UPDATE_INFO_CONFIRM_SUCCESS + + und schliesslich der abschliessende Status an ISMAS zurueckgemeldet: + + ISMAS_CURRENT_PSA_STATUS_CONFIRM + ISMAS_CURRENT_PSA_STATUS_CONFIRM_FAILURE + ISMAS_CURRENT_PSA_STATUS_CONFIRM_SUCCESS + + + + From 7054c181135c9560a4fc728d786b1803c9f25363 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:38:53 +0200 Subject: [PATCH 048/239] Extended UPDATE_STATUS enum. Simplified interface. Read machine_nr, cust_nr, zone_nr from file. --- worker.h | 72 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/worker.h b/worker.h index fca8b6b..d86a338 100644 --- a/worker.h +++ b/worker.h @@ -22,6 +22,43 @@ #endif +enum class UPDATE_STATUS : quint8 { + ISMAS_EMULATE_DATA_AVAILABLE, + ISMAS_UPDATE_REQUEST_PENDING, + ISMAS_UPDATE_REQUEST_FAILURE, + ISMAS_UPDATE_REQUEST_TIMEOUT, + ISMAS_UPDATE_REQUEST_SUCCESS, + GIT_CHECKOUT_BRANCH_REQUEST, + GIT_CHECKOUT_BRANCH_REQUEST_FAILURE, + GIT_CHECKOUT_BRANCH_NOT_EXISTS, + GIT_CHECKOUT_BRANCH_CHECKOUT_ERROR, + GIT_FETCH_UPDATES_REQUEST, + GIT_FETCH_UPDATES_REQUEST_FAILURE, + GIT_FETCH_UPDATES_REQUEST_SSUCCESS, + EXEC_OPKG_COMMANDS_REQUEST, + EXEC_OPKG_COMMANDS_FAILURE, + EXEC_OPKG_COMMAND_FAILURE, + EXEC_OPKG_COMMAND_SUCCESS, + EXEC_OPKG_COMMANDS_SUCCESS, + RSYNC_UPDATES, + RSYNC_UPDATES_FAILURE, + RSYNC_UPDATES_SUCESS, + DEVICE_CONTROLLER_UPDATE, + DEVICE_CONTROLLER_UPDATE_FAILURE, + DEVICE_CONTROLLER_UPDATE_SUCCESS, + JSON_UPDATE, + JSON_UPDATE_FAILURE, + JSON_UPDATE_SUCCESS, + ISMAS_UPDATE_INFO_CONFIRM, + ISMAS_UPDATE_INFO_CONFIRM_FAILURE, + ISMAS_UPDATE_INFO_CONFIRM_SUCCESS, + ISMAS_CURRENT_PSA_STATUS_CONFIRM, + ISMAS_CURRENT_PSA_STATUS_CONFIRM_FAILURE, + ISMAS_CURRENT_PSA_STATUS_CONFIRM_SUCCESS +}; + +#define ISMAS_UPDATE_REQUESTS (6) + class hwinf; class Worker : public QObject { Q_OBJECT @@ -30,38 +67,35 @@ class Worker : public QObject { QTimer m_startUpdateProcess; Update *m_update; ApismClient m_apismClient; - GitClient m_gc; - QString const m_customerId; + int const m_customerNr; + QString const m_customerNrStr; + int const m_machineNr; + int const m_zoneNr; QString const m_workingDirectory; QString const m_branchName; QString const m_customerRepository; + GitClient m_gc; bool m_maintenanceMode; + QString const m_osVersion; int m_ismasUpdateRequests; QTimer m_waitForNewUpdates; IsmasClient m_ismasClient; - int m_machineNr; // setzen - int m_customerNr; + UPDATE_STATUS m_updateStatus; + QString m_statusDescription; - enum { ISMAS_UPDATE_REQUESTS = 6 }; - - enum class UPDATE_STATUS : quint8 { - STARTED, - STOPPED, - ERROR_BACKEND - } m_updateStatus; + void executeOpkgCommand(QString opkgCommand); + QString getOsVersion() const; public: explicit Worker(hwinf *hw, - QString update_ctrl_file, - QString repositoryPath, - QString customerId, + int customerNr, // 281 + int machineNr, + int zoneNr, QString branchName, QString workingDir = ".", bool maintenanceMode = false, - bool testMode = false, - bool executeScriptOnly = false, bool dryRun = false, QObject *parent = nullptr, char const *serialInterface = SERIAL_PORT, @@ -69,9 +103,9 @@ public: ~Worker(); void quit() { return m_workerThread.quit(); } + static int read1stLineOfFile(QString fileName); + signals: - void executeOpkgCommands(QStringList); - void executeOpkgCommand(QString); void handleChangedFiles(QStringList); void summarizeUpload(QStringList); void summarizeRepositoryStatus(); @@ -81,8 +115,6 @@ signals: public slots: void onIsmasResponseReceived(QJsonObject ismasResponse); - void onExecuteOpkgCommands(QStringList opkgCommands); - void onExecuteOpkgCommand(QString opkgCommand); private slots: void askIsmasForNewData(); From c503750e903cfc6b8fa2c8e4e3281408a511feaa Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:43:05 +0200 Subject: [PATCH 049/239] Simplified interface of ctor. onHandleChangedFiles(): split handling of opkg_commands-file and downloading of DC/JSON-Files. rsync to file-system only once these operations were successful. --- worker.cpp | 254 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 174 insertions(+), 80 deletions(-) diff --git a/worker.cpp b/worker.cpp index 762ff17..049795b 100644 --- a/worker.cpp +++ b/worker.cpp @@ -18,25 +18,67 @@ #include "ismas/ismas_client.h" #include "apism/apism_client.h" -Worker::Worker(hwinf *hw, QString update_ctrl_file, QString repositoryPath, - QString customerId, QString branchName, QString workingDirectory, bool maintenanceMode, - bool testMode, bool executeScriptOnly, bool dryRun, QObject *parent, - char const *serialInterface, char const *baudrate) +int Worker::read1stLineOfFile(QString fileName) { + QFile f(fileName); + if (f.exists()) { + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream in(&f); + in.setCodec("UTF-8"); + while(!in.atEnd()) { + return in.readLine().toInt(); + } + } + } + return -1; +} + +Worker::Worker(hwinf *hw, + int customerNr, + int machineNr, + int zoneNr, + QString branchName, + QString workingDirectory, + bool maintenanceMode, + bool dryRun, + QObject *parent, + char const *serialInterface, + char const *baudrate) : m_workerThread("workerThread") , m_apismClient(0, 0, 0, this) // TODO - , m_gc(repositoryPath, customerId, workingDirectory, branchName, this) - , m_customerId(customerId) + , m_customerNr(customerNr) + , m_customerNrStr(QString("customer_") + QString::number(m_customerNr).rightJustified(3, '0')) + , m_machineNr(machineNr) + , m_zoneNr(zoneNr) , m_workingDirectory(workingDirectory) , m_branchName(branchName) - , m_customerRepository(QDir::cleanPath(m_workingDirectory - + QDir::separator() - + m_customerId)) + , m_customerRepository(QDir::cleanPath(m_workingDirectory + QDir::separator() + m_customerNrStr)) + , m_gc(m_customerNrStr, m_customerRepository, m_workingDirectory, m_branchName, this) , m_maintenanceMode(maintenanceMode) + , m_osVersion(getOsVersion()) , m_ismasUpdateRequests(ISMAS_UPDATE_REQUESTS) , m_waitForNewUpdates(this) { - this->moveToThread(&m_workerThread); - //m_apismClient.moveToThread(&m_workerThread); + QDir::setCurrent(m_workingDirectory); + + qInfo() << "CURRENT TIME ..............." << QDateTime::currentDateTime().toString(Qt::ISODate); + qInfo() << "OS VERSION ................." << m_osVersion; + qInfo() << "CUSTOMER_NR ................" << m_customerNr; + qInfo() << "CUSTOMER_NR_STR ............" << m_customerNrStr; + qInfo() << "CUSTOMER_REPOSITORY_PATH ..." << QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(m_customerNrStr); + qInfo() << "CUSTOMER_REPOSITORY ........" << m_customerRepository; + qInfo() << "MACHINE_NR ................." << m_machineNr; + qInfo() << "ZONE_NR ...................." << m_zoneNr; + qInfo() << "BRANCH_NAME ................" << m_branchName; + qInfo() << "WORKING_DIRECTORY .........." << m_workingDirectory; + + QProcess p; + p.start("/bin/systemctl", {"restart", "apism"}); + if (!p.waitForStarted(5000) || !p.waitForFinished(5000)) { + qCritical() << "APISM-RESTART-FAILURE"; + return; + } + + this->moveToThread(&m_workerThread); m_workerThread.start(); int cnt = 0; @@ -48,18 +90,8 @@ Worker::Worker(hwinf *hw, QString update_ctrl_file, QString repositoryPath, QThread::sleep(1); } - m_update = new Update(hw, update_ctrl_file, repositoryPath, customerId, - branchName, workingDirectory, - maintenanceMode, testMode, executeScriptOnly, dryRun, - parent, serialInterface, baudrate); - connect(&m_apismClient, SIGNAL(ismasResponseAvailable(QJsonObject)), this, SLOT(onIsmasResponseReceived(QJsonObject))); - - connect(this, SIGNAL(executeOpkgCommands(QStringList)), this, - SLOT(onExecuteOpkgCommands(QStringList)), Qt::QueuedConnection); - connect(this, SIGNAL(executeOpkgCommand(QString)), this, - SLOT(onExecuteOpkgCommand(QString)), Qt::QueuedConnection); connect(this, SIGNAL(summarizeRepositoryStatus()), this, SLOT(onSummarizeRepositoryStatus()), Qt::QueuedConnection); connect(this, SIGNAL(sendCmdSendVersionToIsmas()), this, @@ -73,16 +105,24 @@ Worker::Worker(hwinf *hw, QString update_ctrl_file, QString repositoryPath, connect(this, SIGNAL(terminateUpdateProcess()), this, SLOT(onTerminateUpdateProcess()), Qt::QueuedConnection); - //connect(this, SIGNAL(workNow()), this, SLOT(work()), Qt::QueuedConnection); - connect(&m_startUpdateProcess, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); - m_startUpdateProcess.setSingleShot(true); - m_startUpdateProcess.start(1000); + QDir customerRepository(m_customerRepository); + if (!customerRepository.exists()) { + if (m_gc.gitCloneAndCheckoutBranch()) { + // do nothing else, not even executing opkg-commands + onFinishUpdateProcess(false); + } + } else { + m_update = new Update(hw, m_customerRepository, m_customerNrStr, + m_branchName, m_workingDirectory, + dryRun, parent, serialInterface, baudrate); - connect(&m_waitForNewUpdates, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); - m_waitForNewUpdates.setSingleShot(false); + connect(&m_startUpdateProcess, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); + m_startUpdateProcess.setSingleShot(true); + m_startUpdateProcess.start(1000); - m_machineNr = 996; - m_customerNr = 281; + connect(&m_waitForNewUpdates, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); + m_waitForNewUpdates.setSingleShot(false); + } } Worker::~Worker() { @@ -101,39 +141,81 @@ Worker::~Worker() { } } +QString Worker::getOsVersion() const { + QString const cmd = QString("echo -n $(cat /etc/os-release | head -n 1 | cut -d'\"' -f2 | tr -d '\"')"); + Command c("bash"); + if (c.execute(m_workingDirectory, QStringList() << "-c" << cmd)) { + return c.getCommandResult(); + } + return "N/A"; +} + void Worker::onHandleChangedFiles(QStringList changedFiles) { qCritical() << QDir::currentPath() << "ON HANDLE CHANGED FILES" << changedFiles; - if (QDir(m_customerRepository).exists()) { - if (QDir::setCurrent(m_customerRepository)) { - QString const params("--recursive " - "--progress " - "--checksum " - "--exclude=.* " - "--include=*.bin " - "--include=*.json " - "--include=opkg_commands " - "--include=*.ini"); - QStringList cmds; - cmds << QString("rsync ") + params.simplified() + " etc/ /etc"; - cmds << QString("rsync ") + params.simplified() + " opt/ /opt"; - QString cmd; - bool error = false; - foreach (cmd, cmds) { - if (!error) { - Command c("bash"); - qInfo() << "EXCUTING CMD..." << cmd; - if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { - qDebug() << c.getCommandResult(); - } else { - qCritical() << "CMD" << cmd << "FAILED"; - error = true; - } + QString opkg_commands; + static const QRegularExpression re("^.*opkg_commands\\s*$"); + static const QRegularExpression comment("^\\s*#.*$"); + int idx = changedFiles.indexOf(re); + if (idx != -1) { + m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMANDS_REQUEST; + m_statusDescription = "EXECUTE OPKG COMMANDS"; + + opkg_commands = changedFiles.takeAt(idx); + + QFile f(opkg_commands); + if (f.open(QIODevice::ReadOnly)) { + QTextStream in(&f); + while (!in.atEnd()) { + QString line = in.readLine(); + if (line.indexOf(comment, 0) == -1) { + // found opkg command + QString opkgCommand = line.trimmed(); + executeOpkgCommand(opkgCommand); } } - if (!error) { - onFinishUpdateProcess(true); - return; + } + + // m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS; + // m_statusDescription = QString("EXECUTE OPKG COMMANDS %1 OK").arg(opkgCommands.join('\n')); + + f.close(); + } + + if (m_update->doUpdate(changedFiles)) { // first update the hardware + // then sync the file-system + if (QDir(m_customerRepository).exists()) { + if (QDir::setCurrent(m_customerRepository)) { + QString const params("--recursive " + "--progress " + "--checksum " + "--exclude=.* " + "--include=*.bin " + "--include=*.json " + "--include=opkg_commands " + "--include=*.ini"); + QStringList cmds; + cmds << QString("rsync ") + params.simplified() + " etc/ /etc"; + cmds << QString("rsync ") + params.simplified() + " opt/ /opt"; + + QString cmd; + bool error = false; + foreach (cmd, cmds) { + if (!error) { + Command c("bash"); + qInfo() << "EXCUTING CMD..." << cmd; + if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { + qDebug() << c.getCommandResult(); + } else { + qCritical() << "CMD" << cmd << "FAILED"; + error = true; + } + } + } + if (!error) { + onFinishUpdateProcess(true); + return; + } } } } @@ -191,24 +273,22 @@ void Worker::onSummarizeRepositoryStatus() { */ } -void Worker::onExecuteOpkgCommands(QStringList opkgCommands) { - QString opkgCommand; - foreach (opkgCommand, opkgCommands) { - emit this->executeOpkgCommand(opkgCommand); - } -} - -void Worker::onExecuteOpkgCommand(QString opkgCommand) { +void Worker::executeOpkgCommand(QString opkgCommand) { Command c(opkgCommand); if (c.execute(m_workingDirectory)) { QString const r = c.getCommandResult(); - qDebug() << opkgCommand << ": RESULT" << r; + m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS; + m_statusDescription = QString("EXECUTE OPKG COMMAND %1 OK").arg(opkgCommand); + } else { + m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE; + m_statusDescription = QString("EXECUTE OPKG COMMAND %1 FAILED").arg(opkgCommand); + onTerminateUpdateProcess(); + return; } } // sollte ParameterResponse heissen void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { - if (!ismasResponse.isEmpty()) { QStringList const keys = ismasResponse.keys(); static QRegularExpression re("^REQ_ISMASPARAMETER.*"); @@ -225,32 +305,44 @@ void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { int customerNr = c.toInt(-1); int machineNr = m.toInt(-1); if (customerNr != m_customerNr) { - qCritical() << "CUSTOMER-NR (" << customerNr << ") !=" - << "LOCAL CUSTOMER-NR (" << m_customerNr << ")"; - m_updateStatus = UPDATE_STATUS::ERROR_BACKEND; + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; + m_statusDescription + = QString("CUSTOMER-NR (%1) != LOCAL CUSTOMER-NR (%2)") + .arg(customerNr).arg(m_customerNr); return; } if (machineNr != m_machineNr) { - qCritical() << "MACHINE-NR (" << machineNr << ") !=" - << "LOCAL MACHINE-NR (" << m_machineNr << ")"; - m_updateStatus = UPDATE_STATUS::ERROR_BACKEND; + m_statusDescription + = QString("MACHINE-NR (%1) != LOCAL MACHINE-NR (%2)") + .arg(machineNr).arg(m_machineNr); + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; return; } } } + // TODO: check if zone_nr is correct + if (keys.contains("Fileupload", Qt::CaseInsensitive)) { QJsonObject fileUpload = ismasResponse["Fileupload"].toObject(); QJsonValue v = fileUpload.value("TRG"); if (!v.isNull() && !v.isUndefined()) { QString const s = v.toString(""); if (s == "WAIT") { + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS; m_ismasUpdateRequests = ISMAS_UPDATE_REQUESTS; - qCritical() << "ISMAS UPDATES AVAILABLE"; + m_statusDescription = "ISMAS UPDATES AVAILABLE"; emit m_gc.ismasUpdatesAvailable(); } } + } else { + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; + m_statusDescription = "NO FILEUPLOAD KEY AVAILABLE"; + return; } } + } else { + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; + m_statusDescription = "NO ISMAS RESPONSE AVAILABLE (EMPTY)"; } } @@ -274,7 +366,7 @@ void Worker::onSendCmdSendVersionToIsmas() { int tariffZone = 1; QString const tariffInfo = "test_tariff_info"; QString const tariffLoadTime = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); - QString const linuxVersion = "test_linux_version"; + // QString const linuxVersion = "test_linux_version"; QString const cpuSerial = "test_cpu_serial"; QString const deviceControllerVersion = "test_dc_version"; QString const deviceControllerGitBlob = "test_dc_blob_2a3b4f50"; @@ -296,7 +388,7 @@ void Worker::onSendCmdSendVersionToIsmas() { tariffZone, tariffInfo, tariffLoadTime, - linuxVersion, + m_osVersion, cpuSerial, deviceControllerVersion, deviceControllerGitBlob, @@ -317,17 +409,19 @@ void Worker::onSendCmdSendVersionToIsmas() { void Worker::askIsmasForNewData() { if (m_maintenanceMode) { + m_updateStatus = UPDATE_STATUS::ISMAS_EMULATE_DATA_AVAILABLE; QString data = m_ismasClient.setUpdatesAvailable(); - m_apismClient.sendUpdateInfoToIsmas(data); + m_apismClient.emulateUpdatesAvailable(data); } + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING; m_apismClient.requestAvailableIsmasUpdates(); if (--m_ismasUpdateRequests > 0) { // if the timer is already running, it will be stopped and restarted. m_waitForNewUpdates.start(10000); + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING; } else { - qCritical() << "REQUESTING ISMAS FOR UPDATES TIMED OUT"; - m_workerThread.quit(); - QApplication::quit(); + m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_TIMEOUT; + onTerminateUpdateProcess(); } } From 43f5f3ecae2351610ea5b08d09bb28f1859d607d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:45:11 +0200 Subject: [PATCH 050/239] Removed usage of any files. Removed any dependencies on git-hooks. Removed handling of opkg-related things: they are done now inside the worker itself. --- update.cpp | 453 +++++++++++++---------------------------------------- update.h | 38 ++--- 2 files changed, 117 insertions(+), 374 deletions(-) diff --git a/update.cpp b/update.cpp index 7cf8527..e626b78 100644 --- a/update.cpp +++ b/update.cpp @@ -22,11 +22,6 @@ #include #include -#define COLUMN_REQUEST (0) -#define COLUMN_NAME (1) -#define COLUMN_DATE_TIME (2) -#define COLUMN_RESULT (3) - #define UPDATE_OPKG (1) #define UPDATE_DC (1) #define UPDATE_PRINTER_TEMPLATES (1) @@ -80,14 +75,10 @@ hwinf *Update::loadDCPlugin(QDir const &plugInDir, QString const &fname) { } Update::Update(hwinf *hw, - QString update_ctrl_file, - QString repositoryPath, - QString customerId, + QString customerRepository, + QString customerNrStr, QString branchName, QString workingDir, - bool maintenanceMode, - bool testMode, - bool executeScriptOnly, bool dryRun, QObject *parent, char const *serialInterface, @@ -96,118 +87,14 @@ Update::Update(hwinf *hw, , m_hw(hw) , m_serialInterface(serialInterface) , m_baudrate(baudrate) - , m_update_ctrl_file(update_ctrl_file) - , m_update_ctrl_file_copy(update_ctrl_file + ".copy") - , m_repositoryPath(repositoryPath) - , m_customerId(customerId) + , m_customerRepository(customerRepository) + , m_customerNrStr(customerNrStr) , m_branchName(branchName) , m_workingDir(workingDir) - , m_maintenanceMode(maintenanceMode) - , m_testMode(testMode) - , m_executeScriptOnly(executeScriptOnly) - , m_dryRun(dryRun) - //, m_apismClient(nullptr, nullptr, nullptr) - , m_init(true) { - - // m_apismClient.sendSelfTest(); - - if (!m_testMode) { - // make sure the files are empty - if (m_update_ctrl_file.exists()) { - if (m_update_ctrl_file.open(QIODevice::ReadWrite | - QIODevice::Truncate | - QIODevice::Text)) { - m_update_ctrl_file.close(); - } - } else { - qCritical() << "Update-file" << m_update_ctrl_file.fileName() - << "does not exist"; - m_init = false; - } - if (m_update_ctrl_file_copy.exists()) { - if (m_update_ctrl_file_copy.open(QIODevice::ReadWrite | - QIODevice::Truncate | - QIODevice::Text)) { - m_update_ctrl_file_copy.close(); - } - } else { - qCritical() << "Update-file-copy" << m_update_ctrl_file_copy.fileName() - << "does not exist"; - m_init = false; - } - } - - // execute update_psa-script - if (m_init) { - if (!m_testMode) { - if ((m_init = execUpdateScript()) == false) { - qCritical() << "UPDATE_SCRIPT FAILED"; - } - } - if (m_init) { - if (!m_update_ctrl_file.open(QIODevice::ReadWrite | QIODevice::Text)) { - qCritical() << "CAN NOT OPEN" << m_update_ctrl_file.fileName(); - m_init = false; - } else { - qDebug() << "OPENED" << m_update_ctrl_file.fileName(); - } - if (!m_update_ctrl_file_copy.open(QIODevice::ReadWrite | QIODevice::Text)) { - qCritical() << "CAN NOT OPEN" << m_update_ctrl_file_copy.fileName(); - m_init = false; - } else { - qDebug() << "OPENED" << m_update_ctrl_file_copy.fileName(); - } - } - } + , m_dryRun(dryRun) { } Update::~Update() { - // make sure the files are closed - m_update_ctrl_file.close(); - m_update_ctrl_file_copy.close(); -} - -bool Update::execUpdateScript() { - // path of update-script 'update_psa' - QString update_psa("/opt/app/tools/atbupdate/update_psa "); - if (m_maintenanceMode) { - update_psa += " -m "; - } - update_psa += " --wdir "; - update_psa += m_workingDir; - - qCritical() << "update_psa: " << update_psa; - - QScopedPointer p(new QProcess(this)); - p->setProcessChannelMode(QProcess::MergedChannels); - - connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); - connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); - - p->start(update_psa); - - if (p->waitForStarted(1000)) { - if (p->state() == QProcess::ProcessState::Running) { - // maintenance_mode: update_psa script enters an infinite loop - int const timeout = (m_maintenanceMode ? 200000: -1); - if (p->waitForFinished(timeout)) { - QString output = p->readAllStandardOutput().toStdString().c_str(); - QStringList lst = output.split('\n'); - for (int i = 0; i < lst.size(); ++i) { - qDebug() << lst[i]; - } - if (p->exitStatus() == QProcess::NormalExit) { - qInfo() << "EXECUTED" << update_psa - << "with code" << p->exitCode(); - return (p->exitCode() == 0); - } - } else { - qCritical() << "update-script TIMEDOUT after" - << timeout/1000 << "seconds"; - } - } - } - return false; } Update::DownloadResult Update::sendStatus(int ret) const { @@ -589,22 +476,6 @@ bool Update::updateDeviceConf(QString jsFile) { return downloadJson(FileTypeJson::DEVICE, 0, jsFile); } -QStringList Update::getLinesToWorkOn() { - QStringList linesToWorkOn; - - QTextStream in(&m_update_ctrl_file); - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - if (line.startsWith("DONE")) { - m_update_ctrl_file_copy.write(line.toUtf8().constData()); - m_update_ctrl_file_copy.write("\n"); - } else { - linesToWorkOn << line; - } - } - return linesToWorkOn; -} - QStringList Update::split(QString line, QChar sep) { QStringList lst; QString next; @@ -639,248 +510,138 @@ void Update::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardError())); } -// bool Update::executeProcess(QString const &cmd); - -bool Update::doUpdate() { +bool Update::doUpdate(QStringList const &filesToWorkOn) { // // ACHTUNG !!! // return true; - /* - The file referred to by 'update_data' has the following structure for - each line: - - # ====================================================================== - # REQUEST | NAME | DATE | RESULT - # ====================================================================== - # where - # - # STATUS: DOWNLOAD, EXECUTE or DONE - # NAME : If starting with 'opkg' it is an opkg-command to be executed. - # Otherwise its the name of a file which has to be updated. - # DATE : 0000-00-00T00:00:00 - # RESULT: SUCCESS or ERROR (possibly with description) - # - */ - - if (!m_init) { - qCritical() << "DO UPDATE: INITIALIZATION OR UPDATE-SCRIPT FAILED"; - return false; - } - - if (m_executeScriptOnly) { // basically a test flag for executing only the - return true; // update script. - } - bool serialOpened = false; bool serialOpen = false; - QStringList linesToWorkOn = getLinesToWorkOn(); - if (linesToWorkOn.size() == 0) { - qCritical() << "No lines to handle in" << m_update_ctrl_file.fileName(); + if (filesToWorkOn.size() == 0) { + qCritical() << "NOTHING TO UPDATE"; return true; } - qDebug() << "open lines..."; - for (int i=0; i< linesToWorkOn.size(); ++i) { - qDebug() << "line" << i << ":" << linesToWorkOn.at(i).trimmed(); + if (!serialOpen) { + if (!isSerialOpen()) { // open serial only if not already open + if ((serialOpened = openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) == false) { + qCritical() << "CANNOT OPEN" << m_serialInterface << "(BAUDRATE=" + << m_baudrate << ")"; + return false; + } + } + serialOpen = true; + qCritical() << "SERIAL OPEN" << m_serialInterface << "(BAUDRATE=" << m_baudrate << ")"; } QList::const_iterator it; - for (it = linesToWorkOn.cbegin(); it != linesToWorkOn.cend(); ++it) { + for (it = filesToWorkOn.cbegin(); it != filesToWorkOn.cend(); ++it) { bool res = false; - QString line = (*it).trimmed(); - if (line.size() == 0 || line.startsWith(QChar('#'))) { - continue; - } - QStringList lst = split(line.trimmed()); - if (lst.size() != 4) { - qCritical() << "PARSING ERROR FOR LINE" - << line << "IN" << m_update_ctrl_file.fileName(); - continue; - } - QString const &request = lst[COLUMN_REQUEST]; - QString const &name = lst[COLUMN_NAME]; + QString fToWorkOn = (*it).trimmed(); - QTextStream out(&m_update_ctrl_file_copy); - // QString const &datetime = lst[COLUMN_DATE_TIME]; - // QString const &result = lst[COLUMN_RESULT]; - qDebug() << "request=" << request << ", name=" << name; - if (request.trimmed() == "DOWNLOAD") { - if (!serialOpen) { - if (!isSerialOpen()) { // open serial only if not already open - if ((serialOpened = openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) == false) { - qCritical() << "CANNOT OPEN" << m_serialInterface << "(BAUDRATE=" - << m_baudrate << ")"; - return false; - } - } - serialOpen = true; - qCritical() << "SERIAL OPEN" << m_serialInterface << "(BAUDRATE=" << m_baudrate << ")"; + + if (fToWorkOn.contains("dc2c", Qt::CaseInsensitive) && + fToWorkOn.endsWith(".bin", Qt::CaseInsensitive)) { + + qDebug() << "sending sw/hw-requests..."; + for (int i=0; i < 3; ++i) { // send explicit reuests to get + // current SW/HW-versions + m_hw->request_DC2_SWversion(); + m_hw->request_DC2_HWversion(); + QThread::sleep(1); } - if (name.contains("dc2c", Qt::CaseInsensitive) && - name.endsWith(".bin", Qt::CaseInsensitive)) { - qDebug() << "sending sw/hw-requests..."; - for (int i=0; i < 3; ++i) { // send explicit reuests to get - // current SW/HW-versions - m_hw->request_DC2_SWversion(); - m_hw->request_DC2_HWversion(); - QThread::sleep(1); - } - QString const hwVersion = m_hw->dc_getHWversion().toLower(); - QString const fwVersion = m_hw->dc_getSWversion().toLower(); - qInfo() << "current dc-hardware-version" << hwVersion; - qInfo() << "current dc-firmware-version" << fwVersion; + QString const hwVersion = m_hw->dc_getHWversion().toLower(); + QString const fwVersion = m_hw->dc_getSWversion().toLower(); + qInfo() << "current dc-hardware-version" << hwVersion; + qInfo() << "current dc-firmware-version" << fwVersion; - QFile fn(name); - QFileInfo linkTarget(fn.symLinkTarget()); - if (!linkTarget.exists()) { // check for broken link - res = false; - } else { - if (false) { - //if (fwVersion.startsWith(linkTarget.completeBaseName())) { - // qCritical() << "current dc-firmware-version" << fwVersion - // << "already installed"; - // res = false; - } else { - res = true; - - qCritical() << "downloading" << name.trimmed() << "->" - << linkTarget.completeBaseName() << "to DC"; -#if UPDATE_DC == 1 - m_hw->dc_autoRequest(false);// default: turn auto-request setting off - QThread::sleep(1); // wait to be sure that there are no more - // commands sent to dc-hardware - qDebug() << "SET AUTO-REQUEST=FALSE"; - - if ((res = updateBinary(name.toStdString().c_str())) == true) { - qCritical() << "downloaded binary" << name; - } - - m_hw->dc_autoRequest(true); // turn auto-request setting on - qDebug() << "SET AUTO-REQUEST=TRUE"; - qDebug() << "WAIT 10 SECS TO RECEIVE RESPONSES..."; - - QThread::sleep(10); // wait to be sure that responses - // have been received - - qCritical() << "updated dc-hardware-version" << m_hw->dc_getHWversion(); - qCritical() << "updated dc-firmware-version" << m_hw->dc_getSWversion(); -#endif - } - } - } else if (name.contains("DC2C_print", Qt::CaseInsensitive) - && name.endsWith(".json", Qt::CaseInsensitive)) { - res = true; -#if UPDATE_PRINTER_TEMPLATES == 1 - int i = name.indexOf("DC2C_print", Qt::CaseInsensitive); - int const templateIdx = name.mid(i).midRef(10, 2).toInt(); - if ((templateIdx < 1) || (templateIdx > 32)) { - qCritical() << "WRONG TEMPLATE INDEX" << templateIdx; - res = false; - } else { - if ((res = updatePrinterTemplate(templateIdx, name))) { - qInfo() << "downloaded printer template"<< name; - } - } -#endif - } else if (name.contains("DC2C_cash", Qt::CaseInsensitive) - && name.endsWith(".json", Qt::CaseInsensitive)) { - res = true; -#if UPDATE_CASH_TEMPLATE == 1 - if ((res = updateCashConf(name))) { - qInfo() << "downloaded cash template"<< name; - } -#endif - } else if (name.contains("DC2C_conf", Qt::CaseInsensitive) - && name.endsWith(".json", Qt::CaseInsensitive)) { - res = true; -#if UPDATE_CONF_TEMPLATE == 1 - if ((res= updateConfig(name))) { - qInfo() << "downloaded config template"<< name; - } -#endif - } else if (name.contains("DC2C_device", Qt::CaseInsensitive) - && name.endsWith(".json", Qt::CaseInsensitive)) { - res = true; -#if UPDATE_DEVICE_TEMPLATE == 1 - if ((res = updateDeviceConf(name))) { - qInfo() << "downloaded device template"<< name; - } -#endif - } else { - qCritical() << "UNKNOWN JSON FILE NAME" << name; + QFile fn(fToWorkOn); + QFileInfo linkTarget(fn.symLinkTarget()); + if (!linkTarget.exists()) { // check for broken link res = false; - } - } else if (request == "EXECUTE" && name.contains("opkg")) { - qInfo() << "starting" << name.trimmed(); - res = true; -#if UPDATE_OPKG == 1 - QScopedPointer p(new QProcess(this)); - p->setProcessChannelMode(QProcess::MergedChannels); - - connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); - connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); - - p->start(name.trimmed()); - - if (p->waitForStarted(1000)) { - if (p->state() == QProcess::ProcessState::Running) { - if (p->waitForFinished(100000)) { - QString output = p->readAllStandardOutput(); - QStringList outputLst = split(output, QChar('\n')); - for (int line=0; line < outputLst.size(); ++line) { - qDebug() << outputLst[line]; - } - if (p->exitStatus() == QProcess::NormalExit) { - qInfo() << "EXECUTED" << name - << "with code" << p->exitCode(); - res = true; - } else { - qCritical() << "PROCESS" << name << "CRASHED"; - } - } else { - qCritical() << "PROCESS" << name << "DID NOT FINISH"; - } - } else { - qCritical() << "WRONG PROCESS STATE" << p->state(); - } } else { - qCritical() << "PROCESS" << name << "TIMEOUT AT START"; + if (false) { + //if (fwVersion.startsWith(linkTarget.completeBaseName())) { + // qCritical() << "current dc-firmware-version" << fwVersion + // << "already installed"; + // res = false; + } else { + res = true; + + qCritical() << "downloading" << fToWorkOn.trimmed() << "->" + << linkTarget.completeBaseName() << "to DC"; +#if UPDATE_DC == 1 + m_hw->dc_autoRequest(false);// default: turn auto-request setting off + QThread::sleep(1); // wait to be sure that there are no more + // commands sent to dc-hardware + qDebug() << "SET AUTO-REQUEST=FALSE"; + + if ((res = updateBinary(fToWorkOn.toStdString().c_str())) == true) { + qCritical() << "downloaded binary" << fToWorkOn; + } + + m_hw->dc_autoRequest(true); // turn auto-request setting on + qDebug() << "SET AUTO-REQUEST=TRUE"; + qDebug() << "WAIT 10 SECS TO RECEIVE RESPONSES..."; + + QThread::sleep(10); // wait to be sure that responses + // have been received + qCritical() << "updated dc-hardware-version" << m_hw->dc_getHWversion(); + qCritical() << "updated dc-firmware-version" << m_hw->dc_getSWversion(); +#endif + } + } + } else if (fToWorkOn.contains("DC2C_print", Qt::CaseInsensitive) + && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { + res = true; +#if UPDATE_PRINTER_TEMPLATES == 1 + int i = fToWorkOn.indexOf("DC2C_print", Qt::CaseInsensitive); + int const templateIdx = fToWorkOn.mid(i).midRef(10, 2).toInt(); + if ((templateIdx < 1) || (templateIdx > 32)) { + qCritical() << "WRONG TEMPLATE INDEX" << templateIdx; + res = false; + } else { + if ((res = updatePrinterTemplate(templateIdx, fToWorkOn))) { + qInfo() << "downloaded printer template"<< fToWorkOn; + } + } +#endif + } else if (fToWorkOn.contains("DC2C_cash", Qt::CaseInsensitive) + && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { + res = true; +#if UPDATE_CASH_TEMPLATE == 1 + if ((res = updateCashConf(name))) { + qInfo() << "downloaded cash template"<< name; + } +#endif + } else if (fToWorkOn.contains("DC2C_conf", Qt::CaseInsensitive) + && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { + res = true; +#if UPDATE_CONF_TEMPLATE == 1 + if ((res= updateConfig(name))) { + qInfo() << "downloaded config template"<< name; + } +#endif + } else if (fToWorkOn.contains("DC2C_device", Qt::CaseInsensitive) + && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { + res = true; +#if UPDATE_DEVICE_TEMPLATE == 1 + if ((res = updateDeviceConf(name))) { + qInfo() << "downloaded device template"<< name; } #endif } else { - // TODO + qCritical() << "UNKNOWN JSON FILE NAME" << fToWorkOn; + res = false; } - out << "DONE, " << name << ", " - << QDateTime::currentDateTime().toString(Qt::ISODate) << ", " - << ((res == true) ? "SUCCESS" : "ERROR") << "\n"; - out.flush(); - } // for (it = openLines.cbegin(); it != openLines.end(); ++it) { m_hw->dc_autoRequest(true); // ALWAYS turn autoRequest ON qDebug() << "SET AUTO-REQUEST=TRUE"; return true; - - // return finishUpdate(linesToWorkOn.size() > 0); -} - -bool Update::finishUpdate(bool swapCtrlFiles) { - if (swapCtrlFiles) { - m_update_ctrl_file.close(); - m_update_ctrl_file_copy.close(); - - //std::ifstream source(m_update_ctrl_file_copy.fileName().toStdString().c_str(), std::ios::binary); - //std::ofstream dest(m_update_ctrl_file.fileName().toStdString().c_str(), std::ios::binary); - - //dest << source.rdbuf(); - //source.close(); - //dest.close(); - } - return true; } diff --git a/update.h b/update.h index 9db3483..c612313 100644 --- a/update.h +++ b/update.h @@ -28,24 +28,12 @@ class Update : public QObject { hwinf *m_hw; char const *m_serialInterface; char const *m_baudrate; - QFile m_update_ctrl_file; - QFile m_update_ctrl_file_copy; - QString m_repositoryPath; - QString m_customerId; // customer_281 + QString m_customerRepository; + QString m_customerNrStr; QString m_branchName; QString m_workingDir; bool m_maintenanceMode; - bool m_testMode; - bool m_executeScriptOnly; bool m_dryRun; - //ApismClient m_apismClient; - - bool m_init; - - bool finishUpdate(bool finish); - QStringList getLinesToWorkOn(); - - bool execUpdateScript(); public: enum class DownloadResult {OK, ERROR, TIMEOUT, NOP}; @@ -56,29 +44,25 @@ public: explicit Update(hwinf *hw, - QString update_ctrl_file, - QString repositoryPath, - QString customerId, + QString customerRepository, + QString customerNrStr, QString branchName, - QString workingDir = ".", - bool maintenanceMode = false, - bool testMode = false, - bool executeScriptOnly = false, + QString workingDir, bool dryRun = false, QObject *parent = nullptr, char const *serialInterface = SERIAL_PORT, char const *baudrate = "115200"); virtual ~Update() override; - bool doUpdate(); + bool doUpdate(QStringList const &linesToWorkOn); - QString customerId() { return m_customerId; } - QString const customerId() const { return m_customerId; } + //QString customerId() { return m_customerId; } + //QString const customerId() const { return m_customerId; } QString branchName() { return m_branchName; } QString const branchName() const { return m_branchName; } - QString repositoryPath() { return m_repositoryPath; } - QString const repositoryPath() const { return m_repositoryPath; } + //QString repositoryPath() { return m_repositoryPath; } + //QString const repositoryPath() const { return m_repositoryPath; } private: static QString jsonType(enum FileTypeJson type); @@ -105,8 +89,6 @@ private: bool downloadJson(enum FileTypeJson type, int templateIdx, QString jsFileToSendToDC) const; - bool executeProcess(QString const &cmd); - private slots: void readyReadStandardOutput(); void readyReadStandardError(); From 5eb33f3e31332d17e61927ce91d869e0cff57171 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:48:04 +0200 Subject: [PATCH 051/239] Minor change: renamed function sendUpdateInfToIsmas() to emulateUpdatesAvailable() for clarity. --- apism/apism_client.cpp | 8 ++++---- apism/apism_client.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apism/apism_client.cpp b/apism/apism_client.cpp index 3d03bbd..5415699 100644 --- a/apism/apism_client.cpp +++ b/apism/apism_client.cpp @@ -61,6 +61,7 @@ void ApismClient::restartApism() { void ApismClient::sendTransaction(const VendingData *vendingData) { + Q_UNUSED(vendingData); #if 0 QScopedPointer transferData(new ISMAS::TransferData()); @@ -411,14 +412,14 @@ void ApismClient::sendCmdSendVersionToIsmas(QString const &msg) { QByteArray data = "#M=APISM#C=CMD_SENDVERSION#J="; data += document.toJson(QJsonDocument::Compact); - printf("data=%s\n", QString(data).toStdString().c_str()); + // printf("data=%s\n", QString(data).toStdString().c_str()); this->currentRequest = ISMAS::REQUEST::NO_REQUEST; this->apismTcpSendClient->sendData(data); } -void ApismClient::sendUpdateInfoToIsmas(QString const &msg) { +void ApismClient::emulateUpdatesAvailable(QString const &msg) { QJsonParseError parseError; QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); if (parseError.error != QJsonParseError::NoError) { @@ -435,7 +436,7 @@ void ApismClient::sendUpdateInfoToIsmas(QString const &msg) { QByteArray data = "#M=APISM#C=CMD_EVENT#J="; data += document.toJson(QJsonDocument::Compact); - printf("data=%s\n", QString(data).toStdString().c_str()); + qDebug() << "EMULATE UPDATES AVAILABLE" << QString(data).toStdString().c_str(); this->currentRequest = ISMAS::REQUEST::NO_REQUEST; this->apismTcpSendClient->sendData(data); @@ -447,7 +448,6 @@ void ApismClient::sendState(const QString & state, const QString & msg) qCritical() << " state: " << state; qCritical() << " msg: " << msg; - QJsonParseError parseError; QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); if (parseError.error != QJsonParseError::NoError) { diff --git a/apism/apism_client.h b/apism/apism_client.h index 85bfb34..dec70a2 100644 --- a/apism/apism_client.h +++ b/apism/apism_client.h @@ -57,7 +57,7 @@ public slots: //void sendEvent(const ATBMachineEvent* machineEvent); void sendState(const QString & state, const QString & msg); - void sendUpdateInfoToIsmas(QString const &msg); + void emulateUpdatesAvailable(QString const &msg); void sendCmdSendVersionToIsmas(QString const &msg); void requestAvailableIsmasUpdates(); From 37c5c7c4f62287bf9e3df9bd63ca807f390ef2a4 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:49:44 +0200 Subject: [PATCH 052/239] Simplified interface. When doing a clone of the repository, do not execute any other commands. --- git/git_client.cpp | 40 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index d71374d..a723455 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -7,20 +7,18 @@ #include -GitClient::GitClient(QString const &repositoryPath, - QString const &customerId, +GitClient::GitClient(QString const &customerNrStr, + QString const &customerRepository, QString const &workingDirectory, QString const &branchName, QObject *parent) : QObject(parent) , m_worker(qobject_cast(parent)) - , m_repositoryPath(repositoryPath) - , m_customerId(customerId) + , m_repositoryPath(QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(customerNrStr)) + , m_customerNr(customerNrStr) , m_workingDirectory(workingDirectory) , m_branchName(branchName) - , m_customerRepository(QDir::cleanPath(m_workingDirectory - + QDir::separator() - + m_customerId)) { + , m_customerRepository(customerRepository) { if (!m_worker) { qCritical() << "ERROR CASTING PARENT TO WORKER FAILED"; } @@ -63,18 +61,6 @@ void GitClient::onIsmasUpdatesAvailable() { if (gitCloneAndCheckoutBranch()) { qInfo() << "CLONED" << m_repositoryPath << "AND CHECKED OUT INTO" << m_customerRepository; - if (m_worker) { - qDebug() << "WORKER EXECUTE OPKG COMMANDS"; - QStringList opkgCommands; - // To make sure that opkg upgrade does not break your system - // because of an unstable connection - // Add a line "option cache cachedir" to /etc/opkg/opkg.conf to - // avoid the --cache option on command line. - opkgCommands << "opkg update"; - //opkgCommands << "opkg --cache cachedir --download-only upgrade"; - //opkgCommands << "opkg --cache cachedir upgrade"; - emit m_worker->executeOpkgCommands(opkgCommands); - } } else { qCritical() << "ERROR CLONING " << m_repositoryPath << "AND/OR CHECKING OUT INTO" << m_customerRepository; @@ -99,7 +85,7 @@ bool GitClient::gitCloneCustomerRepository() { QRegularExpressionMatch match = re.match(result); if (match.hasMatch()) { if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches - if (match.captured(2).trimmed() == m_customerId) { + if (match.captured(2).trimmed() == m_customerNr) { qInfo() << "CLONING" << m_repositoryPath << "OK"; return true; } @@ -112,7 +98,7 @@ bool GitClient::gitCloneCustomerRepository() { } bool GitClient::copyGitConfigFromMaster() { // only allowed when called in - // master branch + // master branch (???) if (QDir(m_customerRepository).exists()) { QString const cp = QString("cp .gitconfig .git/config"); Command c("bash"); @@ -139,9 +125,9 @@ bool GitClient::gitCheckoutBranch() { bool GitClient::gitCloneAndCheckoutBranch() { qInfo() << "CLONE" << m_repositoryPath << "AND CHECKOUT" << m_branchName; if (gitCloneCustomerRepository()) { - if (copyGitConfigFromMaster()) { + //if (copyGitConfigFromMaster()) { return gitCheckoutBranch(); - } + //} } return false; } @@ -191,11 +177,9 @@ std::optional GitClient::gitDiff(QString const &commits) { Hat sich nichts geaendert, so werden auch keine Commits <>..<> angezeigt */ std::optional GitClient::gitFetch() { - QString const &customerRepository - = QDir::cleanPath(m_workingDirectory + QDir::separator() + m_customerId); - if (QDir(customerRepository).exists()) { + if (QDir(m_customerRepository).exists()) { Command c("git fetch"); - if (c.execute(customerRepository)) { + if (c.execute(m_customerRepository)) { QString const s = c.getCommandResult().trimmed(); if (!s.isEmpty()) { QStringList lines = Update::split(s, '\n'); @@ -220,7 +204,7 @@ std::optional GitClient::gitFetch() { } } } else { - qCritical() << "ERROR" << customerRepository << "DOES NOT EXIST"; + qCritical() << "ERROR" << m_customerRepository << "DOES NOT EXIST"; } return std::nullopt; } From 93d62773862c4f363654ab69fc3f72d00dce90d2 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:51:40 +0200 Subject: [PATCH 053/239] Simplified interface. --- git/git_client.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/git_client.h b/git/git_client.h index dd7d97b..cb1f3a5 100644 --- a/git/git_client.h +++ b/git/git_client.h @@ -12,7 +12,7 @@ class GitClient : public QObject { Worker *m_worker; QString const m_repositoryPath; - QString const m_customerId; + QString const m_customerNr; QString const m_workingDirectory; QString const m_branchName; QString const m_customerRepository; @@ -20,8 +20,8 @@ class GitClient : public QObject { bool copyGitConfigFromMaster(); public: - explicit GitClient(QString const &repositoryPath, - QString const &customerId, + explicit GitClient(QString const &customerNrStr, + QString const &repositoryPath, QString const &workingDirectory = QCoreApplication::applicationDirPath(), QString const &branchName = "master", QObject *parent = 0); From 9775792916d4765f3ee392dc72d4a25fcf911ec8 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:52:27 +0200 Subject: [PATCH 054/239] Minor change: extended debug-output. --- process/command.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/process/command.cpp b/process/command.cpp index 25efb3f..93c579c 100644 --- a/process/command.cpp +++ b/process/command.cpp @@ -45,7 +45,8 @@ bool Command::execute(QString workingDirectory, QStringList args) { connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); - qCritical() << "START COMMAND" << m_command << "IN" << workingDirectory; + qCritical() << "START COMMAND" << m_command << "WITH ARGS" << args + << "IN" << workingDirectory; p->setWorkingDirectory(workingDirectory); if (!args.isEmpty()) { @@ -55,8 +56,11 @@ bool Command::execute(QString workingDirectory, QStringList args) { } if (p->waitForStarted(m_waitForStartTimeout)) { + qDebug() << "PROCESS" << m_command << "STARTED"; if (p->state() == QProcess::ProcessState::Running) { + qDebug() << "PROCESS" << m_command << "RUNNING"; if (p->waitForFinished(m_waitForFinishTimeout)) { + qDebug() << "PROCESS" << m_command << "FINISHED"; if (p->exitStatus() == QProcess::NormalExit) { qInfo() << "EXECUTED" << m_command << "with code" << p->exitCode(); From 60084450e6b971c757c59872578bec018372c2e1 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 17 Jul 2023 16:54:18 +0200 Subject: [PATCH 055/239] Removed 't (testMode)' and 'e (executeScript)' options. Adapted call to Worker-ctor. --- main.cpp | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/main.cpp b/main.cpp index 8371607..1a6a7ea 100644 --- a/main.cpp +++ b/main.cpp @@ -84,16 +84,6 @@ int main(int argc, char *argv[]) { QCoreApplication::translate("main", "Maintenance mode for underlying script")); parser.addOption(maintenanceOption); - // test-mode: edit the file update_log.csv and execute the commands - // contained in it. Do not call the update-script. - QCommandLineOption testOption(QStringList() << "t" << "test", - QCoreApplication::translate("main", "Test mode for ATBUpdateTool")); - parser.addOption(testOption); - - QCommandLineOption execScriptOption(QStringList() << "e" << "execute-script-only", - QCoreApplication::translate("main", "ATBUpdateTool executes update-script only. No download of any files.")); - parser.addOption(execScriptOption); - QCommandLineOption dryRunOption(QStringList() << "d" << "dry-run", QCoreApplication::translate("main", "Start ATBUpdateTool in dry-run-mode. No actual actions.")); parser.addOption(dryRunOption); @@ -112,8 +102,6 @@ int main(int argc, char *argv[]) { QString plugInName = parser.value(pluginNameOption); QString workingDir = parser.value(workingDirectoryOption); bool maintenanceMode = parser.isSet(maintenanceOption); - bool testMode = parser.isSet(testOption); - bool executeScriptOnly = parser.isSet(execScriptOption); bool dryRun = parser.isSet(dryRunOption); QString const rtPath = QCoreApplication::applicationDirPath(); @@ -131,8 +119,6 @@ int main(int argc, char *argv[]) { qInfo() << "plugInName ........" << plugInName; qInfo() << "workingDir ........" << workingDir; qInfo() << "maintenanceMode ..." << maintenanceMode; - qInfo() << "testMode .........." << testMode; - qInfo() << "execScriptOnly ...." << executeScriptOnly; qInfo() << "dryRun ............" << dryRun; // before loading the library, delete all possible shared memory segments @@ -146,15 +132,22 @@ int main(int argc, char *argv[]) { // hw->dc_autoRequest(false); QString const update_ctrl_file = "/opt/app/tools/atbupdate/update_log.csv"; - Worker worker(hw, update_ctrl_file, - "https://git.mimbach49.de/GerhardHoffmann/customer_999.git", - "customer_999", - "zg1/zone1", + + int machineNr = Worker::read1stLineOfFile("/etc/machine_nr"); + int customerNr = Worker::read1stLineOfFile("/etc/cust_nr"); + QString customerNrStr = QString("customer_") + QString::number(customerNr).rightJustified(3, '0'); + int zoneNr = Worker::read1stLineOfFile("/etc/zone_nr"); + QString branchName = QString("zg1/zone%1").arg(zoneNr); + + Worker worker(hw, + customerNr, + machineNr, + zoneNr, + branchName, workingDir, maintenanceMode, - testMode, - executeScriptOnly, - dryRun); + dryRun + ); return a.exec(); } From 088d7c8aa096063ede02b3277138e315496fb128 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:35:48 +0200 Subject: [PATCH 056/239] Additinal UPDATE_STATE constants. Added struct UpdateStatus for printing debug messages. Added member variables used for sending SENDCMD to ISMAS.A Added helper functions getATBQTVersion(), getCPUSerial(), getRaucVersion(), getOpkgVersion(), getPluginVersion(), getDCVersion(), getFileSize(). --- worker.h | 57 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/worker.h b/worker.h index d86a338..b997e1a 100644 --- a/worker.h +++ b/worker.h @@ -28,15 +28,17 @@ enum class UPDATE_STATUS : quint8 { ISMAS_UPDATE_REQUEST_FAILURE, ISMAS_UPDATE_REQUEST_TIMEOUT, ISMAS_UPDATE_REQUEST_SUCCESS, + ISMAS_RESPONSE_RECEIVED, GIT_CHECKOUT_BRANCH_REQUEST, GIT_CHECKOUT_BRANCH_REQUEST_FAILURE, GIT_CHECKOUT_BRANCH_NOT_EXISTS, GIT_CHECKOUT_BRANCH_CHECKOUT_ERROR, GIT_FETCH_UPDATES_REQUEST, GIT_FETCH_UPDATES_REQUEST_FAILURE, - GIT_FETCH_UPDATES_REQUEST_SSUCCESS, - EXEC_OPKG_COMMANDS_REQUEST, - EXEC_OPKG_COMMANDS_FAILURE, + GIT_FETCH_UPDATES_REQUEST_SUCCESS, + GIT_PULL_UPDATES_SUCCESS, + GIT_PULL_UPDATES_FAILURE, + EXEC_OPKG_COMMANDS, EXEC_OPKG_COMMAND_FAILURE, EXEC_OPKG_COMMAND_SUCCESS, EXEC_OPKG_COMMANDS_SUCCESS, @@ -57,14 +59,27 @@ enum class UPDATE_STATUS : quint8 { ISMAS_CURRENT_PSA_STATUS_CONFIRM_SUCCESS }; -#define ISMAS_UPDATE_REQUESTS (6) +struct UpdateStatus { + UPDATE_STATUS m_updateStatus; + QString m_statusDescription; + + explicit UpdateStatus(UPDATE_STATUS s, QString const &d) + : m_updateStatus(s), m_statusDescription(d) {} +}; + +QDebug operator<<(QDebug debug, UpdateStatus status); +QString& operator<<(QString &str, UpdateStatus status); + +#define ISMAS_UPDATE_REQUESTS (10) class hwinf; class Worker : public QObject { Q_OBJECT + hwinf *m_hw; WorkerThread m_workerThread; QTimer m_startUpdateProcess; + QTimer m_emergencyTimer; Update *m_update; ApismClient m_apismClient; int const m_customerNr; @@ -73,10 +88,23 @@ class Worker : public QObject { int const m_zoneNr; QString const m_workingDirectory; QString const m_branchName; + QString const m_customerRepositoryPath; QString const m_customerRepository; GitClient m_gc; bool m_maintenanceMode; QString const m_osVersion; + QString const m_atbqtVersion; + QString const m_cpuSerial; + QString const m_raucVersion; + QString const m_opkgVersion; + QString const m_pluginVersionATBDeciceController; + QString const m_pluginVersionIngenicoISelf; + QString const m_pluginVersionMobilisisCalc; + QString const m_pluginVersionMobilisisCalcConfig; + QString const m_pluginVersionPrmCalc; + QString const m_pluginVersionPrmCalcConfig; + QString const m_pluginVersionTcpZvt; + int m_ismasUpdateRequests; QTimer m_waitForNewUpdates; IsmasClient m_ismasClient; @@ -84,9 +112,16 @@ class Worker : public QObject { UPDATE_STATUS m_updateStatus; QString m_statusDescription; - void executeOpkgCommand(QString opkgCommand); QString getOsVersion() const; + QString getATBQTVersion() const; + QString getCPUSerial() const; + QString getRaucVersion() const; + QString getOpkgVersion() const; + QString getPluginVersion(QString const &pluginFileName) const; + QStringList getDCVersion() const; + + qint64 getFileSize(QString const &fileName) const; public: explicit Worker(hwinf *hw, @@ -105,6 +140,15 @@ public: static int read1stLineOfFile(QString fileName); + //friend QDebug operator<<(QDebug debug, Worker const &w) { + // Q_UNUSED(w); + // return debug; + //} + //friend QString& operator<<(QString &str, Worker const &w) { + // Q_UNUSED(w); + // return str; + //} + signals: void handleChangedFiles(QStringList); void summarizeUpload(QStringList); @@ -126,7 +170,4 @@ private slots: void onHandleChangedFiles(QStringList); }; -//Q_DECLARE_METATYPE((QHash)) -//Q_DECLARE_ASSOCIATIVE_CONTAINER_METATYPE(QHash) - #endif // WORKER_H_INCLUDED From 0e0363f131e3077d0be52238a41237f13dc23f0d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:42:18 +0200 Subject: [PATCH 057/239] Additinal UPDATE_STATE constants. Added struct UpdateStatus for printing debug messages. Added member variables used for sending SENDCMD to ISMAS.A Added helper functions getATBQTVersion(), getCPUSerial(), getRaucVersion(), getOpkgVersion(), getPluginVersion(), getDCVersion(), getFileSize(). Removed automatic restart of Apism. Added emergency timer to end application after 10 mintes. onHandleChangedFilenames(): handling of opkg_commands handling of json/dc -> deactivated for the moment. Re-implemented onSendCmdSendVersionToIsmas(): use only one parameter of type PSAInstalled. Implemented operators<<() to print debug messages. --- worker.cpp | 442 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 361 insertions(+), 81 deletions(-) diff --git a/worker.cpp b/worker.cpp index 049795b..910778b 100644 --- a/worker.cpp +++ b/worker.cpp @@ -43,7 +43,8 @@ Worker::Worker(hwinf *hw, QObject *parent, char const *serialInterface, char const *baudrate) - : m_workerThread("workerThread") + : m_hw(hw) + , m_workerThread("workerThread") , m_apismClient(0, 0, 0, this) // TODO , m_customerNr(customerNr) , m_customerNrStr(QString("customer_") + QString::number(m_customerNr).rightJustified(3, '0')) @@ -51,10 +52,22 @@ Worker::Worker(hwinf *hw, , m_zoneNr(zoneNr) , m_workingDirectory(workingDirectory) , m_branchName(branchName) + , m_customerRepositoryPath(QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(m_customerNrStr)) , m_customerRepository(QDir::cleanPath(m_workingDirectory + QDir::separator() + m_customerNrStr)) , m_gc(m_customerNrStr, m_customerRepository, m_workingDirectory, m_branchName, this) , m_maintenanceMode(maintenanceMode) , m_osVersion(getOsVersion()) + , m_atbqtVersion(getATBQTVersion()) + , m_cpuSerial(getCPUSerial()) + , m_raucVersion(getRaucVersion()) + , m_opkgVersion(getOpkgVersion()) + , m_pluginVersionATBDeciceController(getPluginVersion("/opt/app/ATBAPP/plugins/libATBDeviceControllerPlugin.so")) + , m_pluginVersionIngenicoISelf(getPluginVersion("/opt/app/ATBAPP/plugins/libIngenicoISelf_CCPlugin.so")) + , m_pluginVersionMobilisisCalc(getPluginVersion("/opt/app/ATBAPP/plugins/libMOBILISIS_CalculatePricePlugin.so")) + , m_pluginVersionMobilisisCalcConfig(getPluginVersion("/opt/app/ATBAPP/plugins/libMOBILISIS_CalculatePricePlugin_ConfigUi.so")) + , m_pluginVersionPrmCalc(getPluginVersion("/opt/app/ATBAPP/plugins/libPRM_CalculatePricePlugin.so")) + , m_pluginVersionPrmCalcConfig(getPluginVersion("/opt/app/ATBAPP/plugins/libPRM_CalculatePricePlugin_ConfigUi.so")) + , m_pluginVersionTcpZvt(getPluginVersion("/opt/app/ATBAPP/plugins/libTCP_ZVT_CCPlugin.so")) , m_ismasUpdateRequests(ISMAS_UPDATE_REQUESTS) , m_waitForNewUpdates(this) { @@ -62,21 +75,23 @@ Worker::Worker(hwinf *hw, qInfo() << "CURRENT TIME ..............." << QDateTime::currentDateTime().toString(Qt::ISODate); qInfo() << "OS VERSION ................." << m_osVersion; + qInfo() << "ATBQT VERSION .............." << m_atbqtVersion; + qInfo() << "CPU SERIAL ................." << m_cpuSerial; qInfo() << "CUSTOMER_NR ................" << m_customerNr; qInfo() << "CUSTOMER_NR_STR ............" << m_customerNrStr; - qInfo() << "CUSTOMER_REPOSITORY_PATH ..." << QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(m_customerNrStr); + qInfo() << "CUSTOMER_REPOSITORY_PATH ..." << m_customerRepositoryPath; qInfo() << "CUSTOMER_REPOSITORY ........" << m_customerRepository; qInfo() << "MACHINE_NR ................." << m_machineNr; qInfo() << "ZONE_NR ...................." << m_zoneNr; qInfo() << "BRANCH_NAME ................" << m_branchName; qInfo() << "WORKING_DIRECTORY .........." << m_workingDirectory; - QProcess p; - p.start("/bin/systemctl", {"restart", "apism"}); - if (!p.waitForStarted(5000) || !p.waitForFinished(5000)) { - qCritical() << "APISM-RESTART-FAILURE"; - return; - } + //QProcess p; + //p.start("/bin/systemctl", {"restart", "apism"}); + //if (!p.waitForStarted(5000) || !p.waitForFinished(5000)) { + // qCritical() << "APISM-RESTART-FAILURE"; + // return; + //} this->moveToThread(&m_workerThread); m_workerThread.start(); @@ -105,15 +120,22 @@ Worker::Worker(hwinf *hw, connect(this, SIGNAL(terminateUpdateProcess()), this, SLOT(onTerminateUpdateProcess()), Qt::QueuedConnection); + connect(&m_emergencyTimer, SIGNAL(timeout()), this, SLOT(onTerminateUpdateProcess()), Qt::QueuedConnection); + m_emergencyTimer.setSingleShot(true); + m_emergencyTimer.start(1000 * 60 * 10); + QDir customerRepository(m_customerRepository); if (!customerRepository.exists()) { if (m_gc.gitCloneAndCheckoutBranch()) { // do nothing else, not even executing opkg-commands - onFinishUpdateProcess(false); + emit this->finishUpdateProcess(false); } } else { - m_update = new Update(hw, m_customerRepository, m_customerNrStr, - m_branchName, m_workingDirectory, + m_update = new Update(m_hw, + m_customerRepository, + m_customerNrStr, + m_branchName, + m_workingDirectory, dryRun, parent, serialInterface, baudrate); connect(&m_startUpdateProcess, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); @@ -150,36 +172,111 @@ QString Worker::getOsVersion() const { return "N/A"; } +QString Worker::getATBQTVersion() const { + QString const cmd = QString("echo -n $(/opt/app/ATBAPP/ATBQT -v | head -n 2 | cut -d':' -f2)"); + Command c("bash"); + if (c.execute(m_workingDirectory, QStringList() << "-c" << cmd)) { + return c.getCommandResult(); + } + return "N/A"; +} + +QString Worker::getCPUSerial() const { + QString const cmd = QString("echo -n $(cat /proc/cpuinfo | grep -i Serial | cut -d':' -f2)"); + Command c("bash"); + if (c.execute(m_workingDirectory, QStringList() << "-c" << cmd)) { + return c.getCommandResult(); + } + return "N/A"; +} + +QString Worker::getRaucVersion() const { + QString const cmd = QString("echo -n $(rauc --version)"); + Command c("bash"); + if (c.execute(m_workingDirectory, QStringList() << "-c" << cmd)) { + return c.getCommandResult(); + } + return "N/A"; +} + +QString Worker::getOpkgVersion() const { + QString const cmd = QString("echo -n $(opkg --version)"); + Command c("bash"); + if (c.execute(m_workingDirectory, QStringList() << "-c" << cmd)) { + return c.getCommandResult(); + } + return "N/A"; +} + +QString Worker::getPluginVersion(QString const &pluginFileName) const { + QString const cmd = QString("echo -n $(strings %1 | grep \\\"Version\\\" | cut -d':' -f2 | tr -d '\"' | tr -d ',')").arg(pluginFileName); + Command c("bash"); + if (c.execute(m_workingDirectory, QStringList() << "-c" << cmd)) { + return c.getCommandResult(); + } + return "N/A"; +} + +QStringList Worker::getDCVersion() const { + QStringList lst = (QStringList() << "N/A" << "N/A"); + if (m_hw) { + m_hw->dc_autoRequest(true); // turn auto-request setting on + + QByteArray const cmp(8, char(0)); + QByteArray hw(""), sw(""); + for (int i=0; i<5; ++i) { + hw = m_hw->dc_getHWversion().toUtf8(); + sw = m_hw->dc_getSWversion().toUtf8(); + if (!hw.startsWith(cmp)) { + lst.clear(); + qInfo() << hw << sw; + lst << hw << sw; + break; + } + QThread::sleep(1); + } + } + return lst; +} + +qint64 Worker::getFileSize(QString const &fileName) const { + // fileName has to be an absolute path + QFileInfo fInfo(fileName); + return fInfo.exists() ? fInfo.size() : -1; +} + void Worker::onHandleChangedFiles(QStringList changedFiles) { - qCritical() << QDir::currentPath() << "ON HANDLE CHANGED FILES" << changedFiles; QString opkg_commands; static const QRegularExpression re("^.*opkg_commands\\s*$"); static const QRegularExpression comment("^\\s*#.*$"); int idx = changedFiles.indexOf(re); if (idx != -1) { - m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMANDS_REQUEST; - m_statusDescription = "EXECUTE OPKG COMMANDS"; - opkg_commands = changedFiles.takeAt(idx); - QFile f(opkg_commands); - if (f.open(QIODevice::ReadOnly)) { - QTextStream in(&f); - while (!in.atEnd()) { - QString line = in.readLine(); - if (line.indexOf(comment, 0) == -1) { - // found opkg command - QString opkgCommand = line.trimmed(); - executeOpkgCommand(opkgCommand); + qInfo() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMANDS, + QString("EXEC OPKG-COMMANDS FOR ") + opkg_commands); + + if (QDir::setCurrent(m_customerRepository)) { + QFile f(opkg_commands); + if (f.exists()) { + if (f.open(QIODevice::ReadOnly)) { + QTextStream in(&f); + while (!in.atEnd()) { + QString line = in.readLine(); + if (line.indexOf(comment, 0) == -1) { + // found opkg command + QString opkgCommand = line.trimmed(); + executeOpkgCommand(opkgCommand); + } + } + f.close(); + + qInfo() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS, + QString("EXECUTING OPKG-COMMANDS OK")); } } } - - // m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS; - // m_statusDescription = QString("EXECUTE OPKG COMMANDS %1 OK").arg(opkgCommands.join('\n')); - - f.close(); } if (m_update->doUpdate(changedFiles)) { // first update the hardware @@ -213,7 +310,7 @@ void Worker::onHandleChangedFiles(QStringList changedFiles) { } } if (!error) { - onFinishUpdateProcess(true); + emit this->finishUpdateProcess(true); return; } } @@ -277,11 +374,14 @@ void Worker::executeOpkgCommand(QString opkgCommand) { Command c(opkgCommand); if (c.execute(m_workingDirectory)) { QString const r = c.getCommandResult(); - m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS; - m_statusDescription = QString("EXECUTE OPKG COMMAND %1 OK").arg(opkgCommand); + qInfo() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS, + QString("EXECUTE OPKG COMMAND %1 OK: %2") + .arg(opkgCommand) + .arg(c.getCommandResult())); } else { - m_updateStatus = UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE; - m_statusDescription = QString("EXECUTE OPKG COMMAND %1 FAILED").arg(opkgCommand); + qCritical() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE, + QString("EXECUTE OPKG COMMAND %1 FAILED") + .arg(opkgCommand)); onTerminateUpdateProcess(); return; } @@ -289,8 +389,14 @@ void Worker::executeOpkgCommand(QString opkgCommand) { // sollte ParameterResponse heissen void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { + + qInfo() << "IN ON_ISMAS_RESPONSE_RECEIVED" << QThread::currentThread()->objectName(); + if (!ismasResponse.isEmpty()) { QStringList const keys = ismasResponse.keys(); + qInfo() << UpdateStatus(UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED, + QString("RECEIVED JSON WITH KEYS: ") + keys.join(",")); + static QRegularExpression re("^REQ_ISMASPARAMETER.*"); if(keys.indexOf(re) >= 0) { m_waitForNewUpdates.stop(); // stop asking ISMAS for updates @@ -328,10 +434,19 @@ void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { if (!v.isNull() && !v.isUndefined()) { QString const s = v.toString(""); if (s == "WAIT") { - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS; m_ismasUpdateRequests = ISMAS_UPDATE_REQUESTS; - m_statusDescription = "ISMAS UPDATES AVAILABLE"; + + qInfo() << UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS, + "DETECTED AVAILABLE ISMAS-DOWNLOAD"); + + QString const &data = m_ismasClient.updateOfPSAActivated(); + m_apismClient.onSendCmdEventToIsmas(data); + emit m_gc.ismasUpdatesAvailable(); + } else { + // TODO: enorm wichtig + qCritical() << "DID NOT RECEIVE 'WAIT' BUT" << s; + onTerminateUpdateProcess(); } } } else { @@ -339,6 +454,7 @@ void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { m_statusDescription = "NO FILEUPLOAD KEY AVAILABLE"; return; } + } } else { m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; @@ -347,64 +463,108 @@ void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { } void Worker::onFinishUpdateProcess(bool changes) { - qCritical() << "ON FINISH UPDATE PROCESS. CHANGES=" << changes; + Q_UNUSED(changes); + + qInfo() << "ON FINISH UPDATE PROCESS" << QThread::currentThread()->objectName(); + // m_emergencyTimer.stop(); + + onSendCmdSendVersionToIsmas(); // final message to ISMAS + m_workerThread.quit(); QApplication::quit(); + exit(0); } void Worker::onTerminateUpdateProcess() { qCritical() << "ON TERMINATE UPDATE PROCESS"; + + onSendCmdSendVersionToIsmas(); // final message to ISMAS + m_workerThread.quit(); QApplication::quit(); + exit(-1); } void Worker::onSendCmdSendVersionToIsmas() { - QString const tariffVersion = "0.0.1"; - QString const tariffProject = "test_project"; - int tariffZone = 1; - QString const tariffInfo = "test_tariff_info"; - QString const tariffLoadTime = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); - // QString const linuxVersion = "test_linux_version"; - QString const cpuSerial = "test_cpu_serial"; - QString const deviceControllerVersion = "test_dc_version"; - QString const deviceControllerGitBlob = "test_dc_blob_2a3b4f50"; - QString const deviceControllerGitLastCommit = "test_dc_commit_12345abc"; - QString const raucVersion = "test_rauc_version"; - QString const opkgVersion = "test_opkg_version"; - QString const atbQTVersion = "test_qtbqt_version"; - QString const atbQTGitDescribe = "test_atbqt_git_describe"; - QString const deviceControllerPluginVersion = "test_CAmaster_version"; - QString const ingenicoISelfCCPluginVersion = "test_ingenico_plugin_version"; - QString const mobilisisCalculatePricePluginVersion = "test_mobilisis_plugin_version"; - QString const mobilisisCalculatePriceConfigUiVersion = "test_mobilisis_config_ui_plugin"; - QString const prmCalculatePricePluginVersion = "test_prm_calculate_price_plugin"; - QString const prmCalculatePriceConfigUiPluginVersion = "test_prm_calculate_price_config_ui_plugin"; - QString const tcpZVTPluginVersion = "test_tcp_zvt_plugin"; + QStringList const dcVersion = getDCVersion(); + QString const deviceControllerVersionHW = dcVersion.first(); + QString const deviceControllerVersionSW = dcVersion.last(); - QString data = m_ismasClient.updateOfPSASendVersion(tariffVersion, - tariffProject, - tariffZone, - tariffInfo, - tariffLoadTime, - m_osVersion, - cpuSerial, - deviceControllerVersion, - deviceControllerGitBlob, - deviceControllerGitLastCommit, - raucVersion, - opkgVersion, - atbQTVersion, - atbQTGitDescribe, - deviceControllerPluginVersion, - ingenicoISelfCCPluginVersion, - mobilisisCalculatePricePluginVersion, - mobilisisCalculatePriceConfigUiVersion, - prmCalculatePricePluginVersion, - prmCalculatePriceConfigUiPluginVersion, - tcpZVTPluginVersion); - m_apismClient.sendCmdSendVersionToIsmas(data); + qInfo() << "CURRENT DC-HW-VERSION: " << deviceControllerVersionHW; + qInfo() << "CURRENT DC-SW-VERSION: " << deviceControllerVersionSW; + + QString const deviceControllerGitBlob = "N/A"; + QString const deviceControllerGitLastCommit = "N/A"; + + PSAInstalled psaInstalled; + QString printSysDir("/etc/psa_config"); + QString tariffSysDir("/etc/psa_tariff"); + QString absPathName; + + if (m_zoneNr != 0) { + QString const &n = QString("%1").arg(m_zoneNr).rightJustified(2, '0'); + psaInstalled.tariff.name = QString("tariff%1.json").arg(n); + absPathName = QDir::cleanPath(tariffSysDir + QDir::separator() + psaInstalled.tariff.name); + psaInstalled.tariff.blob = m_gc.gitBlob(absPathName); + psaInstalled.tariff.size = getFileSize(absPathName); + psaInstalled.tariff.zone = m_zoneNr; + } + psaInstalled.tariff.project = "Szeged"; + psaInstalled.tariff.info = "N/A"; + psaInstalled.tariff.loadTime = "N/A"; // QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + psaInstalled.tariff.version = "N/A"; + + psaInstalled.hw.linuxVersion = m_osVersion; + psaInstalled.hw.cpuSerial = m_cpuSerial; + + psaInstalled.dc.versionHW = deviceControllerVersionHW; + psaInstalled.dc.versionSW = deviceControllerVersionSW; + psaInstalled.dc.gitBlob = "N/A"; + psaInstalled.dc.gitLastCommit = "N/A"; + psaInstalled.dc.size = -1; + + psaInstalled.sw.raucVersion = m_raucVersion; + psaInstalled.sw.opkgVersion = m_opkgVersion; + psaInstalled.sw.atbQTVersion = m_atbqtVersion; + + psaInstalled.pluginVersion.deviceController = m_pluginVersionATBDeciceController; + psaInstalled.pluginVersion.ingenicoISelfCC = m_pluginVersionIngenicoISelf; + psaInstalled.pluginVersion.mobilisisCalculatePrice = m_pluginVersionMobilisisCalc; + psaInstalled.pluginVersion.mobilisisCalculatePriceConfigUi = m_pluginVersionMobilisisCalcConfig; + psaInstalled.pluginVersion.prmCalculatePrice = m_pluginVersionPrmCalc; + psaInstalled.pluginVersion.prmCalculatePriceConfigUi = m_pluginVersionPrmCalcConfig; + psaInstalled.pluginVersion.tcpZVT = m_pluginVersionTcpZvt; + + psaInstalled.cash.name = "DC2C_cash.json"; + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.cash.name); + psaInstalled.cash.blob = m_gc.gitBlob(absPathName); + psaInstalled.cash.size = getFileSize(absPathName); + + psaInstalled.conf.name = "DC2C_conf.json"; + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.conf.name); + psaInstalled.conf.blob = m_gc.gitBlob(absPathName); + psaInstalled.conf.size = getFileSize(absPathName); + + psaInstalled.device.name = "DC2C_device.json"; + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.device.name); + psaInstalled.device.blob = m_gc.gitBlob(absPathName); + psaInstalled.device.size = getFileSize(absPathName); + + for (int i=0; i < 32; ++i) { + QString const &n = QString("%1").arg(i+1).rightJustified(2, '0'); + psaInstalled.print[i].name = QString("DC2C_print%1.json").arg(n); + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.print[i].name); + psaInstalled.print[i].blob = m_gc.gitBlob(absPathName); + psaInstalled.print[i].size = getFileSize(absPathName); + } + + QString data = m_ismasClient.updateOfPSASendVersion(psaInstalled); + + // printf("data=%s\n", data.toStdString().c_str()); + + m_apismClient.onSendCmdSendVersionToIsmas(data); } void Worker::askIsmasForNewData() { @@ -413,7 +573,14 @@ void Worker::askIsmasForNewData() { QString data = m_ismasClient.setUpdatesAvailable(); m_apismClient.emulateUpdatesAvailable(data); } - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING; + + //m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING; + //m_statusDescription = "Ask ISMAS IF NEW DATA AVAILABLE"; + + qInfo() << UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING, + QString("ASK ISMAS IF NEW DATA AVAILABLE") + + QString(" (%1)").arg(m_ismasUpdateRequests)); + m_apismClient.requestAvailableIsmasUpdates(); if (--m_ismasUpdateRequests > 0) { @@ -425,3 +592,116 @@ void Worker::askIsmasForNewData() { onTerminateUpdateProcess(); } } + +/************************************************************************************************ + * operators + */ +QDebug operator<< (QDebug debug, UpdateStatus status) { + switch(status.m_updateStatus) { + case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: + debug << QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: + debug << QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: + debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: + debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: + debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: + debug << QString("UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_PULL_UPDATES_SUCCESS: + debug << QString("UPDATE_STATUS::GIT_PULL_UPDATES_REQUEST: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: + debug << QString("UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMANDS: + debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: + debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: + debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: + debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: ") + << status.m_statusDescription; + break; + default:; + } + return debug; +} + +QString& operator<< (QString& str, UpdateStatus status) { + switch(status.m_updateStatus) { + case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: + str = QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: + str = QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: + str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: + str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: + str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_PULL_UPDATES_SUCCESS: + str = QString("UPDATE_STATUS::GIT_PULL_UPDATES_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: + str = QString("UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: + str = QString("UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMANDS: + str = QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: + str = QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: + str = QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: + str = QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: "); + str += status.m_statusDescription; + break; + default:; + } + return str; +} From e35fb9dc19eda567adcee4b3bbd942dfb13b4a04 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:43:24 +0200 Subject: [PATCH 058/239] Commented out debug-messages --- process/command.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/process/command.cpp b/process/command.cpp index 93c579c..f4049a6 100644 --- a/process/command.cpp +++ b/process/command.cpp @@ -45,8 +45,8 @@ bool Command::execute(QString workingDirectory, QStringList args) { connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); - qCritical() << "START COMMAND" << m_command << "WITH ARGS" << args - << "IN" << workingDirectory; + //qCritical() << "START COMMAND" << m_command << "WITH ARGS" << args + // << "IN" << workingDirectory; p->setWorkingDirectory(workingDirectory); if (!args.isEmpty()) { @@ -56,14 +56,14 @@ bool Command::execute(QString workingDirectory, QStringList args) { } if (p->waitForStarted(m_waitForStartTimeout)) { - qDebug() << "PROCESS" << m_command << "STARTED"; + //qDebug() << "PROCESS" << m_command << "STARTED"; if (p->state() == QProcess::ProcessState::Running) { - qDebug() << "PROCESS" << m_command << "RUNNING"; + //qDebug() << "PROCESS" << m_command << "RUNNING"; if (p->waitForFinished(m_waitForFinishTimeout)) { - qDebug() << "PROCESS" << m_command << "FINISHED"; + //qDebug() << "PROCESS" << m_command << "FINISHED"; if (p->exitStatus() == QProcess::NormalExit) { - qInfo() << "EXECUTED" << m_command - << "with code" << p->exitCode(); + //qInfo() << "EXECUTED" << m_command + // << "with code" << p->exitCode(); // qInfo() << "RESULT" << m_commandResult; return true; } else { From b8d6c909ebc9205f11c0165b3ae4922911137c94 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:44:26 +0200 Subject: [PATCH 059/239] Minor change: reset output buffer-length to 1024 --- message_handler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_handler.cpp b/message_handler.cpp index 3af5ff1..55d6d7e 100755 --- a/message_handler.cpp +++ b/message_handler.cpp @@ -6,7 +6,7 @@ #include #include -#define OUTPUT_LEN (512) +#define OUTPUT_LEN (1024) static bool installedMsgHandler = false; static QtMsgType debugLevel = QtInfoMsg; From a54f4f94c9edb1ac53abd43183d0fc767cd2dad1 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:45:43 +0200 Subject: [PATCH 060/239] Minor change: remove not needed customerNrStr-variable --- main.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.cpp b/main.cpp index 1a6a7ea..8abe704 100644 --- a/main.cpp +++ b/main.cpp @@ -135,9 +135,12 @@ int main(int argc, char *argv[]) { int machineNr = Worker::read1stLineOfFile("/etc/machine_nr"); int customerNr = Worker::read1stLineOfFile("/etc/cust_nr"); - QString customerNrStr = QString("customer_") + QString::number(customerNr).rightJustified(3, '0'); int zoneNr = Worker::read1stLineOfFile("/etc/zone_nr"); - QString branchName = QString("zg1/zone%1").arg(zoneNr); + QString const branchName = (zoneNr != 0) + ? QString("zg1/zone%1").arg(zoneNr) : "master"; + + QThread::currentThread()->setObjectName("main thread"); + qInfo() << "Main thread" << QThread::currentThreadId(); Worker worker(hw, customerNr, @@ -146,8 +149,7 @@ int main(int argc, char *argv[]) { branchName, workingDir, maintenanceMode, - dryRun - ); + dryRun); return a.exec(); } From 4dc0c71d3f00efc10114036d9e5b2bc7a70adc65 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:46:29 +0200 Subject: [PATCH 061/239] Add -g flag even for release build --- OnDemandUpdatePTU.pro | 1 + 1 file changed, 1 insertion(+) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 9cdd8b2..233468c 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -14,6 +14,7 @@ CONFIG += c++17 console DEFINES+=APP_VERSION=\\\"$$VERSION\\\" +QMAKE_CXXFLAGS += -g QMAKE_CXXFLAGS += -Wno-deprecated-copy # custom target for 'git subtree' From 6caaae6d50e58930a73dcaebfe5180c675d73f14 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:48:42 +0200 Subject: [PATCH 062/239] Add struct PSAInstalled. Changed interface of updateOfPSASendVersion into QString updateOfPSASendVersion(PSAInstalled const &psa); Re-implemented updateOfPSASendVersion(). --- ismas/ismas_client.cpp | 296 ++++++++++++++++++++++++++++++++++------- ismas/ismas_client.h | 126 +++++++++++++++--- 2 files changed, 354 insertions(+), 68 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 8f957fc..7c57811 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -53,32 +53,13 @@ QString IsmasClient::updateNewsToIsmas(char const *event, return buf; } -QString IsmasClient::updateOfPSASendVersion(QString const &tariffVersion, - QString const &tariffProject, - int tariffZone, - QString const &tariffInfo, - QString const &tariffLoadTime, - QString const &linuxVersion, - QString const &cpuSerial, - QString const &deviceControllerVersion, - QString const &deviceControllerGitBlob, - QString const &deviceControllerGitLastCommit, - QString const &raucVersion, - QString const &opkgVersion, - QString const &atbQTVersion, - QString const &atbQTGitDescribe, - QString const &deviceControllerPluginVersion, - QString const &ingenicoISelfCCPluginVersion, - QString const &mobilisisCalculatePricePluginVersion, - QString const &mobilisisCalculatePriceConfigUiVersion, - QString const &prmCalculatePricePluginVersion, - QString const &prmCalculatePriceConfigUiPluginVersion, - QString const &tcpZVTPluginVersion) { - char buf[4096]; +QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { + //static int const constexpr SIZE = 4096*8; + static char buf[4096*2]; memset(buf, 0, sizeof(buf)); QString const ts = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); - QString sendVersionHash; + QString sendVersionHash = "N/A"; // local data="#M=APISM#C=CMD_SENDVERSION#J= snprintf(buf, sizeof(buf)-1, @@ -92,8 +73,152 @@ QString IsmasClient::updateOfPSASendVersion(QString const &tariffVersion, "\"PROJECT\" : \"%s\"," "\"ZONE\" : %d," "\"INFO\" : \"%s\"," + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d," "\"LOADED\" : \"%s\"" "}," + "\"JSON\" : {" + "\"DC2C_CASH\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_CONF\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_DEVICE\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_01\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_02\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_03\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_04\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_05\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_06\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_07\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_08\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_09\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_10\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_11\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_12\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_13\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_14\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_15\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_16\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_17\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_18\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_19\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_20\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_21\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_22\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_23\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_24\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_25\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_26\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_27\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_28\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_29\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_30\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_31\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}," + "\"DC2C_PRINT_32\" : {" + "\"BLOB\" : \"%s\"," + "\"SIZE\" : %d" + "}" + "}," "\"HARDWARE\" : {" "\"DEVICES\" : [\"PTU5\", \"DC\", \"PRINTER\", \"BNA\"]" "}," @@ -105,7 +230,9 @@ QString IsmasClient::updateOfPSASendVersion(QString const &tariffVersion, "\"CPU_SERIAL\" : \"%s\"" "}," "\"DC\" : {" - "\"VERSION\" : \"%s\"," + "\"HW-VERSION\" : \"%s\"," + "\"SW-VERSION\" : \"%s\"," + "\"SIZE\" : %d," "\"GITBLOB\" : \"%s\"," "\"GITLASTCOMMIT\" : \"%s\"" "}," @@ -118,8 +245,7 @@ QString IsmasClient::updateOfPSASendVersion(QString const &tariffVersion, "\"RAUC\" : \"%s\"," "\"OPKG\" : \"%s\"," "\"ATBQT\" : {" - "\"VERSION\" : \"%s\"," - "\"GIT_DESCRIBE\" : \"%s\"" + "\"VERSION\" : \"%s\"" "}" "}," "\"PLUGINS\" : {" @@ -149,32 +275,108 @@ QString IsmasClient::updateOfPSASendVersion(QString const &tariffVersion, ts.toStdString().c_str(), sendVersionHash.toStdString().c_str(), - tariffVersion.toStdString().c_str(), - tariffProject.toStdString().c_str(), - tariffZone, - tariffInfo.toStdString().c_str(), - tariffLoadTime.toStdString().c_str(), + psa.tariff.version.toStdString().c_str(), + psa.tariff.project.toStdString().c_str(), + psa.tariff.zone, + psa.tariff.info.toStdString().c_str(), + psa.tariff.blob.toStdString().c_str(), + psa.tariff.size, + psa.tariff.loadTime.toStdString().c_str(), - linuxVersion.toStdString().c_str(), + psa.cash.blob.toStdString().c_str(), + psa.cash.size, + psa.conf.blob.toStdString().c_str(), + psa.conf.size, + psa.device.blob.toStdString().c_str(), + psa.device.size, - cpuSerial.toStdString().c_str(), + psa.print[0].blob.toStdString().c_str(), + psa.print[0].size, + psa.print[1].blob.toStdString().c_str(), + psa.print[1].size, + psa.print[2].blob.toStdString().c_str(), + psa.print[2].size, + psa.print[3].blob.toStdString().c_str(), + psa.print[3].size, + psa.print[4].blob.toStdString().c_str(), + psa.print[4].size, + psa.print[5].blob.toStdString().c_str(), + psa.print[5].size, + psa.print[6].blob.toStdString().c_str(), + psa.print[6].size, + psa.print[7].blob.toStdString().c_str(), + psa.print[7].size, + psa.print[8].blob.toStdString().c_str(), + psa.print[8].size, + psa.print[9].blob.toStdString().c_str(), + psa.print[9].size, + psa.print[10].blob.toStdString().c_str(), + psa.print[10].size, + psa.print[11].blob.toStdString().c_str(), + psa.print[11].size, + psa.print[12].blob.toStdString().c_str(), + psa.print[12].size, + psa.print[13].blob.toStdString().c_str(), + psa.print[13].size, + psa.print[14].blob.toStdString().c_str(), + psa.print[14].size, + psa.print[15].blob.toStdString().c_str(), + psa.print[15].size, + psa.print[16].blob.toStdString().c_str(), + psa.print[16].size, + psa.print[17].blob.toStdString().c_str(), + psa.print[17].size, + psa.print[18].blob.toStdString().c_str(), + psa.print[18].size, + psa.print[19].blob.toStdString().c_str(), + psa.print[19].size, + psa.print[20].blob.toStdString().c_str(), + psa.print[20].size, + psa.print[21].blob.toStdString().c_str(), + psa.print[21].size, + psa.print[22].blob.toStdString().c_str(), + psa.print[22].size, + psa.print[23].blob.toStdString().c_str(), + psa.print[23].size, + psa.print[24].blob.toStdString().c_str(), + psa.print[24].size, + psa.print[25].blob.toStdString().c_str(), + psa.print[25].size, + psa.print[26].blob.toStdString().c_str(), + psa.print[26].size, + psa.print[27].blob.toStdString().c_str(), + psa.print[27].size, + psa.print[28].blob.toStdString().c_str(), + psa.print[28].size, + psa.print[29].blob.toStdString().c_str(), + psa.print[29].size, + psa.print[30].blob.toStdString().c_str(), + psa.print[30].size, + psa.print[31].blob.toStdString().c_str(), + psa.print[31].size, - deviceControllerVersion.toStdString().c_str(), - deviceControllerGitBlob.toStdString().c_str(), - deviceControllerGitLastCommit.toStdString().c_str(), + psa.hw.linuxVersion.toStdString().c_str(), + psa.hw.cpuSerial.toStdString().c_str(), - raucVersion.toStdString().c_str(), - opkgVersion.toStdString().c_str(), - atbQTVersion.toStdString().c_str(), - atbQTGitDescribe.toStdString().c_str(), + psa.dc.versionHW.toStdString().c_str(), + psa.dc.versionSW.toStdString().c_str(), + psa.dc.size, + psa.dc.gitBlob.toStdString().c_str(), + psa.dc.gitLastCommit.toStdString().c_str(), - deviceControllerPluginVersion.toStdString().c_str(), - ingenicoISelfCCPluginVersion.toStdString().c_str(), - mobilisisCalculatePricePluginVersion.toStdString().c_str(), - mobilisisCalculatePriceConfigUiVersion.toStdString().c_str(), - prmCalculatePricePluginVersion.toStdString().c_str(), - prmCalculatePriceConfigUiPluginVersion.toStdString().c_str(), - tcpZVTPluginVersion.toStdString().c_str()); + psa.sw.raucVersion.toStdString().c_str(), + psa.sw.opkgVersion.toStdString().c_str(), + psa.sw.atbQTVersion.toStdString().c_str(), + + psa.pluginVersion.deviceController.toStdString().c_str(), + psa.pluginVersion.ingenicoISelfCC.toStdString().c_str(), + psa.pluginVersion.mobilisisCalculatePrice.toStdString().c_str(), + psa.pluginVersion.mobilisisCalculatePriceConfigUi.toStdString().c_str(), + psa.pluginVersion.prmCalculatePrice.toStdString().c_str(), + psa.pluginVersion.prmCalculatePriceConfigUi.toStdString().c_str(), + psa.pluginVersion.tcpZVT.toStdString().c_str()); + + printf("buf=%s\n", buf); return buf; } diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index 12946ff..7fbd5a7 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -4,6 +4,110 @@ #include #include +struct PSAInstalled { + struct Tariff { + QString name; + QString version; + QString project; + int zone; + int size; + QString blob; + QString info; + QString loadTime; + } tariff; + + struct HardWare { + QString linuxVersion; + QString cpuSerial; + } hw; + + struct DC { + QString versionHW; + QString versionSW; + QString gitBlob; + QString gitLastCommit; + int size; + } dc; + + struct SoftWare { + QString raucVersion; + QString opkgVersion; + QString atbQTVersion; + } sw; + + struct PluginVersion { + QString deviceController; + QString ingenicoISelfCC; + QString mobilisisCalculatePrice; + QString mobilisisCalculatePriceConfigUi; + QString prmCalculatePrice; + QString prmCalculatePriceConfigUi; + QString tcpZVT; + } pluginVersion; + + struct DC2C { + QString name; + QString blob; + int size; + }; + + DC2C cash; + DC2C conf; + DC2C device; + + DC2C print[32]; + + explicit PSAInstalled() { + tariff.name = "N/A"; + tariff.version = "N/A"; + tariff.project = "N/A"; + tariff.zone = -1; + tariff.size = -1; + tariff.blob = "N/A"; + tariff.info = "N/A"; + tariff.loadTime = "N/A"; + + hw.linuxVersion = "N/A"; + hw.cpuSerial = "N/A"; + + dc.versionHW = "N/A"; + dc.versionSW = "N/A"; + dc.gitBlob = "N/A"; + dc.gitLastCommit = "N/A"; + dc.size = -1; + + sw.raucVersion = "N/A"; + sw.opkgVersion = "N/A"; + sw.atbQTVersion = "N/A"; + + pluginVersion.deviceController = "N/A"; + pluginVersion.ingenicoISelfCC = "N/A"; + pluginVersion.mobilisisCalculatePrice = "N/A"; + pluginVersion.mobilisisCalculatePriceConfigUi = "N/A"; + pluginVersion.prmCalculatePrice = "N/A"; + pluginVersion.prmCalculatePriceConfigUi = "N/A"; + pluginVersion.tcpZVT = "N/A"; + + cash.name = "N/A"; + cash.blob = "N/A"; + cash.size = -1; + + conf.name = "N/A"; + conf.blob = "N/A"; + conf.size = -1; + + device.size = -1; + device.blob = "N/A"; + device.size = -1; + + for (int i=0; i < 32; ++i) { + print[i].size = -1; + print[i].blob = "N/A"; + print[i].size = -1; + } + } +}; + class IsmasClient : public QObject { Q_OBJECT @@ -17,27 +121,7 @@ public: QString updateOfPSAActivated(); QString updateOfPSASucceeded(); - QString updateOfPSASendVersion(QString const &tariffVersion, - QString const &tariffProject, - int tariffZone, - QString const &tariffInfo, - QString const &tariffLoadTime, - QString const &linuxVersion, - QString const &cpuSerial, - QString const &deviceControllerVersion, - QString const &deviceControllerGitBlob, - QString const &deviceControllerGitLastCommit, - QString const &raucVersion, - QString const &opkgVersion, - QString const &atbQTVersion, - QString const &atbQTGitDescribe, - QString const &deviceControllerPluginVersion, - QString const &ingenicoISelfCCPluginVersion, - QString const &mobilisisCalculatePricePluginVersion, - QString const &mobilisisCalculatePriceConfigUiVersion, - QString const &prmCalculatePricePluginVersion, - QString const &prmCalculatePriceConfigUiPluginVersion, - QString const &tcpZVTPluginVersion); + QString updateOfPSASendVersion(PSAInstalled const &psa); QString setUpdatesAvailable(); bool checkForAvailableUpdates(); From 1bdae56342cd189c2e979e801f3c2fbae38a67e9 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:50:54 +0200 Subject: [PATCH 063/239] Added debug messages. Fixed gitBlob(): do not check for existing customer repository. --- git/git_client.cpp | 68 +++++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index a723455..92ef124 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -29,22 +29,36 @@ GitClient::GitClient(QString const &customerNrStr, void GitClient::onIsmasUpdatesAvailable() { if (QDir(m_customerRepository).exists()) { - qInfo() << "FETCHING OF" << m_repositoryPath - << "INTO" << m_customerRepository; + + + qInfo() << UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST, + QString("FETCHING OF ") + m_repositoryPath + + QString(" INTO ") + m_customerRepository); + std::optional changes = gitFetch(); + if (changes) { std::optional changedFileNames = gitDiff(changes.value()); if (changedFileNames) { - for (int i=0;ihandleChangedFiles(changedFileNames.value()); + } else { - qCritical() << "PULL FAILED FOR" << m_repositoryPath - << "IN " << m_customerRepository; + qCritical() << UpdateStatus(UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE, + QString("PULLING NEW FILES ") + + changedFileNames.value().join(",") + + QString(" INTO ") + m_customerRepository + " FAILED"); + emit m_worker->terminateUpdateProcess(); } } else { @@ -53,8 +67,10 @@ void GitClient::onIsmasUpdatesAvailable() { emit m_worker->finishUpdateProcess(false); } } else { - qCritical() << "NO CHANGES IN" << m_repositoryPath - << "(" << m_customerRepository << ")"; + qCritical() << UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE, + QString("NO CHANGES IN ") + m_repositoryPath + + QString(" (%1)").arg(m_customerRepository)); + emit m_worker->finishUpdateProcess(false); } } else { @@ -78,7 +94,7 @@ bool GitClient::gitCloneCustomerRepository() { << "CLONE" << m_repositoryPath << "..."; if (c.execute(m_workingDirectory)) { // execute the command in wd - QString result = c.getCommandResult(); + QString const result = c.getCommandResult(); if (!result.isEmpty()) { // Cloning into 'customer_281'...\n static QRegularExpression re("(^\\s*Cloning\\s+into\\s+[']\\s*)(.*)(\\s*['].*$)"); @@ -91,8 +107,9 @@ bool GitClient::gitCloneCustomerRepository() { } } } - qCritical() << "ERROR CLONE RESULT HAS WRONG FORMAT"; } + qCritical() << "ERROR CLONE RESULT HAS WRONG FORMAT. CLONE_RESULT=" + << result; } return false; } @@ -126,8 +143,14 @@ bool GitClient::gitCloneAndCheckoutBranch() { qInfo() << "CLONE" << m_repositoryPath << "AND CHECKOUT" << m_branchName; if (gitCloneCustomerRepository()) { //if (copyGitConfigFromMaster()) { - return gitCheckoutBranch(); + if (gitCheckoutBranch()) { + return true; + } else { + m_worker->terminateUpdateProcess(); + } //} + } else { + m_worker->terminateUpdateProcess(); } return false; } @@ -181,6 +204,9 @@ std::optional GitClient::gitFetch() { Command c("git fetch"); if (c.execute(m_customerRepository)) { QString const s = c.getCommandResult().trimmed(); + + qCritical() << "GIT RESULT" << s; + if (!s.isEmpty()) { QStringList lines = Update::split(s, '\n'); if (!lines.empty()) { @@ -257,19 +283,17 @@ QString GitClient::gitLastCommit(QString fileName) { return ""; } -// get the blob of the file(name) passed as $1 -// note: this can be used for any file in the filesystem +// fileName has to an absolute path QString GitClient::gitBlob(QString fileName) { - if (QDir(m_customerRepository).exists()) { - QString const filePath - = QDir::cleanPath(m_customerRepository + QDir::separator() + fileName); + QFileInfo fi(fileName); + if (fi.exists()) { QString const gitCommand = QString("git hash-object %1").arg(fileName); Command c(gitCommand); - if (c.execute(m_customerRepository)) { - return c.getCommandResult(); + if (c.execute(m_workingDirectory)) { + return c.getCommandResult().trimmed(); } } - return ""; + return "N/A"; } QString GitClient::gitCommitForBlob(QString blob) { From 2b934f6baf0e6c957fa9f57a57948a9d3815d2d4 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 19 Jul 2023 16:53:45 +0200 Subject: [PATCH 064/239] Added signals void sendCmdSendVersionToIsmas(QString); void sendCmdEventToIsmas(QString); and associated slots: void onSendCmdSendVersionToIsmas(QString); void onSendCmdEventToIsmas(QString); Implemented slots. --- apism/apism_client.cpp | 57 +++++++++++++++++++++++------------------- apism/apism_client.h | 9 +++++-- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/apism/apism_client.cpp b/apism/apism_client.cpp index 5415699..7d13571 100644 --- a/apism/apism_client.cpp +++ b/apism/apism_client.cpp @@ -36,6 +36,8 @@ ApismClient::ApismClient(QObject *eventReceiver, ATBHMIconfig *config, Persisten this, &ApismClient::onRequestResponseClientResponseTimeout); connect(apismTcpSendClient, &ApismTcpClient::responseTimeout, this, &ApismClient::onSendClientResponseTimeout); + connect(this, SIGNAL(sendCmdSendVersionToIsmas(QString)), + this, SLOT(onSendCmdSendVersionToIsmas(QString))); // not needed as APISM closes the socket after we send data, so readyRead() // might not even fire @@ -395,31 +397,7 @@ void ApismClient::requestAvailableIsmasUpdates() { this->apismTcpRequestResponseClient->sendData(data); } -void ApismClient::sendCmdSendVersionToIsmas(QString const &msg) { - QJsonParseError parseError; - QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); - if (parseError.error != QJsonParseError::NoError) { - qCritical() << "INVALID JSON MSG: PARSING FAILED:" - << parseError.error << parseError.errorString(); - return; - } - - if (!document.isObject()) { - qCritical() << "FILE IS NOT A JSON OBJECT!"; - return; - } - - QByteArray data = "#M=APISM#C=CMD_SENDVERSION#J="; - data += document.toJson(QJsonDocument::Compact); - - // printf("data=%s\n", QString(data).toStdString().c_str()); - - this->currentRequest = ISMAS::REQUEST::NO_REQUEST; - this->apismTcpSendClient->sendData(data); - -} - -void ApismClient::emulateUpdatesAvailable(QString const &msg) { +void ApismClient::onSendCmdEventToIsmas(QString msg) { QJsonParseError parseError; QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); if (parseError.error != QJsonParseError::NoError) { @@ -436,12 +414,36 @@ void ApismClient::emulateUpdatesAvailable(QString const &msg) { QByteArray data = "#M=APISM#C=CMD_EVENT#J="; data += document.toJson(QJsonDocument::Compact); - qDebug() << "EMULATE UPDATES AVAILABLE" << QString(data).toStdString().c_str(); + this->currentRequest = ISMAS::REQUEST::NO_REQUEST; + this->apismTcpSendClient->sendData(data); +} + +void ApismClient::onSendCmdSendVersionToIsmas(QString msg) { + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "INVALID JSON MSG: PARSING FAILED:" + << parseError.error << parseError.errorString(); + return; + } + + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + return; + } + + QByteArray data = "#M=APISM#C=CMD_SENDVERSION#J="; + data += document.toJson(QJsonDocument::Compact); this->currentRequest = ISMAS::REQUEST::NO_REQUEST; this->apismTcpSendClient->sendData(data); } +void ApismClient::emulateUpdatesAvailable(QString const &msg) { + qDebug() << "EMULATE UPDATES AVAILABLE. MSG=" << msg; + onSendCmdEventToIsmas(msg); +} + void ApismClient::sendState(const QString & state, const QString & msg) { qCritical() << "ApismClient::sendState(): "; @@ -607,6 +609,9 @@ void ApismClient::sendMininformPingRequest() void ApismClient::onReceivedResponse(QByteArray response) { + + qCritical() << "RECEIVED ------> " << response; + if (this->currentRequest == ISMAS::REQUEST::NO_REQUEST && response == "RECORD SAVED") { // sent by APISM to indicate that record return; // has been saved in DB diff --git a/apism/apism_client.h b/apism/apism_client.h index dec70a2..0f01512 100644 --- a/apism/apism_client.h +++ b/apism/apism_client.h @@ -50,6 +50,9 @@ public: quint32 getLastError(); const QString & getLastErrorDescription(); + ApismTcpClient* getApismTcpSendClient() { return apismTcpSendClient; } + + public slots: void sendSelfTest(); void sendTransaction(const VendingData* vendingData); @@ -58,7 +61,8 @@ public slots: void sendState(const QString & state, const QString & msg); void emulateUpdatesAvailable(QString const &msg); - void sendCmdSendVersionToIsmas(QString const &msg); + void onSendCmdSendVersionToIsmas(QString); + void onSendCmdEventToIsmas(QString); void requestAvailableIsmasUpdates(); @@ -80,7 +84,8 @@ signals: void sendReqSelfResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); void ismasResponseAvailable(QJsonObject ismasResponse); - + void sendCmdSendVersionToIsmas(QString); + void sendCmdEventToIsmas(QString); private slots: // void onSocketError(QAbstractSocket::SocketError socketError); From 1ddd0074b3db3034f92c36a86839bc8189189284 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 20 Jul 2023 09:06:07 +0200 Subject: [PATCH 065/239] Set c++17 for PTU5-YOCTO --- OnDemandUpdatePTU.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 233468c..e253043 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -44,6 +44,8 @@ contains( CONFIG, PTU5 ) { } contains( CONFIG, PTU5_YOCTO ) { greaterThan(QT_MAJOR_VERSION, 4): QT += serialport + QMAKE_CXXFLAGS += -std=c++17 # for GCC >= 4.7 + QMAKE_CXXFLAGS += -Wno-deprecated-copy PTU5BASEPATH = /opt/devel/ptu5 ARCH = PTU5 DEFINES+=PTU5 From ab8acfc7d1568f00b93c0ceaba5542681d5d4ccb Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 31 Jul 2023 16:55:36 +0200 Subject: [PATCH 066/239] use gui-interface for ATBUpdateTool --- mainwindow.cpp | 63 ++++++++++++++++++++++++++++++++++++++++++++ mainwindow.h | 26 ++++++++++++++++++ mainwindow.ui | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..7b60730 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,63 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "worker.h" + +#include + +MainWindow::MainWindow(Worker *worker, QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) + , m_worker(worker) { + ui->setupUi(this); + + connect(ui->start, SIGNAL(clicked()), m_worker, SLOT(update())); + connect(ui->exit, SIGNAL(clicked()), qApp, SLOT(quit())); + + int w = 52; + + QStringList lst; + QString start = QDateTime::currentDateTime().toString(Qt::ISODate); + lst << QString("Start: ") + start.leftJustified(w-10); + lst << QString("").leftJustified(w-3, '='); + lst << QString("Machine number : %1 ").arg(996).leftJustified(w-3); + lst << QString("Customer number : %1 ").arg(281).leftJustified(w-3); + lst << QString("Zone number : %1 (%2)").arg(1).arg("yellow").leftJustified(w-3); + lst << QString("").leftJustified(w-3, '='); + lst << QString("Backend connected ").leftJustified(w-10) + " [ ok]"; + lst << QString("Update trigger set ").leftJustified(w-10) + " [ ok]"; + // lst << QString("Found ISMAS machine number : %1 ").arg(996).leftJustified(w-10) + " [done]"; + // lst << QString("Found ISMAS customer number: %1 ").arg(281).leftJustified(w-10) + " [done]"; + lst << QString("Prepare customer environment ").leftJustified(w-10) + " [done]"; + lst << QString("Found %1 files to update ").arg(5).leftJustified(w-10) + " [done]"; + lst << QString("(") + QString("%1").arg(1).rightJustified(2, ' ') + QString(")") + + (QString(" Update ") + "DC2C_print01.json ").leftJustified(w-14) + " [done]"; + lst << QString("(") + QString("%1").arg(2).rightJustified(2, ' ') + QString(")") + + (QString(" Update ") + "DC2C_print01.json ").leftJustified(w-14) + " [done]"; + lst << QString("(") + QString("%1").arg(3).rightJustified(2, ' ') + QString(")") + + QString(" Update opkg pakets ").leftJustified(w-14) + " [done]"; + lst << QString("(") + QString("%1").arg(4).rightJustified(2, ' ') + QString(")") + + QString(" Update device controller %1 ").arg("04.38").leftJustified(w-14) + " [done]"; + lst << QString("Sync customer environment with filesystem ").leftJustified(w-10) + " [done]"; + lst << QString("(") + QString("%1").arg(5).rightJustified(2, ' ') + QString(")") + + (QString(" Update ") + "tariff01.json ").leftJustified(w-14) + " [done]"; + lst << QString("Send ISMAS notification ").leftJustified(w-10) + " [done]"; + lst << QString("Save log file %1.log").arg(start).leftJustified(w-10) + " [done]"; + lst << QString("").leftJustified(w-3, '='); + lst << QString("").leftJustified(w-13) + " [SUCCESS]"; + + ui->updateStatus->setText(lst.join('\n')); + ui->updateStatus->setEnabled(true); +} + +MainWindow::~MainWindow() { + delete ui; +} + + +void MainWindow::onStartClicked() { + +} + +void MainWindow::onExitClicked() { + +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..366ab20 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,26 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class Worker; +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + MainWindow(Worker *worker, QWidget *parent = nullptr); + ~MainWindow(); + +private slots: + void onStartClicked(); + void onExitClicked(); + +private: + Ui::MainWindow *ui; + Worker *m_worker; +}; +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..46d336c --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,71 @@ + + + MainWindow + + + + 0 + 0 + 370 + 358 + + + + + Source Code Pro + + + + MainWindow + + + + + + 10 + 10 + 351 + 341 + + + + + + + + Terminus + + + + Start + + + + + + + Exit + + + + + + + false + + + + Noto Sans + 8 + false + + + + + + + + + + + From 5c9c9dc917a31b63083c2d358b7ceb186860a3cf Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 31 Jul 2023 16:56:16 +0200 Subject: [PATCH 067/239] add mainwindow and utils --- OnDemandUpdatePTU.pro | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index e253043..99830e5 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -1,10 +1,20 @@ -# QT -= gui -QT += core +QT += core gui QT += widgets serialport network -# QT += network + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = ATBUpdateTool +# The following define makes your compiler emit warnings if you use +# any Qt feature that has been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 VERSION=1.0.0 INCLUDEPATH += plugins @@ -67,6 +77,8 @@ contains( CONFIG, DesktopLinux ) { SOURCES += \ main.cpp \ + mainwindow.cpp \ + utils.cpp \ update.cpp \ git/git_client.cpp \ apism/apism_client.cpp \ @@ -79,6 +91,8 @@ SOURCES += \ HEADERS += \ update.h \ + utils.h \ + mainwindow.h \ git/git_client.h \ apism/apism_client.h \ apism/apism_tcp_client.h \ @@ -90,6 +104,9 @@ HEADERS += \ worker_thread.h \ plugins/interfaces.h +FORMS += \ + mainwindow.ui + OTHER_FILES += \ /opt/app/tools/atbupdate/update_log.csv \ main.cpp.bck \ @@ -100,3 +117,8 @@ OTHER_FILES += \ # git subtree add --prefix DCPlugin https://git.mimbach49.de/GerhardHoffmann/DCPlugin.git master --squash # git subtree pull --prefix DCPlugin https://git.mimbach49.de/GerhardHoffmann/DCPlugin.git master --squash # include(./DCPlugin/DCPlugin.pri) + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target From a94606ca89eddd3f2ab3fdcf0f4a148c197fd168 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:13:05 +0200 Subject: [PATCH 068/239] Added sendRequestReceiveResponse(): static member for communication with APISM, on ports 7777 and 7778. Each request is immediately handled in a synchronous fashion. Added several helper functions to format messages to be sent to APISM/ISMAS. --- ismas/ismas_client.cpp | 378 ++++++++++++++++++++++++++++++++++++++--- ismas/ismas_client.h | 49 +++++- 2 files changed, 399 insertions(+), 28 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 7c57811..a842180 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -3,6 +3,24 @@ #include #include +#include +#include // inet_addr() +#include +#include +#include +#include +#include +#include +#include // bzero() +#include +#include // read(), write(), close() +#include + + +#include +#include +#include + #if 0 # $1: EVENT: U0001 update finished: 100% # U0002 reset TRG @@ -24,6 +42,208 @@ #include #include +void IsmasClient::printDebugMessage(int port, + QString const &clientIP, + int clientPort, + QString const &message) { + qDebug().noquote() + << "\n" + << "SEND-REQUEST-RECEIVE-RESPONSE ..." << "\n" + << "hostname ........" << "127.0.0.1" << "\n" + << "port ............" << port << "\n" + << "local address ..." << clientIP << "\n" + << "local port ......" << clientPort << "\n" + << message; +} + +void IsmasClient::printInfoMessage(int port, + QString const &clientIP, + int clientPort, + QString const &message) { + qInfo().noquote() + << "\n" + << "SEND-REQUEST-RECEIVE-RESPONSE ..." << "\n" + << "hostname ........" << "127.0.0.1" << "\n" + << "port ............" << port << "\n" + << "local address ..." << clientIP << "\n" + << "local port ......" << clientPort << "\n" + << message; +} + +void IsmasClient::printErrorMessage(int port, + QString const &clientIP, + int clientPort, + QString const &message) { + qCritical().noquote() + << "\n" + << "SEND-REQUEST-RECEIVE-RESPONSE ..." << "\n" + << "hostname ........" << "127.0.0.1" << "\n" + << "port ............" << port << "\n" + << "local address ..." << clientIP << "\n" + << "local port ......" << clientPort << "\n" + << message; +} + +std::optional +IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { + + qInfo() << "REQUEST" << request; + + int sockfd; + int r; + errno = 0; + // socket create and verification + if ((sockfd = ::socket(AF_INET, SOCK_STREAM, 0)) == -1) { + qCritical().noquote() + << "\n" + << "SEND-REQUEST-RECEIVE-RESPONSE ..." << "\n" + << "SOCKET CREATION FAILED (" << strerror(errno) << ")"; + return std::nullopt; + } + + struct sockaddr_in servAddr; + bzero(&servAddr, sizeof(servAddr)); + // assign IP, PORT + servAddr.sin_family = AF_INET; + servAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + servAddr.sin_port = htons(port); + // connect the client socket to server socket + if ((r = ::connect(sockfd, (struct sockaddr *)(&servAddr), sizeof(servAddr))) != 0) { + qCritical().noquote() + << "\n" + << "SEND-REQUEST-RECEIVE-RESPONSE ..." << "\n" + << "CONNECTION WITH SERVER FAILED (" << strerror(r) << ")"; + ::close(sockfd); + return std::nullopt; + } + + struct sockaddr_in clientAddr; + bzero(&clientAddr, sizeof(clientAddr)); + socklen_t sockLen = sizeof(clientAddr); + + char clientIP[16]; + bzero(&clientIP, sizeof(clientIP)); + getsockname(sockfd, (struct sockaddr *)(&clientAddr), &sockLen); + inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP)); + unsigned int clientPort = ntohs(clientAddr.sin_port); + + printDebugMessage(port, clientIP, clientPort, QString("CONNECTED TO SERVER")); + + struct timeval tv; + tv.tv_sec = 10; /* 10 secs timeout for read and write */ + + struct linger so_linger; + so_linger.l_onoff = 1; + so_linger.l_linger = 0; + + setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger)); + setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + int flag = 1; + setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(int)); + + static char buf[1024*8]; + bzero(buf, sizeof(buf)); + int const bytesToWrite = strlen(request.toStdString().c_str()); + strncpy(buf, request.toStdString().c_str(), sizeof(buf)-1); + + int bytesWritten = 0; + while (bytesWritten < bytesToWrite) { + int n = ::sendto(sockfd, buf+bytesWritten, bytesToWrite-bytesWritten, 0, NULL, 0); + if (n >= 0) { + bytesWritten += n; + } else { + if (errno == EWOULDBLOCK) { + printErrorMessage(port, clientIP, clientPort, + QString("TIMEOUT (") + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } else + if (errno == EINTR) { + printErrorMessage(port, clientIP, clientPort, + QString("INTERRUPTED BY SIGNAL (1) (") + strerror(errno) + ")"); + continue; + } + } + } + + // DO NOT USE SHUTDOWN! APISM CAN NOT COPE WITH IT + // errno = 0; + // if (shutdown(sockfd, SHUT_WR) < 0) { + // printErrorMessage(port, clientIP, clientPort, + // QString("CANNOT CLOSE WRITING END (") + strerror(errno) + ")"); + // } + + printInfoMessage(port, clientIP, clientPort, QString("MESSAGE SENT ") + buf); + + bzero(buf, sizeof(buf)); + int bytesToRead = sizeof(buf)-1; + int bytesRead = 0; + while (bytesRead < bytesToRead) { + errno = 0; + int n = ::recvfrom(sockfd, buf+bytesRead, bytesToRead-bytesRead, + 0, NULL, NULL); + if (n > 0) { // + bytesRead += n; + } else + if (n == 0) { + // The return value will be 0 when the peer has performed an orderly shutdown. + printErrorMessage(port, clientIP, clientPort, + QString("PEER CLOSED CONNECTION (") + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } else + if (n < 0) { + if (errno == EWOULDBLOCK) { + printErrorMessage(port, clientIP, clientPort, + QString("TIMEOUT (") + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } + if (errno == EINTR) { + printErrorMessage(port, clientIP, clientPort, + QString("INTERRUPTED BY SIGNAL (2) (") + strerror(errno) + ")"); + continue; + } + } + + printInfoMessage(port, clientIP, clientPort, QString("MESSAGE RECEIVED ") + buf); + QString response(buf); + + if (int idx = response.indexOf("{\"error\":\"ISMAS is offline\"}")) { + response = response.mid(0, idx); + } else + if (response == "RECORD SAVED") { + printInfoMessage(port, clientIP, clientPort, "IGNORED 'RECORD SAVED' RESPONSE"); + ::close(sockfd); + return std::nullopt; + } + + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(response.toUtf8(), &parseError)); + if (parseError.error == QJsonParseError::NoError) { + if (document.isObject()) { // done: received valid APISM response + printInfoMessage(port, clientIP, clientPort, + QString("VALID APISM RESPONSE .. \n") + response); + ::close(sockfd); + return response; + } else { + printInfoMessage(port, clientIP, clientPort, + QString("CORRUPTED RESPONSE ") + response); + ::close(sockfd); + return std::nullopt; + } + } else { + printDebugMessage(port, clientIP, clientPort, + QString("PARSE ERROR ") + response + " " + parseError.errorString()); + ::close(sockfd); + return std::nullopt; + } + } + return std::nullopt; +} + QString IsmasClient::updateNewsToIsmas(char const *event, int percent, int resultCode, @@ -53,6 +273,54 @@ QString IsmasClient::updateNewsToIsmas(char const *event, return buf; } +QString IsmasClient::errorBackendNotConnected(QString const &info, + QString const &version) { + return updateNewsToIsmas("U0003", + m_progressInPercent, + RESULT_CODE::INSTALL_ERROR, + "CHECK BACKEND CONNECTIVITY", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::backendConnected(QString const &info, QString const &version) { + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "CHECK BACKEND CONNECTIVITY", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::execOpkgCommand(QString const &info, QString const &version) { + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "EXECUTE OPKG COMMAND", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::updateTriggerSet(QString const &info, QString const &version) { + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "CHECK UPDATE TRIGGER", + info.toStdString().c_str(), + version.toStdString().c_str()); + +} + +QString IsmasClient::errorUpdateTrigger(QString const &info, QString const &version) { + return updateNewsToIsmas("U0003", + m_progressInPercent, + RESULT_CODE::INSTALL_ERROR, + "CHECK UPDATE TRIGGER", + info.toStdString().c_str(), + version.toStdString().c_str()); + +} + QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { //static int const constexpr SIZE = 4096*8; static char buf[4096*2]; @@ -376,39 +644,101 @@ QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { psa.pluginVersion.prmCalculatePriceConfigUi.toStdString().c_str(), psa.pluginVersion.tcpZVT.toStdString().c_str()); - printf("buf=%s\n", buf); + qInfo() << buf; return buf; } - -QString IsmasClient::updateOfPSAActivated() { +QString IsmasClient::updateOfPSAContinues(QString currentStage, + QString currentStageInfo, + QString const &version) { return updateNewsToIsmas("U0010", - 1, - 0, - "activated", - "detected WAIT state", - "1.0.0"); + m_progressInPercent, + RESULT_CODE::SUCCESS, + currentStage.toStdString().c_str(), + currentStageInfo.toStdString().c_str(), + version.toStdString().c_str()); } -QString IsmasClient::updateOfPSASucceeded() { +QString IsmasClient::updateOfPSAStarted(QString const &version) { + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "START", + "detected WAIT state: start update process", + version.toStdString().c_str()); +} + +QString IsmasClient::checkoutBranch(QString const &info, QString const &version) { + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "BRANCH-CHECKOUT", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::cloneAndCheckoutCustomerRepository(QString const &info, QString const &version) { // clone and checkout customer repository + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "CLONE-CHECKOUT", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::gitFetch(QString const &info, QString const &version) { + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "GIT-FETCH", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::errorGitFetch(int resultCode, QString const &info, QString const &version) { + return updateNewsToIsmas("U0003", + m_progressInPercent, + resultCode, + "GIT-FETCH-FAILED", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::updateOfPSAActivated(QString const &version) { // sent even after success + m_progressInPercent = 0; + return updateNewsToIsmas("U0002", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "UPDATE ACTIVATED", + "reset WAIT state", + version.toStdString().c_str()); +} + +QString IsmasClient::updateOfPSASucceeded(QString const &version) { + m_progressInPercent = 0; return updateNewsToIsmas("U0001", - 100, - 0, - "update_succeeded", - "", - "1.0.0"); + m_progressInPercent, + RESULT_CODE::SUCCESS, + "UPDATE SUCCESS", + "update process succeeded", + version.toStdString().c_str()); } -QString IsmasClient::setUpdatesAvailable() { - return updateNewsToIsmas("U0099", - 10, - 0, - "set_updates_available", - "", - ""); +QString IsmasClient::sanityCheckFailed(int resultCode, QString reason, QString const &version) { + return updateNewsToIsmas("U0003", + m_progressInPercent, + resultCode, + "SANITY-CHECK-FAILED", + reason.toStdString().c_str(), + version.toStdString().c_str()); } - - -bool checkForAvailableUpdates(); +QString IsmasClient::updateOfPSAFailed(int resultCode, QString reason, QString const &version) { + return updateNewsToIsmas("U0003", + m_progressInPercent, + resultCode, + "UPDATE ERROR", + reason.toStdString().c_str(), + version.toStdString().c_str()); +} diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index 7fbd5a7..fdda53d 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -111,7 +111,28 @@ struct PSAInstalled { class IsmasClient : public QObject { Q_OBJECT + int m_progressInPercent; public: + explicit IsmasClient() : m_progressInPercent(1) {} + + enum APISM { + DB_PORT = 7777, + DIRECT_PORT = 7778 + }; + + enum RESULT_CODE { + SUCCESS=0, + NO_UPDATE_NECESSARY=1, + BACKUP_FAILED=2, + WRONG_PACKAGE=3, + INSTALL_ERROR=4}; + + static std::optional + sendRequestReceiveResponse(int port, QString const &request); + + int getProgressInPercent() const {return m_progressInPercent; } + void setProgressInPercent(int procent) { m_progressInPercent = procent; } + QString updateNewsToIsmas(char const *event, int percent, int resultCode, @@ -119,12 +140,32 @@ public: char const *step_result, char const *version); - QString updateOfPSAActivated(); - QString updateOfPSASucceeded(); + QString updateOfPSAStarted(QString const &version = QString()); // start of update process + QString cloneAndCheckoutCustomerRepository(QString const &info, QString const &version = QString()); // clone and checkout customer repository + QString checkoutBranch(QString const &info, QString const &version = QString()); // checkout branch + QString errorBackendNotConnected(QString const &info, QString const &version = QString()); // checkout branch + QString backendConnected(QString const &info, QString const &version = QString()); + QString updateTriggerSet(QString const &info, QString const &version = QString()); + QString errorUpdateTrigger(QString const &info, QString const &version = QString()); + QString gitFetch(QString const &info, QString const &version = QString()); + QString execOpkgCommand(QString const &info, QString const &version = QString()); + QString errorGitFetch(int resultCode, QString const &info, QString const &version = QString()); + QString updateOfPSAActivated(QString const &version = QString()); + // and update accepted + QString updateOfPSASucceeded(QString const &version = QString()); // update process succeeded + QString updateOfPSAContinues(QString currentStage, QString currentStageInfo, QString const &version = QString()); + QString updateOfPSAFailed(int resultCode, QString reason, QString const &version = QString()); + QString sanityCheckFailed(int resultCode, QString reason, QString const &version = QString()); + QString updateOfPSASendVersion(PSAInstalled const &psa); - QString setUpdatesAvailable(); - bool checkForAvailableUpdates(); + private: + static void printDebugMessage(int port, QString const &clientIP, int clientPort, + QString const &message); + static void printInfoMessage(int port, QString const &clientIP, int clientPort, + QString const &message); + static void printErrorMessage(int port, QString const &clientIP, int clientPort, + QString const &message); }; #endif // ISMAS_CLIENT_H_INCLUDED From 371cc1a187028f1f9475f490bf29eac7157af275 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:17:10 +0200 Subject: [PATCH 069/239] Removed communication with ISMAS from git client. --- git/git_client.cpp | 112 ++++++++++++++++++--------------------------- git/git_client.h | 12 ++--- 2 files changed, 49 insertions(+), 75 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index 92ef124..3c0303e 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -1,6 +1,7 @@ #include "git_client.h" #include "update.h" #include "worker.h" +#include "utils.h" #include #include @@ -22,67 +23,6 @@ GitClient::GitClient(QString const &customerNrStr, if (!m_worker) { qCritical() << "ERROR CASTING PARENT TO WORKER FAILED"; } - - connect(this, SIGNAL(ismasUpdatesAvailable()), - this, SLOT(onIsmasUpdatesAvailable()), Qt::QueuedConnection); -} - -void GitClient::onIsmasUpdatesAvailable() { - if (QDir(m_customerRepository).exists()) { - - - qInfo() << UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST, - QString("FETCHING OF ") + m_repositoryPath + - QString(" INTO ") + m_customerRepository); - - std::optional changes = gitFetch(); - - if (changes) { - std::optional changedFileNames = gitDiff(changes.value()); - if (changedFileNames) { - - qInfo() << UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS, - QString("FETCHED NEW FILES ") + - changedFileNames.value().join(",") + - QString(" INTO ") + m_customerRepository); - if (gitPull()) { - qInfo() << UpdateStatus(UPDATE_STATUS::GIT_PULL_UPDATES_SUCCESS, - QString("PULL NEW FILES ") + - changedFileNames.value().join(",") + - QString(" INTO ") + m_customerRepository); - - emit m_worker->handleChangedFiles(changedFileNames.value()); - - } else { - qCritical() << UpdateStatus(UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE, - QString("PULLING NEW FILES ") + - changedFileNames.value().join(",") + - QString(" INTO ") + m_customerRepository + " FAILED"); - - emit m_worker->terminateUpdateProcess(); - } - } else { - qCritical() << "NO CHANGES IN" << m_repositoryPath - << "(" << m_customerRepository << ")"; - emit m_worker->finishUpdateProcess(false); - } - } else { - qCritical() << UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE, - QString("NO CHANGES IN ") + m_repositoryPath + - QString(" (%1)").arg(m_customerRepository)); - - emit m_worker->finishUpdateProcess(false); - } - } else { - if (gitCloneAndCheckoutBranch()) { - qInfo() << "CLONED" << m_repositoryPath - << "AND CHECKED OUT INTO" << m_customerRepository; - } else { - qCritical() << "ERROR CLONING " << m_repositoryPath - << "AND/OR CHECKING OUT INTO" << m_customerRepository; - emit m_worker->terminateUpdateProcess(); - } - } } bool GitClient::gitCloneCustomerRepository() { @@ -128,10 +68,31 @@ bool GitClient::copyGitConfigFromMaster() { // only allowed when called in return false; } -bool GitClient::gitCheckoutBranch() { +QStringList GitClient::gitBranchNames() { + // git config --global pager.branch false + QStringList bNames; if (QDir(m_customerRepository).exists()) { + QString gitCommand("git branch -a"); + Command c(gitCommand); + if (c.execute(m_customerRepository)) { + QString const result = c.getCommandResult(); + return result.split('\n'); + } + } + return bNames; +} + +bool GitClient::gitCheckoutBranch() { + // TODO: nachsehen, ob der Branch ueberhaupt existiert + + if (QDir(m_customerRepository).exists()) { + int zoneNr = Utils::read1stLineOfFile("/etc/zone_nr"); + m_branchName = (zoneNr != 0) + ? QString("zg1/zone%1").arg(zoneNr) : "master"; + QString gitCommand("git checkout "); gitCommand += m_branchName; + Command c(gitCommand); return c.execute(m_customerRepository); // execute command in customerRepo } @@ -146,11 +107,13 @@ bool GitClient::gitCloneAndCheckoutBranch() { if (gitCheckoutBranch()) { return true; } else { - m_worker->terminateUpdateProcess(); + // TODO + // m_worker->terminateUpdateProcess(); } //} } else { - m_worker->terminateUpdateProcess(); + // TODO + //m_worker->terminateUpdateProcess(); } return false; } @@ -201,12 +164,18 @@ std::optional GitClient::gitDiff(QString const &commits) { */ std::optional GitClient::gitFetch() { if (QDir(m_customerRepository).exists()) { + + // TODO + UpdateStatus status (UPDATE_STATUS::GIT_FETCH_UPDATES, "GIT FETCH UPDATES"); + qInfo() << status; + + //emit m_worker->sendCmdEventToIsmas( + // m_worker->getIsmasClient().updateOfPSAContinues( + // "GIT FETCH UPDATES", status.m_statusDescription)); + Command c("git fetch"); if (c.execute(m_customerRepository)) { QString const s = c.getCommandResult().trimmed(); - - qCritical() << "GIT RESULT" << s; - if (!s.isEmpty()) { QStringList lines = Update::split(s, '\n'); if (!lines.empty()) { @@ -215,6 +184,15 @@ std::optional GitClient::gitFetch() { QRegularExpressionMatch match = re.match(lines.last()); if (match.hasMatch()) { if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches + // TODO + status = UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS, + QString("GIT SUCCESSFULLY FETCHED ") + m_customerRepository); + qInfo() << status; + + //emit m_worker->sendCmdEventToIsmas( + // m_worker->getIsmasClient().updateOfPSAContinues( + // "GIT_FETCH_UPDATES SUCCESS", status.m_statusDescription)); + return match.captured(2); } else { qCritical() << "ERROR WRONG CAPTURE COUNT FOR 'GIT FETCH'" << re.captureCount(); diff --git a/git/git_client.h b/git/git_client.h index cb1f3a5..7da27c4 100644 --- a/git/git_client.h +++ b/git/git_client.h @@ -2,9 +2,11 @@ #define GIT_CLIENT_H_INCLUDED #include +#include #include #include "process/command.h" +#include "ismas/ismas_client.h" class Worker; class GitClient : public QObject { @@ -14,7 +16,7 @@ class GitClient : public QObject { QString const m_repositoryPath; QString const m_customerNr; QString const m_workingDirectory; - QString const m_branchName; + QString m_branchName; QString const m_customerRepository; bool copyGitConfigFromMaster(); @@ -28,6 +30,7 @@ class GitClient : public QObject { bool gitCloneCustomerRepository(); bool gitCheckoutBranch(); + QStringList gitBranchNames(); QString const workingDirectory() const { return m_workingDirectory; } QString workingDirectory() { return m_workingDirectory; } @@ -50,13 +53,6 @@ class GitClient : public QObject { QString gitBlob(QString fileName); QString gitCommitForBlob(QString blob); bool gitIsFileTracked(QString file2name); - -signals: - void ismasUpdatesAvailable(); - -public slots: - void onIsmasUpdatesAvailable(); - }; #endif // GIT_CLIENT_H_INCLUDED From ce1b1859dfdb61f41e381bd2c6ceba6c3d517acd Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:18:39 +0200 Subject: [PATCH 070/239] Removed apism-client and tcp-client. All done inside of ismas client now. --- OnDemandUpdatePTU.pro | 4 ---- 1 file changed, 4 deletions(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 99830e5..c8d5b82 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -81,8 +81,6 @@ SOURCES += \ utils.cpp \ update.cpp \ git/git_client.cpp \ - apism/apism_client.cpp \ - apism/apism_tcp_client.cpp \ ismas/ismas_client.cpp \ process/command.cpp \ message_handler.cpp \ @@ -94,8 +92,6 @@ HEADERS += \ utils.h \ mainwindow.h \ git/git_client.h \ - apism/apism_client.h \ - apism/apism_tcp_client.h \ apism/ismas_data.h \ ismas/ismas_client.h \ process/command.h \ From eb7d77692b0f8f142274dcbf8ba62ffa4647c737 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:20:07 +0200 Subject: [PATCH 071/239] Removed obsolete apism/tcp-client files. --- apism/apism_client.cpp | 816 ------------------------------------- apism/apism_client.h | 127 ------ apism/apism_tcp_client.cpp | 172 -------- apism/apism_tcp_client.h | 62 --- 4 files changed, 1177 deletions(-) delete mode 100644 apism/apism_client.cpp delete mode 100644 apism/apism_client.h delete mode 100644 apism/apism_tcp_client.cpp delete mode 100644 apism/apism_tcp_client.h diff --git a/apism/apism_client.cpp b/apism/apism_client.cpp deleted file mode 100644 index 7d13571..0000000 --- a/apism/apism_client.cpp +++ /dev/null @@ -1,816 +0,0 @@ -#include "apism_client.h" -//#include "support/VendingData.h" -//#include "support/PersistentData.h" -//#include "support/utils.h" -//#include "ATBHMIconfig.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Q_DECLARE_METATYPE(QAbstractSocket::SocketError) - -ApismClient::ApismClient(QObject *eventReceiver, ATBHMIconfig *config, PersistentData *persistentData, QObject *parent) - : QObject(parent) - , healthEventReceiver(eventReceiver) - , m_config(config) - , persistentData(persistentData) - , lastError(0) - , lastErrorDescription("") - , currentRequestUid("") - , currentRequest(ISMAS::REQUEST::NO_REQUEST) -{ - this->apismTcpSendClient = new ApismTcpClient("127.0.0.1", "7777", this); - this->apismTcpRequestResponseClient = new ApismTcpClient("127.0.0.1", "7778", this); - - connect(apismTcpRequestResponseClient, &ApismTcpClient::receivedData, - this, &ApismClient::onReceivedResponse); - connect(apismTcpRequestResponseClient, &ApismTcpClient::responseTimeout, - this, &ApismClient::onRequestResponseClientResponseTimeout); - connect(apismTcpSendClient, &ApismTcpClient::responseTimeout, - this, &ApismClient::onSendClientResponseTimeout); - connect(this, SIGNAL(sendCmdSendVersionToIsmas(QString)), - this, SLOT(onSendCmdSendVersionToIsmas(QString))); - - // not needed as APISM closes the socket after we send data, so readyRead() - // might not even fire - // connect(&m_socket, SIGNAL(readyRead()), this, SLOT(onReadyRead())); - - // defined for Qt >= 5.15, we have 5.12 - // qRegisterMetaType(); - // connect(&m_socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), - // this, SLOT(onSocketError(&QAbstractSocket::SocketError))); -} - -ApismClient::~ApismClient() { -} - -void ApismClient::restartApism() { - QProcess::startDetached("/bin/systemctl", {"restart", "apism"}); -} - -//void ApismClient::onReadyRead() { // parse APISM response -// QByteArray data = m_socket.readAll(); -// qCritical() << "APISM-RESPONSE = (" << endl << data << endl << ")"; -//} - -void ApismClient::sendTransaction(const VendingData *vendingData) { - - Q_UNUSED(vendingData); - -#if 0 - QScopedPointer transferData(new ISMAS::TransferData()); - - PAYMENT_VARIANTS::TYPE paymentType = vendingData->getParameter("PaymentType").value(); - - - ////////////////////// DEVICE ////////////////////////////////// - bool deviceSet = false; - //QJsonValue tariffId("TariffInfo"); - //transferData->device.insert("TARIFID", tariffId); - //QJsonValue group("group"); - //transferData->device.insert("GROUP", group); - //QJsonValue zone("zone"); - //transferData->device.insert("ZONE", zone); - if (deviceSet) { - transferData->insert("DEVICE", transferData->device); - } - - - ////////////////////// TRANSACTION ///////////////////////////// - - - bool transactionSet = false; - - if (vendingData->hasParameter("TRANSACTION_STATE")) { - QVariant tstate = vendingData->getParameter("TRANSACTION_STATE"); - if (tstate.isValid() && tstate.type() == QVariant::Int) { - transferData->transaction.state = tstate.toInt(); - transferData->transaction.insert("STATE", transferData->transaction.state); - transactionSet = true; - } - } - if (vendingData->hasParameter("TRANSACTION_UID")) { - QVariant tuid = vendingData->getParameter("TRANSACTION_UID"); - if (tuid.isValid() && tuid.type() == QVariant::String) { - transferData->transaction.uid = tuid.toString(); - transferData->transaction.insert("UID", transferData->transaction.uid); - this->persistentData->setLastTransactionUID(tuid.toString()); - transactionSet = true; - } - } - if (vendingData->hasParameter("TRANSACTION_TIMESTAMP")) { - QVariant tstamp = vendingData->getParameter("TRANSACTION_TIMESTAMP"); - if (tstamp.isValid() && tstamp.type() == QVariant::String) { - transferData->transaction.timestamp = tstamp.toString(); - transferData->transaction.insert("TIMESTAMP", transferData->transaction.timestamp); - transactionSet = true; - } - } - if (vendingData->hasParameter("TRANSACTION_TICKET_SEQUENCE_NUMBER")) { - QVariant tsn = vendingData->getParameter("TRANSACTION_TICKET_SEQUENCE_NUMBER"); - if (tsn.isValid() && tsn.type() == QVariant::Int) { - transferData->transaction.seq_tick_number = tsn.toInt(); - transferData->transaction.insert("TICKETNU", transferData->transaction.seq_tick_number); - transactionSet = true; - } - } - if (vendingData->hasParameter("LICENSEPLATE")) { - QVariant lp = vendingData->getParameter("LICENSEPLATE"); - transferData->transaction.userText = QJsonValue::fromVariant(lp); - transferData->transaction.insert("USERTEXT", transferData->transaction.userText); - transferData->transaction.userTextType = "license plate"; - transferData->transaction.insert("USERTEXTTYPE", transferData->transaction.userTextType); - transactionSet = true; - } - - if (transactionSet) { - transferData->insert("TRANSACTION", transferData->transaction); - } - - - ////////////////////// ITEM ////////////////////////////////// - bool itemSet = false; - - if (vendingData->hasParameter("PermitType")) { - QVariant idVariant = vendingData->getParameter("PermitType"); - transferData->item.id = idVariant.toString(); - transferData->item.insert("ID", transferData->item.id); - itemSet = true; - } - if (vendingData->hasParameter("Product")) { - QVariant nameVariant = vendingData->getParameter("Product"); - transferData->item.name = nameVariant.toString(); - transferData->item.insert("NAME", transferData->item.name); - itemSet = true; - } - if (vendingData->hasParameter("PRICE_INFO_GROSS")) { - int priceUint = vendingData->getUintParameter("PRICE_INFO_GROSS"); - transferData->item.price = priceUint; - transferData->item.insert("PRICE", transferData->item.price); - itemSet = true; - } - if (vendingData->hasParameter("PERIOD_START")) { - QVariant startTimeVariant = vendingData->getParameter("PERIOD_START"); - transferData->item.startTime = utils::getISODateTimeWithMsAndOffset(startTimeVariant); - transferData->item.insert("STARTTIME", transferData->item.startTime); - itemSet = true; - } - if (vendingData->hasParameter("PERIOD_END")) { - QVariant endTimeVariant = vendingData->getParameter("PERIOD_END"); - transferData->item.endTime = utils::getISODateTimeWithMsAndOffset(endTimeVariant); - transferData->item.insert("ENDTIME", transferData->item.endTime); - itemSet = true; - } - if (vendingData->hasParameter("ITEM_PRINT_TEXT")) { - QVariant textVariant = vendingData->getParameter("ITEM_PRINT_TEXT"); - transferData->item.printText = textVariant.toString(); - transferData->item.insert("PRINTTEXT", transferData->item.printText); - itemSet = true; - } - - // set static data: - - // currency - if (itemSet) { - transferData->item.currency = this->m_config->getPaymentCurrencyISOCode(); - transferData->item.insert("CURRENCY", transferData->item.currency); - } - - if (itemSet) { - transferData->insert("ITEM", transferData->item); - } - - //////////////////////////////////////////////////////////////////////// - - - ////////////////////// PAYMENT ////////////////////////////// - bool paymentSet = false; - - /////////////////////////// PAYMENT.CASH ///////////////////////////// - if (paymentType == PAYMENT_VARIANTS::TYPE::CASH) { - bool cashSet = false; - - if (vendingData->hasParameter("PaymentCashCoins")) { - QVariant coins = vendingData->getParameter("PaymentCashCoins"); - transferData->payment.cash.coins = coins.toInt(); - transferData->payment.cash.insert("COINS", transferData->payment.cash.coins); - cashSet = true; - } - if (vendingData->hasParameter("PaymentCashChange")) { - QVariant change = vendingData->getParameter("PaymentCashChange"); - transferData->payment.cash.change = change.toInt(); - transferData->payment.cash.insert("CHANGE", transferData->payment.cash.change); - cashSet = true; - } - if (vendingData->hasParameter("PaymentCashOverpaid")) { - QVariant overpaid = vendingData->getParameter("PaymentCashOverpaid"); - transferData->payment.cash.overpaid = overpaid.toInt(); - transferData->payment.cash.insert("OVERPAID", transferData->payment.cash.overpaid); - cashSet = true; - } - - // currency - if (cashSet) { - transferData->payment.cash.currency = this->m_config->getPaymentCurrencyISOCode(); - transferData->payment.cash.insert("CURRENCY", transferData->payment.cash.currency); - } - - if (cashSet) { - transferData->payment.insert("CASH", transferData->payment.cash); - paymentSet = true; - } - } - - /////////////////////////// PAYMENT.CARD ///////////////////////////// - if (paymentType == PAYMENT_VARIANTS::TYPE::CARD) { - paymentSet = true; - bool cardSet = true; - - - transferData->payment.card.insert("CARDNU", "unknown"); - - transferData->payment.card.insert("VALUE", transferData->item.price); - - transferData->payment.card.insert("CARDTYPE", "unknown"); - - - transferData->payment.card.currency = this->m_config->getPaymentCurrencyISOCode(); - transferData->payment.card.insert("CURRENCY", transferData->payment.card.currency); - - // transferData->payment.card.insert("TERMINALID", tid); - //transferData->payment.card.insert("TERMINALRESULT", tresult); - - if (cardSet) { - transferData->payment.insert("CARD", transferData->payment.card); - paymentSet = true; - } - } - - if (paymentSet) { - transferData->insert("PAYMENT", transferData->payment); - } - - //////////////////////////////////////////////////////////////////////// - - - ///////////////////////////// RESULT ///////////////////////////////// - bool resultSet = false; - - - if (vendingData->hasParameter("RESULT_DELIVERY")) { - QVariant delVariant = vendingData->getParameter("RESULT_DELIVERY"); - transferData->result.delivery = delVariant.toJsonValue(); - transferData->result.insert("DELIVERY", transferData->result.delivery); - resultSet = true; - } - if (vendingData->hasParameter("RESULT_RESULT")) { - QVariant resVariant = vendingData->getParameter("RESULT_RESULT"); - transferData->result.result = resVariant.toJsonValue(); - transferData->result.insert("RESULT", transferData->result.result); - resultSet = true; - } - if (vendingData->hasParameter("RESULT_ERROR_CODE")) { - QVariant ecVariant = vendingData->getParameter("RESULT_ERROR_CODE"); - transferData->result.errorCode = ecVariant.toJsonValue(); - transferData->result.insert("ERRORCODE", transferData->result.errorCode); - resultSet = true; - } - if (vendingData->hasParameter("RESULT_ERROR_MESSAGE")) { - QVariant emsgVariant = vendingData->getParameter("RESULT_ERROR_MESSAGE"); - transferData->result.errorMsg = emsgVariant.toJsonValue(); - transferData->result.insert("ERRORMSG", transferData->result.errorMsg); - resultSet = true; - } - - if (resultSet) { - transferData->insert("RESULT", transferData->result); - } - - //////////////////////////////////////////////////////////////////////// - - - QJsonDocument jsonDoc(*transferData); - QByteArray data = "#M=APISM#C=CMD_TRANSACTION#J="; - data += jsonDoc.toJson(QJsonDocument::Compact); - - this->apismTcpSendClient->sendData(data); -} - - -void ApismClient::sendAccount(const QHash & accountDataHash) -{ - QScopedPointer accountData(new ISMAS::AccountData()); - - accountData->coinBox.UID = QUuid::createUuid().toString(QUuid::WithoutBraces); // .mid(0, 8) - accountData->insert("UID", accountData->coinBox.UID); - - accountData->coinBox.ChangeNumber = QJsonValue(static_cast(this->persistentData->getNewCoinboxChangeNumber())); - accountData->insert("COINBOX_CHANGE_NUMBER", accountData->coinBox.ChangeNumber); - - accountData->coinBox.Process = "COINBOX_CHANGE"; - accountData->insert("PROCESS", accountData->coinBox.Process); - - accountData->insert("StartTime", utils::getISODateTimeWithMsAndOffset(persistentData->getAccountStartTime())); - accountData->insert("EndTime", utils::getCurrentISODateTimeWithMsAndOffset()); - accountData->insert("StartHash", persistentData->getFirstTransactionUID()); - accountData->insert("EndHash", persistentData->getLastTransactionUID()); - - // coins - int numberOfCoinVariants = accountDataHash["NumberOfCoinVariants"].toInt(); - for (int i=0; i < numberOfCoinVariants;++i) { - accountData->coinBox.coin.value = accountDataHash["COIN_" + QString::number(i) + "_Value"].toInt(); - accountData->coinBox.coin.numberOfCoins = accountDataHash["COIN_" + QString::number(i) + "_Quantity"].toInt(); - accountData->coinBox.coin.insert("VALUE", accountData->coinBox.coin.value); - accountData->coinBox.coin.insert("QUANTITY", accountData->coinBox.coin.numberOfCoins); - - if (accountDataHash.contains("COIN_" + QString::number(i) + "_Currency")) { - accountData->coinBox.coin.currency = accountDataHash["COIN_" + QString::number(i) + "_Currency"].toString(); - accountData->coinBox.coin.insert("CURRENCY", accountData->coinBox.coin.numberOfCoins); - } - - accountData->insert("COIN_" + QString::number(i), accountData->coinBox.coin); - } - - QJsonDocument jsonDoc(*accountData); - QByteArray data = "#M=APISM#C=CMD_CashboxChange#J="; - data += jsonDoc.toJson(QJsonDocument::Compact); - - this->apismTcpSendClient->sendData(data); - - this->persistentData->clearForNewAccount(); - - persistentData->serializeToFile(); -} - - - -void ApismClient::sendEvent(const ATBMachineEvent* machineEvent) -{ - QScopedPointer eventData(new ISMAS::EventData()); - - eventData->machineEvent.eventID = machineEvent->eventId; - eventData->insert("EVENT_ID", eventData->machineEvent.eventID); - - eventData->machineEvent.deviceName = machineEvent->deviceName; - eventData->insert("DeviceName", eventData->machineEvent.deviceName); - - eventData->machineEvent.reason = ATBMachineEvent::getEventClassString(machineEvent->machineEventClass); - eventData->insert("Reason", eventData->machineEvent.reason); - - eventData->machineEvent.event = machineEvent->eventName; - eventData->insert("Event", eventData->machineEvent.event); - - eventData->machineEvent.eventState = machineEvent->eventState; - eventData->insert("EventState", eventData->machineEvent.eventState); - - eventData->machineEvent.timeStamp = machineEvent->timestamString; - eventData->insert("Timestamp", eventData->machineEvent.timeStamp); - - eventData->machineEvent.parameter = machineEvent->parameterString; - eventData->insert("Parameter", eventData->machineEvent.parameter); - - eventData->machineEvent.secondLevelInfo = machineEvent->secondLevelInfoString; - eventData->insert("SecondLevelInfo", eventData->machineEvent.secondLevelInfo); - - - QJsonDocument jsonDoc(*eventData); - QByteArray data = "#M=APISM#C=CMD_EVENT#J="; - data += jsonDoc.toJson(QJsonDocument::Compact); - - this->apismTcpSendClient->sendData(data); - -#endif - -} - -void ApismClient::requestAvailableIsmasUpdates() { - QByteArray data = "#M=APISM #C=REQ_ISMASParameter #J={}"; - this->currentRequest = ISMAS::REQUEST::ISMAS_PARAMETER; - this->apismTcpRequestResponseClient->sendData(data); -} - -void ApismClient::onSendCmdEventToIsmas(QString msg) { - QJsonParseError parseError; - QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); - if (parseError.error != QJsonParseError::NoError) { - qCritical() << "INVALID JSON MSG: PARSING FAILED:" - << parseError.error << parseError.errorString(); - return; - } - - if (!document.isObject()) { - qCritical() << "FILE IS NOT A JSON OBJECT!"; - return; - } - - QByteArray data = "#M=APISM#C=CMD_EVENT#J="; - data += document.toJson(QJsonDocument::Compact); - - this->currentRequest = ISMAS::REQUEST::NO_REQUEST; - this->apismTcpSendClient->sendData(data); -} - -void ApismClient::onSendCmdSendVersionToIsmas(QString msg) { - QJsonParseError parseError; - QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); - if (parseError.error != QJsonParseError::NoError) { - qCritical() << "INVALID JSON MSG: PARSING FAILED:" - << parseError.error << parseError.errorString(); - return; - } - - if (!document.isObject()) { - qCritical() << "FILE IS NOT A JSON OBJECT!"; - return; - } - - QByteArray data = "#M=APISM#C=CMD_SENDVERSION#J="; - data += document.toJson(QJsonDocument::Compact); - - this->currentRequest = ISMAS::REQUEST::NO_REQUEST; - this->apismTcpSendClient->sendData(data); -} - -void ApismClient::emulateUpdatesAvailable(QString const &msg) { - qDebug() << "EMULATE UPDATES AVAILABLE. MSG=" << msg; - onSendCmdEventToIsmas(msg); -} - -void ApismClient::sendState(const QString & state, const QString & msg) -{ - qCritical() << "ApismClient::sendState(): "; - qCritical() << " state: " << state; - qCritical() << " msg: " << msg; - - QJsonParseError parseError; - QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); - if (parseError.error != QJsonParseError::NoError) { - qCritical() << "ApismClient::sendState() invalid json msg: Parsing failed:" << parseError.error << parseError.errorString(); - return; - } - - if (!document.isObject()) { - qCritical() << "File is not JSON object!"; - return; - } - - - QScopedPointer stateData(new ISMAS::StateData()); - - QJsonObject stateObject = document.object(); - - QJsonArray statesArray; - statesArray.append(stateObject); - - // stateData->insert("TIMESTAMP", utils::getCurrentISODateTimeWithMsAndOffset()); - // stateData->insert("HW_States", statesArray); - - - QJsonDocument jsonDoc(*stateData); - QByteArray data = "#M=APISM#C=CMD_HW_STATUS#J="; - data += jsonDoc.toJson(QJsonDocument::Compact); - - this->apismTcpSendClient->sendData(data); -} - - - - - - -#if 0 -void ApismClient::sendMininformStartRequest(const VendingData* vendingData) -{ - this->currentRequest = ISMAS::REQUEST::START; - - struct MininFormTransferData : public QJsonObject { - struct : public QJsonObject { - QJsonValue uid; // MUST: uuid -> vendorId - QJsonValue posid; // terminal-id - QJsonValue authCode; // approval-code - QJsonValue stan; // MUST - QJsonValue lpn; - QJsonValue transactionTime; // MUST: Zeitstempel Verkauf - QJsonValue parkingStartTime; // MUST: Startzeit - QJsonValue preAuthAmount; - QJsonValue hourlyRate; - QJsonValue vehicleCategory; - QJsonValue langCode; - QJsonValue zoneCode; - } startAction; - }; - - QScopedPointer transferData(new MininFormTransferData()); - - transferData->startAction.uid = vendingData->getParameter("START_UID").toJsonValue(); - this->currentRequestUid = vendingData->getParameter("START_UID").toString(); - transferData->startAction.insert("UID", transferData->startAction.uid); - - transferData->startAction.insert("POSID", vendingData->getParameter("START_POSID").toString()); - - transferData->startAction.insert("AUTHCODE", vendingData->getParameter("START_AuthCode").toString()); - - transferData->startAction.insert("STAN", vendingData->getParameter("START_STAN").toString()); - - transferData->startAction.insert("LPN", vendingData->getParameter("LICENSEPLATE").toString()); - - transferData->startAction.insert("TRANSACTIONTIME", utils::getISODateTimeWithMsAndOffset(vendingData->getParameter("START_TRANSACTIONTIME").toString())); - - transferData->startAction.insert("STARTTIME", utils::getISODateTimeWithMsAndOffset(vendingData->getParameter("START_STARTTIME").toString())); - - transferData->startAction.insert("AMOUNT", static_cast(vendingData->getUintParameter("PRICE_INFO_GROSS"))); - - transferData->startAction.insert("RATE_H", static_cast(vendingData->getUintParameter("PRICE_INFO_RATE_H"))); - - transferData->startAction.insert("VEHICLETYPE", "01"); // Fixed value - - transferData->startAction.insert("LANGUAGE", "HUN"); // Fixed value - - transferData->startAction.insert("ZONE", vendingData->getParameter("MININFORM_ZONE").toString()); - - transferData->insert("STARTACTION", transferData->startAction); - - QJsonDocument jsonDoc(*transferData); - QByteArray data = "#M=APISM#C=REQ_START#J="; // REQ_... -> use port 7778 - data += jsonDoc.toJson(QJsonDocument::Compact); - - this->apismTcpRequestResponseClient->sendData(data); -} - -void ApismClient::sendMininformStopRequest(const VendingData* vendingData) -{ - this->currentRequest = ISMAS::REQUEST::STOP; - - struct MininFormTransferData : public QJsonObject { - struct : public QJsonObject { - QJsonObject uid; // MUST: uuid - QJsonObject posid; // terminal-id - QJsonObject stan; // MUST - QJsonObject lpn; // MUST - QJsonObject stopTime; // MUST: Stop-Zeit - QJsonObject langCode; - QJsonObject deviceId; - } stopAction; - }; - - QScopedPointer transferData(new MininFormTransferData()); - - this->currentRequestUid = QUuid::createUuid().toString(QUuid::WithoutBraces).mid(0, 8); - transferData->stopAction.insert("UID", this->currentRequestUid); - - transferData->stopAction.insert("POSID", vendingData->getParameter("STOP_POSID").toString()); - - transferData->stopAction.insert("STAN", vendingData->getParameter("STOP_STAN").toString()); - - transferData->stopAction.insert("LPN", vendingData->getParameter("LICENSEPLATE").toString()); - - transferData->stopAction.insert("STOPTIME", utils::getISODateTimeWithMsAndOffset(vendingData->getParameter("STOP_STOPTIME"))); - - transferData->stopAction.insert("LANGUAGE", "HUN"); // Fixed value - - transferData->stopAction.insert("DEVICE_ID", this->m_config->getMachineNr()); - - transferData->insert("STOPACTION", transferData->stopAction); - - QJsonDocument jsonDoc(*transferData); - QByteArray data = "#M=APISM#C=REQ_STOP#J="; // REQ_ -> use port 7778 - data += jsonDoc.toJson(QJsonDocument::Compact); - - this->apismTcpRequestResponseClient->sendData(data); -} -#endif - - -void ApismClient::sendSelfTest() { - qDebug() << "SENDING APISM-SELF-TEST"; - this->currentRequest = ISMAS::REQUEST::SELF; - QByteArray data = "#M=APISM#C=REQ_SELF#J={}"; - this->apismTcpRequestResponseClient->sendData(data); -} - -#if 0 -void ApismClient::sendMininformPingRequest() -{ - this->currentRequest = ISMAS::REQUEST::PING; - - QByteArray data = "#M=APISM#C=REQ_Ping#J={\"281\":\"PING\"}"; - - this->apismTcpRequestResponseClient->sendData(data); -} -#endif - - -void ApismClient::onReceivedResponse(QByteArray response) { - - qCritical() << "RECEIVED ------> " << response; - - if (this->currentRequest == ISMAS::REQUEST::NO_REQUEST && - response == "RECORD SAVED") { // sent by APISM to indicate that record - return; // has been saved in DB - } - - // get the root object - QJsonParseError jsonParseError; - QJsonDocument responseDoc = QJsonDocument::fromJson(response, &jsonParseError); - - if (jsonParseError.error != QJsonParseError::NoError) { - qCritical() << "ApismClient::onReceivedResponse() response is no json data:"; - qCritical() << " Error: " << jsonParseError.errorString(); - this->handleISMASResponseError(); - return; - } - - QJsonObject rootObject = responseDoc.object(); - QStringList rootObjectKeys = rootObject.keys(); - - // DEBUG - qDebug() << "ApismClient::onReceivedResponse(): objects: " << rootObjectKeys; - // results to: - // ApismClient::onReceivedResponse(): objects: ("REQ_START#60044_Response", "Response") - - - if(rootObjectKeys.indexOf(QRegularExpression("^REQ_START.*")) >= 0) { - this->private_handleMininformStartResponse(rootObject["Response"].toObject()); - } - else - if(rootObjectKeys.indexOf(QRegularExpression("^REQ_STOP.*")) >= 0) { - this->private_handleMininformStopResponse(rootObject["Response"].toObject()); - } - else - if(rootObjectKeys.indexOf(QRegularExpression("^REQ_SELF.*")) >= 0) { - this->private_handleReqSelfResponse(rootObject["Response"].toObject()); - } - else - if(rootObjectKeys.indexOf(QRegularExpression("^REQ_PING.*")) >= 0) { - this->private_handleReqPingResponse(rootObject["PING"].toObject()); - } else - if(rootObjectKeys.indexOf(QRegularExpression("^REQ_ISMASPARAMETER.*")) >= 0) { - this->private_handleIsmasParameterResponse(rootObject); - } - else { - qCritical() << "ApismClient::onReceivedResponse() for unknown Request: "; - qCritical() << " currentRequestName: " << currentRequest; - qCritical() << " rootObject.keys(): " << rootObjectKeys; - this->handleISMASResponseError(); - return; - } - - this->currentRequest = ISMAS::REQUEST::NO_REQUEST; -} - -void ApismClient::handleISMASResponseError() -{ - switch (this->currentRequest) { - case ISMAS::REQUEST::NO_REQUEST: - qCritical() << "ApismClient::onReceivedResponse() for unknown Request: " << currentRequest; - break; - case ISMAS::REQUEST::START: - emit this->sendMininformStartResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); - break; - case ISMAS::REQUEST::STOP: - emit this->sendMininformStopResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); - break; - case ISMAS::REQUEST::PING: - emit this->sendMininformPingResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); - break; - case ISMAS::REQUEST::SELF: - emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::ERROR_BACKEND, QJsonObject()); - break; - case ISMAS::REQUEST::ISMAS_PARAMETER: - // TODO - // emit - break; - } - this->currentRequest = ISMAS::REQUEST::NO_REQUEST; -} - -/* -{\r\n \"REQ_START#53365_Response\": {\r\n \"Aknoledge\": \"OK\"\r\n }, - \r\n \"Response\": {\r\n \"Result\": \"ERR:Ung�ltiger Datums-String: \"\r\n }\r\n} -*/ - -/* -: ISMAS received: "{\r\n \"REQ_PING#30844_Response\": {\r\n \"Aknoledge\": \"OK\"\r\n }, - "PING": { "IsAviable": "TRUE" } - }" -*/ - -void ApismClient::onSendClientResponseTimeout() -{ - -} - -void ApismClient::onRequestResponseClientResponseTimeout() -{ - switch (this->currentRequest) { - case ISMAS::REQUEST::NO_REQUEST: - qCritical() << "ApismClient::onRequestResponseClientResponseTimeout() for unknown Request: " << currentRequest; - break; - case ISMAS::REQUEST::START: - emit this->sendMininformStartResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); - break; - case ISMAS::REQUEST::STOP: - emit this->sendMininformStopResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); - break; - case ISMAS::REQUEST::PING: - emit this->sendMininformPingResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); - break; - case ISMAS::REQUEST::SELF: - emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::ERROR_TIMEOUT, QJsonObject()); - break; - case ISMAS::REQUEST::ISMAS_PARAMETER: - break; - } - - this->currentRequest = ISMAS::REQUEST::NO_REQUEST; -} - - -void ApismClient::private_handleMininformStartResponse(QJsonObject response) -{ - emit this->sendMininformStartResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); -} - -void ApismClient::private_handleMininformStopResponse(QJsonObject response) -{ - emit this->sendMininformStopResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); -} - -void ApismClient::private_handleReqSelfResponse(QJsonObject response) -{ - emit this->sendReqSelfResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); -} - -void ApismClient::private_handleReqPingResponse(QJsonObject response) -{ - emit this->sendMininformPingResponse(nsApismInterface::RESULT_STATE::SUCCESS, response); -} - -void ApismClient::private_handleIsmasParameterResponse(QJsonObject response) { - emit this->ismasResponseAvailable(response); -} - - - - -/************************************************************************************************ - * operators - */ -QDebug operator<< (QDebug debug, ISMAS::REQUEST request) -{ - switch(request) { - case ISMAS::REQUEST::NO_REQUEST: - debug << QString("ISMAS::REQUEST::NO_REQUEST"); - break; - case ISMAS::REQUEST::START: - debug << QString("ISMAS::REQUEST::START"); - break; - case ISMAS::REQUEST::STOP: - debug << QString("ISMAS::REQUEST::STOP"); - break; - case ISMAS::REQUEST::PING: - debug << QString("ISMAS::REQUEST::PING"); - break; - case ISMAS::REQUEST::SELF: - debug << QString("ISMAS::REQUEST::SELF"); - break; - case ISMAS::REQUEST::ISMAS_PARAMETER: - debug << QString("ISMAS::REQUEST::ISMASPARAMETER"); - break; - } - return debug; -} - -QString& operator<< (QString& str, ISMAS::REQUEST request) -{ - switch(request) { - case ISMAS::REQUEST::NO_REQUEST: - str = QString("ISMAS::REQUEST::NO_REQUEST"); - break; - case ISMAS::REQUEST::START: - str = QString("ISMAS::REQUEST::START"); - break; - case ISMAS::REQUEST::STOP: - str = QString("ISMAS::REQUEST::STOP"); - break; - case ISMAS::REQUEST::PING: - str = QString("ISMAS::REQUEST::PING"); - break; - case ISMAS::REQUEST::SELF: - str = QString("ISMAS::REQUEST::SELF"); - break; - case ISMAS::REQUEST::ISMAS_PARAMETER: - str = QString("ISMAS::REQUEST::ISMASPARAMETER"); - break; - } - return str; -} - diff --git a/apism/apism_client.h b/apism/apism_client.h deleted file mode 100644 index 0f01512..0000000 --- a/apism/apism_client.h +++ /dev/null @@ -1,127 +0,0 @@ -#ifndef APISM_CLIENT_H_INCLUDED -#define APISM_CLIENT_H_INCLUDED - - -#include "ismas_data.h" -#include "apism_tcp_client.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - - - - -QDebug operator<<(QDebug debug, ISMAS::REQUEST request); -QString& operator<<(QString& str, ISMAS::REQUEST request); - -namespace nsApismInterface { - - enum class RESULT_STATE : quint8 { - SUCCESS = 1, - ERROR_BACKEND = 2, // error from backand (e.g. backend replies with error) - ERROR_NETWORK = 3, // error from network (e.g. host not available) - ERROR_TIMEOUT = 4, // the operation timed out - ERROR_PROCESS = 5, // internal plugin error (e.g. bug in implementation) - ERROR_RETRY = 6, // retry operation - INFO = 7 - }; - -} - - -class VendingData; -class ATBHMIconfig; -class PersistentData; -class ATBMachineEvent; -class ApismClient : public QObject { - Q_OBJECT - -public: - explicit ApismClient(QObject *eventReceiver, ATBHMIconfig *config, PersistentData *persistentData, QObject *parent = 0); - ~ApismClient(); - - quint32 getLastError(); - const QString & getLastErrorDescription(); - - ApismTcpClient* getApismTcpSendClient() { return apismTcpSendClient; } - - -public slots: - void sendSelfTest(); - void sendTransaction(const VendingData* vendingData); - // void sendAccount(const QHash &accountDataHash); - //void sendEvent(const ATBMachineEvent* machineEvent); - - void sendState(const QString & state, const QString & msg); - void emulateUpdatesAvailable(QString const &msg); - void onSendCmdSendVersionToIsmas(QString); - void onSendCmdEventToIsmas(QString); - void requestAvailableIsmasUpdates(); - - - //void sendMininformStartRequest(const VendingData* vendingData); - //void sendMininformStopRequest(const VendingData* vendingData); - //void sendMininformPingRequest(); - - void restartApism(); - -signals: - // public signals: - void sendTransactionRespones(nsApismInterface::RESULT_STATE result); - void sendAccountResponse(nsApismInterface::RESULT_STATE result); - - - void sendMininformStartResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); - void sendMininformStopResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); - void sendMininformPingResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); - void sendReqSelfResponse(nsApismInterface::RESULT_STATE result, QJsonObject response); - - void ismasResponseAvailable(QJsonObject ismasResponse); - void sendCmdSendVersionToIsmas(QString); - void sendCmdEventToIsmas(QString); - -private slots: - // void onSocketError(QAbstractSocket::SocketError socketError); - - void onReceivedResponse(QByteArray response); - - void onSendClientResponseTimeout(); - void onRequestResponseClientResponseTimeout(); - -private: - QObject *healthEventReceiver; - ATBHMIconfig *m_config; - - PersistentData *persistentData; - - ApismTcpClient* apismTcpSendClient; - ApismTcpClient* apismTcpRequestResponseClient; - - quint32 lastError; - QString lastErrorDescription; - - QString currentRequestUid; - ISMAS::REQUEST currentRequest; - - - void private_handleMininformStartResponse(QJsonObject response); - void private_handleMininformStopResponse(QJsonObject response); - void private_handlePingResponse(QJsonObject response); - void private_handleReqSelfResponse(QJsonObject response); - void private_handleReqPingResponse(QJsonObject response); - void private_handleIsmasParameterResponse(QJsonObject response); - - void handleISMASResponseError(); - -}; - -// Q_DECLARE_METATYPE(QAbstractSocket::SocketError) - -#endif // APISM_CLIENT_H_INCLUDED diff --git a/apism/apism_tcp_client.cpp b/apism/apism_tcp_client.cpp deleted file mode 100644 index e878b87..0000000 --- a/apism/apism_tcp_client.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include "apism_tcp_client.h" - -#include -#include -#include - -ApismTcpClient::ApismTcpClient(const QString & hostname, - const QString & port, - QObject *parent) - : QObject(parent) - , hostname(hostname) - , port(port) - , responseTimerTimeoutCounter(0) -{ - this->responseTimeoutTimer = new QTimer(this); - this->responseTimeoutTimer->setInterval(10000); - this->responseTimeoutTimer->setSingleShot(true); - - connect(this->responseTimeoutTimer, SIGNAL(timeout()), this, SLOT(onResponseTimeoutTimerTimeout())); - - socket = new QTcpSocket(this); - connect(socket, SIGNAL(connected()), this, SLOT(onSocketConnected())); - connect(socket, SIGNAL(disconnected()), this, SLOT(onSocketDisconnected())); - connect(socket, SIGNAL(readyRead()), this, SLOT(onSocketReadyRead())); - connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(onSocketBytesWritten(qint64))); -} - - - -void ApismTcpClient::connectToHost() { - qCritical() << "ApismTcpClient::connectToHost(this->" << hostname << ", " << port << ")"; - int portNumber = this->port.toInt(); - this->socket->connectToHost(QHostAddress(this->hostname), portNumber); - if (!socket->waitForConnected(10000)) { - qCritical() << "ERROR IN WAIT FOR CONNECTED" << socket->errorString(); - } else { - qDebug() << "connected to" << hostname << ", " << port << ")"; - } -} - -void ApismTcpClient::connectToHost(const QString & hostname, const QString & port) -{ - qCritical() << "ApismTcpClient::connectToHost(" << hostname << ", " << port << ")"; - - int portNumber = port.toInt(); - socket->connectToHost(hostname, portNumber); -} - -void ApismTcpClient::closeConnection() -{ - socket->close(); -} - -bool ApismTcpClient::isConnected() -{ - bool result = false; - QAbstractSocket::SocketState socketState = socket->state(); - - switch (socketState) { - case QAbstractSocket::UnconnectedState: - /* FALLTHRU */ - case QAbstractSocket::HostLookupState: - /* FALLTHRU */ - case QAbstractSocket::ConnectingState: - result = false; - break; - case QAbstractSocket::ConnectedState: - /* FALLTHRU */ - case QAbstractSocket::BoundState: - result = true; - break; - case QAbstractSocket::ClosingState: - /* FALLTHRU */ - case QAbstractSocket::ListeningState: - result = false; - break; - } - - return result; -} - - - - -void ApismTcpClient::sendData(const QByteArray & message) { - qDebug() << "ApismTcpClient::send: " << message; - - this->sendQueue.enqueue(message); - - if (this->isConnected()) { - qCritical() << "ApismTcpClient::send: connected, send" << message; - this->private_sendData(); - } else { - qCritical() << "ApismTcpClient::send: not connected, connect"; - this->connectToHost(); - } -} - -/** - * @brief ApismTcpClient::private_sendData - * - * Precondition is that queue is not empty. - */ -void ApismTcpClient::private_sendData() -{ - // take message from queue - QByteArray ba = this->sendQueue.dequeue(); - - qDebug() << "ApismTcpClient::send: " << QString(ba); - - socket->write(ba); - socket->flush(); - - // start timeoutTimer - this->responseTimeoutTimer->start(); -} - -void ApismTcpClient::onSocketConnected() -{ - qInfo() << "ApismTcpClient: Connected!"; - - if (this->sendQueue.size() > 0) { - this->private_sendData(); - } -} - -void ApismTcpClient::onSocketDisconnected() -{ - qDebug() << "ApismTcpClient: Disconnected!"; - qDebug() << " -> SocketErrorString: " << socket->errorString(); - - if (this->sendQueue.size() > 0) { - this->connectToHost(); - } -} - -void ApismTcpClient::onSocketBytesWritten(qint64 bytes) -{ - Q_UNUSED(bytes) -} - -void ApismTcpClient::onSocketReadyRead() -{ - QByteArray readData; - - // stop timeoutTimer - this->responseTimeoutTimer->stop(); - - readData = socket->readAll(); - - qDebug() << "ISMAS received: " << QString(readData); - - emit this->receivedData(readData); - //QCoreApplication::processEvents(); - - this->socket->close(); -} - - -void ApismTcpClient::onResponseTimeoutTimerTimeout() -{ - if (this->sendQueue.size() == 0) { - return; - } - emit this->responseTimeout(); - - qCritical() << "ApismTcpClient::onResponseTimeoutTimerTimeout() --> skip this message, send next command, if available."; - - // Try next command - this->sendQueue.removeFirst(); - if (this->sendQueue.size() > 0) this->private_sendData(); -} diff --git a/apism/apism_tcp_client.h b/apism/apism_tcp_client.h deleted file mode 100644 index 2b2de10..0000000 --- a/apism/apism_tcp_client.h +++ /dev/null @@ -1,62 +0,0 @@ -#ifndef APISMTCPCLIENT_H -#define APISMTCPCLIENT_H - -#include - -#include -#include -#include -#include - -class QTimer; - -class ApismTcpClient : public QObject -{ - Q_OBJECT -public: - explicit ApismTcpClient(const QString & hostname, const QString & port, QObject *parent = nullptr); - bool isConnected(); - - void connectToHost(); - void connectToHost(const QString & hostname, const QString & port); - - // socket is implicitely closed by APISM - void closeConnection(); - - void sendData(const QByteArray & message); - -public slots: - // socket interface - void onSocketConnected(); - void onSocketDisconnected(); - void onSocketBytesWritten(qint64 bytes); - void onSocketReadyRead(); - -signals: - void receivedData(QByteArray response); - - void responseTimeout(); - -private: - QTcpSocket *socket; - - QQueue sendQueue; - - - QString hostname; - QString port; - - QTimer *responseTimeoutTimer; - quint8 responseTimerTimeoutCounter; - - - void private_sendData(); - - -public slots: - void onResponseTimeoutTimerTimeout(); - - -}; - -#endif // APISMTCPCLIENT_H From b3f4a4086b286ea8894c82e98aef7e2ba315f4b3 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:29:10 +0200 Subject: [PATCH 072/239] Populate text-edit with run-time info from update-process. --- mainwindow.cpp | 106 ++++++++++++++++++++++++++++++++----------------- mainwindow.h | 17 ++++++-- 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 7b60730..f7fa8dd 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,63 +1,97 @@ #include "mainwindow.h" #include "ui_mainwindow.h" #include "worker.h" +#include "utils.h" #include +#include MainWindow::MainWindow(Worker *worker, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) - , m_worker(worker) { + , m_worker(worker) + , m_width(52) { ui->setupUi(this); - connect(ui->start, SIGNAL(clicked()), m_worker, SLOT(update())); - connect(ui->exit, SIGNAL(clicked()), qApp, SLOT(quit())); + ui->updateProgress->setRange(1, 100); + ui->updateProgress->reset(); - int w = 52; + m_startTimer = new QTimer(this); + connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); + m_startTimer->setSingleShot(true); + m_startTimer->start(5 * 1000); + + m_exitTimer = new QTimer(this); + connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); + m_exitTimer->setSingleShot(true); + m_exitTimer->start(1800 * 1000); + + connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); + connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); + connect(ui->start, SIGNAL(clicked()), m_worker, SLOT(update())); + connect(ui->exit, SIGNAL(clicked()), this, SLOT(onQuit())); + connect(m_worker, SIGNAL(stopStartTimer()), this, SLOT(onStopStartTimer())); + connect(m_worker, SIGNAL(restartExitTimer()), this, SLOT(onRestartExitTimer())); + connect(m_worker, SIGNAL(appendText(QString, QString)), this, SLOT(onAppendText(QString, QString))); + connect(m_worker, SIGNAL(showErrorMessage(QString,QString)), this, SLOT(onShowErrorMessage(QString,QString))); + connect(m_worker, SIGNAL(setProgress(quint8)), this, SLOT(onSetProgress(quint8))); QStringList lst; QString start = QDateTime::currentDateTime().toString(Qt::ISODate); - lst << QString("Start: ") + start.leftJustified(w-10); - lst << QString("").leftJustified(w-3, '='); - lst << QString("Machine number : %1 ").arg(996).leftJustified(w-3); - lst << QString("Customer number : %1 ").arg(281).leftJustified(w-3); - lst << QString("Zone number : %1 (%2)").arg(1).arg("yellow").leftJustified(w-3); - lst << QString("").leftJustified(w-3, '='); - lst << QString("Backend connected ").leftJustified(w-10) + " [ ok]"; - lst << QString("Update trigger set ").leftJustified(w-10) + " [ ok]"; - // lst << QString("Found ISMAS machine number : %1 ").arg(996).leftJustified(w-10) + " [done]"; - // lst << QString("Found ISMAS customer number: %1 ").arg(281).leftJustified(w-10) + " [done]"; - lst << QString("Prepare customer environment ").leftJustified(w-10) + " [done]"; - lst << QString("Found %1 files to update ").arg(5).leftJustified(w-10) + " [done]"; - lst << QString("(") + QString("%1").arg(1).rightJustified(2, ' ') + QString(")") - + (QString(" Update ") + "DC2C_print01.json ").leftJustified(w-14) + " [done]"; - lst << QString("(") + QString("%1").arg(2).rightJustified(2, ' ') + QString(")") - + (QString(" Update ") + "DC2C_print01.json ").leftJustified(w-14) + " [done]"; - lst << QString("(") + QString("%1").arg(3).rightJustified(2, ' ') + QString(")") - + QString(" Update opkg pakets ").leftJustified(w-14) + " [done]"; - lst << QString("(") + QString("%1").arg(4).rightJustified(2, ' ') + QString(")") - + QString(" Update device controller %1 ").arg("04.38").leftJustified(w-14) + " [done]"; - lst << QString("Sync customer environment with filesystem ").leftJustified(w-10) + " [done]"; - lst << QString("(") + QString("%1").arg(5).rightJustified(2, ' ') + QString(")") - + (QString(" Update ") + "tariff01.json ").leftJustified(w-14) + " [done]"; - lst << QString("Send ISMAS notification ").leftJustified(w-10) + " [done]"; - lst << QString("Save log file %1.log").arg(start).leftJustified(w-10) + " [done]"; - lst << QString("").leftJustified(w-3, '='); - lst << QString("").leftJustified(w-13) + " [SUCCESS]"; + lst << QString("Start: ") + start.leftJustified(m_width-10); + lst << QString("").leftJustified(m_width-3, '='); + lst << QString("Machine number : %1 ").arg(m_worker->machineNr()).leftJustified(m_width-3); + lst << QString("Customer number : %1 ").arg(m_worker->customerNr()).leftJustified(m_width-3); + lst << QString("Zone number : %1 (%2)").arg(m_worker->zoneNr()).arg(Utils::zoneName(m_worker->zoneNr())).leftJustified(m_width-3); + lst << QString("").leftJustified(m_width-3, '='); ui->updateStatus->setText(lst.join('\n')); ui->updateStatus->setEnabled(true); } MainWindow::~MainWindow() { + delete m_startTimer; + delete m_exitTimer; delete ui; } - -void MainWindow::onStartClicked() { - +void MainWindow::onStopStartTimer() { + m_startTimer->stop(); } -void MainWindow::onExitClicked() { - +void MainWindow::onRestartExitTimer() { + m_exitTimer->stop(); + m_exitTimer->start(5 * 1000); +} + +void MainWindow::onQuit() { + if (!m_worker->updateProcessRunning()) { + qApp->quit(); + } +} + +void MainWindow::onSetProgress(quint8 v) { + qCritical() << "SET VAL" << v; + ui->updateProgress->setValue(v); +} + +void MainWindow::onAppendText(QString text, QString suffix) { + QString editText = ui->updateStatus->toPlainText(); + QStringList lines = editText.split('\n'); + for (int i=0; iupdateStatus->setPlainText(editText); + } else { + editText += QString("\n").leftJustified(m_width-3, '='); + editText += QString("\n").leftJustified(m_width-12) + " [SUCCESS]"; + } + ui->updateStatus->setText(editText); + ui->updateStatus->setEnabled(true); +} + +void MainWindow::onShowErrorMessage(QString title, QString text) { + QMessageBox::critical(this, title, text, QMessageBox::Ok); } diff --git a/mainwindow.h b/mainwindow.h index 366ab20..71cd24e 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -2,12 +2,14 @@ #define MAINWINDOW_H #include +#include QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE -class Worker; +#include "worker.h" + class MainWindow : public QMainWindow { Q_OBJECT @@ -15,12 +17,21 @@ public: MainWindow(Worker *worker, QWidget *parent = nullptr); ~MainWindow(); +public slots: + void onAppendText(QString, QString); + void onShowErrorMessage(QString, QString); + void onSetProgress(quint8); + void onStopStartTimer(); + void onRestartExitTimer(); + private slots: - void onStartClicked(); - void onExitClicked(); + void onQuit(); private: Ui::MainWindow *ui; Worker *m_worker; + int m_width; + QTimer *m_startTimer; + QTimer *m_exitTimer; }; #endif // MAINWINDOW_H From 4e277b4ca6e917233cd354436def3f9edf100914 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:39:08 +0200 Subject: [PATCH 073/239] Added text edit --- mainwindow.ui | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index 46d336c..15a6b01 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -23,31 +23,12 @@ 10 - 10 + 11 351 341 - - - - - Terminus - - - - Start - - - - - - - Exit - - - @@ -62,6 +43,32 @@ + + + + 24 + + + + + + + + Terminus + + + + Start + + + + + + + Exit + + + From e0a68a35f48621f068a4cdccbb28f87ea685ff3d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:39:52 +0200 Subject: [PATCH 074/239] raised output-buffer length to 1024*8 --- message_handler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_handler.cpp b/message_handler.cpp index 55d6d7e..7be2d67 100755 --- a/message_handler.cpp +++ b/message_handler.cpp @@ -6,7 +6,7 @@ #include #include -#define OUTPUT_LEN (1024) +#define OUTPUT_LEN (1024*8) static bool installedMsgHandler = false; static QtMsgType debugLevel = QtInfoMsg; From 2fd1053bf9259e546994f22d90ffb0f6e05656e2 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:40:44 +0200 Subject: [PATCH 075/239] Removed maintenance mode --- main.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/main.cpp b/main.cpp index 8abe704..24f6f4c 100644 --- a/main.cpp +++ b/main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "update.h" #include "git/git_client.h" @@ -27,6 +28,8 @@ #include "apism/apism_client.h" #include "worker_thread.h" #include "worker.h" +#include "mainwindow.h" +#include "utils.h" #include @@ -80,10 +83,6 @@ int main(int argc, char *argv[]) { workingDirectoryOption.setDefaultValue(workingDirectoryDefault); parser.addOption(workingDirectoryOption); - QCommandLineOption maintenanceOption(QStringList() << "m" << "maintenance", - QCoreApplication::translate("main", "Maintenance mode for underlying script")); - parser.addOption(maintenanceOption); - QCommandLineOption dryRunOption(QStringList() << "d" << "dry-run", QCoreApplication::translate("main", "Start ATBUpdateTool in dry-run-mode. No actual actions.")); parser.addOption(dryRunOption); @@ -101,7 +100,6 @@ int main(int argc, char *argv[]) { QString plugInDir = parser.value(pluginDirectoryOption); QString plugInName = parser.value(pluginNameOption); QString workingDir = parser.value(workingDirectoryOption); - bool maintenanceMode = parser.isSet(maintenanceOption); bool dryRun = parser.isSet(dryRunOption); QString const rtPath = QCoreApplication::applicationDirPath(); @@ -118,7 +116,6 @@ int main(int argc, char *argv[]) { qInfo() << "plugInDir ........." << plugInDir; qInfo() << "plugInName ........" << plugInName; qInfo() << "workingDir ........" << workingDir; - qInfo() << "maintenanceMode ..." << maintenanceMode; qInfo() << "dryRun ............" << dryRun; // before loading the library, delete all possible shared memory segments @@ -131,11 +128,9 @@ int main(int argc, char *argv[]) { hwinf *hw = Update::loadDCPlugin(QDir(plugInDir), plugInName); // hw->dc_autoRequest(false); - QString const update_ctrl_file = "/opt/app/tools/atbupdate/update_log.csv"; - - int machineNr = Worker::read1stLineOfFile("/etc/machine_nr"); - int customerNr = Worker::read1stLineOfFile("/etc/cust_nr"); - int zoneNr = Worker::read1stLineOfFile("/etc/zone_nr"); + int machineNr = Utils::read1stLineOfFile("/etc/machine_nr"); + int customerNr = Utils::read1stLineOfFile("/etc/cust_nr"); + int zoneNr = Utils::read1stLineOfFile("/etc/zone_nr"); QString const branchName = (zoneNr != 0) ? QString("zg1/zone%1").arg(zoneNr) : "master"; @@ -148,8 +143,11 @@ int main(int argc, char *argv[]) { zoneNr, branchName, workingDir, - maintenanceMode, dryRun); + MainWindow mw(&worker); + mw.setWindowFlags(Qt::Window | Qt::FramelessWindowHint); + mw.show(); + return a.exec(); } From 7293afd203fec869bd5aced5a5e646f55a6ec8d9 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:44:16 +0200 Subject: [PATCH 076/239] Minor change: include header --- mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index f7fa8dd..9d04a04 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -5,6 +5,7 @@ #include #include +#include MainWindow::MainWindow(Worker *worker, QWidget *parent) : QMainWindow(parent) @@ -71,7 +72,6 @@ void MainWindow::onQuit() { } void MainWindow::onSetProgress(quint8 v) { - qCritical() << "SET VAL" << v; ui->updateProgress->setValue(v); } From d2c0fdf8201ba6f71c10772c4239c24737a558ce Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:44:45 +0200 Subject: [PATCH 077/239] Minor change: remove header --- main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/main.cpp b/main.cpp index 24f6f4c..0bdc500 100644 --- a/main.cpp +++ b/main.cpp @@ -25,7 +25,6 @@ #include "update.h" #include "git/git_client.h" #include "ismas/ismas_client.h" -#include "apism/apism_client.h" #include "worker_thread.h" #include "worker.h" #include "mainwindow.h" From b508c0517d4741aae7dbbb057da1368eae588e89 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:45:20 +0200 Subject: [PATCH 078/239] Reset. Add helper functions read1stLineOfFile() and zoneNr(). --- utils.cpp | 216 +++++++----------------------------------------------- utils.h | 41 +---------- 2 files changed, 29 insertions(+), 228 deletions(-) diff --git a/utils.cpp b/utils.cpp index c91f809..79e5fed 100644 --- a/utils.cpp +++ b/utils.cpp @@ -1,201 +1,35 @@ #include "utils.h" -#include -#include #include -#include -#include #include -#include "interfaces.h" -#include "DCPlugin/include/hwapi.h" -//#include -#include -#include -#include -#include -#include - -#define COLUMN_STATUS (0) -#define COLUMN_NAME (1) -#define COLUMN_DATE_TIME (2) -#define COLUMN_RESULT (3) - -Utils::Utils(QString update_ctrl_file, - QObject *parent, - char const *serialInterface, - char const *baudrate) - : QObject(parent) - , m_hw(new hwapi()) - , m_serialInterface(serialInterface) - , m_baudrate(baudrate) - , m_update_ctrl_file(update_ctrl_file) - , m_update_ctrl_file_copy(update_ctrl_file + ".copy") - , m_in(&m_update_ctrl_file) - , m_out(&m_update_ctrl_file_copy) - , m_init(true) { - - if (!m_update_ctrl_file.exists()) { - qCritical() << "Update-file" << m_update_ctrl_file.fileName() - << "does not exist"; - m_init = false; - } - if (!m_update_ctrl_file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qCritical() << "can not open " << m_update_ctrl_file.fileName() - << "for reading"; - m_init = false; - } - if (!m_update_ctrl_file_copy.open(QIODevice::WriteOnly | QIODevice::Text)) { - qCritical() << "can not open " << m_update_ctrl_file_copy.fileName() - << "for writing"; - m_init = false; - } -} - -Utils::~Utils() { - -} - -void Utils::updateBinary(char const *fileToSendToDC) { - qDebug() << "file to send to DC ..." << fileToSendToDC; - qDebug() << "baudrate ............." << m_baudrate; - qDebug() << "serial interface ....." << m_serialInterface; - m_hw->dc_updateDC(fileToSendToDC, m_baudrate, m_serialInterface); - std::this_thread::sleep_for(std::chrono::milliseconds(3000)); - QCoreApplication::quit(); -} - -void Utils::updatePrinterConf(int nrOfTemplate, char const *fileToSendToDC) { - QVector printTemplates{ nrOfTemplate }; - QVector filesToSend{ fileToSendToDC }; - - m_hw->dc_updatePrinterTemplate(hwapi::FileTypeJson::PRINTER, - printTemplates, filesToSend, - QString(m_baudrate), - QString(m_serialInterface)); - std::this_thread::sleep_for(std::chrono::milliseconds(3000)); - QCoreApplication::quit(); -} - -QStringList Utils::getOpenLines() { - QStringList openLines; - - while (!m_in.atEnd()) { - QString line = m_in.readLine().trimmed(); - // QString.split() is defined >= 5.14 - if (!line.startsWith("OPEN")) { - m_out << line; - } else { - openLines << line; - } - } - return openLines; -} - -bool Utils::doUpdate() { - /* - The file referred to by 'update_data' has the following structure for - each line: - - # ====================================================================== - # STATUS | NAME | DATE | RESULT - # ====================================================================== - # where - # - # STATUS: OPEN or CLOSED - # NAME : If starting with 'opkg' it is an opkg-command to be executed. - # Otherwise its the name of a file which has to be updated. - # DATE : 0000-00-00T00:00:00 - # RESULT: SUCCESS or ERROR (possibly with description) - # - */ - if (!m_init) { - return false; - } - - QStringList openLines = getOpenLines(); - - bool res = false; - QList::const_iterator it; - for (it = openLines.cbegin(); it != openLines.cend(); ++it) { - int start = 0, end; - int column = 0; - QString status, name, datetime, result; - QString line = *it; - while ((end = line.indexOf(QChar(','), start)) != -1) { - QString next = line.mid(start, end).trimmed(); - switch (column) { - case COLUMN_STATUS: - status = next; - break; - case COLUMN_NAME: - name = next; - break; - case COLUMN_DATE_TIME: - datetime = next; - break; - case COLUMN_RESULT: - result = next; - break; +int Utils::read1stLineOfFile(QString fileName) { + QFile f(fileName); + if (f.exists()) { + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream in(&f); + in.setCodec("UTF-8"); + while(!in.atEnd()) { + return in.readLine().toInt(); } - ++column; - start = end + 1; - } - - if (!status.contains("OPEN")) { - qCritical() << "Parsing error for" << m_update_ctrl_file.fileName(); - return false; - } - if (name.contains("dc2c") && name.endsWith(".bin")) { - updateBinary(name.toStdString().c_str()); - res = true; - } else - if (name.contains("DC2C_print") && name.endsWith(".json")) { - int i = name.indexOf("DC2C_print"); - int templateIdx = name.mid(i).midRef(10, 2).toInt(); - updatePrinterConf(templateIdx, name.toStdString().c_str()); - res = true; - } else - if (name.contains("opkg")) { - int i = name.indexOf("opkg "); - QString rest = name.mid(i).trimmed(); - QScopedPointer p(new QProcess(this)); - p->setProcessChannelMode(QProcess::MergedChannels); - p->start("opkg", QStringList() << rest); - if (p->waitForStarted(1000)) { - if (p->state() == QProcess::ProcessState::Running) { - if (p->waitForFinished(10000)) { - QByteArray output = p->readAllStandardOutput(); - qCritical() << output; - res = true; - } - } - } - } else { - // TODO - } - QString resultLine = "CLOSED"; - resultLine += ", " + name; - resultLine += ", " + QDateTime::currentDateTime().toString(Qt::ISODate); - resultLine += ", " + (res == true) ? "SUCCESS" : "ERROR"; - m_out << resultLine; - } // for (it = openLines.cbegin(); it != openLines.end(); ++it) { - - return finishUpdate(openLines.size() > 0); -} - -bool Utils::finishUpdate(bool replaceCtrlFile) { - if (replaceCtrlFile) { - if (!m_update_ctrl_file_copy.exists()) { - return false; - } - if (!m_update_ctrl_file.remove()) { - return false; - } - if (!m_update_ctrl_file_copy.rename(m_update_ctrl_file.fileName())) { - return false; } } - return true; + return -1; +} + +QString Utils::zoneName(quint8 i) { + static constexpr char const *zName[] = { + "", + "purple", + "blue", + "yellow", + "green", + "yellow (mars)", + "green (mars)" + }; + if (i < (sizeof(zName)/sizeof(char const *))) { + return zName[i]; + } + return "N/A"; } diff --git a/utils.h b/utils.h index 4e42753..da7aa97 100644 --- a/utils.h +++ b/utils.h @@ -5,42 +5,9 @@ #include #include -#include +namespace Utils { + int read1stLineOfFile(QString fileName); + QString zoneName(quint8 i); +} -#include "interfaces.h" -#include "DCPlugin/include/hwapi.h" - -#ifdef PTU5 -#define SERIAL_PORT "ttymxc2" -#else -#define SERIAL_PORT "ttyUSB0" -#endif - -class Utils : public QObject { - Q_OBJECT - - std::unique_ptr m_hw; - char const *m_serialInterface; - char const *m_baudrate; - QFile m_update_ctrl_file; - QFile m_update_ctrl_file_copy; - QTextStream m_in; - QTextStream m_out; - - bool m_init; - - void updateBinary(char const *fileToSendToDC); - void updatePrinterConf(int nrOfTemplate, char const *fileToSendToDC); - bool finishUpdate(bool finish); - QStringList getOpenLines(); - static constexpr QChar SEPARATOR = QChar(','); - -public: - explicit Utils(QString update_ctrl_file, - QObject *parent = nullptr, - char const *serialInterface = SERIAL_PORT, - char const *baudrate = "115200"); - virtual ~Utils() override; - bool doUpdate(); -}; #endif // UTILS_H_INCLUDED From 332d689b8c79f388ee44c6d57ce12b09191ed104 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:47:19 +0200 Subject: [PATCH 079/239] Add worker-object to be able to update for the worker. --- update.cpp | 44 +++++++++++++++++++++++++++++++++----------- update.h | 11 ++++------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/update.cpp b/update.cpp index e626b78..15b002f 100644 --- a/update.cpp +++ b/update.cpp @@ -1,4 +1,5 @@ #include "update.h" +#include "worker.h" #include #include @@ -25,9 +26,9 @@ #define UPDATE_OPKG (1) #define UPDATE_DC (1) #define UPDATE_PRINTER_TEMPLATES (1) -#define UPDATE_CASH_TEMPLATE (0) -#define UPDATE_CONF_TEMPLATE (0) -#define UPDATE_DEVICE_TEMPLATE (0) +#define UPDATE_CASH_TEMPLATE (1) +#define UPDATE_CONF_TEMPLATE (1) +#define UPDATE_DEVICE_TEMPLATE (1) static const QMap baudrateMap = { {"1200" , 0}, {"9600" , 1}, {"19200" , 2}, {"38400" , 3}, @@ -75,6 +76,7 @@ hwinf *Update::loadDCPlugin(QDir const &plugInDir, QString const &fname) { } Update::Update(hwinf *hw, + Worker *worker, QString customerRepository, QString customerNrStr, QString branchName, @@ -85,6 +87,7 @@ Update::Update(hwinf *hw, char const *baudrate) : QObject(parent) , m_hw(hw) + , m_worker(worker) , m_serialInterface(serialInterface) , m_baudrate(baudrate) , m_customerRepository(customerRepository) @@ -510,7 +513,7 @@ void Update::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardError())); } -bool Update::doUpdate(QStringList const &filesToWorkOn) { +bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { // // ACHTUNG !!! @@ -542,7 +545,6 @@ bool Update::doUpdate(QStringList const &filesToWorkOn) { bool res = false; QString fToWorkOn = (*it).trimmed(); - if (fToWorkOn.contains("dc2c", Qt::CaseInsensitive) && fToWorkOn.endsWith(".bin", Qt::CaseInsensitive)) { @@ -582,6 +584,10 @@ bool Update::doUpdate(QStringList const &filesToWorkOn) { if ((res = updateBinary(fToWorkOn.toStdString().c_str())) == true) { qCritical() << "downloaded binary" << fToWorkOn; + ++displayIndex; + emit m_worker->appendText(QString("\n(") + QString("%1").arg(displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update ") + QFileInfo(fToWorkOn).fileName(), + Worker::UPDATE_STEP_DONE); } m_hw->dc_autoRequest(true); // turn auto-request setting on @@ -607,6 +613,10 @@ bool Update::doUpdate(QStringList const &filesToWorkOn) { } else { if ((res = updatePrinterTemplate(templateIdx, fToWorkOn))) { qInfo() << "downloaded printer template"<< fToWorkOn; + ++displayIndex; + emit m_worker->appendText(QString("\n(") + QString("%1").arg(displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update ") + QFileInfo(fToWorkOn).fileName(), + Worker::UPDATE_STEP_DONE); } } #endif @@ -614,24 +624,36 @@ bool Update::doUpdate(QStringList const &filesToWorkOn) { && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { res = true; #if UPDATE_CASH_TEMPLATE == 1 - if ((res = updateCashConf(name))) { - qInfo() << "downloaded cash template"<< name; + if ((res = updateCashConf(fToWorkOn))) { + qInfo() << "downloaded cash template"<< fToWorkOn; + ++displayIndex; + emit m_worker->appendText(QString("\n(") + QString("%1").arg(displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update ") + QFileInfo(fToWorkOn).fileName(), + Worker::UPDATE_STEP_DONE); } #endif } else if (fToWorkOn.contains("DC2C_conf", Qt::CaseInsensitive) && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { res = true; #if UPDATE_CONF_TEMPLATE == 1 - if ((res= updateConfig(name))) { - qInfo() << "downloaded config template"<< name; + if ((res= updateConfig(fToWorkOn))) { + qInfo() << "downloaded config template"<< fToWorkOn; + ++displayIndex; + emit m_worker->appendText(QString("\n(") + QString("%1").arg(displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update ") + QFileInfo(fToWorkOn).fileName(), + Worker::UPDATE_STEP_DONE); } #endif } else if (fToWorkOn.contains("DC2C_device", Qt::CaseInsensitive) && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { res = true; #if UPDATE_DEVICE_TEMPLATE == 1 - if ((res = updateDeviceConf(name))) { - qInfo() << "downloaded device template"<< name; + if ((res = updateDeviceConf(fToWorkOn))) { + qInfo() << "downloaded device template"<< fToWorkOn; + ++displayIndex; + emit m_worker->appendText(QString("\n(") + QString("%1").arg(displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update ") + QFileInfo(fToWorkOn).fileName(), + Worker::UPDATE_STEP_DONE); } #endif } else { diff --git a/update.h b/update.h index c612313..24c7d6f 100644 --- a/update.h +++ b/update.h @@ -9,7 +9,6 @@ #include #include "plugins/interfaces.h" -#include "apism/apism_client.h" #ifdef PTU5 #define SERIAL_PORT "ttymxc2" @@ -17,15 +16,12 @@ #define SERIAL_PORT "ttyUSB0" #endif -class Update; - -// TODO: check hardware compatibility -// TODO: opkg commandos - +class Worker; class Update : public QObject { Q_OBJECT hwinf *m_hw; + Worker *m_worker; char const *m_serialInterface; char const *m_baudrate; QString m_customerRepository; @@ -44,6 +40,7 @@ public: explicit Update(hwinf *hw, + Worker *worker, QString customerRepository, QString customerNrStr, QString branchName, @@ -53,7 +50,7 @@ public: char const *serialInterface = SERIAL_PORT, char const *baudrate = "115200"); virtual ~Update() override; - bool doUpdate(QStringList const &linesToWorkOn); + bool doUpdate(int &displayIndex, QStringList const &linesToWorkOn); //QString customerId() { return m_customerId; } //QString const customerId() const { return m_customerId; } From 81a93044381903164ec73b37e122cf13c0de0737 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:50:04 +0200 Subject: [PATCH 080/239] Worker is the work-horse of the update process. Using ismas-client it sends requests to APISM and gets results in a synchronous fashion. Add previous emits to git client and apism client have been removed. --- worker.cpp | 869 +++++++++++++++++++++++++++++++++-------------------- worker.h | 96 +++--- 2 files changed, 600 insertions(+), 365 deletions(-) diff --git a/worker.cpp b/worker.cpp index 910778b..7dc06f0 100644 --- a/worker.cpp +++ b/worker.cpp @@ -12,25 +12,20 @@ #include #include #include +#include +#include +#include + +#include #include "message_handler.h" #include "plugins/interfaces.h" #include "ismas/ismas_client.h" -#include "apism/apism_client.h" -int Worker::read1stLineOfFile(QString fileName) { - QFile f(fileName); - if (f.exists()) { - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - QTextStream in(&f); - in.setCodec("UTF-8"); - while(!in.atEnd()) { - return in.readLine().toInt(); - } - } - } - return -1; -} +QString const Worker::UPDATE_STEP_OK(" [ ok]"); +QString const Worker::UPDATE_STEP_DONE(" [done]"); +QString const Worker::UPDATE_STEP_FAIL(" [fail]"); +QString const Worker::UPDATE_STEP_SUCCESS(" [SUCCESS]"); Worker::Worker(hwinf *hw, int customerNr, @@ -38,14 +33,12 @@ Worker::Worker(hwinf *hw, int zoneNr, QString branchName, QString workingDirectory, - bool maintenanceMode, bool dryRun, QObject *parent, char const *serialInterface, char const *baudrate) : m_hw(hw) , m_workerThread("workerThread") - , m_apismClient(0, 0, 0, this) // TODO , m_customerNr(customerNr) , m_customerNrStr(QString("customer_") + QString::number(m_customerNr).rightJustified(3, '0')) , m_machineNr(machineNr) @@ -54,8 +47,9 @@ Worker::Worker(hwinf *hw, , m_branchName(branchName) , m_customerRepositoryPath(QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(m_customerNrStr)) , m_customerRepository(QDir::cleanPath(m_workingDirectory + QDir::separator() + m_customerNrStr)) + , m_update(new Update(m_hw, this, m_customerRepository, m_customerNrStr, m_branchName, + m_workingDirectory, dryRun, parent, serialInterface, baudrate)) , m_gc(m_customerNrStr, m_customerRepository, m_workingDirectory, m_branchName, this) - , m_maintenanceMode(maintenanceMode) , m_osVersion(getOsVersion()) , m_atbqtVersion(getATBQTVersion()) , m_cpuSerial(getCPUSerial()) @@ -69,7 +63,9 @@ Worker::Worker(hwinf *hw, , m_pluginVersionPrmCalcConfig(getPluginVersion("/opt/app/ATBAPP/plugins/libPRM_CalculatePricePlugin_ConfigUi.so")) , m_pluginVersionTcpZvt(getPluginVersion("/opt/app/ATBAPP/plugins/libTCP_ZVT_CCPlugin.so")) , m_ismasUpdateRequests(ISMAS_UPDATE_REQUESTS) - , m_waitForNewUpdates(this) { + , m_waitForNewUpdates(this) + , m_filesToUpdate() + , m_updateProcessRunning(false) { QDir::setCurrent(m_workingDirectory); @@ -86,13 +82,6 @@ Worker::Worker(hwinf *hw, qInfo() << "BRANCH_NAME ................" << m_branchName; qInfo() << "WORKING_DIRECTORY .........." << m_workingDirectory; - //QProcess p; - //p.start("/bin/systemctl", {"restart", "apism"}); - //if (!p.waitForStarted(5000) || !p.waitForFinished(5000)) { - // qCritical() << "APISM-RESTART-FAILURE"; - // return; - //} - this->moveToThread(&m_workerThread); m_workerThread.start(); @@ -104,47 +93,6 @@ Worker::Worker(hwinf *hw, } QThread::sleep(1); } - - connect(&m_apismClient, SIGNAL(ismasResponseAvailable(QJsonObject)), this, - SLOT(onIsmasResponseReceived(QJsonObject))); - connect(this, SIGNAL(summarizeRepositoryStatus()), this, - SLOT(onSummarizeRepositoryStatus()), Qt::QueuedConnection); - connect(this, SIGNAL(sendCmdSendVersionToIsmas()), this, - SLOT(onSendCmdSendVersionToIsmas()), Qt::QueuedConnection); - connect(this, SIGNAL(summarizeUpload(QStringList)), this, - SLOT(onSummarizeUpload(QStringList)), Qt::QueuedConnection); - connect(this, SIGNAL(handleChangedFiles(QStringList)), this, - SLOT(onHandleChangedFiles(QStringList)), Qt::QueuedConnection); - connect(this, SIGNAL(finishUpdateProcess(bool)), this, - SLOT(onFinishUpdateProcess(bool)), Qt::QueuedConnection); - connect(this, SIGNAL(terminateUpdateProcess()), this, - SLOT(onTerminateUpdateProcess()), Qt::QueuedConnection); - - connect(&m_emergencyTimer, SIGNAL(timeout()), this, SLOT(onTerminateUpdateProcess()), Qt::QueuedConnection); - m_emergencyTimer.setSingleShot(true); - m_emergencyTimer.start(1000 * 60 * 10); - - QDir customerRepository(m_customerRepository); - if (!customerRepository.exists()) { - if (m_gc.gitCloneAndCheckoutBranch()) { - // do nothing else, not even executing opkg-commands - emit this->finishUpdateProcess(false); - } - } else { - m_update = new Update(m_hw, - m_customerRepository, - m_customerNrStr, - m_branchName, - m_workingDirectory, - dryRun, parent, serialInterface, baudrate); - - connect(&m_startUpdateProcess, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); - m_startUpdateProcess.setSingleShot(true); - m_startUpdateProcess.start(1000); - - connect(&m_waitForNewUpdates, SIGNAL(timeout()), this, SLOT(askIsmasForNewData()), Qt::QueuedConnection); - m_waitForNewUpdates.setSingleShot(false); - } } Worker::~Worker() { @@ -163,6 +111,428 @@ Worker::~Worker() { } } +static std::once_flag once; +void Worker::update() { + std::call_once(once, &Worker::privateUpdate, this); +} + +void Worker::privateUpdate() { + // user should not start the update process several times + QPushButton *start = qobject_cast(QObject::sender()); + start->setEnabled(false); + + emit stopStartTimer(); + m_updateProcessRunning = true; + + bool sentIsmasLastVersionNotification = false; + + QDir customerRepository(m_customerRepository); + if (!customerRepository.exists()) { + if (m_gc.gitCloneAndCheckoutBranch()) { + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_SUCCESS, + QString("CLONED AND CHECKED OUT: ") + m_customerRepository); + + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.cloneAndCheckoutCustomerRepository( + m_updateStatus.m_statusDescription)); + + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSASucceeded("")); + + } + } else { + // checkout branch + if (m_gc.gitCheckoutBranch()) { + int progress = 10; + m_ismasClient.setProgressInPercent(progress); + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH, + QString("CHECKED OUT BRANCH: ") + m_gc.branchName()); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.checkoutBranch( + m_updateStatus.m_statusDescription, "")); + + emit setProgress(progress); + if (backendConnected()) { + progress = 20; + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + if (updateTriggerSet()) { + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + if (customerEnvironment()) { + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + if (filesToUpdate()) { + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + if (updateFiles(progress)) { + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + if (syncCustomerRepositoryAndFS()) { + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + if (sendIsmasLastVersionNotification()) { + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + sentIsmasLastVersionNotification = true; + if (saveLogFile()) { + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + emit appendText(QString(""), UPDATE_STEP_SUCCESS); + + // mark update as activated -> this resets the WAIT button + progress = 100; + emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAActivated()); + } + } + } + } + } + } + } + } + } + } + if (!sentIsmasLastVersionNotification) { + // try even if the backend is not connected + sendIsmasLastVersionNotification(); + } + + m_updateProcessRunning = false; + emit restartExitTimer(); +} + +bool Worker::backendConnected() { + static int repeat = 0; + + if (repeat < 3) { + std::optional result + = IsmasClient::sendRequestReceiveResponse( + IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_SELF#J={}"); + if (result) { + QString msg = result.value(); + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "(2) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" + << parseError.error << parseError.errorString(); + return false; + } + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + return false; + } + + QJsonObject obj = document.object(); + QStringList keys = obj.keys(); + for (QString const& key : keys ) { + if (key.contains("CMD_GET_APISMSTATUS_RESPONSE")) { + QJsonValue v = obj.value(key); + if (v.isObject()) { + obj = v.toObject(); + bool ismas = obj.value("ISMAS").toBool(); + QString status = obj.value("Broker").toString(); + + qCritical() << "XXXXXXXXXX STATUS" << status; + + if (ismas) { + if (status == "Connected") { + // do not send, as this would result in a corrupted wait button + // but update the user-interface + emit appendText("\nBackend connected", UPDATE_STEP_OK); + return true; + } + } + if (status.startsWith("Connecting") || status.startsWith("Re-Connecting")) { + QThread::sleep(1); + ++repeat; + return backendConnected(); + } + emit appendText("\nBackend connected", UPDATE_STEP_FAIL); + emit showErrorMessage("Error", "Backend not available"); + } + } + } + } + } + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_NOT_CONNECTED, + QString("NO BACKEND CONNECTION")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.errorBackendNotConnected(m_updateStatus.m_statusDescription, "")); + + return false; +} + +bool Worker::updateTriggerSet() { +// nmap -Pn 62.141.45.68 -p 8883 +// Host is up (0.053s latency). +// +// PORT STATE SERVICE +// 8883/tcp open secure-mqtt + + QString triggerValue(""); + + if (std::optional result + = IsmasClient::sendRequestReceiveResponse( + IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_ISMASPARAMETER#J={}")) { + QString msg = result.value(); + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "(2) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" + << parseError.error << parseError.errorString(); + emit showErrorMessage("check update trigger", + QString("invalid json ") + msg.mid(0, 20)); + return false; + } + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + emit showErrorMessage("check update trigger", + QString("not a json object") + msg); + return false; + } + + QJsonObject obj = document.object(); + // sanity check: cust_nr and machine_nr of PSA correct ? + if (obj.contains("Dev_ID")) { + QJsonValue v = obj.value("Dev_ID"); + if (v.isObject()) { + QJsonObject obj = v.toObject(); + if (obj.contains("Custom_ID") && obj.contains("Device_ID")) { + QJsonValue const c = obj.value("Custom_ID"); + QJsonValue const m = obj.value("Device_ID"); + int customerNr = c.toInt(-1); + int machineNr = m.toInt(-1); + if (customerNr != m_customerNr) { + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + QString("CUSTOMER-NR (%1) != LOCAL CUSTOMER-NR (%2)") + .arg(customerNr).arg(m_customerNr)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + return false; + } + if (machineNr != m_machineNr) { + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + QString("MACHINE-NR (%1) != LOCAL MACHINE-NR (%2)") + .arg(machineNr).arg(m_machineNr)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + return false; + } + } + } + } + if (obj.contains("Fileupload")) { + QJsonValue v = obj.value("Fileupload"); + if (v.isObject()) { + obj = v.toObject(); + if (obj.contains("TRG")) { + v = obj.value("TRG"); + if (v.isString()) { + triggerValue = v.toString(); + if (triggerValue == "WAIT") { + emit appendText("\nUpdate trigger set", UPDATE_STEP_OK); + + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_SET, + QString("UPDATE TRIGGER SET. CONTINUE. ")); + + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateTriggerSet(m_updateStatus.m_statusDescription, "")); + + return true; + } else { + emit showErrorMessage("check update trigger", + QString ("TRG key=<") + triggerValue + + ">\n(reset download button?)"); + } + } + } else { + emit showErrorMessage("check update trigger", "TRG key not contained"); + } + } else { + emit showErrorMessage("check update trigger", "Fileupload not a json-object"); + } + } + } else { + emit showErrorMessage("check update trigger", "no ISMAS response"); + } + + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_NOT_SET_OR_WRONG, + QString("UPDATE-TRIGGER-NOT-SET-OR-WRONG: VALUE=(") + + triggerValue + ")"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.errorUpdateTrigger(m_updateStatus.m_statusDescription, "")); + return false; +} + +bool Worker::customerEnvironment() { + if (QDir(m_customerRepository).exists()) { + if (m_gc.gitCheckoutBranch()) { + emit appendText("\nPrepare customer environment", UPDATE_STEP_DONE); + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH, + QString("CHECKED-OUT BRANCH ") + m_gc.branchName()); + + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.checkoutBranch(m_updateStatus.m_statusDescription, "")); + return true; + } else { + emit showErrorMessage("cust-env", + QString("Checkout ") + m_customerRepository + " failed"); + } + } else { + emit showErrorMessage("cust-env", m_customerRepository + " does not exist"); + } + return false; +} + +bool Worker::filesToUpdate() { + if (std::optional changes = m_gc.gitFetch()) { + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES, + QString("FETCHING OF ") + m_customerRepositoryPath + + QString(" INTO ") + m_customerRepository); + + if (std::optional changedFileNames = m_gc.gitDiff(changes.value())) { + m_filesToUpdate = changedFileNames.value(); + int const size = m_filesToUpdate.size(); + if (size > 1) { + emit appendText(QString("\nFound %1 files to update ").arg(size), UPDATE_STEP_DONE); + } else { + emit appendText(QString("\nFound 1 file to update "), UPDATE_STEP_DONE); + } + return true; + } else { + emit showErrorMessage("files to update", "no files to update (checked-in any files?)"); + } + } else { + emit showErrorMessage("files to update", + QString("no changes in ") + m_customerRepository + + " (checked-in any files?)"); + } + return false; +} + +bool Worker::updateFiles(quint8 percent) { + QStringList filesToDownload; + m_displayIndex = 0; + for (int i = 0; i < m_filesToUpdate.size(); ++i) { + QString fName = m_filesToUpdate.at(i); + if (fName.contains("opkg_commands", Qt::CaseInsensitive)) { + // execute opkg commands + if (QDir::setCurrent(m_customerRepository)) { + QFile f(fName); + if (f.exists()) { + if (f.open(QIODevice::ReadOnly)) { + QTextStream in(&f); + int cmdCount = 0; + while (!in.atEnd()) { + QString line = in.readLine(); + static const QRegularExpression comment("^\\s*#.*$"); + if (line.indexOf(comment, 0) == -1) { + // found opkg command + QString opkgCommand = line.trimmed(); + executeOpkgCommand(opkgCommand); + ++cmdCount; + + m_ismasClient.setProgressInPercent(++percent); + m_updateStatus = UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND, + QString("EXEC OPKG-COMMAND ") + opkgCommand); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.execOpkgCommand(m_updateStatus.m_statusDescription, "")); + } + } + f.close(); + if (cmdCount > 0) { + m_displayIndex = 1; + emit appendText(QString("\n(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update opkg pakets "), UPDATE_STEP_DONE); + } + } + } + } + } else + if (fName.contains("print", Qt::CaseInsensitive)) { + filesToDownload << fName; // download printer-config-files + } else + if (fName == "dc2c.bin") { + filesToDownload << fName; // download device controller + } + } + + qCritical() << "XXXXXXXXXXXXXXXXXXX FILES_TO_WORK_ON" << filesToDownload; + + return m_update->doUpdate(m_displayIndex, filesToDownload); +} + +bool Worker::syncCustomerRepositoryAndFS() { + if (QDir(m_customerRepository).exists()) { + if (QDir::setCurrent(m_customerRepository)) { + QString const params("--recursive " + "--progress " + "--checksum " + "--exclude=.* " + "--include=*.bin " + "--include=*.json " + "--include=opkg_commands " + "--include=*.ini"); + QStringList cmds; + cmds << QString("rsync ") + params.simplified() + " etc/ /etc"; + cmds << QString("rsync ") + params.simplified() + " opt/ /opt"; + + QString cmd; + bool error = false; + foreach (cmd, cmds) { + if (!error) { + Command c("bash"); + qInfo() << "EXECUTING CMD..." << cmd; + if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { + qCritical() << c.getCommandResult() << "SUCCESS"; + } else { + qCritical() << "CMD" << cmd << "FAILED"; + error = true; + } + } + } + if (!error) { + emit appendText(QString("\nSync customer environment with filesystem "), + UPDATE_STEP_DONE); + return true; + } + } + } + return false; +} + +bool Worker::sendIsmasLastVersionNotification() { + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_SENDVERSION#J=") + + m_ismasClient.updateOfPSASendVersion(getPSAInstalled())); + emit appendText(QString("\nSend last version info "), UPDATE_STEP_DONE); + return true; +} + +bool Worker::saveLogFile() { + return true; +} QString Worker::getOsVersion() const { QString const cmd = QString("echo -n $(cat /etc/os-release | head -n 1 | cut -d'\"' -f2 | tr -d '\"')"); Command c("bash"); @@ -245,132 +615,7 @@ qint64 Worker::getFileSize(QString const &fileName) const { return fInfo.exists() ? fInfo.size() : -1; } -void Worker::onHandleChangedFiles(QStringList changedFiles) { - - QString opkg_commands; - static const QRegularExpression re("^.*opkg_commands\\s*$"); - static const QRegularExpression comment("^\\s*#.*$"); - int idx = changedFiles.indexOf(re); - if (idx != -1) { - opkg_commands = changedFiles.takeAt(idx); - - qInfo() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMANDS, - QString("EXEC OPKG-COMMANDS FOR ") + opkg_commands); - - if (QDir::setCurrent(m_customerRepository)) { - QFile f(opkg_commands); - if (f.exists()) { - if (f.open(QIODevice::ReadOnly)) { - QTextStream in(&f); - while (!in.atEnd()) { - QString line = in.readLine(); - if (line.indexOf(comment, 0) == -1) { - // found opkg command - QString opkgCommand = line.trimmed(); - executeOpkgCommand(opkgCommand); - } - } - f.close(); - - qInfo() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS, - QString("EXECUTING OPKG-COMMANDS OK")); - } - } - } - } - - if (m_update->doUpdate(changedFiles)) { // first update the hardware - // then sync the file-system - if (QDir(m_customerRepository).exists()) { - if (QDir::setCurrent(m_customerRepository)) { - QString const params("--recursive " - "--progress " - "--checksum " - "--exclude=.* " - "--include=*.bin " - "--include=*.json " - "--include=opkg_commands " - "--include=*.ini"); - QStringList cmds; - cmds << QString("rsync ") + params.simplified() + " etc/ /etc"; - cmds << QString("rsync ") + params.simplified() + " opt/ /opt"; - - QString cmd; - bool error = false; - foreach (cmd, cmds) { - if (!error) { - Command c("bash"); - qInfo() << "EXCUTING CMD..." << cmd; - if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { - qDebug() << c.getCommandResult(); - } else { - qCritical() << "CMD" << cmd << "FAILED"; - error = true; - } - } - } - if (!error) { - emit this->finishUpdateProcess(true); - return; - } - } - } - } - onTerminateUpdateProcess(); -} - -void Worker::onSummarizeUpload(QStringList changedFiles) { - QDateTime const c = QDateTime::currentDateTime(); - QDate const d = c.date(); - QTime const t = c.time(); - - QString uploadHistoryFile = QString("upload_history_%1%2%3T%4%5%6.txt") - .arg(d.year()).arg(d.month()).arg(d.day()) - .arg(t.hour()).arg(t.minute()).arg(t.second()); - - QFile f(uploadHistoryFile); - if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream out(&f); - QString fName; - foreach (fName, changedFiles) { - QString lastCommit = m_gc.gitLastCommit(fName); - out << fName << ":" << lastCommit << "\n"; - } - } else { - // TODO: error an ISMAS - } -} - -void Worker::onSummarizeRepositoryStatus() { - // TODO - QString dir("/opt/app/tools/atbupdate/customer_999"); - QDirIterator it(dir, QStringList() << "*.jpg", - QDir::Files, QDirIterator::Subdirectories); - while (it.hasNext()) { - qDebug() << it.next(); - if (m_gc.gitIsFileTracked(it.next())) { - QString lastCommit = m_gc.gitLastCommit(it.next()); - } - } - - /* - QString repoStatusHistoryFile = QString("repo_status_history_%1%2%3T%4%5%6.txt") - .arg(d.year()).arg(d.month()).arg(d.day()) - .arg(t.hour()).arg(t.minute()).arg(t.second()); - if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream out(&f); - QString fName; - foreach (fName, changedFiles) { - QString lastCommit = m_gc.gitLastCommit(fName); - out << fName << ":" << lastCommit << "\n"; - } - } else { - // TODO: error an ISMAS - } - */ -} - -void Worker::executeOpkgCommand(QString opkgCommand) { +bool Worker::executeOpkgCommand(QString opkgCommand) { Command c(opkgCommand); if (c.execute(m_workingDirectory)) { QString const r = c.getCommandResult(); @@ -378,115 +623,92 @@ void Worker::executeOpkgCommand(QString opkgCommand) { QString("EXECUTE OPKG COMMAND %1 OK: %2") .arg(opkgCommand) .arg(c.getCommandResult())); + return true; } else { qCritical() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE, QString("EXECUTE OPKG COMMAND %1 FAILED") .arg(opkgCommand)); - onTerminateUpdateProcess(); - return; } + return false; } -// sollte ParameterResponse heissen -void Worker::onIsmasResponseReceived(QJsonObject ismasResponse) { +PSAInstalled Worker::getPSAInstalled() { + QStringList const dcVersion = getDCVersion(); + QString const deviceControllerVersionHW = dcVersion.first(); + QString const deviceControllerVersionSW = dcVersion.last(); - qInfo() << "IN ON_ISMAS_RESPONSE_RECEIVED" << QThread::currentThread()->objectName(); + qInfo() << "CURRENT DC-HW-VERSION: " << deviceControllerVersionHW; + qInfo() << "CURRENT DC-SW-VERSION: " << deviceControllerVersionSW; - if (!ismasResponse.isEmpty()) { - QStringList const keys = ismasResponse.keys(); - qInfo() << UpdateStatus(UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED, - QString("RECEIVED JSON WITH KEYS: ") + keys.join(",")); + QString const deviceControllerGitBlob = "N/A"; + QString const deviceControllerGitLastCommit = "N/A"; - static QRegularExpression re("^REQ_ISMASPARAMETER.*"); - if(keys.indexOf(re) >= 0) { - m_waitForNewUpdates.stop(); // stop asking ISMAS for updates + PSAInstalled psaInstalled; + QString printSysDir("/etc/psa_config"); + QString tariffSysDir("/etc/psa_tariff"); + QString absPathName; - // sanity check: cust_nr and machine_nr of PSA correct ? - if (keys.contains("Dev_ID", Qt::CaseInsensitive)) { - QJsonObject const devId = ismasResponse["Dev_ID"].toObject(); - QStringList const keys = devId.keys(); - if (keys.contains("Custom_ID") && keys.contains("Device_ID")) { - QJsonValue const c = devId.value("Custom_ID"); - QJsonValue const m = devId.value("Device_ID"); - int customerNr = c.toInt(-1); - int machineNr = m.toInt(-1); - if (customerNr != m_customerNr) { - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; - m_statusDescription - = QString("CUSTOMER-NR (%1) != LOCAL CUSTOMER-NR (%2)") - .arg(customerNr).arg(m_customerNr); - return; - } - if (machineNr != m_machineNr) { - m_statusDescription - = QString("MACHINE-NR (%1) != LOCAL MACHINE-NR (%2)") - .arg(machineNr).arg(m_machineNr); - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; - return; - } - } - } - // TODO: check if zone_nr is correct - - if (keys.contains("Fileupload", Qt::CaseInsensitive)) { - QJsonObject fileUpload = ismasResponse["Fileupload"].toObject(); - QJsonValue v = fileUpload.value("TRG"); - if (!v.isNull() && !v.isUndefined()) { - QString const s = v.toString(""); - if (s == "WAIT") { - m_ismasUpdateRequests = ISMAS_UPDATE_REQUESTS; - - qInfo() << UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS, - "DETECTED AVAILABLE ISMAS-DOWNLOAD"); - - QString const &data = m_ismasClient.updateOfPSAActivated(); - m_apismClient.onSendCmdEventToIsmas(data); - - emit m_gc.ismasUpdatesAvailable(); - } else { - // TODO: enorm wichtig - qCritical() << "DID NOT RECEIVE 'WAIT' BUT" << s; - onTerminateUpdateProcess(); - } - } - } else { - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; - m_statusDescription = "NO FILEUPLOAD KEY AVAILABLE"; - return; - } - - } - } else { - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_FAILURE; - m_statusDescription = "NO ISMAS RESPONSE AVAILABLE (EMPTY)"; + if (m_zoneNr != 0) { + QString const &n = QString("%1").arg(m_zoneNr).rightJustified(2, '0'); + psaInstalled.tariff.name = QString("tariff%1.json").arg(n); + absPathName = QDir::cleanPath(tariffSysDir + QDir::separator() + psaInstalled.tariff.name); + psaInstalled.tariff.blob = m_gc.gitBlob(absPathName); + psaInstalled.tariff.size = getFileSize(absPathName); + psaInstalled.tariff.zone = m_zoneNr; } + psaInstalled.tariff.project = "Szeged"; + psaInstalled.tariff.info = "N/A"; + psaInstalled.tariff.loadTime = "N/A"; // QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + psaInstalled.tariff.version = "N/A"; + + psaInstalled.hw.linuxVersion = m_osVersion; + psaInstalled.hw.cpuSerial = m_cpuSerial; + + psaInstalled.dc.versionHW = deviceControllerVersionHW; + psaInstalled.dc.versionSW = deviceControllerVersionSW; + psaInstalled.dc.gitBlob = "N/A"; + psaInstalled.dc.gitLastCommit = "N/A"; + psaInstalled.dc.size = -1; + + psaInstalled.sw.raucVersion = m_raucVersion; + psaInstalled.sw.opkgVersion = m_opkgVersion; + psaInstalled.sw.atbQTVersion = m_atbqtVersion; + + psaInstalled.pluginVersion.deviceController = m_pluginVersionATBDeciceController; + psaInstalled.pluginVersion.ingenicoISelfCC = m_pluginVersionIngenicoISelf; + psaInstalled.pluginVersion.mobilisisCalculatePrice = m_pluginVersionMobilisisCalc; + psaInstalled.pluginVersion.mobilisisCalculatePriceConfigUi = m_pluginVersionMobilisisCalcConfig; + psaInstalled.pluginVersion.prmCalculatePrice = m_pluginVersionPrmCalc; + psaInstalled.pluginVersion.prmCalculatePriceConfigUi = m_pluginVersionPrmCalcConfig; + psaInstalled.pluginVersion.tcpZVT = m_pluginVersionTcpZvt; + + psaInstalled.cash.name = "DC2C_cash.json"; + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.cash.name); + psaInstalled.cash.blob = m_gc.gitBlob(absPathName); + psaInstalled.cash.size = getFileSize(absPathName); + + psaInstalled.conf.name = "DC2C_conf.json"; + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.conf.name); + psaInstalled.conf.blob = m_gc.gitBlob(absPathName); + psaInstalled.conf.size = getFileSize(absPathName); + + psaInstalled.device.name = "DC2C_device.json"; + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.device.name); + psaInstalled.device.blob = m_gc.gitBlob(absPathName); + psaInstalled.device.size = getFileSize(absPathName); + + for (int i=0; i < 32; ++i) { + QString const &n = QString("%1").arg(i+1).rightJustified(2, '0'); + psaInstalled.print[i].name = QString("DC2C_print%1.json").arg(n); + absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.print[i].name); + psaInstalled.print[i].blob = m_gc.gitBlob(absPathName); + psaInstalled.print[i].size = getFileSize(absPathName); + } + + return psaInstalled; } -void Worker::onFinishUpdateProcess(bool changes) { - Q_UNUSED(changes); - - qInfo() << "ON FINISH UPDATE PROCESS" << QThread::currentThread()->objectName(); - // m_emergencyTimer.stop(); - - onSendCmdSendVersionToIsmas(); // final message to ISMAS - - m_workerThread.quit(); - QApplication::quit(); - exit(0); -} - - -void Worker::onTerminateUpdateProcess() { - qCritical() << "ON TERMINATE UPDATE PROCESS"; - - onSendCmdSendVersionToIsmas(); // final message to ISMAS - - m_workerThread.quit(); - QApplication::quit(); - exit(-1); -} - -void Worker::onSendCmdSendVersionToIsmas() { +QString Worker::sendCmdSendVersionToIsmas() { QStringList const dcVersion = getDCVersion(); QString const deviceControllerVersionHW = dcVersion.first(); @@ -560,37 +782,8 @@ void Worker::onSendCmdSendVersionToIsmas() { psaInstalled.print[i].size = getFileSize(absPathName); } - QString data = m_ismasClient.updateOfPSASendVersion(psaInstalled); - - // printf("data=%s\n", data.toStdString().c_str()); - - m_apismClient.onSendCmdSendVersionToIsmas(data); -} - -void Worker::askIsmasForNewData() { - if (m_maintenanceMode) { - m_updateStatus = UPDATE_STATUS::ISMAS_EMULATE_DATA_AVAILABLE; - QString data = m_ismasClient.setUpdatesAvailable(); - m_apismClient.emulateUpdatesAvailable(data); - } - - //m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING; - //m_statusDescription = "Ask ISMAS IF NEW DATA AVAILABLE"; - - qInfo() << UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING, - QString("ASK ISMAS IF NEW DATA AVAILABLE") + - QString(" (%1)").arg(m_ismasUpdateRequests)); - - m_apismClient.requestAvailableIsmasUpdates(); - - if (--m_ismasUpdateRequests > 0) { - // if the timer is already running, it will be stopped and restarted. - m_waitForNewUpdates.start(10000); - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING; - } else { - m_updateStatus = UPDATE_STATUS::ISMAS_UPDATE_REQUEST_TIMEOUT; - onTerminateUpdateProcess(); - } + // QByteArray data = "#M=APISM#C=CMD_SENDVERSION#J="; + return m_ismasClient.updateOfPSASendVersion(psaInstalled); } /************************************************************************************************ @@ -598,16 +791,28 @@ void Worker::askIsmasForNewData() { */ QDebug operator<< (QDebug debug, UpdateStatus status) { switch(status.m_updateStatus) { - case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: - debug << QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: ") + case UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: + debug << QString("UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: ") << status.m_statusDescription; break; - case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: - debug << QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: ") + case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_PENDING: + debug << QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_PENDING: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE: + debug << QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_TIMEOUT: + debug << QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: + debug << QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: ") << status.m_statusDescription; break; - case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: - debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: ") + case UPDATE_STATUS::GIT_FETCH_UPDATES: + debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES: ") << status.m_statusDescription; break; case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: @@ -653,16 +858,20 @@ QDebug operator<< (QDebug debug, UpdateStatus status) { QString& operator<< (QString& str, UpdateStatus status) { switch(status.m_updateStatus) { - case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: - str = QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_PENDING: "); + case UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: + str = QString("UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_PENDING: + str = QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_PENDING: "); str += status.m_statusDescription; break; - case UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: - str = QString("UPDATE_STATUS::ISMAS_UPDATE_REQUEST_SUCCESS: "); + case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: + str = QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: "); str += status.m_statusDescription; break; - case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: - str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST: "); + case UPDATE_STATUS::GIT_FETCH_UPDATES: + str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES: "); str += status.m_statusDescription; break; case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: diff --git a/worker.h b/worker.h index b997e1a..4a4a371 100644 --- a/worker.h +++ b/worker.h @@ -13,7 +13,6 @@ #include "update.h" #include "git/git_client.h" #include "ismas/ismas_client.h" -#include "apism/apism_client.h" #ifdef PTU5 #define SERIAL_PORT "ttymxc2" @@ -23,21 +22,31 @@ enum class UPDATE_STATUS : quint8 { - ISMAS_EMULATE_DATA_AVAILABLE, - ISMAS_UPDATE_REQUEST_PENDING, - ISMAS_UPDATE_REQUEST_FAILURE, - ISMAS_UPDATE_REQUEST_TIMEOUT, - ISMAS_UPDATE_REQUEST_SUCCESS, + NOT_DEFINED, + STEP_OK, + STEP_DONE, + STEP_FAIL, + ISMAS_WAIT_STATE_CHECK_PENDING, + ISMAS_WAIT_STATE_CHECK_FAILURE, + ISMAS_WAIT_STATE_CHECK_TIMEOUT, + ISMAS_WAIT_STATE_CHECK_SUCCESS, ISMAS_RESPONSE_RECEIVED, - GIT_CHECKOUT_BRANCH_REQUEST, + BACKEND_CONNECTED, + BACKEND_NOT_CONNECTED, + UPDATE_TRIGGER_SET, + UPDATE_TRIGGER_NOT_SET_OR_WRONG, + GIT_CLONE_AND_CHECKOUT_SUCCESS, + GIT_CLONE_AND_CHECKOUT_FAILURE, + GIT_CHECKOUT_BRANCH, GIT_CHECKOUT_BRANCH_REQUEST_FAILURE, GIT_CHECKOUT_BRANCH_NOT_EXISTS, GIT_CHECKOUT_BRANCH_CHECKOUT_ERROR, - GIT_FETCH_UPDATES_REQUEST, + GIT_FETCH_UPDATES, GIT_FETCH_UPDATES_REQUEST_FAILURE, GIT_FETCH_UPDATES_REQUEST_SUCCESS, GIT_PULL_UPDATES_SUCCESS, GIT_PULL_UPDATES_FAILURE, + EXEC_OPKG_COMMAND, EXEC_OPKG_COMMANDS, EXEC_OPKG_COMMAND_FAILURE, EXEC_OPKG_COMMAND_SUCCESS, @@ -51,6 +60,8 @@ enum class UPDATE_STATUS : quint8 { JSON_UPDATE, JSON_UPDATE_FAILURE, JSON_UPDATE_SUCCESS, + UPDATE_PROCESS_SUCCESS, + UPDATE_PROCESS_FAILURE, ISMAS_UPDATE_INFO_CONFIRM, ISMAS_UPDATE_INFO_CONFIRM_FAILURE, ISMAS_UPDATE_INFO_CONFIRM_SUCCESS, @@ -63,7 +74,8 @@ struct UpdateStatus { UPDATE_STATUS m_updateStatus; QString m_statusDescription; - explicit UpdateStatus(UPDATE_STATUS s, QString const &d) + explicit UpdateStatus(UPDATE_STATUS s = UPDATE_STATUS::NOT_DEFINED, + QString const &d = QString("")) : m_updateStatus(s), m_statusDescription(d) {} }; @@ -78,10 +90,6 @@ class Worker : public QObject { hwinf *m_hw; WorkerThread m_workerThread; - QTimer m_startUpdateProcess; - QTimer m_emergencyTimer; - Update *m_update; - ApismClient m_apismClient; int const m_customerNr; QString const m_customerNrStr; int const m_machineNr; @@ -90,8 +98,9 @@ class Worker : public QObject { QString const m_branchName; QString const m_customerRepositoryPath; QString const m_customerRepository; + Update *m_update; + IsmasClient m_ismasClient; GitClient m_gc; - bool m_maintenanceMode; QString const m_osVersion; QString const m_atbqtVersion; QString const m_cpuSerial; @@ -107,12 +116,14 @@ class Worker : public QObject { int m_ismasUpdateRequests; QTimer m_waitForNewUpdates; - IsmasClient m_ismasClient; - UPDATE_STATUS m_updateStatus; - QString m_statusDescription; + UpdateStatus m_updateStatus; - void executeOpkgCommand(QString opkgCommand); + QStringList m_filesToUpdate; + bool m_updateProcessRunning; + int m_displayIndex; + + bool executeOpkgCommand(QString opkgCommand); QString getOsVersion() const; QString getATBQTVersion() const; QString getCPUSerial() const; @@ -124,21 +135,31 @@ class Worker : public QObject { qint64 getFileSize(QString const &fileName) const; public: + static const QString UPDATE_STEP_OK; + static const QString UPDATE_STEP_DONE; + static const QString UPDATE_STEP_FAIL; + static const QString UPDATE_STEP_SUCCESS; + explicit Worker(hwinf *hw, int customerNr, // 281 int machineNr, int zoneNr, QString branchName, QString workingDir = ".", - bool maintenanceMode = false, bool dryRun = false, QObject *parent = nullptr, char const *serialInterface = SERIAL_PORT, char const *baudrate = "115200"); ~Worker(); - void quit() { return m_workerThread.quit(); } - static int read1stLineOfFile(QString fileName); + IsmasClient &getIsmasClient() { return m_ismasClient; } + IsmasClient const &getIsmasClient() const { return m_ismasClient; } + + bool updateProcessRunning() const { return m_updateProcessRunning; } + + int machineNr() const { return m_machineNr; } + int customerNr() const { return m_customerNr; } + int zoneNr() const { return m_zoneNr; } //friend QDebug operator<<(QDebug debug, Worker const &w) { // Q_UNUSED(w); @@ -150,24 +171,29 @@ public: //} signals: - void handleChangedFiles(QStringList); - void summarizeUpload(QStringList); - void summarizeRepositoryStatus(); - void sendCmdSendVersionToIsmas(); - void finishUpdateProcess(bool changes); - void terminateUpdateProcess(); + void appendText(QString, QString); + void showErrorMessage(QString title, QString description); + void setProgress(quint8); + void stopStartTimer(); + void restartExitTimer(); public slots: - void onIsmasResponseReceived(QJsonObject ismasResponse); + void update(); private slots: - void askIsmasForNewData(); - void onSendCmdSendVersionToIsmas(); - void onSummarizeRepositoryStatus(); - void onFinishUpdateProcess(bool changes); - void onTerminateUpdateProcess(); - void onSummarizeUpload(QStringList); - void onHandleChangedFiles(QStringList); + bool backendConnected(); + bool updateTriggerSet(); + bool customerEnvironment(); + bool filesToUpdate(); + bool updateFiles(quint8 percent); + bool syncCustomerRepositoryAndFS(); + bool sendIsmasLastVersionNotification(); + bool saveLogFile(); + +private: + PSAInstalled getPSAInstalled(); + QString sendCmdSendVersionToIsmas(); + void privateUpdate(); }; #endif // WORKER_H_INCLUDED From 6a4a852fd6e97af8c372d4b23e8338228893afa0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 15:52:01 +0200 Subject: [PATCH 081/239] Updated for new device controller lib (CAmaster) --- plugins/interfaces.h | 74 +++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/plugins/interfaces.h b/plugins/interfaces.h index 288d533..88f3eee 100644 --- a/plugins/interfaces.h +++ b/plugins/interfaces.h @@ -3,7 +3,7 @@ #include -#define THIS_IS_CA_MASTER + struct T_emp { @@ -131,17 +131,17 @@ struct T_vaultRecord //16 char label3buffer[4]; // mw > - // Verkauf, Tür zu: - uint32_t VKcoinsInserted[16]; // nur für Wechsler, soviel wurde eingeworfen - uint32_t VKcoinsReturned[6]; // nur für Wechsler, Anzahl Münzen pro Typ, soviel wurde zurückgegeben + // Verkauf, Tuer zu: + uint32_t VKcoinsInserted[16]; // nur fuer Wechsler, soviel wurde eingeworfen + uint32_t VKcoinsReturned[6]; // nur fuer Wechsler, Anzahl Muenzen pro Typ, soviel wurde zurueckgegeben //88 - // Service, Tür offen: - uint16_t ServCoinsInserted[16]; // nur für Wechsler, soviel wurde eingeworfen - uint16_t ServCoinsReturned[6]; // nur für Wechsler, Anzahl Münzen pro Typ, soviel wurde zurückgegeben + // Service, Tuer offen: + uint16_t ServCoinsInserted[16]; // nur fuer Wechsler, soviel wurde eingeworfen + uint16_t ServCoinsReturned[6]; // nur fuer Wechsler, Anzahl Muenzen pro Typ, soviel wurde zurueckgegeben uint16_t resint3; uint16_t resint4; - uint16_t currentTubeContent[6]; // nur für Wechsler, aktueller Füllstand + uint16_t currentTubeContent[6]; // nur fuer Wechsler, aktueller Fuellstand uint16_t resint5; uint16_t resint6; // 56 @@ -201,7 +201,7 @@ struct T_moduleCondition uint8_t temper; uint8_t poweronTest; - uint8_t doorState; // 1: alles zu 200: t?r offen + bit1(S) +bit2(CB) + bit3(CB) + uint8_t doorState; // 1: alles zu 200: tuer offen + bit1(S) +bit2(CB) + bit3(CB) uint8_t doorWasOpened; // 1: all doors are closed 200: any door was just opened uint8_t changer; // can only be tested by usage @@ -315,7 +315,7 @@ struct T_devices // set by master, used(1) or notused (0) or type 2....20 UCHAR kindOfPrinter; // 0:off 1:Gebe - UCHAR kindOfCoinChecker; // 0: without 1=EMP820 2=EMP900 3=currenza c² (MW) + UCHAR kindOfCoinChecker; // 0: without 1=EMP820 2=EMP900 3=currenza Csquare (MW) UCHAR kindOfMifareReader; // by now only stronglink SL025 =1 UCHAR suppressSleepMode; // 0:sleep allowed 1: no sleep @@ -357,7 +357,7 @@ public: // Furthermore the Cashagent-Library answers with status strings about sending and reading result // $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ -#ifdef THIS_IS_CA_MASTER + virtual bool dc_openSerial(int BaudNr, QString BaudStr, QString ComName, uint8_t connect) const =0; // Command: open serial interface // BaudNr: 0:1200 1:9600 2:19200 3:38400 4:57600 5:115200 @@ -368,7 +368,7 @@ public: virtual void dc_closeSerial(void) const =0; // Command: close serial interface in order to save power while power down // or if another port must be used -#endif + virtual bool dc_isPortOpen(void) const =0; // returns true if port open (don't send unless open. Sending to closed port will crash program) @@ -394,7 +394,7 @@ public: // get data back in "payLoad", max 64 byte, can be used for diagnosis // retval = nr of bytes received. If host buffer too small then // only plBufSiz bytes are copied to "payLoad" - // plBufSiz­z=size of host buffer + // plBufSiz = size of host buffer virtual void dc_setWakeFrequency(uint8_t period) const =0; // RTC wakes DC2 (and PTU) by hardware signal every 32seconds @@ -403,7 +403,7 @@ public: virtual void dc_OrderToReset(void) const =0; // want DC2 to reset (in order to start Bootloader) -#ifdef THIS_IS_CA_MASTER + virtual QString dc_getSerialState(void) const =0; // get result of opening-command like "ttyS0 opened with 115200 8N1! // or error messages like "comport not available..." @@ -411,7 +411,7 @@ public: virtual void dc_clrSerialStateText(void) const =0; // clear above text to avoid multiple repetive displaying -#endif + virtual void bl_sendDataDirectly(uint8_t length, uint8_t *buf) const =0; // send without protocol frame, needed for the DC bootloader @@ -496,10 +496,10 @@ public: // Analog values: virtual uint32_t dc_getTemperature(void) const =0; - // in Sax-Format 0...400 (0=-50,0°C 100=0,0°C 141=20,5°C 400=150,0°C) + // in Sax-Format 0...400 (0=-50,0degC 100=0,0degC 141=20,5degC 400=150,0degC) virtual QString dc_getTemperaturStr(void) const =0; - // as string like "-12,5°C" + // as string like "-12,5degC" virtual uint32_t dc_getVoltage(void) const =0; // as value in mV, 0...65,535V @@ -760,7 +760,7 @@ public: uint8_t kindOfModem, uint8_t kindOfCredit ) const =0; // enable hardware in device controller: // kindOfPrinter: 0:off 1: GPT4672 (only this one implemented) - // kindOfCoinChecker: 0:off 1:EMP820 2:EMP900 3: C²_changer + // kindOfCoinChecker: 0:off 1:EMP820 2:EMP900 3: Csquare_changer // kindOfMifareReader: 0:off 1: SL025 (only this one implemented) // suppressSleep: 0:sleep allowed 1: sleep surpressed for special reason // kindOfModem: 0:off 1: ATB_Sunlink_LTE (not yet implemented) @@ -886,7 +886,7 @@ public: // send 5 byte: byte 0,1: speed 5...250 mm/s // byte2: density 0....(25)....50 // byte3: alignment 'l', 'c', 'r' = left, center, right - // byte4: orientation 0, 90, 180 = 0°, 90°, 180° rotation (by now not supported!) + // byte4: orientation 0, 90, 180 = 0deg, 90deg, 180deg rotation (by now not supported!) // not batched! don't use twice within 100ms virtual void prn_movePaper(uint8_t wayInMm, uint8_t direction) const =0; @@ -1018,7 +1018,7 @@ public: // use for changer -#ifdef THIS_IS_CA_MASTER + virtual QString dc_getTxt4RsDiagWin(void) const =0; virtual void dc_clrTxt4RsDiagWin(void) const =0; virtual QString dc_get2ndTxt4RsDiagWin(void) const =0; @@ -1033,7 +1033,6 @@ public: virtual void dc_clrTxt4dataStateLine(void) const =0; virtual QString dc_getdatifLine(void) const =0; virtual void dc_clrTxt4datifLine(void) const =0; -#endif @@ -1178,7 +1177,7 @@ public: virtual uint16_t getLastInsertedCoin(void) const =0; virtual bool getAllInsertedCoins(uint16_t *types, uint16_t *values) const =0; - // alle bei diesem Verkauf eingeworfenen Münzen sind gespeichert, max 64 + // alle bei diesem Verkauf eingeworfenen Muenzen sind gespeichert, max 64 virtual bool cash_cancelPayment(void) const =0; @@ -1232,6 +1231,8 @@ public: virtual uint8_t prn_getPrintResult() const =0; + + virtual uint8_t prn_getCurrentPrinterState() const =0; // 0: printer OK // bit0: near paper end bit1: no paper @@ -1244,6 +1245,8 @@ public: virtual void sys_sendDeviceParameter(struct T_devices *deviceSettings) const =0; virtual void sys_restoreDeviceParameter(struct T_devices *deviceSettings) const =0; + // attention: only applies if function "sys_sendDeviceParameter()" was used to send this settings before + // cannot be used to see settings programmed by JsonFile virtual bool sys_areDCdataValid(void) const =0; @@ -1268,6 +1271,33 @@ public: virtual bool dc_isAutoRequestOn(void) const =0; + virtual uint16_t log_getLatestAccountNumber(void) const=0; + // new function 27.6.2023 + // latest = highest + + virtual uint8_t log_getAvailableVaultBlocks(void) const=0; + // return 0x0011 1111 if all 6 blocks are loaded (one bit per block) + + virtual uint8_t log_getAnswerToLastSlaveRequest(void) const =0; + // use only for ONE request/command + // return: 0xFF: result unknown by now as sending is ongoing + // 0=OK + // 1= wrong length 2=wrong start sign 5= wrong crc + // 6= slave: master cmd was wrong 7: slave: could not write/read data + // 8=timeout, got no response from slave + + + // use for important and extended commands (print several templates, print ticket...) + virtual void log_startSupervision(void) const =0; + + virtual uint8_t log_getAnswerToLastCmdBatch(void) const =0; + // 0xFF: no command sent by now + // 0: started, in progress + // 1: done and OK + // 2: done and error + + virtual bool log_getVaultData(uint8_t *data) const =0; + // get vault record in linear 8bit buffer with 320 byte From 0b7d504a7acc034139e7e292c3483284998d3191 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 16:53:19 +0200 Subject: [PATCH 082/239] Fixed missing git pull command. Fixed missing update for text-edit when only clone the customer repository. --- worker.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/worker.cpp b/worker.cpp index 7dc06f0..68531ce 100644 --- a/worker.cpp +++ b/worker.cpp @@ -141,6 +141,14 @@ void Worker::privateUpdate() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSASucceeded("")); + emit setProgress(95); + m_ismasClient.setProgressInPercent(95); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); + + //emit setProgress(100); + //m_ismasClient.setProgressInPercent(100); + //emit appendText(QString(""), UPDATE_STEP_SUCCESS); } } else { // checkout branch @@ -203,6 +211,9 @@ void Worker::privateUpdate() { if (!sentIsmasLastVersionNotification) { // try even if the backend is not connected sendIsmasLastVersionNotification(); + emit setProgress(100); + m_ismasClient.setProgressInPercent(100); + emit appendText(QString(""), UPDATE_STEP_SUCCESS); } m_updateProcessRunning = false; @@ -418,6 +429,9 @@ bool Worker::filesToUpdate() { } else { emit appendText(QString("\nFound 1 file to update "), UPDATE_STEP_DONE); } + if (!m_gc.gitPull()) { + emit showErrorMessage("files to update", "pulling files failed"); + } return true; } else { emit showErrorMessage("files to update", "no files to update (checked-in any files?)"); From a995cae000d1549a3ffd77142fed3989b5b66f8a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 16:54:47 +0200 Subject: [PATCH 083/239] set exit timer to 10 secs --- mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 9d04a04..79a0ab2 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -62,7 +62,7 @@ void MainWindow::onStopStartTimer() { void MainWindow::onRestartExitTimer() { m_exitTimer->stop(); - m_exitTimer->start(5 * 1000); + m_exitTimer->start(10 * 1000); } void MainWindow::onQuit() { From 0ee92f018133a2c84b34358b5da8974f07299bbc Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 2 Aug 2023 17:51:35 +0200 Subject: [PATCH 084/239] disbale exit-button as long as update process is running --- mainwindow.cpp | 10 ++++++++++ mainwindow.h | 2 ++ worker.cpp | 2 ++ worker.h | 2 ++ 4 files changed, 16 insertions(+) diff --git a/mainwindow.cpp b/mainwindow.cpp index 79a0ab2..e426c0c 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -31,6 +31,8 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); connect(ui->start, SIGNAL(clicked()), m_worker, SLOT(update())); connect(ui->exit, SIGNAL(clicked()), this, SLOT(onQuit())); + connect(m_worker, SIGNAL(disableExit()), this, SLOT(onDisableExit())); + connect(m_worker, SIGNAL(enableExit()), this, SLOT(onEnableExit())); connect(m_worker, SIGNAL(stopStartTimer()), this, SLOT(onStopStartTimer())); connect(m_worker, SIGNAL(restartExitTimer()), this, SLOT(onRestartExitTimer())); connect(m_worker, SIGNAL(appendText(QString, QString)), this, SLOT(onAppendText(QString, QString))); @@ -60,6 +62,14 @@ void MainWindow::onStopStartTimer() { m_startTimer->stop(); } +void MainWindow::onDisableExit() { + ui->exit->setEnabled(false); +} + +void MainWindow::onEnableExit() { + ui->exit->setEnabled(true); +} + void MainWindow::onRestartExitTimer() { m_exitTimer->stop(); m_exitTimer->start(10 * 1000); diff --git a/mainwindow.h b/mainwindow.h index 71cd24e..63af2ee 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -23,6 +23,8 @@ public slots: void onSetProgress(quint8); void onStopStartTimer(); void onRestartExitTimer(); + void onEnableExit(); + void onDisableExit(); private slots: void onQuit(); diff --git a/worker.cpp b/worker.cpp index 68531ce..6f3c001 100644 --- a/worker.cpp +++ b/worker.cpp @@ -122,6 +122,7 @@ void Worker::privateUpdate() { start->setEnabled(false); emit stopStartTimer(); + emit disableExit(); m_updateProcessRunning = true; bool sentIsmasLastVersionNotification = false; @@ -217,6 +218,7 @@ void Worker::privateUpdate() { } m_updateProcessRunning = false; + emit enableExit(); emit restartExitTimer(); } diff --git a/worker.h b/worker.h index 4a4a371..3aba0db 100644 --- a/worker.h +++ b/worker.h @@ -176,6 +176,8 @@ signals: void setProgress(quint8); void stopStartTimer(); void restartExitTimer(); + void enableExit(); + void disableExit(); public slots: void update(); From 9b08420ac1b5e7b804e7f2f6949a8e022d07883e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 3 Aug 2023 09:06:50 +0200 Subject: [PATCH 085/239] Use exit() instead of quit() to be able to add a returnCode in case of failure. --- mainwindow.cpp | 15 ++++++++++----- worker.cpp | 48 +++++++++++++++++++++++++++++++++++++++--------- worker.h | 2 ++ 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index e426c0c..b46b310 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -77,7 +77,8 @@ void MainWindow::onRestartExitTimer() { void MainWindow::onQuit() { if (!m_worker->updateProcessRunning()) { - qApp->quit(); + // qApp->quit(); + qApp->exit(m_worker->returnCode()); } } @@ -91,12 +92,16 @@ void MainWindow::onAppendText(QString text, QString suffix) { for (int i=0; iupdateStatus->setPlainText(editText); - } else { + if (suffix.contains("[SUCCESS]")) { editText += QString("\n").leftJustified(m_width-3, '='); editText += QString("\n").leftJustified(m_width-12) + " [SUCCESS]"; + } else + if (suffix.contains(" [fail]")) { + editText += QString("\n").leftJustified(m_width-3, '='); + editText += QString("\n").leftJustified(m_width-9) + " [fail]"; + } else { + editText += text.leftJustified(m_width-9) + suffix; + ui->updateStatus->setPlainText(editText); } ui->updateStatus->setText(editText); ui->updateStatus->setEnabled(true); diff --git a/worker.cpp b/worker.cpp index 6f3c001..f143a96 100644 --- a/worker.cpp +++ b/worker.cpp @@ -65,7 +65,8 @@ Worker::Worker(hwinf *hw, , m_ismasUpdateRequests(ISMAS_UPDATE_REQUESTS) , m_waitForNewUpdates(this) , m_filesToUpdate() - , m_updateProcessRunning(false) { + , m_updateProcessRunning(false) + , m_returnCode(0) { QDir::setCurrent(m_workingDirectory); @@ -127,6 +128,7 @@ void Worker::privateUpdate() { bool sentIsmasLastVersionNotification = false; + m_returnCode = -1; QDir customerRepository(m_customerRepository); if (!customerRepository.exists()) { if (m_gc.gitCloneAndCheckoutBranch()) { @@ -146,10 +148,7 @@ void Worker::privateUpdate() { m_ismasClient.setProgressInPercent(95); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); - - //emit setProgress(100); - //m_ismasClient.setProgressInPercent(100); - //emit appendText(QString(""), UPDATE_STEP_SUCCESS); + m_returnCode = 0; } } else { // checkout branch @@ -199,22 +198,51 @@ void Worker::privateUpdate() { IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); + + m_returnCode = 0; + } else { + m_returnCode = -9; } + } else { + m_returnCode = -8; } + } else { + m_returnCode = -7; } + } else { + m_returnCode = -6; } + } else { + m_returnCode = -5; } + } else { + m_returnCode = -4; } + } else { + m_returnCode = -3; } + } else { + m_returnCode = -2; } + } else { + m_returnCode = -1; } } if (!sentIsmasLastVersionNotification) { // try even if the backend is not connected - sendIsmasLastVersionNotification(); emit setProgress(100); m_ismasClient.setProgressInPercent(100); - emit appendText(QString(""), UPDATE_STEP_SUCCESS); + + if (m_returnCode == 0) { + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSASucceeded("")); + emit appendText(QString(""), UPDATE_STEP_SUCCESS); + } else { + emit appendText(QString(""), UPDATE_STEP_FAIL); + } + + sendIsmasLastVersionNotification(); } m_updateProcessRunning = false; @@ -253,7 +281,7 @@ bool Worker::backendConnected() { bool ismas = obj.value("ISMAS").toBool(); QString status = obj.value("Broker").toString(); - qCritical() << "XXXXXXXXXX STATUS" << status; + qCritical() << "STATUS" << status; if (ismas) { if (status == "Connected") { @@ -494,7 +522,9 @@ bool Worker::updateFiles(quint8 percent) { } } - qCritical() << "XXXXXXXXXXXXXXXXXXX FILES_TO_WORK_ON" << filesToDownload; + if (filesToDownload.size() > 0) { + qCritical() << "FILES_TO_WORK_ON" << filesToDownload; + } return m_update->doUpdate(m_displayIndex, filesToDownload); } diff --git a/worker.h b/worker.h index 3aba0db..94a01e4 100644 --- a/worker.h +++ b/worker.h @@ -122,6 +122,7 @@ class Worker : public QObject { QStringList m_filesToUpdate; bool m_updateProcessRunning; int m_displayIndex; + int m_returnCode; bool executeOpkgCommand(QString opkgCommand); QString getOsVersion() const; @@ -156,6 +157,7 @@ public: IsmasClient const &getIsmasClient() const { return m_ismasClient; } bool updateProcessRunning() const { return m_updateProcessRunning; } + bool returnCode() const { return m_returnCode; } int machineNr() const { return m_machineNr; } int customerNr() const { return m_customerNr; } From 8889aaca2a750f2685826d21170e736ca6c3ff26 Mon Sep 17 00:00:00 2001 From: Siegfried Siegert Date: Thu, 3 Aug 2023 09:44:08 +0200 Subject: [PATCH 086/239] Fix: path for deployment --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index c8d5b82..a711fb5 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -116,5 +116,5 @@ OTHER_FILES += \ # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin -else: unix:!android: target.path = /opt/$${TARGET}/bin +else: unix:!android: target.path = /opt/app/tools/atbupdate/$${TARGET} !isEmpty(target.path): INSTALLS += target From ef88fdc9a4559cb32a88265f91ab48705cabc9ce Mon Sep 17 00:00:00 2001 From: Siegfried Siegert Date: Thu, 3 Aug 2023 09:50:26 +0200 Subject: [PATCH 087/239] Fix: config for deployment --- OnDemandUpdatePTU.pro | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index a711fb5..84e9d3f 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -114,7 +114,12 @@ OTHER_FILES += \ # git subtree pull --prefix DCPlugin https://git.mimbach49.de/GerhardHoffmann/DCPlugin.git master --squash # include(./DCPlugin/DCPlugin.pri) -# Default rules for deployment. -qnx: target.path = /tmp/$${TARGET}/bin -else: unix:!android: target.path = /opt/app/tools/atbupdate/$${TARGET} -!isEmpty(target.path): INSTALLS += target + + +########################################################################################## +# for running program on target through QtCreator +contains( CONFIG, PTU5 ) { + qnx: target.path = /tmp/$${TARGET}/bin + else: unix:!android: target.path = /opt/app/tools/atbupdate/ + !isEmpty(target.path): INSTALLS += target +} From c6e98f50c2ffe0f296ad1a5eba7ae6156e12980f Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 3 Aug 2023 10:14:09 +0200 Subject: [PATCH 088/239] call onQuit() directly whem timer runs out --- mainwindow.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index b46b310..374000f 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -28,7 +28,8 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) m_exitTimer->start(1800 * 1000); connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); - connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); + //connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); + connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(onQuit())); connect(ui->start, SIGNAL(clicked()), m_worker, SLOT(update())); connect(ui->exit, SIGNAL(clicked()), this, SLOT(onQuit())); connect(m_worker, SIGNAL(disableExit()), this, SLOT(onDisableExit())); @@ -76,10 +77,10 @@ void MainWindow::onRestartExitTimer() { } void MainWindow::onQuit() { - if (!m_worker->updateProcessRunning()) { - // qApp->quit(); - qApp->exit(m_worker->returnCode()); - } + // if (!m_worker->updateProcessRunning()) { + qCritical() << "ON QUIT: EXIT CODE" << QString::number(m_worker->returnCode()); + qApp->exit(m_worker->returnCode()); + //} } void MainWindow::onSetProgress(quint8 v) { @@ -108,5 +109,5 @@ void MainWindow::onAppendText(QString text, QString suffix) { } void MainWindow::onShowErrorMessage(QString title, QString text) { - QMessageBox::critical(this, title, text, QMessageBox::Ok); + // QMessageBox::critical(this, title, text, QMessageBox::Ok); } From 48073ab1f0de5b094d2c4671c087c934a83e7d4b Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Thu, 3 Aug 2023 10:14:23 +0200 Subject: [PATCH 089/239] update to next version --- plugins/interfaces.h | 73 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/plugins/interfaces.h b/plugins/interfaces.h index 88f3eee..d9f31b6 100644 --- a/plugins/interfaces.h +++ b/plugins/interfaces.h @@ -128,13 +128,11 @@ struct T_vaultRecord uint32_t AbsReserve; uint32_t AbsNrOfCuts; -//16 char label3buffer[4]; // mw > // Verkauf, Tuer zu: uint32_t VKcoinsInserted[16]; // nur fuer Wechsler, soviel wurde eingeworfen uint32_t VKcoinsReturned[6]; // nur fuer Wechsler, Anzahl Muenzen pro Typ, soviel wurde zurueckgegeben -//88 // Service, Tuer offen: uint16_t ServCoinsInserted[16]; // nur fuer Wechsler, soviel wurde eingeworfen @@ -144,12 +142,10 @@ struct T_vaultRecord uint16_t currentTubeContent[6]; // nur fuer Wechsler, aktueller Fuellstand uint16_t resint5; uint16_t resint6; -// 56 char label4buffer[4]; // box> uint16_t coinsInVault[16]; uint16_t billsInStacker[8]; -// 48 char label5buffer[4]; // val> // actually constant unless exchange rate is changed @@ -159,11 +155,17 @@ struct T_vaultRecord uint16_t exchangeRate; uint16_t resint9; -// 64 + // new from 1.8.23 + uint32_t cutsSinceCBchange; + uint32_t CBcontent_cent; + uint32_t CBnrofCoins; - char endofblock[4]; // end> + char endofblock[4]; // end +// 332 bytes -// 316 byte Block im Speicher + uint16_t CRC16; // Xmodem16 from startbuffer[0] to endofblock[3] + uint16_t resint11; + char endofRecord[4]; // ---- }; @@ -317,7 +319,8 @@ struct T_devices UCHAR kindOfPrinter; // 0:off 1:Gebe UCHAR kindOfCoinChecker; // 0: without 1=EMP820 2=EMP900 3=currenza Csquare (MW) UCHAR kindOfMifareReader; // by now only stronglink SL025 =1 - UCHAR suppressSleepMode; // 0:sleep allowed 1: no sleep + UCHAR solarPower; // 1:sleep allowed 0: no sleep + //UCHAR suppressSleepMode; // 0:sleep allowed 1: no sleep UCHAR kindOfModem; // 0:off 1:Sunlink UCHAR kindOfCreditcard; // 0:off 1:Feig NFC @@ -336,6 +339,8 @@ struct T_devices UINT VaultFullWarnLevel; UINT VaultFullErrorLevel; + UINT BattEmptyWarnLevel; + UINT BattEmptyErrorLevel; }; @@ -1151,14 +1156,13 @@ public: // to be forwarded to Ismas virtual bool prn_printAccountReceipt(void) const =0; + // print all 8 backuped accounting receipts // return true if sending to DC OK, false if cmd-stack is full virtual bool prn_printTestTicket(void) const =0; // return true if sending to DC OK, false if cmd-stack is full - - virtual bool cash_startPayment(uint32_t amount) const =0; // 17.4.23TS: extended to 32bit @@ -1273,7 +1277,7 @@ public: virtual uint16_t log_getLatestAccountNumber(void) const=0; // new function 27.6.2023 - // latest = highest + // latest = highest of the backup's virtual uint8_t log_getAvailableVaultBlocks(void) const=0; // return 0x0011 1111 if all 6 blocks are loaded (one bit per block) @@ -1297,7 +1301,45 @@ public: // 2: done and error virtual bool log_getVaultData(uint8_t *data) const =0; - // get vault record in linear 8bit buffer with 320 byte + // get vault record in linear 8bit buffer with 384 byte + + + + // new from 1.8.23 + virtual bool prn_printOneAccountReceipt(uint16_t accountNr) const =0; + // print one out of eight stored last accounting receipts + // function log_getHoldAccountNumbers() gives a list of acc-Nr. of the stored receipts + + virtual bool prn_printAllAvailAccountReceipts(void) const =0; + // same as: prn_printAccountReceipt() from line 1153 + // return true if sending to DC OK, false if cmd-stack is full + + virtual bool log_verifyVaultRecordByCrc(void) const =0; + // return true if CRC16 is correct, data are 100% OK. Security level 1:65536 + // verification is strongly recommended before further processing + // in case of "false"-result please reload from DC + + + virtual uint16_t log_DC_getNextAccountNumber(void) const=0; + // the current cash box content will be backuped with this number on next cashbox-change + + virtual void log_DC_setNextAccountNumber(uint16_t newAccountingNumber) const=0; + // the current cash box content will be backuped with this number on next cashbox-change + // use only in case of hardware replacements or errors which derailed the number + + virtual void log_DC_deleteAllVaultrecordsInDc(void) const=0; + // use only in case of hardware replacements or errors which derailed the number + + virtual void log_DC_deleteAllTotalCounters(void) const=0; + // use only in case of hardware replacements or errors which derailed the number + + virtual void dc_setNewCustomerNumber(uint16_t newCustNr) const =0; + + virtual void dc_setNewMachineNumber(uint16_t newMachNr) const =0; + + virtual void dc_setNewBorough(uint16_t newBorough) const =0; + + virtual void dc_setNewZone(uint16_t newZone) const =0; @@ -1356,6 +1398,10 @@ signals: // 15.06.2023 V4.2 bring into same order as hwapi in order to set the THIS_IS_CA_MASTER correct // 19.06.2023 V4.3 added some qCriticals to see emits +// 01.08.2023 V4.4 some new values at the end of struct T_vaultRecord +// two more values in struct T_devices +// 7 new functions at the end of the file + //#define HWINF_iid "Atb.Psa2020.software.HWapi/3.1" //#define HWINF_iid "Atb.Psa1256ptu5.software.HWapi/3.1" @@ -1366,7 +1412,8 @@ signals: //#define HWINF_iid "Atb.Psa1256ptu5.software.HWapi/4.0" //#define HWINF_iid "Atb.Psa1256ptu5.software.HWapi/4.1" //#define HWINF_iid "Atb.Psa1256ptu5.software.HWapi/4.2" -#define HWINF_iid "Atb.Psa1256ptu5.software.HWapi/4.3" +//#define HWINF_iid "Atb.Psa1256ptu5.software.HWapi/4.3" +#define HWINF_iid "Atb.Psa1256ptu5.software.HWapi/4.4" From 82352713f1c64abc71ae3493d91f0780a21103e0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:30:18 +0200 Subject: [PATCH 090/239] Added 6 more buttons for future use and for the fullscreen layout. --- mainwindow.ui | 112 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 26 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index 15a6b01..b702349 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 370 - 358 + 800 + 480 @@ -22,34 +22,13 @@ - 10 + 11 11 - 351 - 341 + 781 + 461 - - - - false - - - - Noto Sans - 8 - false - - - - - - - - 24 - - - @@ -69,6 +48,87 @@ + + + + false + + + + + + + + + + false + + + + + + + + + + false + + + + + + + + + + false + + + + + + + + + + false + + + + + + + + + + false + + + + + + + + + + false + + + + Noto Sans + 8 + false + + + + + + + + 1 + + + From c054668eac0ff4fe5da79cf422b1e3fc0dc94d10 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:31:12 +0200 Subject: [PATCH 091/239] Ask for the reurn code of the process, not only for process exit status. --- process/command.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/process/command.cpp b/process/command.cpp index f4049a6..4b9047a 100644 --- a/process/command.cpp +++ b/process/command.cpp @@ -62,10 +62,11 @@ bool Command::execute(QString workingDirectory, QStringList args) { if (p->waitForFinished(m_waitForFinishTimeout)) { //qDebug() << "PROCESS" << m_command << "FINISHED"; if (p->exitStatus() == QProcess::NormalExit) { - //qInfo() << "EXECUTED" << m_command - // << "with code" << p->exitCode(); - // qInfo() << "RESULT" << m_commandResult; - return true; + if (p->exitCode() == 0) { + return true; + } else { + qCritical() << "EXECUTED" << m_command << "with code" << p->exitCode(); + } } else { qCritical() << "PROCESS" << m_command << "CRASHED with code" << p->exitCode(); From c62299aa72267b8be861b6c4d8a42d7f5f52b26e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:33:25 +0200 Subject: [PATCH 092/239] Read from /mnt/system_data/ instead of /etc. --- git/git_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index 3c0303e..403c987 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -86,7 +86,7 @@ bool GitClient::gitCheckoutBranch() { // TODO: nachsehen, ob der Branch ueberhaupt existiert if (QDir(m_customerRepository).exists()) { - int zoneNr = Utils::read1stLineOfFile("/etc/zone_nr"); + int zoneNr = Utils::read1stLineOfFile("/mnt/system_data/zone_nr"); m_branchName = (zoneNr != 0) ? QString("zg1/zone%1").arg(zoneNr) : "master"; From f88b0edb2ac7ad8bd6ed60552dc98eeac92b43b0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:35:42 +0200 Subject: [PATCH 093/239] Fix for the case when several branches are edited: 'git fetch' will display several lines then, not only one. --- git/git_client.cpp | 67 ++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index 403c987..a45e9db 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -165,13 +165,7 @@ std::optional GitClient::gitDiff(QString const &commits) { std::optional GitClient::gitFetch() { if (QDir(m_customerRepository).exists()) { - // TODO - UpdateStatus status (UPDATE_STATUS::GIT_FETCH_UPDATES, "GIT FETCH UPDATES"); - qInfo() << status; - - //emit m_worker->sendCmdEventToIsmas( - // m_worker->getIsmasClient().updateOfPSAContinues( - // "GIT FETCH UPDATES", status.m_statusDescription)); + qCritical() << "BRANCH NAME" << m_branchName; Command c("git fetch"); if (c.execute(m_customerRepository)) { @@ -179,36 +173,51 @@ std::optional GitClient::gitFetch() { if (!s.isEmpty()) { QStringList lines = Update::split(s, '\n'); if (!lines.empty()) { - // 409f198..6c22726 zg1/zone1 -> origin/zg1/zone1 - static QRegularExpression re("(^\\s*)([0-9A-Fa-f]+..[0-9A-Fa-f]+)(.*$)"); - QRegularExpressionMatch match = re.match(lines.last()); - if (match.hasMatch()) { - if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches - // TODO - status = UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS, - QString("GIT SUCCESSFULLY FETCHED ") + m_customerRepository); - qInfo() << status; - - //emit m_worker->sendCmdEventToIsmas( - // m_worker->getIsmasClient().updateOfPSAContinues( - // "GIT_FETCH_UPDATES SUCCESS", status.m_statusDescription)); - - return match.captured(2); - } else { - qCritical() << "ERROR WRONG CAPTURE COUNT FOR 'GIT FETCH'" << re.captureCount(); + int zoneNr = Utils::read1stLineOfFile("/mnt/system_data/zone_nr"); + m_branchName = (zoneNr != 0) ? QString("zg1/zone%1").arg(zoneNr) : "master"; + // lines can look like this: + // From https://git.mimbach49.de/GerhardHoffmann/customer_281 + // 41ec581..5d25ac3 master -> origin/master + // ff10f57..43530a1 zg1/zone1 -> origin/zg1/zone1 + // 6ed893f..5d9882c zg1/zone2 -> origin/zg1/zone2 + // 4384d17..77045d8 zg1/zone3 -> origin/zg1/zone3 + // 89d2812..36a0d74 zg1/zone5 -> origin/zg1/zone5 + bool found = false; + for (int i=0; i < lines.size(); ++i) { + if (lines.at(i).contains(m_branchName)) { + found = true; + // 409f198..6c22726 zg1/zone1 -> origin/zg1/zone1 + static QRegularExpression re("(^\\s*)([0-9A-Fa-f]+..[0-9A-Fa-f]+)(.*$)"); + QRegularExpressionMatch match = re.match(lines.at(i)); + if (match.hasMatch()) { + if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches + return match.captured(2); + } else { + emit m_worker->showErrorMessage("git fetch", + QString("(wrong cap-count (%1)").arg(re.captureCount())); + } + } else { + emit m_worker->showErrorMessage("git fetch", + "regex-match for commits"); + } } - } else { - qCritical() << "ERROR NO MATCH OF COMMITS FOR 'GIT FETCH'"; + } + if (!found) { + emit m_worker->showErrorMessage("git fetch", + QString("unkown branch name ") + m_branchName); } } else { - qCritical() << "ERROR WRONG FORMAT FOR RESULT FOR 'GIT FETCH'" << s; + emit m_worker->showErrorMessage("git fetch", + QString("wrong format for result of 'git fetch' ") + s); } } else { - qCritical() << "ERROR EMPTY RESULT FROM 'GIT FETCH'"; + emit m_worker->showErrorMessage("git fetch", + "empty result for 'git fetch'"); } } } else { - qCritical() << "ERROR" << m_customerRepository << "DOES NOT EXIST"; + emit m_worker->showErrorMessage("git fetch", + QString("repository ") + m_customerRepository + " does not exist"); } return std::nullopt; } From d2d730589b1f5608d798caa3ca063e29609c48e3 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:38:33 +0200 Subject: [PATCH 094/239] changed return type of returnCode() ti 'int' --- worker.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker.h b/worker.h index 94a01e4..6a745cb 100644 --- a/worker.h +++ b/worker.h @@ -157,7 +157,7 @@ public: IsmasClient const &getIsmasClient() const { return m_ismasClient; } bool updateProcessRunning() const { return m_updateProcessRunning; } - bool returnCode() const { return m_returnCode; } + int returnCode() const { return m_returnCode; } int machineNr() const { return m_machineNr; } int customerNr() const { return m_customerNr; } From 19274546c97476f63fb250604425efd184c1e911 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:39:17 +0200 Subject: [PATCH 095/239] set main window fullscreen --- main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/main.cpp b/main.cpp index 0bdc500..246bf97 100644 --- a/main.cpp +++ b/main.cpp @@ -146,6 +146,7 @@ int main(int argc, char *argv[]) { MainWindow mw(&worker); mw.setWindowFlags(Qt::Window | Qt::FramelessWindowHint); + mw.setWindowState(Qt::WindowFullScreen); mw.show(); return a.exec(); From 96fb50e68debd16eb3ec3db88c0b1b6bd4e5ffcb Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:44:16 +0200 Subject: [PATCH 096/239] Moved init. of text-edit upwards. Hide reserved buttons. --- mainwindow.cpp | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 374000f..5f59a26 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -17,6 +17,25 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) ui->updateProgress->setRange(1, 100); ui->updateProgress->reset(); + QStringList lst; + QString start = QDateTime::currentDateTime().toString(Qt::ISODate); + lst << QString("Start: ") + start.leftJustified(m_width-10); + lst << QString("").leftJustified(m_width-3, '='); + lst << QString("Machine number : %1 ").arg(m_worker->machineNr()).leftJustified(m_width-3); + lst << QString("Customer number : %1 ").arg(m_worker->customerNr()).leftJustified(m_width-3); + lst << QString("Zone number : %1 (%2)").arg(m_worker->zoneNr()).arg(Utils::zoneName(m_worker->zoneNr())).leftJustified(m_width-3); + lst << QString("").leftJustified(m_width-3, '='); + + ui->updateStatus->setText(lst.join('\n')); + ui->updateStatus->setEnabled(true); + + ui->reserved_1->setVisible(false); + ui->reserved_2->setVisible(false); + ui->reserved_3->setVisible(false); + ui->reserved_4->setVisible(false); + ui->reserved_5->setVisible(false); + ui->reserved_6->setVisible(false); + m_startTimer = new QTimer(this); connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); m_startTimer->setSingleShot(true); @@ -40,15 +59,6 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) connect(m_worker, SIGNAL(showErrorMessage(QString,QString)), this, SLOT(onShowErrorMessage(QString,QString))); connect(m_worker, SIGNAL(setProgress(quint8)), this, SLOT(onSetProgress(quint8))); - QStringList lst; - QString start = QDateTime::currentDateTime().toString(Qt::ISODate); - lst << QString("Start: ") + start.leftJustified(m_width-10); - lst << QString("").leftJustified(m_width-3, '='); - lst << QString("Machine number : %1 ").arg(m_worker->machineNr()).leftJustified(m_width-3); - lst << QString("Customer number : %1 ").arg(m_worker->customerNr()).leftJustified(m_width-3); - lst << QString("Zone number : %1 (%2)").arg(m_worker->zoneNr()).arg(Utils::zoneName(m_worker->zoneNr())).leftJustified(m_width-3); - lst << QString("").leftJustified(m_width-3, '='); - ui->updateStatus->setText(lst.join('\n')); ui->updateStatus->setEnabled(true); } From 427a272f8f7accdfd272e60c035e459054d51e1a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:45:19 +0200 Subject: [PATCH 097/239] Connect exit-button with clicked(). --- mainwindow.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 5f59a26..ff7e07b 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -47,8 +47,7 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) m_exitTimer->start(1800 * 1000); connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); - //connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); - connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(onQuit())); + connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); connect(ui->start, SIGNAL(clicked()), m_worker, SLOT(update())); connect(ui->exit, SIGNAL(clicked()), this, SLOT(onQuit())); connect(m_worker, SIGNAL(disableExit()), this, SLOT(onDisableExit())); From 44ad3caf2be684dc89f564bb899e67cd9ad83d9d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:45:56 +0200 Subject: [PATCH 098/239] Stop exit timer in onQuit. Activate error message boxes. --- mainwindow.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index ff7e07b..07a9c4a 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -86,13 +86,13 @@ void MainWindow::onRestartExitTimer() { } void MainWindow::onQuit() { - // if (!m_worker->updateProcessRunning()) { - qCritical() << "ON QUIT: EXIT CODE" << QString::number(m_worker->returnCode()); + m_exitTimer->stop(); + qCritical() << QString("ON QUIT: EXIT CODE %1").arg(m_worker->returnCode()); qApp->exit(m_worker->returnCode()); - //} } void MainWindow::onSetProgress(quint8 v) { + qCritical() << "ON SET PROGRESS" << v; ui->updateProgress->setValue(v); } @@ -118,5 +118,5 @@ void MainWindow::onAppendText(QString text, QString suffix) { } void MainWindow::onShowErrorMessage(QString title, QString text) { - // QMessageBox::critical(this, title, text, QMessageBox::Ok); + QMessageBox::critical(this, title, text, QMessageBox::Ok); } From 4ad370ea46c131bda14f867b3a50d5c3df3a68bf Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:48:40 +0200 Subject: [PATCH 099/239] Extended displayed messages in text-edit. --- worker.cpp | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/worker.cpp b/worker.cpp index f143a96..a0d17dc 100644 --- a/worker.cpp +++ b/worker.cpp @@ -132,6 +132,8 @@ void Worker::privateUpdate() { QDir customerRepository(m_customerRepository); if (!customerRepository.exists()) { if (m_gc.gitCloneAndCheckoutBranch()) { + emit appendText("\nInitializing customer environment", UPDATE_STEP_DONE); + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_SUCCESS, QString("CLONED AND CHECKED OUT: ") + m_customerRepository); @@ -144,13 +146,17 @@ void Worker::privateUpdate() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSASucceeded("")); - emit setProgress(95); - m_ismasClient.setProgressInPercent(95); + emit appendText(QString(""), UPDATE_STEP_SUCCESS); + + emit setProgress(100); + m_ismasClient.setProgressInPercent(100); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); + m_returnCode = 0; } } else { + qCritical() << "CHECKOUT BRANCH..."; // checkout branch if (m_gc.gitCheckoutBranch()) { int progress = 10; @@ -228,23 +234,19 @@ void Worker::privateUpdate() { m_returnCode = -1; } } + + emit setProgress(100); + m_ismasClient.setProgressInPercent(100); + if (!sentIsmasLastVersionNotification) { // try even if the backend is not connected - emit setProgress(100); - m_ismasClient.setProgressInPercent(100); - - if (m_returnCode == 0) { - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateOfPSASucceeded("")); - emit appendText(QString(""), UPDATE_STEP_SUCCESS); - } else { - emit appendText(QString(""), UPDATE_STEP_FAIL); - } - sendIsmasLastVersionNotification(); } + if (m_returnCode != 0) { + emit appendText(QString("Update process "), UPDATE_STEP_FAIL); + } + m_updateProcessRunning = false; emit enableExit(); emit restartExitTimer(); From c4f12ce75af1f28caafbd633bdabf5b06cea536e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:49:32 +0200 Subject: [PATCH 100/239] Added terminal-debug-output. --- worker.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worker.cpp b/worker.cpp index a0d17dc..0e70561 100644 --- a/worker.cpp +++ b/worker.cpp @@ -256,11 +256,13 @@ bool Worker::backendConnected() { static int repeat = 0; if (repeat < 3) { + qCritical() << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; std::optional result = IsmasClient::sendRequestReceiveResponse( IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_SELF#J={}"); if (result) { QString msg = result.value(); + qCritical() << "In backendConnected() -> APISM response" << msg; QJsonParseError parseError; QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); if (parseError.error != QJsonParseError::NoError) { @@ -283,7 +285,7 @@ bool Worker::backendConnected() { bool ismas = obj.value("ISMAS").toBool(); QString status = obj.value("Broker").toString(); - qCritical() << "STATUS" << status; + qCritical() << "In backendConnected() STATUS" << status; if (ismas) { if (status == "Connected") { @@ -305,6 +307,8 @@ bool Worker::backendConnected() { } } } + qCritical() << "In backendConnected() ERROR"; + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_NOT_CONNECTED, QString("NO BACKEND CONNECTION")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, @@ -553,9 +557,14 @@ bool Worker::syncCustomerRepositoryAndFS() { Command c("bash"); qInfo() << "EXECUTING CMD..." << cmd; if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { - qCritical() << c.getCommandResult() << "SUCCESS"; + QStringList result = c.getCommandResult().split('\n'); + for (int i = 0; i < result.size(); ++i) { + qCritical() << result.at(i); + } + qCritical() << "SUCCESS"; } else { qCritical() << "CMD" << cmd << "FAILED"; + qCritical() << c.getCommandResult().split('\n'); error = true; } } From d57914957d5f5280a33271f5a324b15bd9b9e0cd Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:50:19 +0200 Subject: [PATCH 101/239] Add text-edit entry if git pull is successful. --- worker.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/worker.cpp b/worker.cpp index 0e70561..180fb53 100644 --- a/worker.cpp +++ b/worker.cpp @@ -465,10 +465,11 @@ bool Worker::filesToUpdate() { } else { emit appendText(QString("\nFound 1 file to update "), UPDATE_STEP_DONE); } - if (!m_gc.gitPull()) { - emit showErrorMessage("files to update", "pulling files failed"); + if (m_gc.gitPull()) { + emit appendText(QString("\nFetch changes files "), UPDATE_STEP_DONE); + return true; } - return true; + emit showErrorMessage("files to update", "pulling files failed"); } else { emit showErrorMessage("files to update", "no files to update (checked-in any files?)"); } From 9c44656104868de748461871e1c8d75779be9d75 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:52:57 +0200 Subject: [PATCH 102/239] Add parameter -vv fot the rsync command to see more debugoutput. --- worker.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worker.cpp b/worker.cpp index 180fb53..70775d9 100644 --- a/worker.cpp +++ b/worker.cpp @@ -539,7 +539,8 @@ bool Worker::updateFiles(quint8 percent) { bool Worker::syncCustomerRepositoryAndFS() { if (QDir(m_customerRepository).exists()) { if (QDir::setCurrent(m_customerRepository)) { - QString const params("--recursive " + QString const params("-vv " + "--recursive " "--progress " "--checksum " "--exclude=.* " From 6b4c4865494128e47ec90319b339df4474ff4d86 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 13:53:55 +0200 Subject: [PATCH 103/239] Add m_ismasClient.updateOfPSASucceeded() for a successful run. --- worker.cpp | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/worker.cpp b/worker.cpp index 70775d9..d96a372 100644 --- a/worker.cpp +++ b/worker.cpp @@ -167,44 +167,56 @@ void Worker::privateUpdate() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.checkoutBranch( m_updateStatus.m_statusDescription, "")); - emit setProgress(progress); - if (backendConnected()) { + + qCritical() << "CHECKED OUT BRANCH"; + if (backendConnected()) { qCritical() << "BACKEND CONNECTED"; progress = 20; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); - if (updateTriggerSet()) { + if (updateTriggerSet()) { qCritical() << "UPDATE TRIGGER SET"; + progress = 30; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); - if (customerEnvironment()) { + if (customerEnvironment()) { qCritical() << "CUSTOMER ENVIRONMENT"; + progress = 40; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); - if (filesToUpdate()) { + if (filesToUpdate()) { qCritical() << "FILES TO UPDATE"; + progress = 50; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); - if (updateFiles(progress)) { + if (updateFiles(progress)) { qCritical() << "UPDATE FILES"; + progress = 60; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); - if (syncCustomerRepositoryAndFS()) { + if (syncCustomerRepositoryAndFS()) { qCritical() << "SYNC REPOSITORY"; + progress = 70; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); - if (sendIsmasLastVersionNotification()) { + if (sendIsmasLastVersionNotification()) { qCritical() << "SEND LAST NOTIFICATION"; + progress = 80; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); sentIsmasLastVersionNotification = true; - if (saveLogFile()) { + if (saveLogFile()) { qCritical() << "SAVE LOG FILE"; + progress = 90; emit setProgress(progress); m_ismasClient.setProgressInPercent(progress); emit appendText(QString(""), UPDATE_STEP_SUCCESS); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSASucceeded("")); + // mark update as activated -> this resets the WAIT button progress = 100; emit setProgress(progress); + m_ismasClient.setProgressInPercent(progress); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); - m_returnCode = 0; } else { m_returnCode = -9; From b09ccfd4f582f67e3c92d4b41b4180de7c4b4dfb Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 4 Aug 2023 14:10:47 +0200 Subject: [PATCH 104/239] Add custom ProgressEvent class for future use. --- OnDemandUpdatePTU.pro | 2 ++ progress_event.cpp | 19 +++++++++++++++++++ progress_event.h | 22 ++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 progress_event.cpp create mode 100644 progress_event.h diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 84e9d3f..3c83520 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -77,6 +77,7 @@ contains( CONFIG, DesktopLinux ) { SOURCES += \ main.cpp \ + progress_event.cpp \ mainwindow.cpp \ utils.cpp \ update.cpp \ @@ -89,6 +90,7 @@ SOURCES += \ HEADERS += \ update.h \ + progress_event.h \ utils.h \ mainwindow.h \ git/git_client.h \ diff --git a/progress_event.cpp b/progress_event.cpp new file mode 100644 index 0000000..9f07341 --- /dev/null +++ b/progress_event.cpp @@ -0,0 +1,19 @@ +#include "progress_event.h" + +QEvent::Type ProgressEvent::customEventType = QEvent::None; + +ProgressEvent::ProgressEvent() + : QEvent(ProgressEvent::type()) + , m_progressPercent(0) { +} + +ProgressEvent::~ProgressEvent() { +} + +QEvent::Type ProgressEvent::type() { + if (customEventType == QEvent::None) { + int generatedType = QEvent::registerEventType(); + customEventType = static_cast(generatedType); + } + return customEventType; +} diff --git a/progress_event.h b/progress_event.h new file mode 100644 index 0000000..3a67e0b --- /dev/null +++ b/progress_event.h @@ -0,0 +1,22 @@ +#ifndef PROGRESS_EVENT_H_INCLUDED +#define PROGRESS_EVENT_H_INCLUDED + +#include + +class ProgressEvent : public QEvent { + + int m_progressPercent; +public: + ProgressEvent(); + virtual ~ProgressEvent(); + static QEvent::Type type(); + + void setProgress(int progressPercent) { m_progressPercent = progressPercent; } + int progressPercent() { return m_progressPercent; } + int progressPercent() const { return m_progressPercent; } +private: + static QEvent::Type customEventType; +}; + + +#endif // PROGRESS_EVENT_H_INCLUDED From cf9033e898cf5a18935bb3c26deafa5df1cdc140 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 5 Aug 2023 18:50:50 +0200 Subject: [PATCH 105/239] Send custom event from worker(-thread) to MainWindow in order to update progress bar. --- main.cpp | 2 ++ mainwindow.cpp | 14 ++++++++------ mainwindow.h | 4 +++- progress_event.cpp | 4 ++-- progress_event.h | 2 +- worker.cpp | 34 +++++++++++++++++++++------------- worker.h | 8 +++++++- 7 files changed, 44 insertions(+), 24 deletions(-) diff --git a/main.cpp b/main.cpp index 246bf97..47c2dce 100644 --- a/main.cpp +++ b/main.cpp @@ -145,6 +145,8 @@ int main(int argc, char *argv[]) { dryRun); MainWindow mw(&worker); + worker.setMainWindow(&mw); + mw.setWindowFlags(Qt::Window | Qt::FramelessWindowHint); mw.setWindowState(Qt::WindowFullScreen); mw.show(); diff --git a/mainwindow.cpp b/mainwindow.cpp index 07a9c4a..fd5f34d 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -2,6 +2,7 @@ #include "ui_mainwindow.h" #include "worker.h" #include "utils.h" +#include "progress_event.h" #include #include @@ -56,7 +57,6 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) connect(m_worker, SIGNAL(restartExitTimer()), this, SLOT(onRestartExitTimer())); connect(m_worker, SIGNAL(appendText(QString, QString)), this, SLOT(onAppendText(QString, QString))); connect(m_worker, SIGNAL(showErrorMessage(QString,QString)), this, SLOT(onShowErrorMessage(QString,QString))); - connect(m_worker, SIGNAL(setProgress(quint8)), this, SLOT(onSetProgress(quint8))); ui->updateStatus->setText(lst.join('\n')); ui->updateStatus->setEnabled(true); @@ -68,6 +68,13 @@ MainWindow::~MainWindow() { delete ui; } +void MainWindow::customEvent(QEvent *event) { + if (event->type() == ProgressEvent::type()) { + int progress = ((ProgressEvent *)(event))->progressPercent(); + ui->updateProgress->setValue(progress); + } +} + void MainWindow::onStopStartTimer() { m_startTimer->stop(); } @@ -91,11 +98,6 @@ void MainWindow::onQuit() { qApp->exit(m_worker->returnCode()); } -void MainWindow::onSetProgress(quint8 v) { - qCritical() << "ON SET PROGRESS" << v; - ui->updateProgress->setValue(v); -} - void MainWindow::onAppendText(QString text, QString suffix) { QString editText = ui->updateStatus->toPlainText(); QStringList lines = editText.split('\n'); diff --git a/mainwindow.h b/mainwindow.h index 63af2ee..46e5ef9 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -13,6 +13,9 @@ QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT +protected: + void customEvent(QEvent *event) override; + public: MainWindow(Worker *worker, QWidget *parent = nullptr); ~MainWindow(); @@ -20,7 +23,6 @@ public: public slots: void onAppendText(QString, QString); void onShowErrorMessage(QString, QString); - void onSetProgress(quint8); void onStopStartTimer(); void onRestartExitTimer(); void onEnableExit(); diff --git a/progress_event.cpp b/progress_event.cpp index 9f07341..4b2c043 100644 --- a/progress_event.cpp +++ b/progress_event.cpp @@ -2,9 +2,9 @@ QEvent::Type ProgressEvent::customEventType = QEvent::None; -ProgressEvent::ProgressEvent() +ProgressEvent::ProgressEvent(int progressPercent) : QEvent(ProgressEvent::type()) - , m_progressPercent(0) { + , m_progressPercent(progressPercent) { } ProgressEvent::~ProgressEvent() { diff --git a/progress_event.h b/progress_event.h index 3a67e0b..5b78c0e 100644 --- a/progress_event.h +++ b/progress_event.h @@ -7,7 +7,7 @@ class ProgressEvent : public QEvent { int m_progressPercent; public: - ProgressEvent(); + explicit ProgressEvent(int progressPercent); virtual ~ProgressEvent(); static QEvent::Type type(); diff --git a/worker.cpp b/worker.cpp index d96a372..cc25155 100644 --- a/worker.cpp +++ b/worker.cpp @@ -21,6 +21,8 @@ #include "message_handler.h" #include "plugins/interfaces.h" #include "ismas/ismas_client.h" +#include "progress_event.h" +#include "mainwindow.h" QString const Worker::UPDATE_STEP_OK(" [ ok]"); QString const Worker::UPDATE_STEP_DONE(" [done]"); @@ -112,13 +114,19 @@ Worker::~Worker() { } } +void Worker::setProgress(int progress) { + if (m_mainWindow) { + QApplication::postEvent(m_mainWindow, new ProgressEvent(progress)); + } +} + static std::once_flag once; void Worker::update() { + // user should not start the update process several times std::call_once(once, &Worker::privateUpdate, this); } void Worker::privateUpdate() { - // user should not start the update process several times QPushButton *start = qobject_cast(QObject::sender()); start->setEnabled(false); @@ -148,7 +156,7 @@ void Worker::privateUpdate() { emit appendText(QString(""), UPDATE_STEP_SUCCESS); - emit setProgress(100); + setProgress(100); m_ismasClient.setProgressInPercent(100); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); @@ -167,41 +175,41 @@ void Worker::privateUpdate() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.checkoutBranch( m_updateStatus.m_statusDescription, "")); - emit setProgress(progress); + setProgress(progress); qCritical() << "CHECKED OUT BRANCH"; if (backendConnected()) { qCritical() << "BACKEND CONNECTED"; progress = 20; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); if (updateTriggerSet()) { qCritical() << "UPDATE TRIGGER SET"; progress = 30; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); if (customerEnvironment()) { qCritical() << "CUSTOMER ENVIRONMENT"; progress = 40; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); if (filesToUpdate()) { qCritical() << "FILES TO UPDATE"; progress = 50; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); if (updateFiles(progress)) { qCritical() << "UPDATE FILES"; progress = 60; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); if (syncCustomerRepositoryAndFS()) { qCritical() << "SYNC REPOSITORY"; progress = 70; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); if (sendIsmasLastVersionNotification()) { qCritical() << "SEND LAST NOTIFICATION"; progress = 80; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); sentIsmasLastVersionNotification = true; if (saveLogFile()) { qCritical() << "SAVE LOG FILE"; progress = 90; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); emit appendText(QString(""), UPDATE_STEP_SUCCESS); @@ -211,7 +219,7 @@ void Worker::privateUpdate() { // mark update as activated -> this resets the WAIT button progress = 100; - emit setProgress(progress); + setProgress(progress); m_ismasClient.setProgressInPercent(progress); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, @@ -247,7 +255,7 @@ void Worker::privateUpdate() { } } - emit setProgress(100); + setProgress(100); m_ismasClient.setProgressInPercent(100); if (!sentIsmasLastVersionNotification) { diff --git a/worker.h b/worker.h index 6a745cb..348b5b6 100644 --- a/worker.h +++ b/worker.h @@ -84,6 +84,7 @@ QString& operator<<(QString &str, UpdateStatus status); #define ISMAS_UPDATE_REQUESTS (10) +class MainWindow; class hwinf; class Worker : public QObject { Q_OBJECT @@ -124,6 +125,8 @@ class Worker : public QObject { int m_displayIndex; int m_returnCode; + MainWindow *m_mainWindow; + bool executeOpkgCommand(QString opkgCommand); QString getOsVersion() const; QString getATBQTVersion() const; @@ -135,6 +138,8 @@ class Worker : public QObject { qint64 getFileSize(QString const &fileName) const; + void setProgress(int progress); + public: static const QString UPDATE_STEP_OK; static const QString UPDATE_STEP_DONE; @@ -153,6 +158,8 @@ public: char const *baudrate = "115200"); ~Worker(); + void setMainWindow(MainWindow *mainWindow) { m_mainWindow = mainWindow; } + IsmasClient &getIsmasClient() { return m_ismasClient; } IsmasClient const &getIsmasClient() const { return m_ismasClient; } @@ -175,7 +182,6 @@ public: signals: void appendText(QString, QString); void showErrorMessage(QString title, QString description); - void setProgress(quint8); void stopStartTimer(); void restartExitTimer(); void enableExit(); From 4594c913e0d980279a2828bfd89f55ef71531cda Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 6 Aug 2023 07:34:17 +0200 Subject: [PATCH 106/239] Using syslog for debugging. --- main.cpp | 5 ++- message_handler.cpp | 100 +++++++++++++++++--------------------------- message_handler.h | 7 +++- 3 files changed, 46 insertions(+), 66 deletions(-) diff --git a/main.cpp b/main.cpp index 47c2dce..eaf6a9f 100644 --- a/main.cpp +++ b/main.cpp @@ -46,14 +46,15 @@ int main(int argc, char *argv[]) { } // qputenv("XDG_RUNTIME_DIR", "/run/user/0"); + openlog("ATB-UPDATE", LOG_PERROR | LOG_PID | LOG_CONS, LOG_USER); + QApplication a(argc, argv); QApplication::setApplicationName("ATBUpdateTool"); QApplication::setApplicationVersion(APP_VERSION); if (!messageHandlerInstalled()) { // change internal qt-QDebug-handling atbInstallMessageHandler(atbDebugOutput); - setDebugLevel(QtMsgType::QtInfoMsg); - //setDebugLevel(QtMsgType::QtDebugMsg); + setDebugLevel(LOG_NOTICE); } QCommandLineParser parser; diff --git a/message_handler.cpp b/message_handler.cpp index 7be2d67..996bd2f 100755 --- a/message_handler.cpp +++ b/message_handler.cpp @@ -6,13 +6,13 @@ #include #include -#define OUTPUT_LEN (1024*8) +static char const *DBG_NAME[] = { "DBG ", "WARN ", "CRIT ", "FATAL", "INFO " }; static bool installedMsgHandler = false; -static QtMsgType debugLevel = QtInfoMsg; +static int debugLevel = LOG_NOTICE; -QtMsgType getDebugLevel() { return debugLevel; } -void setDebugLevel(QtMsgType newDebugLevel) { +int getDebugLevel() { return debugLevel; } +void setDebugLevel(int newDebugLevel) { debugLevel = newDebugLevel; } @@ -45,75 +45,51 @@ QtMessageHandler atbInstallMessageHandler(QtMessageHandler handler) { /// #if (QT_VERSION > QT_VERSION_CHECK(5, 0, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) void atbDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - // static constexpr const char *format = "dd.MM.yyyy hh:mm:ss"; - QByteArray localMsg = msg.toLocal8Bit(); - QString fileName(context.file ? context.file : "N/A"); - QString function(context.function ? context.function : "N/A"); - char buf[OUTPUT_LEN]{}; - memset(buf, 0x00, sizeof(buf)); - QString const datetime = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); - switch (type) { - case QtDebugMsg: { - if (debugLevel == QtDebugMsg) { - snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", - datetime.toStdString().c_str(), - function.toStdString().c_str(), - fileName.toStdString().c_str(), - context.line, - localMsg.constData()); - fprintf(stderr, "%s\n", buf); + Q_UNUSED(context); + QString const localMsg = QString(DBG_NAME[type]) + msg.toLocal8Bit(); + + switch (debugLevel) { + case LOG_DEBUG: { // debug-level message + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); + } break; + case LOG_INFO: { // informational message + if (type != QtDebugMsg) { + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); } } break; - case QtInfoMsg: { - if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg) { - snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", - datetime.toStdString().c_str(), - function.toStdString().c_str(), - fileName.toStdString().c_str(), - context.line, - localMsg.constData()); - fprintf(stderr, "%s\n", buf); + case LOG_NOTICE: { // normal, but significant, condition + if (type != QtDebugMsg) { + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); } } break; - case QtWarningMsg: { - if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg || debugLevel == QtWarningMsg) { - snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", - datetime.toStdString().c_str(), - function.toStdString().c_str(), - fileName.toStdString().c_str(), - context.line, - localMsg.constData()); - fprintf(stderr, "%s\n", buf); + case LOG_WARNING: { // warning conditions + if (type != QtInfoMsg && type != QtDebugMsg) { + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); } } break; - case QtCriticalMsg: { - if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg - || debugLevel == QtWarningMsg || debugLevel == QtCriticalMsg) { - snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", - datetime.toStdString().c_str(), - function.toStdString().c_str(), - fileName.toStdString().c_str(), - context.line, - localMsg.constData()); - fprintf(stderr, "%s\n", buf); + case LOG_ERR: { // error conditions + if (type != QtInfoMsg && type != QtDebugMsg && type != QtWarningMsg) { + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); } } break; - case QtFatalMsg: { - if (debugLevel == QtInfoMsg || debugLevel == QtDebugMsg - || debugLevel == QtWarningMsg || debugLevel == QtCriticalMsg - || debugLevel == QtFatalMsg) { - snprintf(buf, sizeof(buf)-1, "%s DEBG [%s:%s:%04u] %s", - datetime.toStdString().c_str(), - function.toStdString().c_str(), - fileName.toStdString().c_str(), - context.line, - localMsg.constData()); - fprintf(stderr, "%s\n", buf); + case LOG_CRIT: { // critical conditions + if (type != QtInfoMsg && type != QtDebugMsg && type != QtWarningMsg) { + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); + } + } break; + case LOG_ALERT: { // action must be taken immediately + if (type != QtInfoMsg && type != QtDebugMsg && type != QtWarningMsg) { + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); + } + } break; + case LOG_EMERG: { // system is unusable + if (type != QtInfoMsg && type != QtDebugMsg && type != QtWarningMsg) { + syslog(LOG_DEBUG, "%s", localMsg.toStdString().c_str()); } } break; default: { - fprintf(stderr, "%s No ErrorLevel defined! %s\n", - datetime.toStdString().c_str(), msg.toStdString().c_str()); + //fprintf(stderr, "%s No ErrorLevel defined! %s\n", + // datetime.toStdString().c_str(), msg.toStdString().c_str()); } } } diff --git a/message_handler.h b/message_handler.h index 18e923b..98c4d7e 100755 --- a/message_handler.h +++ b/message_handler.h @@ -2,9 +2,12 @@ #define MESSAGE_HANDLER_H_INCLUDED #include +#ifdef __linux__ +#include +#endif -QtMsgType getDebugLevel(); -void setDebugLevel(QtMsgType newDebugLevel); +int getDebugLevel(); +void setDebugLevel(int newDebugLevel); bool messageHandlerInstalled(); QtMessageHandler atbInstallMessageHandler(QtMessageHandler handler); From a8994423f46d34700a812115e1950dfaa71d3fde Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 6 Aug 2023 14:11:41 +0200 Subject: [PATCH 107/239] Change font size for full screen window. --- mainwindow.ui | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index b702349..9c7b471 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -19,7 +19,7 @@ MainWindow - + 11 @@ -115,9 +115,10 @@ - Noto Sans - 8 - false + Monospace + 14 + 75 + true From 1fd2269753fcf5ac4498916b2f341dd901108913 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 6 Aug 2023 14:14:47 +0200 Subject: [PATCH 108/239] onAppendText() only appends text. onReplaceLast() replaces the last line in the text edit window. --- mainwindow.cpp | 51 ++++++++++++++++++++++++++++++++++---------------- mainwindow.h | 3 ++- worker.cpp | 16 ++++++++++++++-- worker.h | 3 ++- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index fd5f34d..38f11fa 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -12,10 +12,10 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_worker(worker) - , m_width(52) { + , m_width(70) { ui->setupUi(this); - ui->updateProgress->setRange(1, 100); + ui->updateProgress->setRange(0, 100); ui->updateProgress->reset(); QStringList lst; @@ -57,6 +57,7 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) connect(m_worker, SIGNAL(restartExitTimer()), this, SLOT(onRestartExitTimer())); connect(m_worker, SIGNAL(appendText(QString, QString)), this, SLOT(onAppendText(QString, QString))); connect(m_worker, SIGNAL(showErrorMessage(QString,QString)), this, SLOT(onShowErrorMessage(QString,QString))); + connect(m_worker, SIGNAL(replaceLast(QString, QString)), this, SLOT(onReplaceLast(QString,QString))); ui->updateStatus->setText(lst.join('\n')); ui->updateStatus->setEnabled(true); @@ -71,7 +72,11 @@ MainWindow::~MainWindow() { void MainWindow::customEvent(QEvent *event) { if (event->type() == ProgressEvent::type()) { int progress = ((ProgressEvent *)(event))->progressPercent(); - ui->updateProgress->setValue(progress); + if (progress > 0) { + ui->updateProgress->setValue(progress); + } else { + ui->updateProgress->reset(); + } } } @@ -100,22 +105,36 @@ void MainWindow::onQuit() { void MainWindow::onAppendText(QString text, QString suffix) { QString editText = ui->updateStatus->toPlainText(); + if (suffix.size() > 0) { + editText += QString("\n").leftJustified(m_width-3, '='); + editText += " "; + editText += (QString("\n") + text).leftJustified(m_width - (2 + suffix.size()) ) + suffix; + } else { + editText += text.leftJustified(m_width-9); + } + QStringList lines = editText.split('\n'); for (int i=0; iupdateStatus->setPlainText(editText); + ui->updateStatus->setEnabled(true); +} + +void MainWindow::onReplaceLast(QString text, QString suffix) { + QString editText = ui->updateStatus->toPlainText(); + QStringList lines = editText.split('\n'); + if (lines.size() > 0) { + lines.removeLast(); + lines += text.leftJustified(m_width-10) + suffix; } - if (suffix.contains("[SUCCESS]")) { - editText += QString("\n").leftJustified(m_width-3, '='); - editText += QString("\n").leftJustified(m_width-12) + " [SUCCESS]"; - } else - if (suffix.contains(" [fail]")) { - editText += QString("\n").leftJustified(m_width-3, '='); - editText += QString("\n").leftJustified(m_width-9) + " [fail]"; - } else { - editText += text.leftJustified(m_width-9) + suffix; - ui->updateStatus->setPlainText(editText); - } - ui->updateStatus->setText(editText); + + for (int i=0; iupdateStatus->setText(lines.join('\n')); ui->updateStatus->setEnabled(true); } diff --git a/mainwindow.h b/mainwindow.h index 46e5ef9..e71ed78 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -21,7 +21,8 @@ public: ~MainWindow(); public slots: - void onAppendText(QString, QString); + void onAppendText(QString, QString suffix = ""); + void onReplaceLast(QString, QString); void onShowErrorMessage(QString, QString); void onStopStartTimer(); void onRestartExitTimer(); diff --git a/worker.cpp b/worker.cpp index cc25155..b45efdb 100644 --- a/worker.cpp +++ b/worker.cpp @@ -26,7 +26,7 @@ QString const Worker::UPDATE_STEP_OK(" [ ok]"); QString const Worker::UPDATE_STEP_DONE(" [done]"); -QString const Worker::UPDATE_STEP_FAIL(" [fail]"); +QString const Worker::UPDATE_STEP_FAIL(" [FAIL]"); QString const Worker::UPDATE_STEP_SUCCESS(" [SUCCESS]"); Worker::Worker(hwinf *hw, @@ -130,6 +130,18 @@ void Worker::privateUpdate() { QPushButton *start = qobject_cast(QObject::sender()); start->setEnabled(false); + //emit appendText("\nConnecting backend ..."); + // + //for (int i=0;i <= 100; ++i) { + // setProgress(i); + // QThread::msleep(100); + //} + //emit replaceLast("Connecting backend ...", UPDATE_STEP_OK); + //emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); + //emit appendText(QString("UPDATE "), UPDATE_STEP_FAIL); + // setProgress(0); + // return; + emit stopStartTimer(); emit disableExit(); m_updateProcessRunning = true; @@ -211,7 +223,7 @@ void Worker::privateUpdate() { progress = 90; setProgress(progress); m_ismasClient.setProgressInPercent(progress); - emit appendText(QString(""), UPDATE_STEP_SUCCESS); + emit appendText(QString("Update process "), UPDATE_STEP_SUCCESS); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + diff --git a/worker.h b/worker.h index 348b5b6..5daea58 100644 --- a/worker.h +++ b/worker.h @@ -180,7 +180,8 @@ public: //} signals: - void appendText(QString, QString); + void appendText(QString, QString suffix = ""); + void replaceLast(QString, QString); void showErrorMessage(QString title, QString description); void stopStartTimer(); void restartExitTimer(); From 4ff3b0efdfb8f0289b471916000f7b1cbb639e66 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 6 Aug 2023 20:44:26 +0200 Subject: [PATCH 109/239] Advance the progress bar in the foreground when a long running task in the background (e.g. git clone). --- mainwindow.cpp | 52 +++++++++++++++++++++++++++++++++++++++++----- mainwindow.h | 7 +++++++ progress_event.cpp | 3 ++- progress_event.h | 6 +++++- worker.cpp | 46 ++++++++++++++++++++++++++++------------ worker.h | 6 ++++-- 6 files changed, 98 insertions(+), 22 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 38f11fa..7791fcb 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -12,7 +12,9 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_worker(worker) - , m_width(70) { + , m_width(70) + , m_progressRunning(false) + , m_progressValue(0) { ui->setupUi(this); ui->updateProgress->setRange(0, 100); @@ -71,11 +73,51 @@ MainWindow::~MainWindow() { void MainWindow::customEvent(QEvent *event) { if (event->type() == ProgressEvent::type()) { - int progress = ((ProgressEvent *)(event))->progressPercent(); - if (progress > 0) { - ui->updateProgress->setValue(progress); + ProgressEvent *pevent = (ProgressEvent *)event; + int const progress = pevent->progressPercent(); + QObject const *sender = pevent->sender(); + if (sender == this) { + switch(progress) { + case 0: { + ui->updateProgress->reset(); + } break; + case START_PROGRESS_LOOP: { + m_progressRunning = true; + ui->updateProgress->reset(); + m_progressValue = 10; + QApplication::postEvent(this, new ProgressEvent(this, m_progressValue)); + } break; + case STOP_PROGRESS_LOOP: { + m_progressRunning = false; + m_progressValue -= 10; + m_worker->setProgress(m_progressValue/10); + } break; + default: { + if (m_progressRunning) { + m_progressValue = progress; + ui->updateProgress->setValue(progress/10); + QApplication::postEvent(this, new ProgressEvent(this, progress+10)); + QThread::msleep(100); + } + } + } + } else + if (sender == m_worker) { + switch(progress) { + case 0: { + ui->updateProgress->reset(); + } break; + case START_PROGRESS_LOOP: { + QApplication::postEvent(this, new ProgressEvent(this, START_PROGRESS_LOOP)); + } break; + case STOP_PROGRESS_LOOP: { + QApplication::postEvent(this, new ProgressEvent(this, STOP_PROGRESS_LOOP)); + } break; + default:{ + ui->updateProgress->setValue(progress); + }} } else { - ui->updateProgress->reset(); + qCritical() << "!!! UNKNOWN SENDER !!!"; } } } diff --git a/mainwindow.h b/mainwindow.h index e71ed78..df47262 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -20,6 +20,11 @@ public: MainWindow(Worker *worker, QWidget *parent = nullptr); ~MainWindow(); + static const int START_PROGRESS_LOOP = -1; + static const int STOP_PROGRESS_LOOP = -2; + + int progressValue() const { return m_progressValue; } + public slots: void onAppendText(QString, QString suffix = ""); void onReplaceLast(QString, QString); @@ -38,5 +43,7 @@ private: int m_width; QTimer *m_startTimer; QTimer *m_exitTimer; + bool m_progressRunning; + int m_progressValue; }; #endif // MAINWINDOW_H diff --git a/progress_event.cpp b/progress_event.cpp index 4b2c043..8cbf0a0 100644 --- a/progress_event.cpp +++ b/progress_event.cpp @@ -2,8 +2,9 @@ QEvent::Type ProgressEvent::customEventType = QEvent::None; -ProgressEvent::ProgressEvent(int progressPercent) +ProgressEvent::ProgressEvent(QObject const *sender, int progressPercent) : QEvent(ProgressEvent::type()) + , m_sender(sender) , m_progressPercent(progressPercent) { } diff --git a/progress_event.h b/progress_event.h index 5b78c0e..e8bd4be 100644 --- a/progress_event.h +++ b/progress_event.h @@ -5,12 +5,16 @@ class ProgressEvent : public QEvent { + QObject const *m_sender; int m_progressPercent; public: - explicit ProgressEvent(int progressPercent); + explicit ProgressEvent(QObject const *sender, int progressPercent); virtual ~ProgressEvent(); static QEvent::Type type(); + QObject const *sender() { return m_sender; } + QObject const *sender() const { return m_sender; } + void setProgress(int progressPercent) { m_progressPercent = progressPercent; } int progressPercent() { return m_progressPercent; } int progressPercent() const { return m_progressPercent; } diff --git a/worker.cpp b/worker.cpp index b45efdb..4b2c72e 100644 --- a/worker.cpp +++ b/worker.cpp @@ -68,7 +68,8 @@ Worker::Worker(hwinf *hw, , m_waitForNewUpdates(this) , m_filesToUpdate() , m_updateProcessRunning(false) - , m_returnCode(0) { + , m_returnCode(0) + , m_progressValue{0} { QDir::setCurrent(m_workingDirectory); @@ -116,10 +117,19 @@ Worker::~Worker() { void Worker::setProgress(int progress) { if (m_mainWindow) { - QApplication::postEvent(m_mainWindow, new ProgressEvent(progress)); + m_progressValue = progress; + QApplication::postEvent(m_mainWindow, new ProgressEvent(this, progress)); } } +void Worker::startProgressLoop() { + QApplication::postEvent(m_mainWindow, new ProgressEvent(this, MainWindow::START_PROGRESS_LOOP)); +} + +void Worker::stopProgressLoop() { + QApplication::postEvent(m_mainWindow, new ProgressEvent(this, MainWindow::STOP_PROGRESS_LOOP)); +} + static std::once_flag once; void Worker::update() { // user should not start the update process several times @@ -130,17 +140,27 @@ void Worker::privateUpdate() { QPushButton *start = qobject_cast(QObject::sender()); start->setEnabled(false); - //emit appendText("\nConnecting backend ..."); - // - //for (int i=0;i <= 100; ++i) { - // setProgress(i); - // QThread::msleep(100); - //} - //emit replaceLast("Connecting backend ...", UPDATE_STEP_OK); - //emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); - //emit appendText(QString("UPDATE "), UPDATE_STEP_FAIL); - // setProgress(0); - // return; +#if 0 + emit appendText("\nConnecting backend ..."); + + for (int i=0;i <= 100; ++i) { + setProgress(i); + QThread::msleep(100); + } + emit replaceLast("Connecting backend ...", UPDATE_STEP_OK); + emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); + emit appendText(QString("UPDATE "), UPDATE_STEP_FAIL); + setProgress(0); + + startProgressLoop(); + QThread::sleep(5); // long running process + stopProgressLoop(); + for (int i=m_mainWindow->progressValue()/10; i <= 100; ++i) { + setProgress(i); + QThread::msleep(100); + } + return; +#endif emit stopStartTimer(); emit disableExit(); diff --git a/worker.h b/worker.h index 5daea58..2a60b0c 100644 --- a/worker.h +++ b/worker.h @@ -126,6 +126,7 @@ class Worker : public QObject { int m_returnCode; MainWindow *m_mainWindow; + int m_progressValue; bool executeOpkgCommand(QString opkgCommand); QString getOsVersion() const; @@ -138,8 +139,6 @@ class Worker : public QObject { qint64 getFileSize(QString const &fileName) const; - void setProgress(int progress); - public: static const QString UPDATE_STEP_OK; static const QString UPDATE_STEP_DONE; @@ -159,6 +158,7 @@ public: ~Worker(); void setMainWindow(MainWindow *mainWindow) { m_mainWindow = mainWindow; } + void setProgress(int progress); IsmasClient &getIsmasClient() { return m_ismasClient; } IsmasClient const &getIsmasClient() const { return m_ismasClient; } @@ -205,6 +205,8 @@ private: PSAInstalled getPSAInstalled(); QString sendCmdSendVersionToIsmas(); void privateUpdate(); + void startProgressLoop(); + void stopProgressLoop(); }; #endif // WORKER_H_INCLUDED From 8b66c47e49d2a71209250a55b0a9af28e56f1191 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:50:14 +0200 Subject: [PATCH 110/239] Added errorGitClone() --- ismas/ismas_client.h | 1 + 1 file changed, 1 insertion(+) diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index fdda53d..9274e8c 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -144,6 +144,7 @@ public: QString cloneAndCheckoutCustomerRepository(QString const &info, QString const &version = QString()); // clone and checkout customer repository QString checkoutBranch(QString const &info, QString const &version = QString()); // checkout branch QString errorBackendNotConnected(QString const &info, QString const &version = QString()); // checkout branch + QString errorGitClone(int percent, QString const &info, QString const &version = QString()); QString backendConnected(QString const &info, QString const &version = QString()); QString updateTriggerSet(QString const &info, QString const &version = QString()); QString errorUpdateTrigger(QString const &info, QString const &version = QString()); From ce72d3d14d7c5f24fa4c9de3a9bfffe105e4588e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:50:59 +0200 Subject: [PATCH 111/239] For the error EWOULDBLOCK try again 10 times until quitting. --- ismas/ismas_client.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index a842180..2ffa602 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -148,6 +148,7 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { int const bytesToWrite = strlen(request.toStdString().c_str()); strncpy(buf, request.toStdString().c_str(), sizeof(buf)-1); + int loop = 0; int bytesWritten = 0; while (bytesWritten < bytesToWrite) { int n = ::sendto(sockfd, buf+bytesWritten, bytesToWrite-bytesWritten, 0, NULL, 0); @@ -155,14 +156,18 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { bytesWritten += n; } else { if (errno == EWOULDBLOCK) { + if (++loop < 10) { + QThread::msleep(500); + continue; + } printErrorMessage(port, clientIP, clientPort, - QString("TIMEOUT (") + strerror(errno) + ")"); + QString("WRITE TIMEOUT %1(").arg(loop) + strerror(errno) + ")"); ::close(sockfd); return std::nullopt; } else if (errno == EINTR) { printErrorMessage(port, clientIP, clientPort, - QString("INTERRUPTED BY SIGNAL (1) (") + strerror(errno) + ")"); + QString("WRITE INTERRUPTED BY SIGNAL (1) (") + strerror(errno) + ")"); continue; } } @@ -177,6 +182,7 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { printInfoMessage(port, clientIP, clientPort, QString("MESSAGE SENT ") + buf); + loop = 0; bzero(buf, sizeof(buf)); int bytesToRead = sizeof(buf)-1; int bytesRead = 0; @@ -196,8 +202,12 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { } else if (n < 0) { if (errno == EWOULDBLOCK) { + if (++loop < 10) { + QThread::msleep(500); + continue; + } printErrorMessage(port, clientIP, clientPort, - QString("TIMEOUT (") + strerror(errno) + ")"); + QString("READ TIMEOUT %1(").arg(loop) + strerror(errno) + ")"); ::close(sockfd); return std::nullopt; } From 223c7a8f8d73fa512a584029482a78bb933c1d21 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:51:47 +0200 Subject: [PATCH 112/239] Added errorGitClone(). --- ismas/ismas_client.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 2ffa602..998d4b3 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -293,6 +293,15 @@ QString IsmasClient::errorBackendNotConnected(QString const &info, version.toStdString().c_str()); } +QString IsmasClient::errorGitClone(int percent, QString const &info, QString const &version) { + return updateNewsToIsmas("U0003", + percent, + RESULT_CODE::INSTALL_ERROR, + "CLONE CUSTOMER REPOSITORY FAILED", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + QString IsmasClient::backendConnected(QString const &info, QString const &version) { return updateNewsToIsmas("U0010", m_progressInPercent, From 9a55ce18e42bf55fe169519b7bf9fcf2ef772e9e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:53:04 +0200 Subject: [PATCH 113/239] Chawnged font to Misc Fixed --- mainwindow.ui | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index 9c7b471..5f269f8 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -115,9 +115,8 @@ - Monospace - 14 - 75 + Misc Fixed + 11 true From 4d2d38e45cdc0c1e6c196827fc6475b063441cd7 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:53:48 +0200 Subject: [PATCH 114/239] Default parameter for onReplaceLast() --- mainwindow.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainwindow.h b/mainwindow.h index df47262..3895467 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -27,7 +27,7 @@ public: public slots: void onAppendText(QString, QString suffix = ""); - void onReplaceLast(QString, QString); + void onReplaceLast(QString, QString suffix = ""); void onShowErrorMessage(QString, QString); void onStopStartTimer(); void onRestartExitTimer(); From 4caa0c0d83cc53464ee1f22be0e47880b2ea6e9f Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:55:52 +0200 Subject: [PATCH 115/239] Removed obsolete out-commented lines. --- worker.cpp | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/worker.cpp b/worker.cpp index 4b2c72e..53a409f 100644 --- a/worker.cpp +++ b/worker.cpp @@ -140,28 +140,6 @@ void Worker::privateUpdate() { QPushButton *start = qobject_cast(QObject::sender()); start->setEnabled(false); -#if 0 - emit appendText("\nConnecting backend ..."); - - for (int i=0;i <= 100; ++i) { - setProgress(i); - QThread::msleep(100); - } - emit replaceLast("Connecting backend ...", UPDATE_STEP_OK); - emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); - emit appendText(QString("UPDATE "), UPDATE_STEP_FAIL); - setProgress(0); - - startProgressLoop(); - QThread::sleep(5); // long running process - stopProgressLoop(); - for (int i=m_mainWindow->progressValue()/10; i <= 100; ++i) { - setProgress(i); - QThread::msleep(100); - } - return; -#endif - emit stopStartTimer(); emit disableExit(); m_updateProcessRunning = true; From 6d43cf4c9fb9b392b43df3a6e1637d3bebf9fd7f Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:56:51 +0200 Subject: [PATCH 116/239] Send custom events to the progress bar according to the state of the update process. Changed the handling of messages for the text edit. --- worker.cpp | 244 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 169 insertions(+), 75 deletions(-) diff --git a/worker.cpp b/worker.cpp index 53a409f..f307e0d 100644 --- a/worker.cpp +++ b/worker.cpp @@ -149,8 +149,14 @@ void Worker::privateUpdate() { m_returnCode = -1; QDir customerRepository(m_customerRepository); if (!customerRepository.exists()) { + emit appendText("\nInitializing customer environment ..."); + startProgressLoop(); if (m_gc.gitCloneAndCheckoutBranch()) { - emit appendText("\nInitializing customer environment", UPDATE_STEP_DONE); + stopProgressLoop(); + emit replaceLast("Initializing customer environment", UPDATE_STEP_DONE); + + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_SUCCESS, QString("CLONED AND CHECKED OUT: ") + m_customerRepository); @@ -160,112 +166,110 @@ void Worker::privateUpdate() { m_ismasClient.cloneAndCheckoutCustomerRepository( m_updateStatus.m_statusDescription)); + setProgress(progress + 10); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSASucceeded("")); - emit appendText(QString(""), UPDATE_STEP_SUCCESS); - setProgress(100); m_ismasClient.setProgressInPercent(100); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); m_returnCode = 0; + } else { + stopProgressLoop(); + + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); + + emit replaceLast("Initializing customer environment", UPDATE_STEP_FAIL); + + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_FAILURE, + QString("CLONE OR CHECKOUT FAILED: ") + m_customerRepository); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.errorGitClone(100, m_updateStatus.m_statusDescription)); + + m_returnCode = -3; } } else { - qCritical() << "CHECKOUT BRANCH..."; - // checkout branch + qInfo() << "CHECKOUT BRANCH..."; + emit appendText("\nInitializing customer environment ..."); + startProgressLoop(); if (m_gc.gitCheckoutBranch()) { - int progress = 10; - m_ismasClient.setProgressInPercent(progress); + stopProgressLoop(); + emit replaceLast("Initializing customer environment ...", UPDATE_STEP_DONE); + setProgress(100); + + m_ismasClient.setProgressInPercent(10); m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH, QString("CHECKED OUT BRANCH: ") + m_gc.branchName()); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.checkoutBranch( m_updateStatus.m_statusDescription, "")); - setProgress(progress); qCritical() << "CHECKED OUT BRANCH"; if (backendConnected()) { qCritical() << "BACKEND CONNECTED"; - progress = 20; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); + m_ismasClient.setProgressInPercent(20); if (updateTriggerSet()) { qCritical() << "UPDATE TRIGGER SET"; - progress = 30; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); + m_ismasClient.setProgressInPercent(30); if (customerEnvironment()) { qCritical() << "CUSTOMER ENVIRONMENT"; - progress = 40; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); + m_ismasClient.setProgressInPercent(40); if (filesToUpdate()) { qCritical() << "FILES TO UPDATE"; - progress = 50; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); - if (updateFiles(progress)) { qCritical() << "UPDATE FILES"; - progress = 60; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); + m_ismasClient.setProgressInPercent(50); + if (updateFiles(50)) { qCritical() << "UPDATE FILES"; + m_ismasClient.setProgressInPercent(60); if (syncCustomerRepositoryAndFS()) { qCritical() << "SYNC REPOSITORY"; - progress = 70; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); + m_ismasClient.setProgressInPercent(70); if (sendIsmasLastVersionNotification()) { qCritical() << "SEND LAST NOTIFICATION"; - progress = 80; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); + m_ismasClient.setProgressInPercent(80); sentIsmasLastVersionNotification = true; if (saveLogFile()) { qCritical() << "SAVE LOG FILE"; - progress = 90; - setProgress(progress); - m_ismasClient.setProgressInPercent(progress); - emit appendText(QString("Update process "), UPDATE_STEP_SUCCESS); + m_ismasClient.setProgressInPercent(90); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSASucceeded("")); // mark update as activated -> this resets the WAIT button - progress = 100; - setProgress(progress); - - m_ismasClient.setProgressInPercent(progress); + m_ismasClient.setProgressInPercent(95); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAActivated()); + m_returnCode = 0; } else { - m_returnCode = -9; + m_returnCode = -12; } } else { - m_returnCode = -8; + m_returnCode = -11; } } else { - m_returnCode = -7; + m_returnCode = -10; } } else { - m_returnCode = -6; + m_returnCode = -9; } } else { - m_returnCode = -5; + m_returnCode = -8; } } else { - m_returnCode = -4; + m_returnCode = -7; } } else { - m_returnCode = -3; + m_returnCode = -6; } } else { - m_returnCode = -2; + m_returnCode = -5; } } else { - m_returnCode = -1; + m_returnCode = -4; } } - setProgress(100); m_ismasClient.setProgressInPercent(100); if (!sentIsmasLastVersionNotification) { @@ -274,9 +278,13 @@ void Worker::privateUpdate() { } if (m_returnCode != 0) { - emit appendText(QString("Update process "), UPDATE_STEP_FAIL); + stopProgressLoop(); + emit appendText(QString("UPDATE "), UPDATE_STEP_FAIL); + } else { + emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); } + setProgress(100); m_updateProcessRunning = false; emit enableExit(); emit restartExitTimer(); @@ -285,26 +293,39 @@ void Worker::privateUpdate() { bool Worker::backendConnected() { static int repeat = 0; + if (repeat == 0) { + emit appendText("\nConnecting backend ..."); + } + if (repeat < 3) { - qCritical() << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; + qInfo() << repeat << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; + startProgressLoop(); std::optional result = IsmasClient::sendRequestReceiveResponse( IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_SELF#J={}"); if (result) { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); + QString msg = result.value(); - qCritical() << "In backendConnected() -> APISM response" << msg; + qInfo() << "In backendConnected() -> APISM response" << msg; QJsonParseError parseError; QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); if (parseError.error != QJsonParseError::NoError) { qCritical() << "(2) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" << parseError.error << parseError.errorString(); + emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); return false; } if (!document.isObject()) { qCritical() << "FILE IS NOT A JSON OBJECT!"; + emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); return false; } + setProgress(progress + 10); + QJsonObject obj = document.object(); QStringList keys = obj.keys(); for (QString const& key : keys ) { @@ -314,14 +335,13 @@ bool Worker::backendConnected() { obj = v.toObject(); bool ismas = obj.value("ISMAS").toBool(); QString status = obj.value("Broker").toString(); - - qCritical() << "In backendConnected() STATUS" << status; - + qInfo() << "In backendConnected() STATUS" << status; if (ismas) { if (status == "Connected") { // do not send, as this would result in a corrupted wait button // but update the user-interface - emit appendText("\nBackend connected", UPDATE_STEP_OK); + setProgress(100); + emit replaceLast("Connecting backend ...", UPDATE_STEP_OK); return true; } } @@ -330,14 +350,20 @@ bool Worker::backendConnected() { ++repeat; return backendConnected(); } - emit appendText("\nBackend connected", UPDATE_STEP_FAIL); - emit showErrorMessage("Error", "Backend not available"); } } } + } else { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); } } - qCritical() << "In backendConnected() ERROR"; + + setProgress(100); + + emit replaceLast("Connecting backend", UPDATE_STEP_FAIL); + emit showErrorMessage("Error", "Backend not available"); m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_NOT_CONNECTED, QString("NO BACKEND CONNECTION")); @@ -355,11 +381,17 @@ bool Worker::updateTriggerSet() { // PORT STATE SERVICE // 8883/tcp open secure-mqtt + emit appendText("\nUpdate trigger set ..."); QString triggerValue(""); + startProgressLoop(); if (std::optional result = IsmasClient::sendRequestReceiveResponse( IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_ISMASPARAMETER#J={}")) { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); + QString msg = result.value(); QJsonParseError parseError; QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); @@ -368,15 +400,19 @@ bool Worker::updateTriggerSet() { << parseError.error << parseError.errorString(); emit showErrorMessage("check update trigger", QString("invalid json ") + msg.mid(0, 20)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } if (!document.isObject()) { qCritical() << "FILE IS NOT A JSON OBJECT!"; emit showErrorMessage("check update trigger", QString("not a json object") + msg); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } + setProgress(progress + 1); + QJsonObject obj = document.object(); // sanity check: cust_nr and machine_nr of PSA correct ? if (obj.contains("Dev_ID")) { @@ -398,9 +434,11 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } if (machineNr != m_machineNr) { + setProgress(100); m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, QString("MACHINE-NR (%1) != LOCAL MACHINE-NR (%2)") .arg(machineNr).arg(m_machineNr)); @@ -410,11 +448,15 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } } } } + + setProgress(progress + 1); + if (obj.contains("Fileupload")) { QJsonValue v = obj.value("Fileupload"); if (v.isObject()) { @@ -424,8 +466,7 @@ bool Worker::updateTriggerSet() { if (v.isString()) { triggerValue = v.toString(); if (triggerValue == "WAIT") { - emit appendText("\nUpdate trigger set", UPDATE_STEP_OK); - + setProgress(100); m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_SET, QString("UPDATE TRIGGER SET. CONTINUE. ")); @@ -433,6 +474,7 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateTriggerSet(m_updateStatus.m_statusDescription, "")); + emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); return true; } else { emit showErrorMessage("check update trigger", @@ -448,72 +490,111 @@ bool Worker::updateTriggerSet() { } } } else { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); emit showErrorMessage("check update trigger", "no ISMAS response"); } + setProgress(100); m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_NOT_SET_OR_WRONG, QString("UPDATE-TRIGGER-NOT-SET-OR-WRONG: VALUE=(") + triggerValue + ")"); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.errorUpdateTrigger(m_updateStatus.m_statusDescription, "")); + + emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); return false; } bool Worker::customerEnvironment() { + emit appendText("\nPrepare customer environment ..."); if (QDir(m_customerRepository).exists()) { + startProgressLoop(); if (m_gc.gitCheckoutBranch()) { - emit appendText("\nPrepare customer environment", UPDATE_STEP_DONE); + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH, QString("CHECKED-OUT BRANCH ") + m_gc.branchName()); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.checkoutBranch(m_updateStatus.m_statusDescription, "")); + + setProgress(100); + emit replaceLast("Prepare customer environment ...", UPDATE_STEP_DONE); return true; } else { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); emit showErrorMessage("cust-env", QString("Checkout ") + m_customerRepository + " failed"); } } else { emit showErrorMessage("cust-env", m_customerRepository + " does not exist"); } + + setProgress(100); + emit replaceLast("Prepare customer environment ...", UPDATE_STEP_FAIL); return false; } bool Worker::filesToUpdate() { + emit appendText("\nFetch changes files ..."); + startProgressLoop(); if (std::optional changes = m_gc.gitFetch()) { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES, QString("FETCHING OF ") + m_customerRepositoryPath + QString(" INTO ") + m_customerRepository); + setProgress(progress + 10); if (std::optional changedFileNames = m_gc.gitDiff(changes.value())) { - m_filesToUpdate = changedFileNames.value(); - int const size = m_filesToUpdate.size(); - if (size > 1) { - emit appendText(QString("\nFound %1 files to update ").arg(size), UPDATE_STEP_DONE); - } else { - emit appendText(QString("\nFound 1 file to update "), UPDATE_STEP_DONE); - } + setProgress(progress + 20); if (m_gc.gitPull()) { - emit appendText(QString("\nFetch changes files "), UPDATE_STEP_DONE); + emit replaceLast(QString("Fetch changes files ..."), UPDATE_STEP_DONE); + m_filesToUpdate = changedFileNames.value(); + int const size = m_filesToUpdate.size(); + if (size > 1) { + emit appendText(QString("Found %1 files to update ").arg(size), UPDATE_STEP_DONE); + } else { + emit appendText("Found 1 file to update ", UPDATE_STEP_DONE); + } return true; } emit showErrorMessage("files to update", "pulling files failed"); } else { emit showErrorMessage("files to update", "no files to update (checked-in any files?)"); } + setProgress(progress + 30); } else { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress + 30); + emit showErrorMessage("files to update", QString("no changes in ") + m_customerRepository + " (checked-in any files?)"); } + + emit replaceLast(QString("Fetch changes files ..."), UPDATE_STEP_FAIL); + setProgress(100); return false; } bool Worker::updateFiles(quint8 percent) { + emit appendText("\n( ) Update opkg pakets ..."); QStringList filesToDownload; m_displayIndex = 0; + + startProgressLoop(); for (int i = 0; i < m_filesToUpdate.size(); ++i) { QString fName = m_filesToUpdate.at(i); if (fName.contains("opkg_commands", Qt::CaseInsensitive)) { @@ -544,8 +625,8 @@ bool Worker::updateFiles(quint8 percent) { f.close(); if (cmdCount > 0) { m_displayIndex = 1; - emit appendText(QString("\n(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") - + QString(" Update opkg pakets "), UPDATE_STEP_DONE); + emit replaceLast(QString("(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update opkg pakets ... "), UPDATE_STEP_DONE); } } } @@ -563,12 +644,19 @@ bool Worker::updateFiles(quint8 percent) { qCritical() << "FILES_TO_WORK_ON" << filesToDownload; } - return m_update->doUpdate(m_displayIndex, filesToDownload); + bool const ret = m_update->doUpdate(m_displayIndex, filesToDownload); + stopProgressLoop(); + setProgress(100); + return ret; } bool Worker::syncCustomerRepositoryAndFS() { + setProgress(0); + emit appendText("\nSync customer environment with filesystem ..."); if (QDir(m_customerRepository).exists()) { if (QDir::setCurrent(m_customerRepository)) { + int progress = 10; + setProgress(progress); QString const params("-vv " "--recursive " "--progress " @@ -585,6 +673,8 @@ bool Worker::syncCustomerRepositoryAndFS() { QString cmd; bool error = false; foreach (cmd, cmds) { + progress += 5; + setProgress(progress); if (!error) { Command c("bash"); qInfo() << "EXECUTING CMD..." << cmd; @@ -601,13 +691,17 @@ bool Worker::syncCustomerRepositoryAndFS() { } } } + progress += 5; + setProgress(progress); if (!error) { - emit appendText(QString("\nSync customer environment with filesystem "), - UPDATE_STEP_DONE); + setProgress(100); + emit replaceLast(QString("Sync customer environment with filesystem ..."), UPDATE_STEP_DONE); return true; } } } + setProgress(100); + emit replaceLast(QString("Sync customer environment with filesystem ..."), UPDATE_STEP_FAIL); return false; } @@ -615,7 +709,7 @@ bool Worker::sendIsmasLastVersionNotification() { IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_SENDVERSION#J=") + m_ismasClient.updateOfPSASendVersion(getPSAInstalled())); - emit appendText(QString("\nSend last version info "), UPDATE_STEP_DONE); + emit appendText(QString("Send last version info "), UPDATE_STEP_DONE); return true; } From cdb045b72bf991fc41e3b53473cea85311207f46 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 13:59:44 +0200 Subject: [PATCH 117/239] Fixed onAppendText() and onReplaceLast(): Watch out for special suffixes. --- mainwindow.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 7791fcb..2e1943b 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -97,7 +97,7 @@ void MainWindow::customEvent(QEvent *event) { m_progressValue = progress; ui->updateProgress->setValue(progress/10); QApplication::postEvent(this, new ProgressEvent(this, progress+10)); - QThread::msleep(100); + QThread::msleep(150); } } } @@ -147,9 +147,12 @@ void MainWindow::onQuit() { void MainWindow::onAppendText(QString text, QString suffix) { QString editText = ui->updateStatus->toPlainText(); - if (suffix.size() > 0) { - editText += QString("\n").leftJustified(m_width-3, '='); - editText += " "; + if (!suffix.isNull() && suffix.size() > 0) { + qInfo() << "TEXT" << text << "SUFFIX" << suffix; + if (suffix == Worker::UPDATE_STEP_SUCCESS || suffix == Worker::UPDATE_STEP_FAIL) { + editText += QString("\n").leftJustified(m_width-3, '='); + editText += " "; + } editText += (QString("\n") + text).leftJustified(m_width - (2 + suffix.size()) ) + suffix; } else { editText += text.leftJustified(m_width-9); From 329c770aa0b1cfeb27c9cf79427a7cd55ea97baa Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 7 Aug 2023 14:01:51 +0200 Subject: [PATCH 118/239] Use a message box with color red and a timer to click on ok after 5 secs. --- mainwindow.cpp | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 2e1943b..e6b4e79 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -160,29 +160,51 @@ void MainWindow::onAppendText(QString text, QString suffix) { QStringList lines = editText.split('\n'); for (int i=0; iupdateStatus->setPlainText(editText); + ui->updateStatus->setPlainText(editText.trimmed()); ui->updateStatus->setEnabled(true); } void MainWindow::onReplaceLast(QString text, QString suffix) { + qInfo() << "REPL TEXT" << text << "SUFFIX" << suffix; + QString editText = ui->updateStatus->toPlainText(); QStringList lines = editText.split('\n'); if (lines.size() > 0) { lines.removeLast(); - lines += text.leftJustified(m_width-10) + suffix; + if (!suffix.isNull() && suffix.size() > 0 && suffix != "\n") { + lines += text.leftJustified(m_width-10) + suffix; + } else { + lines += text.leftJustified(m_width-10); + } } for (int i=0; iupdateStatus->setText(lines.join('\n')); + ui->updateStatus->setText(lines.join('\n').trimmed()); ui->updateStatus->setEnabled(true); } - void MainWindow::onShowErrorMessage(QString title, QString text) { - QMessageBox::critical(this, title, text, QMessageBox::Ok); + text = text.leftJustified(50, ' '); + QMessageBox msgBox(QMessageBox::NoIcon, title, + text, QMessageBox::Ok, + nullptr, Qt::FramelessWindowHint); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.setStyleSheet("QDialog {background-color: red;}" + "QPushButton {background-color: blue;}"); + QTimer *t = new QTimer(this); + connect(t, SIGNAL(timeout()), msgBox.defaultButton(), SLOT(click())); + t->setSingleShot(true); + t->start(5 * 1000); + + if(msgBox.exec() == QMessageBox::Ok) { + // do something + } else { + // do something else + } + disconnect(t, SIGNAL(timeout()), msgBox.defaultButton(), SLOT(click())); } From c2b52aa91d11a269a9b11bd95f65d48d8cdfdc09 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 15:01:43 +0200 Subject: [PATCH 119/239] Only check for keyword RECORD to be more general. --- ismas/ismas_client.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 998d4b3..2a8b19a 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -224,8 +224,8 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { if (int idx = response.indexOf("{\"error\":\"ISMAS is offline\"}")) { response = response.mid(0, idx); } else - if (response == "RECORD SAVED") { - printInfoMessage(port, clientIP, clientPort, "IGNORED 'RECORD SAVED' RESPONSE"); + if (response.contains("RECORD")) { // RECORD SAVED or RECORD WRITE ABORTED + printInfoMessage(port, clientIP, clientPort, QString("IGNORED '") + response + "' RESPONSE"); ::close(sockfd); return std::nullopt; } From 89c2d3a8ae344b9aa01cc4632106457229951826 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 15:03:14 +0200 Subject: [PATCH 120/239] Added some debug output. --- git/git_client.cpp | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index a45e9db..e428056 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -48,8 +48,9 @@ bool GitClient::gitCloneCustomerRepository() { } } } - qCritical() << "ERROR CLONE RESULT HAS WRONG FORMAT. CLONE_RESULT=" - << result; + qCritical() << QString(80, '*'); + qCritical() << "ERROR CLONE RESULT HAS WRONG FORMAT. CLONE_RESULT=" << result; + qCritical() << QString(80, '*'); } return false; } @@ -96,7 +97,9 @@ bool GitClient::gitCheckoutBranch() { Command c(gitCommand); return c.execute(m_customerRepository); // execute command in customerRepo } + qCritical() << QString(80, '*'); qCritical() << "ERROR" << m_customerRepository << "DOES NOT EXIST"; + qCritical() << QString(80, '*'); return false; } @@ -108,12 +111,12 @@ bool GitClient::gitCloneAndCheckoutBranch() { return true; } else { // TODO - // m_worker->terminateUpdateProcess(); } //} } else { - // TODO - //m_worker->terminateUpdateProcess(); + qCritical() << QString(80, '*'); + qInfo() << "CLONE" << m_repositoryPath << "AND CHECKOUT FAILED"; + qCritical() << QString(80, '*'); } return false; } @@ -164,8 +167,9 @@ std::optional GitClient::gitDiff(QString const &commits) { */ std::optional GitClient::gitFetch() { if (QDir(m_customerRepository).exists()) { - - qCritical() << "BRANCH NAME" << m_branchName; + qCritical() << QString(80, '*'); + qInfo() << "BRANCH NAME" << m_branchName; + qCritical() << QString(80, '*'); Command c("git fetch"); if (c.execute(m_customerRepository)) { @@ -198,26 +202,39 @@ std::optional GitClient::gitFetch() { } } else { emit m_worker->showErrorMessage("git fetch", - "regex-match for commits"); + "no regex-match for commits"); + qCritical() << QString(80, '*'); + qCritical() << "NO REGEX MATCH FOR COMMITS"; + qCritical() << QString(80, '*'); } } } if (!found) { emit m_worker->showErrorMessage("git fetch", QString("unkown branch name ") + m_branchName); + qCritical() << QString(80, '*'); + qCritical() << "UNKNOWN BRANCH NAME" << m_branchName; + qCritical() << QString(80, '*'); } } else { emit m_worker->showErrorMessage("git fetch", QString("wrong format for result of 'git fetch' ") + s); + qCritical() << QString(80, '*'); + qCritical() << "WRONG FORMAT FOR RESULT OF 'GIT FETCH'" << s; + qCritical() << QString(80, '*'); } } else { - emit m_worker->showErrorMessage("git fetch", - "empty result for 'git fetch'"); + emit m_worker->showErrorMessage("git fetch", "empty result for 'git fetch'"); + qCritical() << QString(80, '*'); + qCritical() << "EMPTY RESULT FOR 'GIT FETCH'"; + qCritical() << QString(80, '*'); } } } else { - emit m_worker->showErrorMessage("git fetch", - QString("repository ") + m_customerRepository + " does not exist"); + emit m_worker->showErrorMessage("git fetch", QString("repository ") + m_customerRepository + " does not exist"); + qCritical() << QString(80, '*'); + qCritical() << "REPOSITORY" << m_customerRepository << "DOES NOT EXIST"; + qCritical() << QString(80, '*'); } return std::nullopt; } @@ -238,7 +255,9 @@ bool GitClient::gitPull() { qInfo() << "PULLED INTO" << m_customerRepository; return true; } + qCritical() << QString(80, '*'); qCritical() << "PULL INTO" << m_customerRepository << "FAILED"; + qCritical() << QString(80, '*'); } return false; } From 7c7adc94e668d3df60c7814c47d71c29d52a879e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 15:05:04 +0200 Subject: [PATCH 121/239] Removed Start-button, because it is not needed. Removed buttons reserved for future use. --- mainwindow.ui | 90 ++++++--------------------------------------------- 1 file changed, 9 insertions(+), 81 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index 5f269f8..670d881 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -22,96 +22,31 @@ - 11 - 11 + 10 + 10 781 461 - - - - - Terminus - - - - Start - - - - + Exit - - - - false - - - + + + + 1 - - - - false - - - - - - - - - - false - - - - - - - - - - false - - - - - - - - - - false - - - - - - - - - - false - - - - - - - + - false + true @@ -122,13 +57,6 @@ - - - - 1 - - - From 5db7b4224e9fcaa8fa0017bc20379af9981b2672 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 15:06:20 +0200 Subject: [PATCH 122/239] Made start/stopProgress() public. --- worker.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worker.h b/worker.h index 2a60b0c..1542539 100644 --- a/worker.h +++ b/worker.h @@ -127,6 +127,7 @@ class Worker : public QObject { MainWindow *m_mainWindow; int m_progressValue; + bool m_withoutIsmasDirectPort; bool executeOpkgCommand(QString opkgCommand); QString getOsVersion() const; @@ -159,6 +160,8 @@ public: void setMainWindow(MainWindow *mainWindow) { m_mainWindow = mainWindow; } void setProgress(int progress); + void startProgressLoop(); + void stopProgressLoop(); IsmasClient &getIsmasClient() { return m_ismasClient; } IsmasClient const &getIsmasClient() const { return m_ismasClient; } @@ -205,8 +208,6 @@ private: PSAInstalled getPSAInstalled(); QString sendCmdSendVersionToIsmas(); void privateUpdate(); - void startProgressLoop(); - void stopProgressLoop(); }; #endif // WORKER_H_INCLUDED From 5f1376cf1ecd59246bf45ca2491a51107d4d58d6 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 15:08:22 +0200 Subject: [PATCH 123/239] Removed buttons reserved for future use. Start application automatically, not via Start-Button. --- mainwindow.cpp | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index e6b4e79..5b49d02 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -32,15 +32,9 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) ui->updateStatus->setText(lst.join('\n')); ui->updateStatus->setEnabled(true); - ui->reserved_1->setVisible(false); - ui->reserved_2->setVisible(false); - ui->reserved_3->setVisible(false); - ui->reserved_4->setVisible(false); - ui->reserved_5->setVisible(false); - ui->reserved_6->setVisible(false); - m_startTimer = new QTimer(this); - connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); + // connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); + connect(m_startTimer, SIGNAL(timeout()), m_worker, SLOT(update())); m_startTimer->setSingleShot(true); m_startTimer->start(5 * 1000); @@ -49,9 +43,6 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) m_exitTimer->setSingleShot(true); m_exitTimer->start(1800 * 1000); - connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); - connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); - connect(ui->start, SIGNAL(clicked()), m_worker, SLOT(update())); connect(ui->exit, SIGNAL(clicked()), this, SLOT(onQuit())); connect(m_worker, SIGNAL(disableExit()), this, SLOT(onDisableExit())); connect(m_worker, SIGNAL(enableExit()), this, SLOT(onEnableExit())); @@ -97,7 +88,7 @@ void MainWindow::customEvent(QEvent *event) { m_progressValue = progress; ui->updateProgress->setValue(progress/10); QApplication::postEvent(this, new ProgressEvent(this, progress+10)); - QThread::msleep(150); + QThread::msleep(100); } } } @@ -136,7 +127,7 @@ void MainWindow::onEnableExit() { void MainWindow::onRestartExitTimer() { m_exitTimer->stop(); - m_exitTimer->start(10 * 1000); + m_exitTimer->start(60 * 1000); } void MainWindow::onQuit() { @@ -158,7 +149,7 @@ void MainWindow::onAppendText(QString text, QString suffix) { editText += text.leftJustified(m_width-9); } - QStringList lines = editText.split('\n'); + QStringList const lines = editText.split('\n'); for (int i=0; i Date: Wed, 9 Aug 2023 15:09:44 +0200 Subject: [PATCH 124/239] Work with device controller file directly, not via link. Add startProgress/stopProgress(). --- update.cpp | 73 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/update.cpp b/update.cpp index 15b002f..8b20404 100644 --- a/update.cpp +++ b/update.cpp @@ -8,6 +8,7 @@ #include #include #include +#include //#include //#include @@ -350,21 +351,25 @@ bool Update::downloadBinaryToDC(QString const &bFile) const { 10 : bl_stopBL() // leave bl and start (the new) application */ bool Update::updateBinary(char const *fileToSendToDC) { - qInfo() << "updating device controller binary" << fileToSendToDC; + qInfo() << "UPDATING DEVICE CONTROLLER BINARY" << fileToSendToDC; QFile fn(fileToSendToDC); bool r; if ((r = fn.exists()) == true) { - QString const linkTarget = fn.symLinkTarget(); - QFileInfo fi(linkTarget); - qInfo() << " updating binary (size=" << linkTarget << fi.size() << ")"; - if ((r = updateDC(linkTarget)) == true) { - qInfo() << " updating binary (size=" << linkTarget << fi.size() << ") done"; + QFileInfo fi(fn); + qInfo() << " UPDATING BINARY" << fi.fileName() << "(size=" << fi.size() << ")"; + if ((r = updateDC(fileToSendToDC)) == true) { + qCritical() << QString(80, '*'); + qInfo() << " UPDATING BINARY" << fi.fileName() << "(size=" << fi.size() << ") DONE"; + qCritical() << QString(80, '*'); } else { - qCritical() << "updating binary (size=" << linkTarget << fi.size() << ")... FAILED"; + qCritical() << QString(80, '*'); + qCritical() << " UPDATING BINARY" << fi.fileName() << "(size=" << fi.size() << ") FAILED"; + qCritical() << QString(80, '*'); } } else { - qCritical() << "symlink" << fileToSendToDC - << "does not exist -> NO UPDATE OF DC FIRMWARE"; + qCritical() << QString(80, '*'); + qCritical() << fileToSendToDC << "does not exist -> NO UPDATE OF DC FIRMWARE"; + qCritical() << QString(80, '*'); } return r; } @@ -514,7 +519,6 @@ void Update::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { } bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { - // // ACHTUNG !!! // @@ -523,11 +527,6 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { bool serialOpened = false; bool serialOpen = false; - if (filesToWorkOn.size() == 0) { - qCritical() << "NOTHING TO UPDATE"; - return true; - } - if (!serialOpen) { if (!isSerialOpen()) { // open serial only if not already open if ((serialOpened = openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) == false) { @@ -537,18 +536,23 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { } } serialOpen = true; - qCritical() << "SERIAL OPEN" << m_serialInterface << "(BAUDRATE=" << m_baudrate << ")"; + qInfo() << "SERIAL OPEN" << m_serialInterface << "(BAUDRATE=" << m_baudrate << ")"; } + bool res = false; QList::const_iterator it; for (it = filesToWorkOn.cbegin(); it != filesToWorkOn.cend(); ++it) { - bool res = false; + m_worker->startProgressLoop(); QString fToWorkOn = (*it).trimmed(); + fToWorkOn = QDir::cleanPath(m_customerRepository + QDir::separator() + fToWorkOn); - if (fToWorkOn.contains("dc2c", Qt::CaseInsensitive) && - fToWorkOn.endsWith(".bin", Qt::CaseInsensitive)) { + static const QRegularExpression version("^.*dc2c[.][0-9][0-9][.][0-9][0-9][.]bin.*$"); + if (fToWorkOn.contains(version)) { + + qInfo() << QString(80, '*'); + qInfo() << "DO-UPDATE FILE-TO-WORK-ON" << fToWorkOn; + qInfo() << QString(80, '*'); - qDebug() << "sending sw/hw-requests..."; for (int i=0; i < 3; ++i) { // send explicit reuests to get // current SW/HW-versions m_hw->request_DC2_SWversion(); @@ -558,12 +562,16 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { QString const hwVersion = m_hw->dc_getHWversion().toLower(); QString const fwVersion = m_hw->dc_getSWversion().toLower(); + qInfo() << "current dc-hardware-version" << hwVersion; qInfo() << "current dc-firmware-version" << fwVersion; QFile fn(fToWorkOn); - QFileInfo linkTarget(fn.symLinkTarget()); - if (!linkTarget.exists()) { // check for broken link + QFileInfo finfo(fn); + if (!fn.exists()) { // check for broken link + qCritical() << QString(80, '*'); + qCritical() << "FILE-TO-WORK-ON" << fn << "DOES NOT EXIST"; + qCritical() << QString(80, '*'); res = false; } else { if (false) { @@ -574,13 +582,12 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { } else { res = true; - qCritical() << "downloading" << fToWorkOn.trimmed() << "->" - << linkTarget.completeBaseName() << "to DC"; + qInfo() << "DOWNLOADING" << finfo.completeBaseName() << "TO DC"; #if UPDATE_DC == 1 m_hw->dc_autoRequest(false);// default: turn auto-request setting off QThread::sleep(1); // wait to be sure that there are no more // commands sent to dc-hardware - qDebug() << "SET AUTO-REQUEST=FALSE"; + qInfo() << "SET AUTO-REQUEST=FALSE"; if ((res = updateBinary(fToWorkOn.toStdString().c_str())) == true) { qCritical() << "downloaded binary" << fToWorkOn; @@ -591,13 +598,13 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { } m_hw->dc_autoRequest(true); // turn auto-request setting on - qDebug() << "SET AUTO-REQUEST=TRUE"; - qDebug() << "WAIT 10 SECS TO RECEIVE RESPONSES..."; + qInfo() << "SET AUTO-REQUEST=TRUE"; + qInfo() << "WAIT 10 SECS TO RECEIVE RESPONSES..."; QThread::sleep(10); // wait to be sure that responses // have been received - qCritical() << "updated dc-hardware-version" << m_hw->dc_getHWversion(); - qCritical() << "updated dc-firmware-version" << m_hw->dc_getSWversion(); + qInfo() << "updated dc-hardware-version" << m_hw->dc_getHWversion(); + qInfo() << "updated dc-firmware-version" << m_hw->dc_getSWversion(); #endif } } @@ -660,10 +667,16 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { qCritical() << "UNKNOWN JSON FILE NAME" << fToWorkOn; res = false; } + m_worker->stopProgressLoop(); + m_worker->setProgress(100); + + if (res == false) { + break; + } } // for (it = openLines.cbegin(); it != openLines.end(); ++it) { m_hw->dc_autoRequest(true); // ALWAYS turn autoRequest ON qDebug() << "SET AUTO-REQUEST=TRUE"; - return true; + return res; } From 72cb738af5d0105856b5fe01f01765eb9a707ce1 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 16:12:56 +0200 Subject: [PATCH 125/239] Removed handling of pipe symbol (if available in git output). --- git/git_client.cpp | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index e428056..d28c798 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -135,23 +135,41 @@ std::optional GitClient::gitDiff(QString const &commits) { if (c.execute(m_customerRepository)) { // execute command in local customerRepo QString s = c.getCommandResult().trimmed(); QStringList lines = Update::split(s, '\n'); + + qInfo() << QString(80, '*'); + qInfo() << "GIT DIFF RESULT" << lines; + qInfo() << QString(80, '*'); + QStringList fileNames; // each line has the format "etc/psa_config/DC2C_print01.json | 1 + // or the format "etc/psa_config/DC2C_print01.json (new) | 1 + // the filenames are relativ to the repository for (int i = 0; i < lines.size(); ++i) { - // TODO: koennte auch (delete) kommen ? - int newIndex = lines.at(i).indexOf("(new)"); // for new files - // int goneIndex = lines.at(i).indexOf("(gone)"); // for removed files + QString const &line = lines.at(i); + int newIndex = line.indexOf("(new"); // for new files + int goneIndex = line.indexOf("(gone"); // for removed files + int modeIndex = line.indexOf("(mode"); + // int pipeIndex = line.indexOf('|'); if (newIndex != -1) { - QString fileName = lines.at(i).mid(0, newIndex).trimmed(); - fileNames << fileName; - } else { - int pipeIndex = lines.at(i).indexOf('|'); - if (pipeIndex != -1) { - QString fileName = lines.at(i).mid(0, pipeIndex).trimmed(); - fileNames << fileName; - } + QString file = line.left(newIndex).trimmed(); + qInfo() << "FILE (NEW)" << file; + fileNames << file; + } else + if (modeIndex != -1) { + QString const file = line.left(modeIndex).trimmed(); + qInfo() << "FILE (MODE)" << file; + fileNames << file; + } else + //if (pipeIndex != -1) { + // QString const file = line.left(pipeIndex).trimmed(); + // qInfo() << "FILE (PIPE)" << file; + // fileNames << file; + //} else + if (goneIndex != -1) { + QString const file = line.left(goneIndex).trimmed(); + qCritical() << QString(80, '*'); + qCritical() << "FILE (GONE)" << file; + qCritical() << QString(80, '*'); } } if (!fileNames.isEmpty()) { From 927197d0d1353b138fd9b23a8932afb6f65a1d18 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 16:14:59 +0200 Subject: [PATCH 126/239] Removed restart of APISM. --- worker.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/worker.cpp b/worker.cpp index f307e0d..aa73545 100644 --- a/worker.cpp +++ b/worker.cpp @@ -69,7 +69,8 @@ Worker::Worker(hwinf *hw, , m_filesToUpdate() , m_updateProcessRunning(false) , m_returnCode(0) - , m_progressValue{0} { + , m_progressValue(0) + , m_withoutIsmasDirectPort(false) /* useful for testing */ { QDir::setCurrent(m_workingDirectory); @@ -137,15 +138,24 @@ void Worker::update() { } void Worker::privateUpdate() { - QPushButton *start = qobject_cast(QObject::sender()); - start->setEnabled(false); - - emit stopStartTimer(); - emit disableExit(); m_updateProcessRunning = true; bool sentIsmasLastVersionNotification = false; + //emit appendText("\nRestart APISM ..."); + //startProgressLoop(); + //Command c("systemctl restart apism"); + //if (c.execute("/tmp")) { + // QThread::sleep(10); // give APISM some time to reconnect + // stopProgressLoop(); + // emit replaceLast("Restart APISM ...", UPDATE_STEP_DONE); + //} else { + // stopProgressLoop(); + // emit replaceLast("Restart APISM ...", UPDATE_STEP_FAIL); + //} + + emit disableExit(); + m_returnCode = -1; QDir customerRepository(m_customerRepository); if (!customerRepository.exists()) { From e523d3cc2cb9f63d1cbc4b24d40843e2734f1002 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 16:16:36 +0200 Subject: [PATCH 127/239] Added/chenged some debug output. --- worker.cpp | 107 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 29 deletions(-) diff --git a/worker.cpp b/worker.cpp index aa73545..64faaad 100644 --- a/worker.cpp +++ b/worker.cpp @@ -214,30 +214,30 @@ void Worker::privateUpdate() { setProgress(100); m_ismasClient.setProgressInPercent(10); - m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH, - QString("CHECKED OUT BRANCH: ") + m_gc.branchName()); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.checkoutBranch( - m_updateStatus.m_statusDescription, "")); + //m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH, + // QString("CHECKED OUT BRANCH: ") + m_gc.branchName()); + //IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + // QString("#M=APISM#C=CMD_EVENT#J=") + + // m_ismasClient.checkoutBranch( + // m_updateStatus.m_statusDescription, "")); - qCritical() << "CHECKED OUT BRANCH"; - if (backendConnected()) { qCritical() << "BACKEND CONNECTED"; + qInfo() << "CHECKED OUT BRANCH"; + if (backendConnected()) { m_ismasClient.setProgressInPercent(20); - if (updateTriggerSet()) { qCritical() << "UPDATE TRIGGER SET"; + if (updateTriggerSet()) { m_ismasClient.setProgressInPercent(30); - if (customerEnvironment()) { qCritical() << "CUSTOMER ENVIRONMENT"; + if (customerEnvironment()) { m_ismasClient.setProgressInPercent(40); - if (filesToUpdate()) { qCritical() << "FILES TO UPDATE"; + if (filesToUpdate()) { m_ismasClient.setProgressInPercent(50); - if (updateFiles(50)) { qCritical() << "UPDATE FILES"; + if (updateFiles(50)) { m_ismasClient.setProgressInPercent(60); - if (syncCustomerRepositoryAndFS()) { qCritical() << "SYNC REPOSITORY"; + if (syncCustomerRepositoryAndFS()) { m_ismasClient.setProgressInPercent(70); - if (sendIsmasLastVersionNotification()) { qCritical() << "SEND LAST NOTIFICATION"; + if (sendIsmasLastVersionNotification()) { m_ismasClient.setProgressInPercent(80); sentIsmasLastVersionNotification = true; - if (saveLogFile()) { qCritical() << "SAVE LOG FILE"; + if (saveLogFile()) { m_ismasClient.setProgressInPercent(90); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, @@ -345,7 +345,8 @@ bool Worker::backendConnected() { obj = v.toObject(); bool ismas = obj.value("ISMAS").toBool(); QString status = obj.value("Broker").toString(); - qInfo() << "In backendConnected() STATUS" << status; + qInfo() << "REPEAT" << repeat << "In backendConnected() Broker=<" + << status << ">, ISMAS=<" << (ismas ? "true>" : "false>"); if (ismas) { if (status == "Connected") { // do not send, as this would result in a corrupted wait button @@ -536,6 +537,7 @@ bool Worker::customerEnvironment() { setProgress(100); emit replaceLast("Prepare customer environment ...", UPDATE_STEP_DONE); + qInfo() << "PREPARE CUSTOMER ENVIRONMENT DONE"; return true; } else { stopProgressLoop(); @@ -543,9 +545,15 @@ bool Worker::customerEnvironment() { setProgress(progress); emit showErrorMessage("cust-env", QString("Checkout ") + m_customerRepository + " failed"); + qCritical() << QString(80, '*'); + qCritical() << "CHECKOUT OF" << m_customerRepository << "FAILED"; + qCritical() << QString(80, '*'); } } else { emit showErrorMessage("cust-env", m_customerRepository + " does not exist"); + qCritical() << QString(80, '*'); + qCritical() << m_customerRepository << "DOES NOT EXIST"; + qCritical() << QString(80, '*'); } setProgress(100); @@ -571,17 +579,35 @@ bool Worker::filesToUpdate() { if (m_gc.gitPull()) { emit replaceLast(QString("Fetch changes files ..."), UPDATE_STEP_DONE); m_filesToUpdate = changedFileNames.value(); + + qInfo() << QString(80, '*'); + qInfo() << "FILES-TO-UPDATE" << m_filesToUpdate; + qInfo() << QString(80, '*'); + int const size = m_filesToUpdate.size(); if (size > 1) { - emit appendText(QString("Found %1 files to update ").arg(size), UPDATE_STEP_DONE); + emit appendText(QString("Found %1 files to update :").arg(size), UPDATE_STEP_DONE); + for (int i = 0; i < size; ++i) { + emit appendText(QString("\n ") + m_filesToUpdate.at(i)); + } } else { - emit appendText("Found 1 file to update ", UPDATE_STEP_DONE); + emit appendText("Found 1 file to update :", UPDATE_STEP_DONE); + emit appendText(QString("\n ") + m_filesToUpdate.at(0)); } return true; } emit showErrorMessage("files to update", "pulling files failed"); + + qCritical() << QString(80, '*'); + qCritical() << "PULLING FILES FAILED"; + qCritical() << QString(80, '*'); + } else { emit showErrorMessage("files to update", "no files to update (checked-in any files?)"); + + qCritical() << QString(80, '*'); + qCritical() << "NO FILES TO UPDATE (CHECKED IN ANY FILES?)"; + qCritical() << QString(80, '*'); } setProgress(progress + 30); } else { @@ -592,6 +618,10 @@ bool Worker::filesToUpdate() { emit showErrorMessage("files to update", QString("no changes in ") + m_customerRepository + " (checked-in any files?)"); + + qCritical() << QString(80, '*'); + qCritical() << "NO CHANGES IN" << m_customerRepository << "(CHECKED IN ANY FILES?)"; + qCritical() << QString(80, '*'); } emit replaceLast(QString("Fetch changes files ..."), UPDATE_STEP_FAIL); @@ -600,14 +630,19 @@ bool Worker::filesToUpdate() { } bool Worker::updateFiles(quint8 percent) { - emit appendText("\n( ) Update opkg pakets ..."); QStringList filesToDownload; m_displayIndex = 0; startProgressLoop(); for (int i = 0; i < m_filesToUpdate.size(); ++i) { QString fName = m_filesToUpdate.at(i); + + qInfo() << QString(80, '*'); + qInfo() << "FNAME" << fName; + qInfo() << QString(80, '*'); + if (fName.contains("opkg_commands", Qt::CaseInsensitive)) { + emit appendText("\n( ) Update opkg pakets ..."); // execute opkg commands if (QDir::setCurrent(m_customerRepository)) { QFile f(fName); @@ -644,19 +679,29 @@ bool Worker::updateFiles(quint8 percent) { } else if (fName.contains("print", Qt::CaseInsensitive)) { filesToDownload << fName; // download printer-config-files - } else - if (fName == "dc2c.bin") { - filesToDownload << fName; // download device controller + } else { + static const QRegularExpression version("^.*dc2c[.][0-9][0-9][.][0-9][0-9][.]bin.*$"); + if (fName.contains(version)) { + filesToDownload << fName; // download device controller + } } } - if (filesToDownload.size() > 0) { - qCritical() << "FILES_TO_WORK_ON" << filesToDownload; - } - - bool const ret = m_update->doUpdate(m_displayIndex, filesToDownload); stopProgressLoop(); setProgress(100); + bool ret = true; + + if (filesToDownload.size() > 0) { + qInfo() << QString(80, '*'); + qInfo() << "FILES_TO_WORK_ON" << filesToDownload; + qInfo() << QString(80, '*'); + ret = m_update->doUpdate(m_displayIndex, filesToDownload); + } else { + qInfo() << QString(80, '*'); + qInfo() << "NO FILES_TO_WORK_ON"; + qInfo() << QString(80, '*'); + } + return ret; } @@ -691,12 +736,14 @@ bool Worker::syncCustomerRepositoryAndFS() { if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { QStringList result = c.getCommandResult().split('\n'); for (int i = 0; i < result.size(); ++i) { - qCritical() << result.at(i); + qInfo() << result.at(i); } - qCritical() << "SUCCESS"; + qInfo() << "SUCCESS"; } else { + qCritical() << QString(80, '*'); qCritical() << "CMD" << cmd << "FAILED"; qCritical() << c.getCommandResult().split('\n'); + qCritical() << QString(80, '*'); error = true; } } @@ -818,9 +865,11 @@ bool Worker::executeOpkgCommand(QString opkgCommand) { .arg(c.getCommandResult())); return true; } else { + qCritical() << QString(80, '*'); qCritical() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE, QString("EXECUTE OPKG COMMAND %1 FAILED") .arg(opkgCommand)); + qCritical() << QString(80, '*'); } return false; } From 042e6dfa38ff8bfd8c153d3c3dbbfc7311430b97 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 9 Aug 2023 16:17:28 +0200 Subject: [PATCH 128/239] Try to establish a connection to backend 50x. --- worker.cpp | 248 +++++++++++++++++++++++++++++------------------------ 1 file changed, 136 insertions(+), 112 deletions(-) diff --git a/worker.cpp b/worker.cpp index 64faaad..d28716e 100644 --- a/worker.cpp +++ b/worker.cpp @@ -301,14 +301,14 @@ void Worker::privateUpdate() { } bool Worker::backendConnected() { - static int repeat = 0; - - if (repeat == 0) { - emit appendText("\nConnecting backend ..."); + if (m_withoutIsmasDirectPort) { // useful for testing + return true; } - if (repeat < 3) { - qInfo() << repeat << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; + emit appendText("\nConnecting backend ..."); + + for (int repeat = 0; repeat < 50; ++repeat) { + qInfo() << "REPEAT" << repeat << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; startProgressLoop(); std::optional result = IsmasClient::sendRequestReceiveResponse( @@ -334,7 +334,7 @@ bool Worker::backendConnected() { return false; } - setProgress(progress + 10); + setProgress(progress + 1); QJsonObject obj = document.object(); QStringList keys = obj.keys(); @@ -356,11 +356,11 @@ bool Worker::backendConnected() { return true; } } - if (status.startsWith("Connecting") || status.startsWith("Re-Connecting")) { - QThread::sleep(1); - ++repeat; - return backendConnected(); - } + emit showErrorMessage("Check backend connection", + QString ("REPEAT %1 Broker=<").arg(repeat) + + status + ">, ISMAS=<" + (ismas ? "true>" : "false>")); + QThread::sleep(6); + continue; } } } @@ -386,125 +386,145 @@ bool Worker::backendConnected() { } bool Worker::updateTriggerSet() { -// nmap -Pn 62.141.45.68 -p 8883 -// Host is up (0.053s latency). -// -// PORT STATE SERVICE -// 8883/tcp open secure-mqtt + if (m_withoutIsmasDirectPort) { // useful for testing + return true; + } emit appendText("\nUpdate trigger set ..."); QString triggerValue(""); - startProgressLoop(); - if (std::optional result - = IsmasClient::sendRequestReceiveResponse( - IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_ISMASPARAMETER#J={}")) { - stopProgressLoop(); - int progress = (m_mainWindow->progressValue()/10) + 10; - setProgress(progress); + for (int repeat = 0; repeat < 50; ++repeat) { + startProgressLoop(); + if (std::optional result + = IsmasClient::sendRequestReceiveResponse( + IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_ISMASPARAMETER#J={}")) { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); - QString msg = result.value(); - QJsonParseError parseError; - QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); - if (parseError.error != QJsonParseError::NoError) { - qCritical() << "(2) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" - << parseError.error << parseError.errorString(); - emit showErrorMessage("check update trigger", - QString("invalid json ") + msg.mid(0, 20)); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); - return false; - } - if (!document.isObject()) { - qCritical() << "FILE IS NOT A JSON OBJECT!"; - emit showErrorMessage("check update trigger", - QString("not a json object") + msg); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); - return false; - } + QString msg = result.value(); - setProgress(progress + 1); + qInfo() << "REPEAT" << repeat << "APISM RESPONSE" << msg; - QJsonObject obj = document.object(); - // sanity check: cust_nr and machine_nr of PSA correct ? - if (obj.contains("Dev_ID")) { - QJsonValue v = obj.value("Dev_ID"); - if (v.isObject()) { - QJsonObject obj = v.toObject(); - if (obj.contains("Custom_ID") && obj.contains("Device_ID")) { - QJsonValue const c = obj.value("Custom_ID"); - QJsonValue const m = obj.value("Device_ID"); - int customerNr = c.toInt(-1); - int machineNr = m.toInt(-1); - if (customerNr != m_customerNr) { - m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, - QString("CUSTOMER-NR (%1) != LOCAL CUSTOMER-NR (%2)") - .arg(customerNr).arg(m_customerNr)); - emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - m_updateStatus.m_statusDescription)); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); - return false; - } - if (machineNr != m_machineNr) { - setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, - QString("MACHINE-NR (%1) != LOCAL MACHINE-NR (%2)") - .arg(machineNr).arg(m_machineNr)); - emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - m_updateStatus.m_statusDescription)); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); - return false; - } - } + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "(2) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" + << parseError.error << parseError.errorString(); + emit showErrorMessage("check update trigger", + QString("invalid json ") + msg.mid(0, 20)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; + } + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + emit showErrorMessage("check update trigger", + QString("not a json object") + msg); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; } - } - setProgress(progress + 1); + setProgress(progress + 1); - if (obj.contains("Fileupload")) { - QJsonValue v = obj.value("Fileupload"); - if (v.isObject()) { - obj = v.toObject(); - if (obj.contains("TRG")) { - v = obj.value("TRG"); - if (v.isString()) { - triggerValue = v.toString(); - if (triggerValue == "WAIT") { - setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_SET, - QString("UPDATE TRIGGER SET. CONTINUE. ")); + QJsonObject obj = document.object(); + // sanity check: cust_nr and machine_nr of PSA correct ? + // note: this check has to be done here, as the cust_nr and the machine_nr + // of the PSA are sent by ISMAS. + if (obj.contains("Dev_ID")) { + QJsonValue v = obj.value("Dev_ID"); + if (v.isObject()) { + QJsonObject obj = v.toObject(); + if (obj.contains("Custom_ID") && obj.contains("Device_ID")) { + QJsonValue const c = obj.value("Custom_ID"); + QJsonValue const m = obj.value("Device_ID"); + int customerNr = c.toInt(-1); + int machineNr = m.toInt(-1); + if (customerNr != m_customerNr) { + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + QString("CUSTOMER-NR (%1) != LOCAL CUSTOMER-NR (%2)") + .arg(customerNr).arg(m_customerNr)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateTriggerSet(m_updateStatus.m_statusDescription, "")); + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; + } + if (machineNr != m_machineNr) { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + QString("MACHINE-NR (%1) != LOCAL MACHINE-NR (%2)") + .arg(machineNr).arg(m_machineNr)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); - return true; - } else { - emit showErrorMessage("check update trigger", - QString ("TRG key=<") + triggerValue - + ">\n(reset download button?)"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; } } - } else { - emit showErrorMessage("check update trigger", "TRG key not contained"); } - } else { - emit showErrorMessage("check update trigger", "Fileupload not a json-object"); } + + setProgress(progress + 1); + + if (obj.contains("Fileupload")) { + QJsonValue v = obj.value("Fileupload"); + if (v.isObject()) { + obj = v.toObject(); + if (obj.contains("TRG")) { + v = obj.value("TRG"); + if (v.isString()) { + triggerValue = v.toString(); + + qInfo() << "REPEAT" << repeat + << "In updateTriggerSet() TRG value=<" + << triggerValue << ">"; + + if (triggerValue == "WAIT") { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_SET, + QString("UPDATE TRIGGER SET. CONTINUE. ")); + + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateTriggerSet(m_updateStatus.m_statusDescription, "")); + + emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); + return true; + } else { + emit showErrorMessage("check update trigger", + QString ("TRG value=<") + triggerValue + + ">\n(reset download button?)"); + QThread::sleep(6); + continue; + } + } + } else { + emit showErrorMessage("check update trigger", "TRG key not contained"); + } + } else { + emit showErrorMessage("check update trigger", "Fileupload not a json-object"); + } + } + + if (obj.contains("error")) { + QString value = obj.value("error").toString(); + emit showErrorMessage("check update trigger", QString("REPEAT %1 error=<").arg(repeat) + value + ">"); + qInfo() << "REPEAT" << repeat << "In updateTriggerSet() error=<" + << value << ">"; + } + } else { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); + emit showErrorMessage("check update trigger", "no ISMAS response"); + QThread::sleep(6); } - } else { - stopProgressLoop(); - int progress = (m_mainWindow->progressValue()/10) + 10; - setProgress(progress); - emit showErrorMessage("check update trigger", "no ISMAS response"); } setProgress(100); @@ -672,6 +692,10 @@ bool Worker::updateFiles(quint8 percent) { m_displayIndex = 1; emit replaceLast(QString("(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") + QString(" Update opkg pakets ... "), UPDATE_STEP_DONE); + } else { + m_displayIndex = 1; + emit replaceLast(QString("(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update opkg pakets ... "), UPDATE_STEP_FAIL); } } } From 04d5061d795f3dfd3f21acb7936064f3e4b08485 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 10:49:34 +0200 Subject: [PATCH 129/239] Added some constants. --- worker.h | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/worker.h b/worker.h index 1542539..28db75d 100644 --- a/worker.h +++ b/worker.h @@ -32,6 +32,9 @@ enum class UPDATE_STATUS : quint8 { ISMAS_WAIT_STATE_CHECK_SUCCESS, ISMAS_RESPONSE_RECEIVED, BACKEND_CONNECTED, + BACKEND_CHECK, + BACKEND_CHECK_FAILURE, + ISMAS_BACKEND_CHECK_FAILURE, BACKEND_NOT_CONNECTED, UPDATE_TRIGGER_SET, UPDATE_TRIGGER_NOT_SET_OR_WRONG, @@ -59,6 +62,7 @@ enum class UPDATE_STATUS : quint8 { DEVICE_CONTROLLER_UPDATE_SUCCESS, JSON_UPDATE, JSON_UPDATE_FAILURE, + JSON_PARSE_FAILURE, JSON_UPDATE_SUCCESS, UPDATE_PROCESS_SUCCESS, UPDATE_PROCESS_FAILURE, @@ -67,7 +71,13 @@ enum class UPDATE_STATUS : quint8 { ISMAS_UPDATE_INFO_CONFIRM_SUCCESS, ISMAS_CURRENT_PSA_STATUS_CONFIRM, ISMAS_CURRENT_PSA_STATUS_CONFIRM_FAILURE, - ISMAS_CURRENT_PSA_STATUS_CONFIRM_SUCCESS + ISMAS_CURRENT_PSA_STATUS_CONFIRM_SUCCESS, + ISMAS_SANITY_CHECK_OK, + ISMAS_UPDATE_TRIGGER_SET_FAILURE, + PSA_UPDATE_FILES_FAILED, + GIT_CHECK_FILES_TO_UPDATE_SUCCESS, + ISMAS_SEND_LAST_VERSION_FAILED, + SAVE_LOG_FILES_FAILED }; struct UpdateStatus { From 6c4b02cb5622b18a7f1dd9f2ed50359845527308 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 10:52:31 +0200 Subject: [PATCH 130/239] Use print-utils to print some debug messages. --- worker.cpp | 65 +++++++++++++++++------------------------------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/worker.cpp b/worker.cpp index d28716e..031b44f 100644 --- a/worker.cpp +++ b/worker.cpp @@ -23,6 +23,7 @@ #include "ismas/ismas_client.h" #include "progress_event.h" #include "mainwindow.h" +#include "utils.h" QString const Worker::UPDATE_STEP_OK(" [ ok]"); QString const Worker::UPDATE_STEP_DONE(" [done]"); @@ -93,7 +94,7 @@ Worker::Worker(hwinf *hw, int cnt = 0; while (!m_workerThread.isRunning()) { if (++cnt > 5) { - qCritical() << "starting worker thread FAILED"; + Utils::printCriticalErrorMsg("starting worker thread FAILED"); return; } QThread::sleep(1); @@ -106,7 +107,7 @@ Worker::~Worker() { while (!m_workerThread.isFinished()) { if (!m_workerThread.wait(1000)) { if (++cnt > 5) { - qCritical() << "stopping worker thread FAILED"; + Utils::printCriticalErrorMsg("stopping worker thread FAILED"); return; } } @@ -565,15 +566,11 @@ bool Worker::customerEnvironment() { setProgress(progress); emit showErrorMessage("cust-env", QString("Checkout ") + m_customerRepository + " failed"); - qCritical() << QString(80, '*'); - qCritical() << "CHECKOUT OF" << m_customerRepository << "FAILED"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("CHECKOUT OF " + m_customerRepository + "FAILED")); } } else { emit showErrorMessage("cust-env", m_customerRepository + " does not exist"); - qCritical() << QString(80, '*'); - qCritical() << m_customerRepository << "DOES NOT EXIST"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(m_customerRepository + " DOES NOT EXIST"); } setProgress(100); @@ -600,9 +597,7 @@ bool Worker::filesToUpdate() { emit replaceLast(QString("Fetch changes files ..."), UPDATE_STEP_DONE); m_filesToUpdate = changedFileNames.value(); - qInfo() << QString(80, '*'); - qInfo() << "FILES-TO-UPDATE" << m_filesToUpdate; - qInfo() << QString(80, '*'); + Utils::printInfoMsg("FILES-TO-UPDATE " + m_filesToUpdate.join(',')); int const size = m_filesToUpdate.size(); if (size > 1) { @@ -617,17 +612,11 @@ bool Worker::filesToUpdate() { return true; } emit showErrorMessage("files to update", "pulling files failed"); - - qCritical() << QString(80, '*'); - qCritical() << "PULLING FILES FAILED"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg("PULLING FILES FAILED"); } else { emit showErrorMessage("files to update", "no files to update (checked-in any files?)"); - - qCritical() << QString(80, '*'); - qCritical() << "NO FILES TO UPDATE (CHECKED IN ANY FILES?)"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg("NO FILES TO UPDATE (CHECKED IN ANY FILES?)"); } setProgress(progress + 30); } else { @@ -639,9 +628,9 @@ bool Worker::filesToUpdate() { QString("no changes in ") + m_customerRepository + " (checked-in any files?)"); - qCritical() << QString(80, '*'); - qCritical() << "NO CHANGES IN" << m_customerRepository << "(CHECKED IN ANY FILES?)"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg("NO CHANGES IN " + + m_customerRepository + + " (CHECKED IN ANY FILES?)"); } emit replaceLast(QString("Fetch changes files ..."), UPDATE_STEP_FAIL); @@ -655,11 +644,8 @@ bool Worker::updateFiles(quint8 percent) { startProgressLoop(); for (int i = 0; i < m_filesToUpdate.size(); ++i) { - QString fName = m_filesToUpdate.at(i); - - qInfo() << QString(80, '*'); - qInfo() << "FNAME" << fName; - qInfo() << QString(80, '*'); + QString const fName = m_filesToUpdate.at(i); + Utils::printInfoMsg(QString("FNAME ") + fName); if (fName.contains("opkg_commands", Qt::CaseInsensitive)) { emit appendText("\n( ) Update opkg pakets ..."); @@ -716,14 +702,10 @@ bool Worker::updateFiles(quint8 percent) { bool ret = true; if (filesToDownload.size() > 0) { - qInfo() << QString(80, '*'); - qInfo() << "FILES_TO_WORK_ON" << filesToDownload; - qInfo() << QString(80, '*'); + Utils::printInfoMsg(QString("FILES_TO_DOWNLOAD_TO_PSA_HW ") + filesToDownload.join(',')); ret = m_update->doUpdate(m_displayIndex, filesToDownload); } else { - qInfo() << QString(80, '*'); - qInfo() << "NO FILES_TO_WORK_ON"; - qInfo() << QString(80, '*'); + Utils::printCriticalErrorMsg("NO FILES_TO_DOWNLOAD_TO_PSA_HW"); } return ret; @@ -762,12 +744,9 @@ bool Worker::syncCustomerRepositoryAndFS() { for (int i = 0; i < result.size(); ++i) { qInfo() << result.at(i); } - qInfo() << "SUCCESS"; } else { - qCritical() << QString(80, '*'); - qCritical() << "CMD" << cmd << "FAILED"; - qCritical() << c.getCommandResult().split('\n'); - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("CMD ") + cmd + " FAILED: " + + c.getCommandResult()); error = true; } } @@ -883,17 +862,13 @@ bool Worker::executeOpkgCommand(QString opkgCommand) { Command c(opkgCommand); if (c.execute(m_workingDirectory)) { QString const r = c.getCommandResult(); - qInfo() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS, - QString("EXECUTE OPKG COMMAND %1 OK: %2") + Utils::printInfoMsg(QString("EXECUTE OPKG COMMAND %1 OK: %2") .arg(opkgCommand) .arg(c.getCommandResult())); return true; } else { - qCritical() << QString(80, '*'); - qCritical() << UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE, - QString("EXECUTE OPKG COMMAND %1 FAILED") - .arg(opkgCommand)); - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("EXECUTE OPKG COMMAND %1 FAILED") + .arg(opkgCommand)); } return false; } From edf1d105e707f8efae375c49d88bca896c013836 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 10:55:53 +0200 Subject: [PATCH 131/239] Deactivate backendConnected(). --- worker.cpp | 291 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 192 insertions(+), 99 deletions(-) diff --git a/worker.cpp b/worker.cpp index 031b44f..eaf6836 100644 --- a/worker.cpp +++ b/worker.cpp @@ -68,7 +68,7 @@ Worker::Worker(hwinf *hw, , m_ismasUpdateRequests(ISMAS_UPDATE_REQUESTS) , m_waitForNewUpdates(this) , m_filesToUpdate() - , m_updateProcessRunning(false) + , m_updateProcessRunning(true) , m_returnCode(0) , m_progressValue(0) , m_withoutIsmasDirectPort(false) /* useful for testing */ { @@ -206,77 +206,122 @@ void Worker::privateUpdate() { m_returnCode = -3; } } else { - qInfo() << "CHECKOUT BRANCH..."; - emit appendText("\nInitializing customer environment ..."); - startProgressLoop(); - if (m_gc.gitCheckoutBranch()) { - stopProgressLoop(); - emit replaceLast("Initializing customer environment ...", UPDATE_STEP_DONE); - setProgress(100); + m_ismasClient.setProgressInPercent(10); + if (backendConnected()) { + m_ismasClient.setProgressInPercent(20); + if (updateTriggerSet()) { + m_ismasClient.setProgressInPercent(30); + if (customerEnvironment()) { + m_ismasClient.setProgressInPercent(40); + if (filesToUpdate()) { + // send message to ISMAS about files which have been + // checked in into git repository + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECK_FILES_TO_UPDATE_SUCCESS, + QString("Files to update: ") + m_filesToUpdate.join(',')); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAContinues("CHECK-FILES-TO-UPDATE", + m_updateStatus.m_statusDescription)); + m_ismasClient.setProgressInPercent(50); + if (updateFiles(50)) { + m_ismasClient.setProgressInPercent(60); + if (syncCustomerRepositoryAndFS()) { + m_ismasClient.setProgressInPercent(70); + if (sendIsmasLastVersionNotification()) { + m_ismasClient.setProgressInPercent(80); + sentIsmasLastVersionNotification = true; + if (saveLogFile()) { + m_ismasClient.setProgressInPercent(90); - m_ismasClient.setProgressInPercent(10); - //m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH, - // QString("CHECKED OUT BRANCH: ") + m_gc.branchName()); - //IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - // QString("#M=APISM#C=CMD_EVENT#J=") + - // m_ismasClient.checkoutBranch( - // m_updateStatus.m_statusDescription, "")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSASucceeded("")); - qInfo() << "CHECKED OUT BRANCH"; - if (backendConnected()) { - m_ismasClient.setProgressInPercent(20); - if (updateTriggerSet()) { - m_ismasClient.setProgressInPercent(30); - if (customerEnvironment()) { - m_ismasClient.setProgressInPercent(40); - if (filesToUpdate()) { - m_ismasClient.setProgressInPercent(50); - if (updateFiles(50)) { - m_ismasClient.setProgressInPercent(60); - if (syncCustomerRepositoryAndFS()) { - m_ismasClient.setProgressInPercent(70); - if (sendIsmasLastVersionNotification()) { - m_ismasClient.setProgressInPercent(80); - sentIsmasLastVersionNotification = true; - if (saveLogFile()) { - m_ismasClient.setProgressInPercent(90); + // mark update as activated -> this resets the WAIT button + m_ismasClient.setProgressInPercent(95); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAActivated()); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateOfPSASucceeded("")); - - // mark update as activated -> this resets the WAIT button - m_ismasClient.setProgressInPercent(95); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateOfPSAActivated()); - - m_returnCode = 0; - } else { - m_returnCode = -12; - } + m_returnCode = 0; } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::SAVE_LOG_FILES_FAILED, + QString("Saving log files failed")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "SAVE-LOG-FILES", + m_updateStatus.m_statusDescription)); m_returnCode = -11; } } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_SEND_LAST_VERSION_FAILED, + QString("Sending ISMAS last version failed")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "ISMAS-SEND-LAST-VERSION", + m_updateStatus.m_statusDescription)); m_returnCode = -10; } } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::RSYNC_UPDATES_FAILURE, + QString("Syncing files to update failed")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "RSYNC-UPDATE-FILES", + m_updateStatus.m_statusDescription)); m_returnCode = -9; } } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::PSA_UPDATE_FILES_FAILED, + QString("Updating files failed")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "UPDATE-FILES", + m_updateStatus.m_statusDescription)); m_returnCode = -8; } } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE, + QString("No files to update")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "FETCH-FILES-TO-UPDATE", + m_updateStatus.m_statusDescription)); m_returnCode = -7; } } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH_REQUEST_FAILURE, + QString("Configuring customer environment failed")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "GIT-CHECKOUT-BRANCH", + m_updateStatus.m_statusDescription)); m_returnCode = -6; } } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET_FAILURE, + QString("ISMAS update trigger wrong")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "CHECK-UPDATE-TRIGGER", + m_updateStatus.m_statusDescription)); m_returnCode = -5; } } else { + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_BACKEND_CHECK_FAILURE, + QString("ISMAS backend not available")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "ISMAS-BACKEND-CHECK", + m_updateStatus.m_statusDescription)); m_returnCode = -4; } } @@ -302,45 +347,64 @@ void Worker::privateUpdate() { } bool Worker::backendConnected() { - if (m_withoutIsmasDirectPort) { // useful for testing - return true; - } + // deactivated: REQ_SELF does not really help. Observation was that even + // id ISMAS is reported as 'true', a following check of the update-trigger + // button has no access to ISMAS. + return true; emit appendText("\nConnecting backend ..."); - for (int repeat = 0; repeat < 50; ++repeat) { - qInfo() << "REPEAT" << repeat << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; - startProgressLoop(); - std::optional result - = IsmasClient::sendRequestReceiveResponse( - IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_SELF#J={}"); - if (result) { - stopProgressLoop(); - int progress = (m_mainWindow->progressValue()/10) + 10; - setProgress(progress); + if (false) { // so linker removes dead code + for (int repeat = 0; repeat < 50; ++repeat) { + qInfo() << "REPEAT" << repeat << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; + startProgressLoop(); + std::optional result + = IsmasClient::sendRequestReceiveResponse( + IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_SELF#J={}"); + if (result) { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); - QString msg = result.value(); - qInfo() << "In backendConnected() -> APISM response" << msg; - QJsonParseError parseError; - QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); - if (parseError.error != QJsonParseError::NoError) { - qCritical() << "(2) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" - << parseError.error << parseError.errorString(); - emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); - return false; - } - if (!document.isObject()) { - qCritical() << "FILE IS NOT A JSON OBJECT!"; - emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); - return false; - } + QString msg = result.value(); + qInfo() << "In backendConnected() -> APISM response" << msg; + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "(1) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" + << parseError.error << parseError.errorString(); + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, + QString("(2) INVALID JSON %1 %2 %3") + .arg(msg) + .arg(parseError.error) + .arg(parseError.errorString())); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); + return false; + } + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, + QString("NOT A JSON-OBJECT %1").arg(msg)); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); + return false; + } - setProgress(progress + 1); + setProgress(progress + 1); - QJsonObject obj = document.object(); - QStringList keys = obj.keys(); - for (QString const& key : keys ) { - if (key.contains("CMD_GET_APISMSTATUS_RESPONSE")) { + QJsonObject obj = document.object(); + QStringList keys = obj.keys().filter("CMD_GET_APISMSTATUS_RESPONSE"); + if (keys.size() == 1) { + QString const key = keys.at(0); QJsonValue v = obj.value(key); if (v.isObject()) { obj = v.toObject(); @@ -357,32 +421,61 @@ bool Worker::backendConnected() { return true; } } - emit showErrorMessage("Check backend connection", - QString ("REPEAT %1 Broker=<").arg(repeat) - + status + ">, ISMAS=<" + (ismas ? "true>" : "false>")); + + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK, + QString ("REPEAT %1 Broker=<").arg(repeat) + + status + ">, ISMAS=<" + (ismas ? "true>" : "false>")); + //IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + // QString("#M=APISM#C=CMD_EVENT#J=") + + // m_ismasClient.updateOfPSAContinues("BACKEND-CHECK", m_updateStatus.m_statusDescription)); + + qInfo() << "BACKEND-CHECK" << m_updateStatus.m_statusDescription; + emit showErrorMessage("Check backend connection", m_updateStatus.m_statusDescription); QThread::sleep(6); continue; + } else { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK_FAILURE, + "CMD_GET_APISM_STATUS_RESPONSE KEY NOT A JSON-OBJECT"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("check backend connection", m_updateStatus.m_statusDescription); + emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); + return false; } + } else { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK_FAILURE, + "CMD_GET_APISMSTATUS_RESPONSE KEY NOT AVAILABLE"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("check backend connection", m_updateStatus.m_statusDescription); + emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); + return false; } + + } else { + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + setProgress(progress); } - } else { - stopProgressLoop(); - int progress = (m_mainWindow->progressValue()/10) + 10; - setProgress(progress); } + + setProgress(100); + + emit replaceLast("Connecting backend", UPDATE_STEP_FAIL); + emit showErrorMessage("Error", "Backend not available"); + + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_NOT_CONNECTED, + QString("NO BACKEND CONNECTION")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.errorBackendNotConnected(m_updateStatus.m_statusDescription, "")); } - - setProgress(100); - - emit replaceLast("Connecting backend", UPDATE_STEP_FAIL); - emit showErrorMessage("Error", "Backend not available"); - - m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_NOT_CONNECTED, - QString("NO BACKEND CONNECTION")); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.errorBackendNotConnected(m_updateStatus.m_statusDescription, "")); - return false; } From 79906df12ec49a4bb1bf347b71ca634ead6e1d63 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 10:56:36 +0200 Subject: [PATCH 132/239] Add some qt debug aoutput. --- worker.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worker.cpp b/worker.cpp index eaf6836..62c063e 100644 --- a/worker.cpp +++ b/worker.cpp @@ -488,6 +488,8 @@ bool Worker::updateTriggerSet() { QString triggerValue(""); for (int repeat = 0; repeat < 50; ++repeat) { + qInfo() << "UPDATE TRIGGER SET -> REPEAT" << repeat; + startProgressLoop(); if (std::optional result = IsmasClient::sendRequestReceiveResponse( From 746565dbe089d8349655ad45e78f488b51cb0313 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 10:58:29 +0200 Subject: [PATCH 133/239] Add messages sent to ISMAS. --- worker.cpp | 133 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 113 insertions(+), 20 deletions(-) diff --git a/worker.cpp b/worker.cpp index 62c063e..fdd8d11 100644 --- a/worker.cpp +++ b/worker.cpp @@ -507,6 +507,16 @@ bool Worker::updateTriggerSet() { if (parseError.error != QJsonParseError::NoError) { qCritical() << "(2) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" << parseError.error << parseError.errorString(); + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, + QString("(2) INVALID JSON %1 %2 %3") + .arg(msg) + .arg(parseError.error) + .arg(parseError.errorString())); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", QString("invalid json ") + msg.mid(0, 20)); emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); @@ -514,8 +524,14 @@ bool Worker::updateTriggerSet() { } if (!document.isObject()) { qCritical() << "FILE IS NOT A JSON OBJECT!"; - emit showErrorMessage("check update trigger", - QString("not a json object") + msg); + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, + QString("NOT A JSON-OBJECT %1").arg(msg)); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("check update trigger", QString("not a json object") + msg); emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } @@ -562,8 +578,44 @@ bool Worker::updateTriggerSet() { emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } + + qInfo() << "MACHINE-AND-CUSTOMER-CHECK" << m_updateStatus.m_statusDescription; + + } else { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + "Dev_ID DOES NOT CONTAIN Custom_ID AND/OR Device_ID"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; } + } else { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + "Dev_ID KEY NOT A JSON-OBJECT"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; } + } else { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + "Dev_ID KEY NOT AVAILABLE"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; } setProgress(progress + 1); @@ -581,31 +633,72 @@ bool Worker::updateTriggerSet() { << "In updateTriggerSet() TRG value=<" << triggerValue << ">"; - if (triggerValue == "WAIT") { - setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_SET, - QString("UPDATE TRIGGER SET. CONTINUE. ")); + if (triggerValue == "WAIT") { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_SANITY_CHECK_OK, + QString("MACHINE-NR (%1) AND CUST-NR (%2) OK") + .arg(m_machineNr).arg(m_customerNr)); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAContinues("MACHINE-AND-CUSTOMER-CHECK", + m_updateStatus.m_statusDescription)); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateTriggerSet(m_updateStatus.m_statusDescription, "")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_SET, + QString("UPDATE TRIGGER SET. CONTINUE. ")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateTriggerSet(m_updateStatus.m_statusDescription, "")); - emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); - return true; - } else { - emit showErrorMessage("check update trigger", - QString ("TRG value=<") + triggerValue - + ">\n(reset download button?)"); - QThread::sleep(6); - continue; - } + emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); + return true; + } else { + // if the download-button once has the wrong value, it will never recover + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + QString("TRIGGER-VALUE ") + triggerValue + " NOT 'WAIT'"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + return false; } } else { - emit showErrorMessage("check update trigger", "TRG key not contained"); + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + "TRG KEY NOT AVAILABLE"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + return false; } } else { - emit showErrorMessage("check update trigger", "Fileupload not a json-object"); + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + "Fileupload NOT A JSON-OBJECT"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + return false; } + } else { + setProgress(100); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, + "Fileupload KEY NOT AVAILABLE"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); + return false; } if (obj.contains("error")) { From a8941f4ef4ca4abe83786822bf7ed16cad5cea71 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 10:59:26 +0200 Subject: [PATCH 134/239] Set progress values. --- worker.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worker.cpp b/worker.cpp index fdd8d11..c93067a 100644 --- a/worker.cpp +++ b/worker.cpp @@ -535,8 +535,8 @@ bool Worker::updateTriggerSet() { emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } - - setProgress(progress + 1); + progress += 1; + setProgress(progress); QJsonObject obj = document.object(); // sanity check: cust_nr and machine_nr of PSA correct ? @@ -552,6 +552,7 @@ bool Worker::updateTriggerSet() { int customerNr = c.toInt(-1); int machineNr = m.toInt(-1); if (customerNr != m_customerNr) { + setProgress(100); m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, QString("CUSTOMER-NR (%1) != LOCAL CUSTOMER-NR (%2)") .arg(customerNr).arg(m_customerNr)); @@ -617,8 +618,8 @@ bool Worker::updateTriggerSet() { emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } - - setProgress(progress + 1); + progress += 1; + setProgress(progress); if (obj.contains("Fileupload")) { QJsonValue v = obj.value("Fileupload"); @@ -702,6 +703,8 @@ bool Worker::updateTriggerSet() { } if (obj.contains("error")) { + progress += 1; + setProgress(progress); QString value = obj.value("error").toString(); emit showErrorMessage("check update trigger", QString("REPEAT %1 error=<").arg(repeat) + value + ">"); qInfo() << "REPEAT" << repeat << "In updateTriggerSet() error=<" @@ -710,6 +713,7 @@ bool Worker::updateTriggerSet() { } else { stopProgressLoop(); int progress = (m_mainWindow->progressValue()/10) + 10; + progress += 1; setProgress(progress); emit showErrorMessage("check update trigger", "no ISMAS response"); QThread::sleep(6); From a37a22d3f9d62412710313072fbf3cf63d58d84e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:00:02 +0200 Subject: [PATCH 135/239] Show message boxes when running UpdateTool manually. --- worker.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/worker.cpp b/worker.cpp index c93067a..cff8793 100644 --- a/worker.cpp +++ b/worker.cpp @@ -556,12 +556,11 @@ bool Worker::updateTriggerSet() { m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, QString("CUSTOMER-NR (%1) != LOCAL CUSTOMER-NR (%2)") .arg(customerNr).arg(m_customerNr)); - emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } @@ -570,12 +569,11 @@ bool Worker::updateTriggerSet() { m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, QString("MACHINE-NR (%1) != LOCAL MACHINE-NR (%2)") .arg(machineNr).arg(m_machineNr)); - emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); + emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); return false; } @@ -630,9 +628,9 @@ bool Worker::updateTriggerSet() { if (v.isString()) { triggerValue = v.toString(); - qInfo() << "REPEAT" << repeat - << "In updateTriggerSet() TRG value=<" - << triggerValue << ">"; + qInfo() << "REPEAT" << repeat + << "In updateTriggerSet() TRG value=<" + << triggerValue << ">"; if (triggerValue == "WAIT") { setProgress(100); From a550d5500435ab7f7b55ae9b3a8f008bc4d6298f Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:00:52 +0200 Subject: [PATCH 136/239] Get triggerValue directly from JSON. --- worker.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worker.cpp b/worker.cpp index cff8793..82834a2 100644 --- a/worker.cpp +++ b/worker.cpp @@ -624,9 +624,7 @@ bool Worker::updateTriggerSet() { if (v.isObject()) { obj = v.toObject(); if (obj.contains("TRG")) { - v = obj.value("TRG"); - if (v.isString()) { - triggerValue = v.toString(); + triggerValue = obj.value("TRG").toString(); qInfo() << "REPEAT" << repeat << "In updateTriggerSet() TRG value=<" From 1af136e39d43df544f19fc579b92b6b253535a4d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:01:26 +0200 Subject: [PATCH 137/239] Set delay when trying to fetch value of update-trigger. --- worker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/worker.cpp b/worker.cpp index 82834a2..fda3f39 100644 --- a/worker.cpp +++ b/worker.cpp @@ -705,6 +705,7 @@ bool Worker::updateTriggerSet() { emit showErrorMessage("check update trigger", QString("REPEAT %1 error=<").arg(repeat) + value + ">"); qInfo() << "REPEAT" << repeat << "In updateTriggerSet() error=<" << value << ">"; + QThread::sleep(6); } } else { stopProgressLoop(); From 8f26bfee0faff2abe596127106543ec495bf3167 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:02:15 +0200 Subject: [PATCH 138/239] Get last commit date and loadtime of tariff-file. --- worker.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worker.cpp b/worker.cpp index fda3f39..95cedbc 100644 --- a/worker.cpp +++ b/worker.cpp @@ -1076,19 +1076,23 @@ PSAInstalled Worker::getPSAInstalled() { PSAInstalled psaInstalled; QString printSysDir("/etc/psa_config"); QString tariffSysDir("/etc/psa_tariff"); + QString tariffRepoDir("etc/psa_tariff"); QString absPathName; + QString absPathNameRepository; if (m_zoneNr != 0) { QString const &n = QString("%1").arg(m_zoneNr).rightJustified(2, '0'); psaInstalled.tariff.name = QString("tariff%1.json").arg(n); absPathName = QDir::cleanPath(tariffSysDir + QDir::separator() + psaInstalled.tariff.name); psaInstalled.tariff.blob = m_gc.gitBlob(absPathName); + absPathNameRepository = QDir::cleanPath(tariffRepoDir + QDir::separator() + psaInstalled.tariff.name); + psaInstalled.tariff.lastCommit = m_gc.gitLastCommit(absPathNameRepository); psaInstalled.tariff.size = getFileSize(absPathName); psaInstalled.tariff.zone = m_zoneNr; + psaInstalled.tariff.loadTime = Utils::getTariffLoadTime(absPathName); } psaInstalled.tariff.project = "Szeged"; psaInstalled.tariff.info = "N/A"; - psaInstalled.tariff.loadTime = "N/A"; // QDateTime::currentDateTime().toString(Qt::ISODateWithMs); psaInstalled.tariff.version = "N/A"; psaInstalled.hw.linuxVersion = m_osVersion; From 202e83268ba834a724069d85b27a66fa1ee52e22 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:03:21 +0200 Subject: [PATCH 139/239] Added utilities void printCriticalErrorMsg(QString const &errorMsg); void printInfoMsg(QString const &infoMsg); void printLineEditInfo(QStringList const &lines); QString getTariffLoadTime(QString fileName); --- utils.cpp | 46 +++++++++++++++++++++++++++++++++++++++++++++- utils.h | 7 +++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/utils.cpp b/utils.cpp index 79e5fed..d17a7ea 100644 --- a/utils.cpp +++ b/utils.cpp @@ -1,8 +1,9 @@ #include "utils.h" +#include "message_handler.h" #include #include - +#include int Utils::read1stLineOfFile(QString fileName) { QFile f(fileName); @@ -33,3 +34,46 @@ QString Utils::zoneName(quint8 i) { } return "N/A"; } + +void Utils::printCriticalErrorMsg(QString const &errorMsg) { + qCritical() << QString(80, '!'); + qCritical() << errorMsg; + qCritical() << QString(80, '!'); +} + +void Utils::printInfoMsg(QString const &infoMsg) { + qCritical() << QString(80, '='); + qCritical() << infoMsg; + qCritical() << QString(80, '='); + +} + +void Utils::printLineEditInfo(QStringList const &lines) { + if (getDebugLevel() == LOG_DEBUG) { + for (int i=0; i #include +#include #include +#include +#include namespace Utils { int read1stLineOfFile(QString fileName); QString zoneName(quint8 i); + void printCriticalErrorMsg(QString const &errorMsg); + void printInfoMsg(QString const &infoMsg); + void printLineEditInfo(QStringList const &lines); + QString getTariffLoadTime(QString fileName); } #endif // UTILS_H_INCLUDED From 9eb458c4c530b44ad40d2f56be97a71b64a3c2ad Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:05:02 +0200 Subject: [PATCH 140/239] Start Application after 1 second delay. --- mainwindow.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 5b49d02..df5594f 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -33,10 +33,9 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) ui->updateStatus->setEnabled(true); m_startTimer = new QTimer(this); - // connect(m_startTimer, SIGNAL(timeout()), ui->start, SLOT(click())); connect(m_startTimer, SIGNAL(timeout()), m_worker, SLOT(update())); m_startTimer->setSingleShot(true); - m_startTimer->start(5 * 1000); + m_startTimer->start(1000); m_exitTimer = new QTimer(this); connect(m_exitTimer, SIGNAL(timeout()), ui->exit, SLOT(click())); From ed6df986531337e089dd0b2c9c056c3f06f2e6d7 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:05:33 +0200 Subject: [PATCH 141/239] Set progress bar timer to 500ms. Yield thread to give worker thread its share of CPU time. --- mainwindow.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index df5594f..5d53966 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -87,7 +87,7 @@ void MainWindow::customEvent(QEvent *event) { m_progressValue = progress; ui->updateProgress->setValue(progress/10); QApplication::postEvent(this, new ProgressEvent(this, progress+10)); - QThread::msleep(100); + QThread::msleep(500); } } } @@ -110,6 +110,8 @@ void MainWindow::customEvent(QEvent *event) { qCritical() << "!!! UNKNOWN SENDER !!!"; } } + + QThread::yieldCurrentThread(); } void MainWindow::onStopStartTimer() { From c338884fc707e672de65315182d3670e78f9da64 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:06:38 +0200 Subject: [PATCH 142/239] Use print-util in utils.h. --- mainwindow.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 5d53966..a3e4396 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -150,10 +150,7 @@ void MainWindow::onAppendText(QString text, QString suffix) { editText += text.leftJustified(m_width-9); } - QStringList const lines = editText.split('\n'); - for (int i=0; iupdateStatus->setPlainText(editText.trimmed()); ui->updateStatus->setEnabled(true); From a932ed54710552fa1e2342531022a4d130081fc8 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:07:16 +0200 Subject: [PATCH 143/239] Hide button on error message box. --- mainwindow.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index a3e4396..ae79de8 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -170,21 +170,20 @@ void MainWindow::onReplaceLast(QString text, QString suffix) { } } - for (int i=0; iupdateStatus->setText(lines.join('\n').trimmed()); ui->updateStatus->setEnabled(true); } + void MainWindow::onShowErrorMessage(QString title, QString text) { text = text.leftJustified(50, ' '); QMessageBox msgBox(QMessageBox::NoIcon, title, text, QMessageBox::Ok, nullptr, Qt::FramelessWindowHint); msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setStyleSheet("QDialog {background-color: red;}" - "QPushButton {background-color: blue;}"); + msgBox.defaultButton()->setVisible(false); + QTimer *t = new QTimer(this); connect(t, SIGNAL(timeout()), msgBox.defaultButton(), SLOT(click())); t->setSingleShot(true); @@ -195,5 +194,4 @@ void MainWindow::onShowErrorMessage(QString title, QString text) { } else { // do something else } - disconnect(t, SIGNAL(timeout()), msgBox.defaultButton(), SLOT(click())); } From 853787cd4b334d7da26532f653f4dc57c410caac Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:09:51 +0200 Subject: [PATCH 144/239] Added lastCommit to tariff-struct. Added jsonParseFailed(). --- ismas/ismas_client.cpp | 14 ++++++++++++-- ismas/ismas_client.h | 4 +++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 2a8b19a..d563e75 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -753,11 +753,21 @@ QString IsmasClient::sanityCheckFailed(int resultCode, QString reason, QString c version.toStdString().c_str()); } -QString IsmasClient::updateOfPSAFailed(int resultCode, QString reason, QString const &version) { +QString IsmasClient::jsonParseFailed(int resultCode, QString reason, QString const &version) { return updateNewsToIsmas("U0003", m_progressInPercent, resultCode, - "UPDATE ERROR", + "JSON-PARSE-ERROR", + reason.toStdString().c_str(), + version.toStdString().c_str()); +} + +QString IsmasClient::updateOfPSAFailed(int resultCode, QString step, + QString reason, QString const &version) { + return updateNewsToIsmas("U0003", + m_progressInPercent, + resultCode, + step.toStdString().c_str(), reason.toStdString().c_str(), version.toStdString().c_str()); } diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index 9274e8c..be5407a 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -12,6 +12,7 @@ struct PSAInstalled { int zone; int size; QString blob; + QString lastCommit; QString info; QString loadTime; } tariff; @@ -155,8 +156,9 @@ public: // and update accepted QString updateOfPSASucceeded(QString const &version = QString()); // update process succeeded QString updateOfPSAContinues(QString currentStage, QString currentStageInfo, QString const &version = QString()); - QString updateOfPSAFailed(int resultCode, QString reason, QString const &version = QString()); + QString updateOfPSAFailed(int resultCode, QString step, QString reason, QString const &version = QString()); QString sanityCheckFailed(int resultCode, QString reason, QString const &version = QString()); + QString jsonParseFailed(int resultCode, QString reason, QString const &version = QString()); QString updateOfPSASendVersion(PSAInstalled const &psa); From 34334676fc87e5330245bc2223a13085d00642d7 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:11:14 +0200 Subject: [PATCH 145/239] Use print-utis from utils.h. --- git/git_client.cpp | 45 ++++++++++----------------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index d28c798..43a1c68 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -48,9 +48,7 @@ bool GitClient::gitCloneCustomerRepository() { } } } - qCritical() << QString(80, '*'); - qCritical() << "ERROR CLONE RESULT HAS WRONG FORMAT. CLONE_RESULT=" << result; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("ERROR CLONE RESULT HAS WRONG FORMAT. CLONE_RESULT=") + result); } return false; } @@ -97,9 +95,7 @@ bool GitClient::gitCheckoutBranch() { Command c(gitCommand); return c.execute(m_customerRepository); // execute command in customerRepo } - qCritical() << QString(80, '*'); - qCritical() << "ERROR" << m_customerRepository << "DOES NOT EXIST"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("ERROR ") + m_customerRepository + " DOES NOT EXIST"); return false; } @@ -113,11 +109,8 @@ bool GitClient::gitCloneAndCheckoutBranch() { // TODO } //} - } else { - qCritical() << QString(80, '*'); - qInfo() << "CLONE" << m_repositoryPath << "AND CHECKOUT FAILED"; - qCritical() << QString(80, '*'); } + Utils::printCriticalErrorMsg(QString("CLONE ") + m_repositoryPath + " AND CHECKOUT FAILED"); return false; } @@ -134,11 +127,7 @@ std::optional GitClient::gitDiff(QString const &commits) { Command c(gitCommand); if (c.execute(m_customerRepository)) { // execute command in local customerRepo QString s = c.getCommandResult().trimmed(); - QStringList lines = Update::split(s, '\n'); - - qInfo() << QString(80, '*'); - qInfo() << "GIT DIFF RESULT" << lines; - qInfo() << QString(80, '*'); + Utils::printInfoMsg("GIT DIFF RESULT " + s); QStringList fileNames; // each line has the format "etc/psa_config/DC2C_print01.json | 1 + @@ -185,9 +174,7 @@ std::optional GitClient::gitDiff(QString const &commits) { */ std::optional GitClient::gitFetch() { if (QDir(m_customerRepository).exists()) { - qCritical() << QString(80, '*'); qInfo() << "BRANCH NAME" << m_branchName; - qCritical() << QString(80, '*'); Command c("git fetch"); if (c.execute(m_customerRepository)) { @@ -221,38 +208,28 @@ std::optional GitClient::gitFetch() { } else { emit m_worker->showErrorMessage("git fetch", "no regex-match for commits"); - qCritical() << QString(80, '*'); - qCritical() << "NO REGEX MATCH FOR COMMITS"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg("NO REGEX MATCH FOR COMMITS"); } } } if (!found) { emit m_worker->showErrorMessage("git fetch", QString("unkown branch name ") + m_branchName); - qCritical() << QString(80, '*'); - qCritical() << "UNKNOWN BRANCH NAME" << m_branchName; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg("UNKNOWN BRANCH NAME " + m_branchName); } } else { emit m_worker->showErrorMessage("git fetch", QString("wrong format for result of 'git fetch' ") + s); - qCritical() << QString(80, '*'); - qCritical() << "WRONG FORMAT FOR RESULT OF 'GIT FETCH'" << s; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("WRONG FORMAT FOR RESULT OF 'GIT FETCH' ") + s); } } else { emit m_worker->showErrorMessage("git fetch", "empty result for 'git fetch'"); - qCritical() << QString(80, '*'); - qCritical() << "EMPTY RESULT FOR 'GIT FETCH'"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg("EMPTY RESULT FOR 'GIT FETCH'"); } } } else { emit m_worker->showErrorMessage("git fetch", QString("repository ") + m_customerRepository + " does not exist"); - qCritical() << QString(80, '*'); - qCritical() << "REPOSITORY" << m_customerRepository << "DOES NOT EXIST"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("REPOSITORY ") + m_customerRepository + " DOES NOT EXIST"); } return std::nullopt; } @@ -273,9 +250,7 @@ bool GitClient::gitPull() { qInfo() << "PULLED INTO" << m_customerRepository; return true; } - qCritical() << QString(80, '*'); - qCritical() << "PULL INTO" << m_customerRepository << "FAILED"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg(QString("PULL INTO " + m_customerRepository + " FAILED")); } return false; } From 0e97ce7dc26ecd9ced86b4b8e55bdcd24a052771 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:12:09 +0200 Subject: [PATCH 146/239] Take the pipe symbol into account in output of git getch command. --- git/git_client.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index 43a1c68..d2c3616 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -129,6 +129,7 @@ std::optional GitClient::gitDiff(QString const &commits) { QString s = c.getCommandResult().trimmed(); Utils::printInfoMsg("GIT DIFF RESULT " + s); + QStringList lines = Update::split(s, '\n'); QStringList fileNames; // each line has the format "etc/psa_config/DC2C_print01.json | 1 + // or the format "etc/psa_config/DC2C_print01.json (new) | 1 + @@ -138,7 +139,7 @@ std::optional GitClient::gitDiff(QString const &commits) { int newIndex = line.indexOf("(new"); // for new files int goneIndex = line.indexOf("(gone"); // for removed files int modeIndex = line.indexOf("(mode"); - // int pipeIndex = line.indexOf('|'); + int pipeIndex = line.indexOf('|'); if (newIndex != -1) { QString file = line.left(newIndex).trimmed(); qInfo() << "FILE (NEW)" << file; @@ -149,16 +150,14 @@ std::optional GitClient::gitDiff(QString const &commits) { qInfo() << "FILE (MODE)" << file; fileNames << file; } else - //if (pipeIndex != -1) { - // QString const file = line.left(pipeIndex).trimmed(); - // qInfo() << "FILE (PIPE)" << file; - // fileNames << file; - //} else if (goneIndex != -1) { QString const file = line.left(goneIndex).trimmed(); - qCritical() << QString(80, '*'); qCritical() << "FILE (GONE)" << file; - qCritical() << QString(80, '*'); + } else + if (pipeIndex != -1) { + QString const file = line.left(pipeIndex).trimmed(); + qInfo() << "FILE (PIPE)" << file; + fileNames << file; } } if (!fileNames.isEmpty()) { From 769626581f777a54bb7792242d5a987f6e46ca26 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 11:12:59 +0200 Subject: [PATCH 147/239] Use head -n 1 in gitLastCommit(). --- git/git_client.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index d2c3616..78defa4 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -268,13 +268,15 @@ QString GitClient::gitLastCommit(QString fileName) { if (QDir(m_customerRepository).exists()) { QString const filePath = QDir::cleanPath(m_customerRepository + QDir::separator() + fileName); - QString const gitCommand = QString("git log %1 | head -n 1").arg(fileName); - Command c("bash"); - if (c.execute(m_customerRepository, QStringList() << "-c" << gitCommand)) { - QString const r = c.getCommandResult(); - int const idx = r.indexOf("commit "); - if (idx != -1) { - return r.mid(idx + 8).trimmed(); + if (QFile(filePath).exists()) { + QString const gitCommand = QString("git log %1 | head -n 1").arg(fileName); + Command c("bash"); + if (c.execute(m_customerRepository, QStringList() << "-c" << gitCommand)) { + QString const r = c.getCommandResult(); + int const idx = r.indexOf("commit "); + if (idx != -1) { + return r.mid(idx + 8).trimmed(); + } } } } From 4968942cc2fea95f95a924fbaa6ac2139a9cfe7c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 12:19:10 +0200 Subject: [PATCH 148/239] Added finalResult() -> send final message to ISMAS in any case. --- ismas/ismas_client.cpp | 21 +++++++++++++++++++++ ismas/ismas_client.h | 3 +++ 2 files changed, 24 insertions(+) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index d563e75..d405da7 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -762,6 +762,27 @@ QString IsmasClient::jsonParseFailed(int resultCode, QString reason, QString con version.toStdString().c_str()); } +std::optional IsmasClient::finalResult(int resultCode, QString reason, QString const &version) { + m_progressInPercent = 0; + if (resultCode == RESULT_CODE::SUCCESS) { + return updateNewsToIsmas("U0002", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "FINAL-UPDATE-RESULT", + "(re-)set WAIT state", + version.toStdString().c_str()); + } + if (resultCode == RESULT_CODE::INSTALL_ERROR) { + return updateNewsToIsmas("U0003", + m_progressInPercent, + resultCode, + "FINAL-UPDATE-RESULT", + reason.toStdString().c_str(), + version.toStdString().c_str()); + } + return std::nullopt; +} + QString IsmasClient::updateOfPSAFailed(int resultCode, QString step, QString reason, QString const &version) { return updateNewsToIsmas("U0003", diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index be5407a..97374b8 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -4,6 +4,8 @@ #include #include +#include + struct PSAInstalled { struct Tariff { QString name; @@ -159,6 +161,7 @@ public: QString updateOfPSAFailed(int resultCode, QString step, QString reason, QString const &version = QString()); QString sanityCheckFailed(int resultCode, QString reason, QString const &version = QString()); QString jsonParseFailed(int resultCode, QString reason, QString const &version = QString()); + std::optional finalResult(int resultCode, QString reason, QString const &version = QString()); QString updateOfPSASendVersion(PSAInstalled const &psa); From effe41bac9565ecb6bc8d4030b39ce23638da37f Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 12:20:09 +0200 Subject: [PATCH 149/239] Use finalResult() to sent a final message to ISMAS about the result of the update process. --- worker.cpp | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/worker.cpp b/worker.cpp index 95cedbc..ddd17eb 100644 --- a/worker.cpp +++ b/worker.cpp @@ -327,20 +327,38 @@ void Worker::privateUpdate() { } m_ismasClient.setProgressInPercent(100); + setProgress(100); + + if (m_returnCode != 0) { + stopProgressLoop(); + emit appendText(QString("UPDATE "), UPDATE_STEP_FAIL); + + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_FAILURE, + QString("Update process failed")); + if (std::optional s = m_ismasClient.finalResult(IsmasClient::RESULT_CODE::INSTALL_ERROR, + "FINAL-UPDATE-RESULT", + m_updateStatus.m_statusDescription)) { + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + s.value()); + } + } else { + emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); + + m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_SUCCESS, + QString("Update process succeeded")); + if (std::optional s = m_ismasClient.finalResult(IsmasClient::RESULT_CODE::SUCCESS, + "FINAL-UPDATE-RESULT", + m_updateStatus.m_statusDescription)) { + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + s.value()); + } + } if (!sentIsmasLastVersionNotification) { // try even if the backend is not connected sendIsmasLastVersionNotification(); } - if (m_returnCode != 0) { - stopProgressLoop(); - emit appendText(QString("UPDATE "), UPDATE_STEP_FAIL); - } else { - emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); - } - - setProgress(100); m_updateProcessRunning = false; emit enableExit(); emit restartExitTimer(); From 86c996d7acd3331c80f58583f936acea639d3b95 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 11 Aug 2023 12:24:51 +0200 Subject: [PATCH 150/239] Set final version to 1.1.1w --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 3c83520..2fe6d00 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -15,7 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -VERSION=1.0.0 +VERSION=1.1.1 INCLUDEPATH += plugins From 86064979b47ad88d8eb487a663cc0cb9613a79e9 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 14 Aug 2023 14:28:23 +0200 Subject: [PATCH 151/239] Add memeber-variable for exitCode of executed process. --- process/command.cpp | 5 +++-- process/command.h | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/process/command.cpp b/process/command.cpp index 4b9047a..3f0f3e9 100644 --- a/process/command.cpp +++ b/process/command.cpp @@ -8,7 +8,8 @@ Command::Command(QString const &command, int start_timeout, int finish_timeout) : m_command(command.trimmed()) , m_commandResult("") , m_waitForStartTimeout(start_timeout) - , m_waitForFinishTimeout(finish_timeout) { + , m_waitForFinishTimeout(finish_timeout) + , m_exitCode(-1) { } QString Command::getCommandResult() const { @@ -62,7 +63,7 @@ bool Command::execute(QString workingDirectory, QStringList args) { if (p->waitForFinished(m_waitForFinishTimeout)) { //qDebug() << "PROCESS" << m_command << "FINISHED"; if (p->exitStatus() == QProcess::NormalExit) { - if (p->exitCode() == 0) { + if ((m_exitCode = p->exitCode()) == 0) { return true; } else { qCritical() << "EXECUTED" << m_command << "with code" << p->exitCode(); diff --git a/process/command.h b/process/command.h index 6b79543..bc796ee 100644 --- a/process/command.h +++ b/process/command.h @@ -16,15 +16,17 @@ class Command : public QObject { QString m_commandResult; int m_waitForStartTimeout; int m_waitForFinishTimeout; - + int m_exitCode; public: explicit Command(QString const &command, int start_timeout = 100000, int finish_timeout = 100000); QString getCommandResult() const; + QString command() const { return m_command; } bool execute(QString workingDirectory, QStringList args = QStringList()); + int exitCode() const { return m_exitCode; } private slots: void readyReadStandardOutput(); From 66d02147200081c15e72c4e7a7a9eda4eaf8d635 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 14 Aug 2023 14:33:12 +0200 Subject: [PATCH 152/239] Always look for {"error": "ISMAS is offline"} first. Allow for empty update-trgger (try again) rsync: mkdir -p the necessary directories. --- worker.cpp | 73 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/worker.cpp b/worker.cpp index ddd17eb..b6a0f22 100644 --- a/worker.cpp +++ b/worker.cpp @@ -497,12 +497,14 @@ bool Worker::backendConnected() { return false; } +#define CHECK_UPDATE_TRIGGER_SET "Check update trigger ..." + bool Worker::updateTriggerSet() { if (m_withoutIsmasDirectPort) { // useful for testing return true; } - emit appendText("\nUpdate trigger set ..."); + emit appendText("\n" CHECK_UPDATE_TRIGGER_SET); QString triggerValue(""); for (int repeat = 0; repeat < 50; ++repeat) { @@ -518,7 +520,7 @@ bool Worker::updateTriggerSet() { QString msg = result.value(); - qInfo() << "REPEAT" << repeat << "APISM RESPONSE" << msg; + qInfo() << "REPEAT" << repeat << "APISM RESPONSE (" << msg << ")"; QJsonParseError parseError; QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); @@ -537,7 +539,7 @@ bool Worker::updateTriggerSet() { m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", QString("invalid json ") + msg.mid(0, 20)); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } if (!document.isObject()) { @@ -550,13 +552,25 @@ bool Worker::updateTriggerSet() { m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", QString("not a json object") + msg); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } progress += 1; setProgress(progress); QJsonObject obj = document.object(); + + // always look for an 'error' first + if (obj.contains("error")) { + progress += 1; + setProgress(progress); + QString value = obj.value("error").toString(); + emit showErrorMessage("check update trigger", QString("REPEAT %1 error=<").arg(repeat) + value + ">"); + qInfo() << "REPEAT" << repeat << "In updateTriggerSet() error=<" + << value << ">"; + QThread::sleep(6); + continue; + } // sanity check: cust_nr and machine_nr of PSA correct ? // note: this check has to be done here, as the cust_nr and the machine_nr // of the PSA are sent by ISMAS. @@ -579,7 +593,7 @@ bool Worker::updateTriggerSet() { m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } if (machineNr != m_machineNr) { @@ -592,7 +606,7 @@ bool Worker::updateTriggerSet() { m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } @@ -607,7 +621,7 @@ bool Worker::updateTriggerSet() { m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } } else { @@ -619,7 +633,7 @@ bool Worker::updateTriggerSet() { m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } } else { @@ -631,7 +645,7 @@ bool Worker::updateTriggerSet() { m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } progress += 1; @@ -664,19 +678,28 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateTriggerSet(m_updateStatus.m_statusDescription, "")); - emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_DONE); return true; + } else + if (QRegExp("\\s*").exactMatch(triggerValue)) { // check for whitespace + stopProgressLoop(); + int progress = (m_mainWindow->progressValue()/10) + 10; + progress += 1; + setProgress(progress); + emit showErrorMessage("check update trigger", "empty update-trigger"); + QThread::sleep(6); + continue; } else { // if the download-button once has the wrong value, it will never recover setProgress(100); m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE, - QString("TRIGGER-VALUE ") + triggerValue + " NOT 'WAIT'"); + QString("TRIGGER-VALUE=<") + triggerValue + "> NOT 'WAIT'"); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } } else { @@ -687,7 +710,7 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); return false; } @@ -699,7 +722,7 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); return false; } @@ -711,20 +734,10 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, m_updateStatus.m_statusDescription)); - emit replaceLast("Update trigger set ...", UPDATE_STEP_FAIL); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); emit showErrorMessage("check update trigger", m_updateStatus.m_statusDescription); return false; } - - if (obj.contains("error")) { - progress += 1; - setProgress(progress); - QString value = obj.value("error").toString(); - emit showErrorMessage("check update trigger", QString("REPEAT %1 error=<").arg(repeat) + value + ">"); - qInfo() << "REPEAT" << repeat << "In updateTriggerSet() error=<" - << value << ">"; - QThread::sleep(6); - } } else { stopProgressLoop(); int progress = (m_mainWindow->progressValue()/10) + 10; @@ -743,7 +756,7 @@ bool Worker::updateTriggerSet() { QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.errorUpdateTrigger(m_updateStatus.m_statusDescription, "")); - emit replaceLast("Update trigger set ...", UPDATE_STEP_OK); + emit replaceLast(CHECK_UPDATE_TRIGGER_SET, UPDATE_STEP_FAIL); return false; } @@ -919,10 +932,16 @@ bool Worker::updateFiles(quint8 percent) { } bool Worker::syncCustomerRepositoryAndFS() { + // this step is currently needed only for updating tariff-files setProgress(0); emit appendText("\nSync customer environment with filesystem ..."); if (QDir(m_customerRepository).exists()) { if (QDir::setCurrent(m_customerRepository)) { + Command md("bash"); + if (!md.execute(m_customerRepository, + QStringList() << "-c" << "mkdir -p /etc/psa_config /etc/psa_update /etc/dc /etc/psa_tariff")) { + qCritical() << "COULD NOT EXECUTE '" << md.command() << "' exitCode=(" << md.exitCode() << ")"; + } int progress = 10; setProgress(progress); QString const params("-vv " @@ -953,7 +972,7 @@ bool Worker::syncCustomerRepositoryAndFS() { } } else { Utils::printCriticalErrorMsg(QString("CMD ") + cmd + " FAILED: " - + c.getCommandResult()); + + c.getCommandResult() + QString(" EXIT_CODE=(%1)").arg(c.exitCode())); error = true; } } From 8d528f0f554ec071eb77d8cdf4a20476c059b767 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 14 Aug 2023 14:35:54 +0200 Subject: [PATCH 153/239] SO_SNDTIMEO and SO_RCVTIMEO socket options have turned out not to be reliable. Use select() for detecting timeout on socket (read and write). --- ismas/ismas_client.cpp | 146 ++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 38 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index d405da7..8e94f5b 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -137,7 +137,12 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { so_linger.l_onoff = 1; so_linger.l_linger = 0; + int maxfdp1; + fd_set rset; + fd_set wset; + setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger)); + // no reliable, but does not harm, as we use select() as well setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); int flag = 1; @@ -151,24 +156,56 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { int loop = 0; int bytesWritten = 0; while (bytesWritten < bytesToWrite) { - int n = ::sendto(sockfd, buf+bytesWritten, bytesToWrite-bytesWritten, 0, NULL, 0); - if (n >= 0) { - bytesWritten += n; - } else { - if (errno == EWOULDBLOCK) { - if (++loop < 10) { - QThread::msleep(500); - continue; - } - printErrorMessage(port, clientIP, clientPort, - QString("WRITE TIMEOUT %1(").arg(loop) + strerror(errno) + ")"); - ::close(sockfd); - return std::nullopt; - } else + errno = 0; + FD_ZERO(&wset); + FD_SET(sockfd, &wset); + maxfdp1 = sockfd + 1; + tv.tv_sec = 60; /* 60 secs timeout for read and write -> APISM cuts the connection after 30s */ + tv.tv_usec = 0; + + int const w = select(maxfdp1, NULL, &wset, NULL, &tv); + if (w < 0) { // error if (errno == EINTR) { printErrorMessage(port, clientIP, clientPort, - QString("WRITE INTERRUPTED BY SIGNAL (1) (") + strerror(errno) + ")"); + QString("INTERRUPTED BY SIGNAL (1) (") + strerror(errno) + ")"); continue; + } else { + printErrorMessage(port, clientIP, clientPort, + QString("SELECT-ERROR (WRITE) %1(").arg(loop) + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } + } else + if (w == 0) { // timeout + printErrorMessage(port, clientIP, clientPort, + QString("SELECT-TIMEOUT (WRITE) %1(").arg(loop) + strerror(errno) + ")"); + if (++loop < 10) { + QThread::msleep(500); + continue; + } + ::close(sockfd); + return std::nullopt; + } else + if (w > 0) { + int n = ::sendto(sockfd, buf+bytesWritten, bytesToWrite-bytesWritten, 0, NULL, 0); + if (n >= 0) { + bytesWritten += n; + } else { + if (errno == EWOULDBLOCK) { + if (++loop < 10) { + QThread::msleep(500); + continue; + } + printErrorMessage(port, clientIP, clientPort, + QString("WRITE TIMEOUT %1(").arg(loop) + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } else + if (errno == EINTR) { + printErrorMessage(port, clientIP, clientPort, + QString("WRITE INTERRUPTED BY SIGNAL (1) (") + strerror(errno) + ")"); + continue; + } } } } @@ -188,33 +225,66 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { int bytesRead = 0; while (bytesRead < bytesToRead) { errno = 0; - int n = ::recvfrom(sockfd, buf+bytesRead, bytesToRead-bytesRead, - 0, NULL, NULL); - if (n > 0) { // - bytesRead += n; - } else - if (n == 0) { - // The return value will be 0 when the peer has performed an orderly shutdown. - printErrorMessage(port, clientIP, clientPort, - QString("PEER CLOSED CONNECTION (") + strerror(errno) + ")"); - ::close(sockfd); - return std::nullopt; - } else - if (n < 0) { - if (errno == EWOULDBLOCK) { - if (++loop < 10) { - QThread::msleep(500); - continue; - } - printErrorMessage(port, clientIP, clientPort, - QString("READ TIMEOUT %1(").arg(loop) + strerror(errno) + ")"); - ::close(sockfd); - return std::nullopt; - } + FD_ZERO(&rset); + FD_SET(sockfd, &rset); + maxfdp1 = sockfd + 1; + tv.tv_sec = 60; /* 60 secs timeout for read and write */ + tv.tv_usec = 0; + + int const r = select(maxfdp1, &rset, NULL, NULL, &tv); + if (r < 0) { // error if (errno == EINTR) { printErrorMessage(port, clientIP, clientPort, QString("INTERRUPTED BY SIGNAL (2) (") + strerror(errno) + ")"); continue; + } else { + printErrorMessage(port, clientIP, clientPort, + QString("SELECT-ERROR (READ) %1(").arg(loop) + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } + } else + if (r == 0) { // timeout + printErrorMessage(port, clientIP, clientPort, + QString("SELECT-TIMEOUT (READ) %1(").arg(loop) + strerror(errno) + ")"); + if (++loop < 10) { + QThread::msleep(500); + continue; + } + ::close(sockfd); + return std::nullopt; + } else + if (r > 0) { + if (FD_ISSET(sockfd, &rset)) { + int n = ::recvfrom(sockfd, buf+bytesRead, bytesToRead-bytesRead, + 0, NULL, NULL); + if (n > 0) { // + bytesRead += n; + } else + if (n == 0) { + // The return value will be 0 when the peer has performed an orderly shutdown. + printErrorMessage(port, clientIP, clientPort, + QString("PEER CLOSED CONNECTION (") + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } else + if (n < 0) { + if (errno == EWOULDBLOCK) { // check just in case + if (++loop < 10) { + QThread::msleep(500); + continue; + } + printErrorMessage(port, clientIP, clientPort, + QString("READ TIMEOUT %1(").arg(loop) + strerror(errno) + ")"); + ::close(sockfd); + return std::nullopt; + } + if (errno == EINTR) { + printErrorMessage(port, clientIP, clientPort, + QString("INTERRUPTED BY SIGNAL (2) (") + strerror(errno) + ")"); + continue; + } + } } } From 259da8200e00e9e4cb4ca3b300aa8bfcd31007d3 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 14 Aug 2023 15:08:00 +0200 Subject: [PATCH 154/239] Fixed known defaults for starting ATBUpdateTool. --- main.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/main.cpp b/main.cpp index eaf6a9f..1317043 100644 --- a/main.cpp +++ b/main.cpp @@ -65,7 +65,7 @@ int main(int argc, char *argv[]) { QCommandLineOption pluginDirectoryOption(QStringList() << "plugin-directory" << "plugin-directory", QCoreApplication::translate("main", "Where to find dc-plugin."), QCoreApplication::translate("main", "directory")); - QString const pluginDefault = "./plugins"; + QString const pluginDefault = "/usr/lib"; pluginDirectoryOption.setDefaultValue(pluginDefault); parser.addOption(pluginDirectoryOption); @@ -79,7 +79,7 @@ int main(int argc, char *argv[]) { QCommandLineOption workingDirectoryOption(QStringList() << "working-directory" << "working-directory", QCoreApplication::translate("main", "working directory of update-script."), QCoreApplication::translate("main", "directory")); - QString const workingDirectoryDefault = "."; + QString const workingDirectoryDefault = "/opt/app/tools/atbupdate"; workingDirectoryOption.setDefaultValue(workingDirectoryDefault); parser.addOption(workingDirectoryOption); @@ -87,14 +87,6 @@ int main(int argc, char *argv[]) { QCoreApplication::translate("main", "Start ATBUpdateTool in dry-run-mode. No actual actions.")); parser.addOption(dryRunOption); - // TODO: - // add some additional parameters - // --dry-run - // -d: only update device-controller firmware - // -j: only update json-files - // -o: only execute opkg-commnds - // -f: force. update_psa shall always perform a 'git pull' - // Process the actual command line arguments given by the user parser.process(a); QString plugInDir = parser.value(pluginDirectoryOption); @@ -103,9 +95,6 @@ int main(int argc, char *argv[]) { bool dryRun = parser.isSet(dryRunOption); QString const rtPath = QCoreApplication::applicationDirPath(); - if (plugInDir == pluginDefault) { - plugInDir = (rtPath + "/" + pluginDefault); - } if (!QDir(plugInDir).exists()) { qCritical() << plugInDir << "does not exists, but has to contain dc-library"; From ad93e536f03b760625e0897d1761be400a485bda Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 10:33:08 +0200 Subject: [PATCH 155/239] Added gitShowReason(): get lastCommit, message and date of last commit to insert inti sendLastVersion-message to ISMAS. --- git/git_client.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ git/git_client.h | 1 + 2 files changed, 42 insertions(+) diff --git a/git/git_client.cpp b/git/git_client.cpp index 78defa4..35475f2 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -114,6 +114,47 @@ bool GitClient::gitCloneAndCheckoutBranch() { return false; } +QStringList GitClient::gitShowReason() { + QStringList lst; + if (QDir(m_customerRepository).exists()) { + // %h: commit (short form) + // %s: commit message + // %cI: commit date, strict ISO 8601 format + Command c("git show -s --format=\"c=%h m=%s d=%cI\""); + if (c.execute(m_customerRepository)) { + QString const s = c.getCommandResult().trimmed(); + int const c = s.indexOf("c="); + int const m = s.indexOf("m="); + int const d = s.indexOf("d="); + QString commit{""}, msg{""}, date{""}; + if (c != -1) { + int start = c + 2; + if (m >= start) { + int length = m - start; + commit = s.mid(start, length).trimmed(); + + start = m + 2; + if (d >= start) { + length = d - start; + msg = s.mid(start, length).trimmed(); + + start = d + 2; + date = s.mid(start); + } + } + + if (!commit.isEmpty() && !msg.isEmpty() && !date.isEmpty()) { + lst << commit << msg << date; + } + } + } + } else { + qCritical() << "CUSTOMER_REPOSITORY" << m_customerRepository + << "DOES NOT EXIST"; + } + return lst; +} + /* Zu beachten: wird eine datei neu hinzugefuegt (git add/commit) dann aber gleich wieder geloscht, so wird sie im diff nicht angezeigt. diff --git a/git/git_client.h b/git/git_client.h index 7da27c4..0faca95 100644 --- a/git/git_client.h +++ b/git/git_client.h @@ -50,6 +50,7 @@ class GitClient : public QObject { std::optional gitMerge(); QString gitLastCommit(QString fileName); + QStringList gitShowReason(); QString gitBlob(QString fileName); QString gitCommitForBlob(QString blob); bool gitIsFileTracked(QString file2name); From 7631c05e22f2b53e223a228febb7c588b626c681 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 10:34:55 +0200 Subject: [PATCH 156/239] Insert last commit-id, message and date of last commit into sendLastVersion-message to ISMAS. --- ismas/ismas_client.cpp | 14 ++++++++------ ismas/ismas_client.h | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 8e94f5b..170eea3 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -231,6 +231,8 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { tv.tv_sec = 60; /* 60 secs timeout for read and write */ tv.tv_usec = 0; + QString const selectStart = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + int const r = select(maxfdp1, &rset, NULL, NULL, &tv); if (r < 0) { // error if (errno == EINTR) { @@ -264,7 +266,8 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { if (n == 0) { // The return value will be 0 when the peer has performed an orderly shutdown. printErrorMessage(port, clientIP, clientPort, - QString("PEER CLOSED CONNECTION (") + strerror(errno) + ")"); + QString("PEER CLOSED CONNECTION (") + strerror(errno) + ") START AT" + + selectStart + " NOW " + QDateTime::currentDateTime().toString(Qt::ISODateWithMs)); ::close(sockfd); return std::nullopt; } else @@ -415,13 +418,11 @@ QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { static char buf[4096*2]; memset(buf, 0, sizeof(buf)); - QString const ts = QDateTime::currentDateTime().toString(Qt::ISODateWithMs); - QString sendVersionHash = "N/A"; - // local data="#M=APISM#C=CMD_SENDVERSION#J= snprintf(buf, sizeof(buf)-1, "{" "\"VERSION_INFO\" : {" + "\"REASON\":\"%s\"," "\"CREATED\":\"%s\"," "\"HASH\":\"%s\"" "}," @@ -629,8 +630,9 @@ QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { "}" "}" "}", - ts.toStdString().c_str(), - sendVersionHash.toStdString().c_str(), + psa.versionInfo.reason.toStdString().c_str(), + psa.versionInfo.created.toStdString().c_str(), + psa.versionInfo.lastCommit.toStdString().c_str(), psa.tariff.version.toStdString().c_str(), psa.tariff.project.toStdString().c_str(), diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index 97374b8..e5cc70f 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -7,6 +7,12 @@ #include struct PSAInstalled { + struct VersionInfo { + QString created; + QString reason; + QString lastCommit; + } versionInfo; + struct Tariff { QString name; QString version; From f20be9ddcfef700ae5777b1ac3842d07c057129c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 10:37:31 +0200 Subject: [PATCH 157/239] Removed obsolete sendCmdSendVersionToIsmas(). --- worker.h | 1 - 1 file changed, 1 deletion(-) diff --git a/worker.h b/worker.h index 28db75d..c5a8cc2 100644 --- a/worker.h +++ b/worker.h @@ -216,7 +216,6 @@ private slots: private: PSAInstalled getPSAInstalled(); - QString sendCmdSendVersionToIsmas(); void privateUpdate(); }; From 1eba5338e47d30e56bf5e0b053fe4b3235ea8908 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 10:37:59 +0200 Subject: [PATCH 158/239] Removed obsolete sendCmdSendVersionToIsmas(). --- worker.cpp | 78 ------------------------------------------------------ 1 file changed, 78 deletions(-) diff --git a/worker.cpp b/worker.cpp index b6a0f22..f31df53 100644 --- a/worker.cpp +++ b/worker.cpp @@ -1179,84 +1179,6 @@ PSAInstalled Worker::getPSAInstalled() { return psaInstalled; } -QString Worker::sendCmdSendVersionToIsmas() { - - QStringList const dcVersion = getDCVersion(); - QString const deviceControllerVersionHW = dcVersion.first(); - QString const deviceControllerVersionSW = dcVersion.last(); - - qInfo() << "CURRENT DC-HW-VERSION: " << deviceControllerVersionHW; - qInfo() << "CURRENT DC-SW-VERSION: " << deviceControllerVersionSW; - - QString const deviceControllerGitBlob = "N/A"; - QString const deviceControllerGitLastCommit = "N/A"; - - PSAInstalled psaInstalled; - QString printSysDir("/etc/psa_config"); - QString tariffSysDir("/etc/psa_tariff"); - QString absPathName; - - if (m_zoneNr != 0) { - QString const &n = QString("%1").arg(m_zoneNr).rightJustified(2, '0'); - psaInstalled.tariff.name = QString("tariff%1.json").arg(n); - absPathName = QDir::cleanPath(tariffSysDir + QDir::separator() + psaInstalled.tariff.name); - psaInstalled.tariff.blob = m_gc.gitBlob(absPathName); - psaInstalled.tariff.size = getFileSize(absPathName); - psaInstalled.tariff.zone = m_zoneNr; - } - psaInstalled.tariff.project = "Szeged"; - psaInstalled.tariff.info = "N/A"; - psaInstalled.tariff.loadTime = "N/A"; // QDateTime::currentDateTime().toString(Qt::ISODateWithMs); - psaInstalled.tariff.version = "N/A"; - - psaInstalled.hw.linuxVersion = m_osVersion; - psaInstalled.hw.cpuSerial = m_cpuSerial; - - psaInstalled.dc.versionHW = deviceControllerVersionHW; - psaInstalled.dc.versionSW = deviceControllerVersionSW; - psaInstalled.dc.gitBlob = "N/A"; - psaInstalled.dc.gitLastCommit = "N/A"; - psaInstalled.dc.size = -1; - - psaInstalled.sw.raucVersion = m_raucVersion; - psaInstalled.sw.opkgVersion = m_opkgVersion; - psaInstalled.sw.atbQTVersion = m_atbqtVersion; - - psaInstalled.pluginVersion.deviceController = m_pluginVersionATBDeciceController; - psaInstalled.pluginVersion.ingenicoISelfCC = m_pluginVersionIngenicoISelf; - psaInstalled.pluginVersion.mobilisisCalculatePrice = m_pluginVersionMobilisisCalc; - psaInstalled.pluginVersion.mobilisisCalculatePriceConfigUi = m_pluginVersionMobilisisCalcConfig; - psaInstalled.pluginVersion.prmCalculatePrice = m_pluginVersionPrmCalc; - psaInstalled.pluginVersion.prmCalculatePriceConfigUi = m_pluginVersionPrmCalcConfig; - psaInstalled.pluginVersion.tcpZVT = m_pluginVersionTcpZvt; - - psaInstalled.cash.name = "DC2C_cash.json"; - absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.cash.name); - psaInstalled.cash.blob = m_gc.gitBlob(absPathName); - psaInstalled.cash.size = getFileSize(absPathName); - - psaInstalled.conf.name = "DC2C_conf.json"; - absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.conf.name); - psaInstalled.conf.blob = m_gc.gitBlob(absPathName); - psaInstalled.conf.size = getFileSize(absPathName); - - psaInstalled.device.name = "DC2C_device.json"; - absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.device.name); - psaInstalled.device.blob = m_gc.gitBlob(absPathName); - psaInstalled.device.size = getFileSize(absPathName); - - for (int i=0; i < 32; ++i) { - QString const &n = QString("%1").arg(i+1).rightJustified(2, '0'); - psaInstalled.print[i].name = QString("DC2C_print%1.json").arg(n); - absPathName = QDir::cleanPath(printSysDir + QDir::separator() + psaInstalled.print[i].name); - psaInstalled.print[i].blob = m_gc.gitBlob(absPathName); - psaInstalled.print[i].size = getFileSize(absPathName); - } - - // QByteArray data = "#M=APISM#C=CMD_SENDVERSION#J="; - return m_ismasClient.updateOfPSASendVersion(psaInstalled); -} - /************************************************************************************************ * operators */ From e700a408752d764ef175092d777c28db7dfdfe41 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 10:38:36 +0200 Subject: [PATCH 159/239] Initialize psaInstalled.versionInfo to be sent to ISMAS in sendLastVersion(). --- worker.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/worker.cpp b/worker.cpp index f31df53..f72be12 100644 --- a/worker.cpp +++ b/worker.cpp @@ -1117,6 +1117,17 @@ PSAInstalled Worker::getPSAInstalled() { QString absPathName; QString absPathNameRepository; + psaInstalled.versionInfo.lastCommit = ""; + psaInstalled.versionInfo.reason = ""; + psaInstalled.versionInfo.created = ""; + + QStringList versionInfo = m_gc.gitShowReason(); + if (versionInfo.size() == 3) { + psaInstalled.versionInfo.lastCommit = versionInfo.at(0); + psaInstalled.versionInfo.reason = versionInfo.at(1); + psaInstalled.versionInfo.created = versionInfo.at(2); + } + if (m_zoneNr != 0) { QString const &n = QString("%1").arg(m_zoneNr).rightJustified(2, '0'); psaInstalled.tariff.name = QString("tariff%1.json").arg(n); From 0f2ee0349fd2a0c275eee4885e3c489d43e8b734 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 10:39:46 +0200 Subject: [PATCH 160/239] Improved debug output. --- process/command.cpp | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/process/command.cpp b/process/command.cpp index 3f0f3e9..22b03a4 100644 --- a/process/command.cpp +++ b/process/command.cpp @@ -2,6 +2,7 @@ #include #include +#include #include Command::Command(QString const &command, int start_timeout, int finish_timeout) @@ -40,46 +41,64 @@ void Command::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { } bool Command::execute(QString workingDirectory, QStringList args) { + + if (!QDir::setCurrent(workingDirectory)) { + qCritical() << "SET WORKING_DIRECTORY" << workingDirectory + << "FAILED FOR" << m_command; + return false; + } + QScopedPointer p(new QProcess(this)); + p->setWorkingDirectory(workingDirectory); p->setProcessChannelMode(QProcess::MergedChannels); connect(&(*p), SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); connect(&(*p), SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); - //qCritical() << "START COMMAND" << m_command << "WITH ARGS" << args - // << "IN" << workingDirectory; - - p->setWorkingDirectory(workingDirectory); if (!args.isEmpty()) { + qDebug() << "START COMMAND" << m_command << "WITH ARGS" << args + << "IN" << p->workingDirectory(); p->start(m_command, args); } else { + qDebug() << "START COMMAND" << m_command + << "IN" << p->workingDirectory(); p->start(m_command); } if (p->waitForStarted(m_waitForStartTimeout)) { - //qDebug() << "PROCESS" << m_command << "STARTED"; + qDebug() << "PROCESS" << m_command << "STARTED IN" << p->workingDirectory(); if (p->state() == QProcess::ProcessState::Running) { - //qDebug() << "PROCESS" << m_command << "RUNNING"; + qDebug() << "PROCESS" << m_command << "RUNNING IN" << p->workingDirectory(); if (p->waitForFinished(m_waitForFinishTimeout)) { - //qDebug() << "PROCESS" << m_command << "FINISHED"; + qDebug() << "PROCESS" << m_command << "FINISHED IN" << p->workingDirectory(); if (p->exitStatus() == QProcess::NormalExit) { if ((m_exitCode = p->exitCode()) == 0) { + qDebug() << "EXECUTED" << m_command + << "with code" << m_exitCode + << "IN" << p->workingDirectory(); return true; } else { - qCritical() << "EXECUTED" << m_command << "with code" << p->exitCode(); + qCritical() << "EXECUTED" << m_command + << "with code" << m_exitCode + << "IN" << p->workingDirectory(); } } else { qCritical() << "PROCESS" << m_command << "CRASHED with code" - << p->exitCode(); + << p->exitCode() + << "IN" << p->workingDirectory(); } } else { - qCritical() << "PROCESS" << m_command << "DID NOT FINISH"; + qCritical() << "PROCESS" << m_command + << "DID NOT FINISH" + << "IN" << p->workingDirectory(); } } else { - qCritical() << "WRONG PROCESS STATE" << p->state(); + qCritical() << "WRONG PROCESS STATE" << p->state() + << "IN" << p->workingDirectory(); } } else { - qCritical() << "PROCESS" << m_command << "TIMEOUT AT START"; + qCritical() << "PROCESS" << m_command << "TIMEOUT AT START" + << "IN" << p->workingDirectory(); } return false; } From 9b4d0494c81cd66a756ba0d0db95ff7351ffccb8 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 12:41:42 +0200 Subject: [PATCH 161/239] Streamlined handling of UPDATE_STATUS. --- worker.cpp | 254 ++++++++++++++++++++++++++++++++++++++--------------- worker.h | 36 +------- 2 files changed, 187 insertions(+), 103 deletions(-) diff --git a/worker.cpp b/worker.cpp index f72be12..4c46673 100644 --- a/worker.cpp +++ b/worker.cpp @@ -169,7 +169,7 @@ void Worker::privateUpdate() { int progress = (m_mainWindow->progressValue()/10) + 10; setProgress(progress); - m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_SUCCESS, + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_SUCCESS, QString("CLONED AND CHECKED OUT: ") + m_customerRepository); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, @@ -197,7 +197,7 @@ void Worker::privateUpdate() { emit replaceLast("Initializing customer environment", UPDATE_STEP_FAIL); - m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_FAILURE, + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_FAILURE, QString("CLONE OR CHECKOUT FAILED: ") + m_customerRepository); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + @@ -295,7 +295,7 @@ void Worker::privateUpdate() { m_returnCode = -7; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH_REQUEST_FAILURE, + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH_FAILURE, QString("Configuring customer environment failed")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + @@ -315,7 +315,7 @@ void Worker::privateUpdate() { m_returnCode = -5; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_BACKEND_CHECK_FAILURE, + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK_FAILURE, QString("ISMAS backend not available")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + @@ -373,7 +373,7 @@ bool Worker::backendConnected() { emit appendText("\nConnecting backend ..."); if (false) { // so linker removes dead code - for (int repeat = 0; repeat < 50; ++repeat) { + for (int repeat = 0; repeat < 100; ++repeat) { qInfo() << "REPEAT" << repeat << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; startProgressLoop(); std::optional result @@ -507,7 +507,7 @@ bool Worker::updateTriggerSet() { emit appendText("\n" CHECK_UPDATE_TRIGGER_SET); QString triggerValue(""); - for (int repeat = 0; repeat < 50; ++repeat) { + for (int repeat = 0; repeat < 100; ++repeat) { qInfo() << "UPDATE TRIGGER SET -> REPEAT" << repeat; startProgressLoop(); @@ -672,7 +672,7 @@ bool Worker::updateTriggerSet() { m_ismasClient.updateOfPSAContinues("MACHINE-AND-CUSTOMER-CHECK", m_updateStatus.m_statusDescription)); - m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_SET, + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET, QString("UPDATE TRIGGER SET. CONTINUE. ")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + @@ -749,8 +749,8 @@ bool Worker::updateTriggerSet() { } setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_TRIGGER_NOT_SET_OR_WRONG, - QString("UPDATE-TRIGGER-NOT-SET-OR-WRONG: VALUE=(") + + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_NOT_SET_OR_WRONG, + QString("ISMAS_UPDATE-TRIGGER-NOT-SET-OR-WRONG: VALUE=(") + triggerValue + ")"); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + @@ -1195,6 +1195,90 @@ PSAInstalled Worker::getPSAInstalled() { */ QDebug operator<< (QDebug debug, UpdateStatus status) { switch(status.m_updateStatus) { + case UPDATE_STATUS::ISMAS_SEND_LAST_VERSION_FAILED: + debug << QString("UPDATE_STATUS::ISMAS_SEND_LAST_VERSION_FAILED: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::SAVE_LOG_FILES_FAILED: + debug << QString("UPDATE_STATUS::SAVE_LOG_FILES_FAILED: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CHECK_FILES_TO_UPDATE_SUCCESS: + debug << QString("UPDATE_STATUS::GIT_CHECK_FILES_TO_UPDATE_SUCCESS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::PSA_UPDATE_FILES_FAILED: + debug << QString("UPDATE_STATUS::PSA_UPDATE_FILES_FAILED: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::RSYNC_UPDATES_SUCCESS: + debug << QString("UPDATE_STATUS::RSYNC_UPDATES_SUCCESS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::RSYNC_UPDATES_FAILURE: + debug << QString("UPDATE_STATUS::RSYNC_UPDATES_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMAND: + debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMAND: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_SUCCESS: + debug << QString("UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_SUCCESS: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_FAILURE: + debug << QString("UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::NOT_DEFINED: + debug << QString("UPDATE_STATUS::NOT_DEFINED: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::UPDATE_PROCESS_FAILURE: + debug << QString("UPDATE_STATUS::UPDATE_PROCESS_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET_FAILURE: + debug << QString("UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CHECKOUT_BRANCH_FAILURE: + debug << QString("UPDATE_STATUS::GIT_CHECKOUT_BRANCH_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CHECKOUT_BRANCH: + debug << QString("UPDATE_STATUS::GIT_CHECKOUT_BRANCH: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_NOT_SET_OR_WRONG: + debug << QString("UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_NOT_SET_OR_WRONG: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET: + debug << QString("UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_SANITY_CHECK_OK: + debug << QString("UPDATE_STATUS::ISMAS_SANITY_CHECK_OK: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::JSON_PARSE_FAILURE: + debug << QString("UPDATE_STATUS::JSON_PARSE_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::BACKEND_CHECK: + debug << QString("UPDATE_STATUS::BACKEND_CHECK: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::BACKEND_CHECK_FAILURE: + debug << QString("UPDATE_STATUS::BACKEND_CHECK_FAILURE: ") + << status.m_statusDescription; + break; + case UPDATE_STATUS::BACKEND_NOT_CONNECTED: + debug << QString("UPDATE_STATUS::BACKEND_NOT_CONNECTED: ") + << status.m_statusDescription; + break; case UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: debug << QString("UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: ") << status.m_statusDescription; @@ -1207,14 +1291,6 @@ QDebug operator<< (QDebug debug, UpdateStatus status) { debug << QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE: ") << status.m_statusDescription; break; - case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_TIMEOUT: - debug << QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_: ") - << status.m_statusDescription; - break; - case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: - debug << QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: ") - << status.m_statusDescription; - break; case UPDATE_STATUS::GIT_FETCH_UPDATES: debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES: ") << status.m_statusDescription; @@ -1223,45 +1299,109 @@ QDebug operator<< (QDebug debug, UpdateStatus status) { debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: ") << status.m_statusDescription; break; - case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: - debug << QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: ") - << status.m_statusDescription; - break; case UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: debug << QString("UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: ") << status.m_statusDescription; break; - case UPDATE_STATUS::GIT_PULL_UPDATES_SUCCESS: - debug << QString("UPDATE_STATUS::GIT_PULL_UPDATES_REQUEST: ") - << status.m_statusDescription; - break; - case UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: - debug << QString("UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: ") - << status.m_statusDescription; - break; case UPDATE_STATUS::EXEC_OPKG_COMMANDS: debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS: ") << status.m_statusDescription; break; - case UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: - debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: ") - << status.m_statusDescription; - break; - case UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: - debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: ") - << status.m_statusDescription; - break; - case UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: - debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: ") - << status.m_statusDescription; - break; - default:; + // default:; } return debug; } QString& operator<< (QString& str, UpdateStatus status) { switch(status.m_updateStatus) { + case UPDATE_STATUS::ISMAS_SEND_LAST_VERSION_FAILED: + str = QString("UPDATE_STATUS::ISMAS_SEND_LAST_VERSION_FAILED: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::SAVE_LOG_FILES_FAILED: + str = QString("UPDATE_STATUS::SAVE_LOG_FILES_FAILED: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CHECK_FILES_TO_UPDATE_SUCCESS: + str = QString("UPDATE_STATUS::GIT_CHECK_FILES_TO_UPDATE_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::PSA_UPDATE_FILES_FAILED: + str = QString("UPDATE_STATUS::PSA_UPDATE_FILES_FAILED: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::RSYNC_UPDATES_SUCCESS: + str = QString("UPDATE_STATUS::RSYNC_UPDATES_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::RSYNC_UPDATES_FAILURE: + str = QString("UPDATE_STATUS::RSYNC_UPDATES_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::EXEC_OPKG_COMMAND: + str = QString("UPDATE_STATUS::EXEC_OPKG_COMMAND: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_SUCCESS: + str = QString("UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_SUCCESS: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_FAILURE: + str = QString("UPDATE_STATUS::GIT_CLONE_AND_CHECKOUT_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::NOT_DEFINED: + str = QString("UPDATE_STATUS::NOT_DEFINED: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::UPDATE_PROCESS_FAILURE: + str = QString("UPDATE_STATUS::UPDATE_PROCESS_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET_FAILURE: + str = QString("UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CHECKOUT_BRANCH_FAILURE: + str = QString("UPDATE_STATUS::GIT_CHECKOUT_BRANCH_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::GIT_CHECKOUT_BRANCH: + str = QString("UPDATE_STATUS::GIT_CHECKOUT_BRANCH: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_NOT_SET_OR_WRONG: + str = QString("UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_NOT_SET_OR_WRONG: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE: + str = QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET: + str = QString("UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::ISMAS_SANITY_CHECK_OK: + str = QString("UPDATE_STATUS::ISMAS_SANITY_CHECK_OK: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::JSON_PARSE_FAILURE: + str = QString("UPDATE_STATUS::JSON_PARSE_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::BACKEND_CHECK: + str = QString("UPDATE_STATUS::BACKEND_CHECK: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::BACKEND_CHECK_FAILURE: + str = QString("UPDATE_STATUS::BACKEND_CHECK_FAILURE: "); + str += status.m_statusDescription; + break; + case UPDATE_STATUS::BACKEND_NOT_CONNECTED: + str = QString("UPDATE_STATUS::BACKEND_NOT_CONNECTED: "); + str += status.m_statusDescription; + break; case UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: str = QString("UPDATE_STATUS::UPDATE_PROCESS_SUCCESS: "); str += status.m_statusDescription; @@ -1270,10 +1410,6 @@ QString& operator<< (QString& str, UpdateStatus status) { str = QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_PENDING: "); str += status.m_statusDescription; break; - case UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: - str = QString("UPDATE_STATUS::ISMAS_WAIT_STATE_CHECK_SUCCESS: "); - str += status.m_statusDescription; - break; case UPDATE_STATUS::GIT_FETCH_UPDATES: str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES: "); str += status.m_statusDescription; @@ -1282,18 +1418,6 @@ QString& operator<< (QString& str, UpdateStatus status) { str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE: "); str += status.m_statusDescription; break; - case UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: - str = QString("UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_SUCCESS: "); - str += status.m_statusDescription; - break; - case UPDATE_STATUS::GIT_PULL_UPDATES_SUCCESS: - str = QString("UPDATE_STATUS::GIT_PULL_UPDATES_SUCCESS: "); - str += status.m_statusDescription; - break; - case UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: - str = QString("UPDATE_STATUS::GIT_PULL_UPDATES_FAILURE: "); - str += status.m_statusDescription; - break; case UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: str = QString("UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: "); str += status.m_statusDescription; @@ -1302,19 +1426,7 @@ QString& operator<< (QString& str, UpdateStatus status) { str = QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS: "); str += status.m_statusDescription; break; - case UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: - str = QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS_SUCCESS: "); - str += status.m_statusDescription; - break; - case UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: - str = QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_SUCCESS: "); - str += status.m_statusDescription; - break; - case UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: - str = QString("UPDATE_STATUS::EXEC_OPKG_COMMAND_FAILURE: "); - str += status.m_statusDescription; - break; - default:; + //default:; } return str; } diff --git a/worker.h b/worker.h index c5a8cc2..c6b8557 100644 --- a/worker.h +++ b/worker.h @@ -23,55 +23,27 @@ enum class UPDATE_STATUS : quint8 { NOT_DEFINED, - STEP_OK, - STEP_DONE, - STEP_FAIL, ISMAS_WAIT_STATE_CHECK_PENDING, ISMAS_WAIT_STATE_CHECK_FAILURE, - ISMAS_WAIT_STATE_CHECK_TIMEOUT, - ISMAS_WAIT_STATE_CHECK_SUCCESS, ISMAS_RESPONSE_RECEIVED, - BACKEND_CONNECTED, BACKEND_CHECK, BACKEND_CHECK_FAILURE, - ISMAS_BACKEND_CHECK_FAILURE, BACKEND_NOT_CONNECTED, - UPDATE_TRIGGER_SET, - UPDATE_TRIGGER_NOT_SET_OR_WRONG, + ISMAS_UPDATE_TRIGGER_SET, + ISMAS_UPDATE_TRIGGER_NOT_SET_OR_WRONG, GIT_CLONE_AND_CHECKOUT_SUCCESS, GIT_CLONE_AND_CHECKOUT_FAILURE, GIT_CHECKOUT_BRANCH, - GIT_CHECKOUT_BRANCH_REQUEST_FAILURE, - GIT_CHECKOUT_BRANCH_NOT_EXISTS, - GIT_CHECKOUT_BRANCH_CHECKOUT_ERROR, + GIT_CHECKOUT_BRANCH_FAILURE, GIT_FETCH_UPDATES, GIT_FETCH_UPDATES_REQUEST_FAILURE, - GIT_FETCH_UPDATES_REQUEST_SUCCESS, - GIT_PULL_UPDATES_SUCCESS, - GIT_PULL_UPDATES_FAILURE, EXEC_OPKG_COMMAND, EXEC_OPKG_COMMANDS, - EXEC_OPKG_COMMAND_FAILURE, - EXEC_OPKG_COMMAND_SUCCESS, - EXEC_OPKG_COMMANDS_SUCCESS, - RSYNC_UPDATES, RSYNC_UPDATES_FAILURE, - RSYNC_UPDATES_SUCESS, - DEVICE_CONTROLLER_UPDATE, - DEVICE_CONTROLLER_UPDATE_FAILURE, - DEVICE_CONTROLLER_UPDATE_SUCCESS, - JSON_UPDATE, - JSON_UPDATE_FAILURE, + RSYNC_UPDATES_SUCCESS, JSON_PARSE_FAILURE, - JSON_UPDATE_SUCCESS, UPDATE_PROCESS_SUCCESS, UPDATE_PROCESS_FAILURE, - ISMAS_UPDATE_INFO_CONFIRM, - ISMAS_UPDATE_INFO_CONFIRM_FAILURE, - ISMAS_UPDATE_INFO_CONFIRM_SUCCESS, - ISMAS_CURRENT_PSA_STATUS_CONFIRM, - ISMAS_CURRENT_PSA_STATUS_CONFIRM_FAILURE, - ISMAS_CURRENT_PSA_STATUS_CONFIRM_SUCCESS, ISMAS_SANITY_CHECK_OK, ISMAS_UPDATE_TRIGGER_SET_FAILURE, PSA_UPDATE_FILES_FAILED, From 5263b7de0f995e034329305b830632039da730f8 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 16 Aug 2023 12:42:10 +0200 Subject: [PATCH 162/239] Removed some DEBUG-output. --- mainwindow.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index ae79de8..72dd90c 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -140,7 +140,7 @@ void MainWindow::onQuit() { void MainWindow::onAppendText(QString text, QString suffix) { QString editText = ui->updateStatus->toPlainText(); if (!suffix.isNull() && suffix.size() > 0) { - qInfo() << "TEXT" << text << "SUFFIX" << suffix; + //qInfo() << "TEXT" << text << "SUFFIX" << suffix; if (suffix == Worker::UPDATE_STEP_SUCCESS || suffix == Worker::UPDATE_STEP_FAIL) { editText += QString("\n").leftJustified(m_width-3, '='); editText += " "; @@ -157,8 +157,7 @@ void MainWindow::onAppendText(QString text, QString suffix) { } void MainWindow::onReplaceLast(QString text, QString suffix) { - qInfo() << "REPL TEXT" << text << "SUFFIX" << suffix; - + //qInfo() << "REPL TEXT" << text << "SUFFIX" << suffix; QString editText = ui->updateStatus->toPlainText(); QStringList lines = editText.split('\n'); if (lines.size() > 0) { From beec9c2f9d7aad1a72580436b86cdb4c0d894d89 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:37:24 +0200 Subject: [PATCH 163/239] Added properties to text edit: Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded, QAbstractScrollArea::AdjustToContents. --- mainwindow.ui | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mainwindow.ui b/mainwindow.ui index 670d881..75739e4 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -55,6 +55,15 @@ true + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustToContents + From 503b7c64f98ff503e7f8b824b79f790f7b55ba10 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:39:00 +0200 Subject: [PATCH 164/239] Added some comments to prepare for future change. --- main.cpp | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/main.cpp b/main.cpp index 1317043..471c45b 100644 --- a/main.cpp +++ b/main.cpp @@ -31,6 +31,7 @@ #include "utils.h" #include +#include #ifdef PTU5 #define SERIAL_PORT "ttymxc2" @@ -38,6 +39,93 @@ #define SERIAL_PORT "ttyUSB0" #endif +#if 0 +static QGraphicsProxyWidget *createItem(const QSizeF &minimum = QSizeF(100.0, 100.0), + const QSizeF &preferred = QSize(150.0, 100.0), + const QSizeF &maximum = QSizeF(200.0, 100.0), + const QString &name = "0") +{ + QGraphicsProxyWidget *w = new QGraphicsProxyWidget; + w->setWidget(new QPushButton(name)); + w->setData(0, name); + w->setMinimumSize(minimum); + w->setPreferredSize(preferred); + w->setMaximumSize(maximum); + + w->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + return w; +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + QGraphicsScene scene; + scene.setSceneRect(0, 0, 800, 480); + + QSizeF minSize(30, 100); + QSizeF prefSize(210, 100); + QSizeF maxSize(300, 100); + + QGraphicsProxyWidget *a = createItem(minSize, prefSize, maxSize, "A"); + QGraphicsProxyWidget *b = createItem(minSize, prefSize, maxSize, "B"); + QGraphicsProxyWidget *c = createItem(minSize, prefSize, maxSize, "C"); + QGraphicsProxyWidget *d = createItem(minSize, prefSize, maxSize, "D"); + QGraphicsProxyWidget *e = createItem(minSize, prefSize, maxSize, "E"); + QGraphicsProxyWidget *f = createItem(QSizeF(30, 50), QSizeF(150, 50), maxSize, "F (overflow)"); + QGraphicsProxyWidget *g = createItem(QSizeF(30, 50), QSizeF(30, 100), maxSize, "G (overflow)"); + + QGraphicsAnchorLayout *l = new QGraphicsAnchorLayout; + l->setSpacing(0); + + QGraphicsWidget *w = new QGraphicsWidget(0, Qt::Window); + w->setPos(20, 20); + w->setLayout(l); + + // vertical + l->addAnchor(a, Qt::AnchorTop, l, Qt::AnchorTop); + l->addAnchor(b, Qt::AnchorTop, l, Qt::AnchorTop); + + l->addAnchor(c, Qt::AnchorTop, a, Qt::AnchorBottom); + l->addAnchor(c, Qt::AnchorTop, b, Qt::AnchorBottom); + l->addAnchor(c, Qt::AnchorBottom, d, Qt::AnchorTop); + l->addAnchor(c, Qt::AnchorBottom, e, Qt::AnchorTop); + + l->addAnchor(d, Qt::AnchorBottom, l, Qt::AnchorBottom); + l->addAnchor(e, Qt::AnchorBottom, l, Qt::AnchorBottom); + + l->addAnchor(c, Qt::AnchorTop, f, Qt::AnchorTop); + l->addAnchor(c, Qt::AnchorVerticalCenter, f, Qt::AnchorBottom); + l->addAnchor(f, Qt::AnchorBottom, g, Qt::AnchorTop); + l->addAnchor(c, Qt::AnchorBottom, g, Qt::AnchorBottom); + + // horizontal + l->addAnchor(l, Qt::AnchorLeft, a, Qt::AnchorLeft); + l->addAnchor(l, Qt::AnchorLeft, d, Qt::AnchorLeft); + l->addAnchor(a, Qt::AnchorRight, b, Qt::AnchorLeft); + + l->addAnchor(a, Qt::AnchorRight, c, Qt::AnchorLeft); + l->addAnchor(c, Qt::AnchorRight, e, Qt::AnchorLeft); + + l->addAnchor(b, Qt::AnchorRight, l, Qt::AnchorRight); + l->addAnchor(e, Qt::AnchorRight, l, Qt::AnchorRight); + l->addAnchor(d, Qt::AnchorRight, e, Qt::AnchorLeft); + + l->addAnchor(l, Qt::AnchorLeft, f, Qt::AnchorLeft); + l->addAnchor(l, Qt::AnchorLeft, g, Qt::AnchorLeft); + l->addAnchor(f, Qt::AnchorRight, g, Qt::AnchorRight); + + + scene.addItem(w); + scene.setBackgroundBrush(Qt::darkGreen); + QGraphicsView view(&scene); + + view.show(); + + return app.exec(); +} +#endif + // argv[1]: file to send to dc int main(int argc, char *argv[]) { QByteArray const value = qgetenv("LC_ALL"); @@ -52,6 +140,22 @@ int main(int argc, char *argv[]) { QApplication::setApplicationName("ATBUpdateTool"); QApplication::setApplicationVersion(APP_VERSION); +#if 0 + // vorbereitend + QGraphicsScene scene; + scene.setSceneRect(0, 0, 800, 480); + + QGraphicsAnchorLayout *l = new QGraphicsAnchorLayout; + l->setSpacing(0); + + QGraphicsWidget *w = new QGraphicsWidget(0, Qt::Window); + w->setPos(20, 20); + w->setLayout(l); + + QGraphicsView view(&scene); + // view.show(); +#endif + if (!messageHandlerInstalled()) { // change internal qt-QDebug-handling atbInstallMessageHandler(atbDebugOutput); setDebugLevel(LOG_NOTICE); From 385a7b7b004b4b4ec71edc619cde4354bfdfa67c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:41:16 +0200 Subject: [PATCH 165/239] Added utility rstrip() to remove whitespace at the right end of a string. --- utils.cpp | 9 +++++++++ utils.h | 1 + 2 files changed, 10 insertions(+) diff --git a/utils.cpp b/utils.cpp index d17a7ea..92bf620 100644 --- a/utils.cpp +++ b/utils.cpp @@ -77,3 +77,12 @@ QString Utils::getTariffLoadTime(QString fileName) { return "N/A"; } +QString Utils::rstrip(QString const &str) { + int n = str.size() - 1; + for (; n >= 0; --n) { + if (!str.at(n).isSpace()) { + return str.left(n + 1); + } + } + return ""; +} diff --git a/utils.h b/utils.h index 86db3a6..79f838b 100644 --- a/utils.h +++ b/utils.h @@ -15,6 +15,7 @@ namespace Utils { void printInfoMsg(QString const &infoMsg); void printLineEditInfo(QStringList const &lines); QString getTariffLoadTime(QString fileName); + QString rstrip(QString const &str); } #endif // UTILS_H_INCLUDED From 0559ff64e2e2c455407c740e5d87f8c1fbcd90a4 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:42:47 +0200 Subject: [PATCH 166/239] Extended some debug output --- ismas/ismas_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 170eea3..1f4dd5e 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -217,7 +217,7 @@ IsmasClient::sendRequestReceiveResponse(int port, QString const &request) { // QString("CANNOT CLOSE WRITING END (") + strerror(errno) + ")"); // } - printInfoMessage(port, clientIP, clientPort, QString("MESSAGE SENT ") + buf); + printInfoMessage(port, clientIP, clientPort, QString("MESSAGE SENT <<<") + buf + ">>>"); loop = 0; bzero(buf, sizeof(buf)); From 2ac8c4cfc6c6d92403ec00413affa7f9f86c8803 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:46:57 +0200 Subject: [PATCH 167/239] Added scrollDownTextEdit() -> text edit is supposed to scroll down autmatically in case too much info has to be displayed. --- mainwindow.cpp | 10 ++++++++++ mainwindow.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/mainwindow.cpp b/mainwindow.cpp index 72dd90c..02fd3c9 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -7,6 +7,7 @@ #include #include #include +#include MainWindow::MainWindow(Worker *worker, QWidget *parent) : QMainWindow(parent) @@ -137,6 +138,15 @@ void MainWindow::onQuit() { qApp->exit(m_worker->returnCode()); } +void MainWindow::scrollDownTextEdit() { + ui->updateStatus->setEnabled(true); + + QTextCursor tmpCursor = ui->updateStatus->textCursor(); + tmpCursor.movePosition(QTextCursor::End); + ui->updateStatus->setTextCursor(tmpCursor); + ui->updateStatus->ensureCursorVisible(); +} + void MainWindow::onAppendText(QString text, QString suffix) { QString editText = ui->updateStatus->toPlainText(); if (!suffix.isNull() && suffix.size() > 0) { diff --git a/mainwindow.h b/mainwindow.h index 3895467..c217a3a 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -38,6 +38,8 @@ private slots: void onQuit(); private: + void scrollDownTextEdit(); + Ui::MainWindow *ui; Worker *m_worker; int m_width; From 17ddfd0ddd94da9ad4c8004809412a32295f6294 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:48:29 +0200 Subject: [PATCH 168/239] Added slot onReplaceLast() to handle adding a QStringList to the text edit. --- mainwindow.cpp | 35 +++++++++++++++++++++++++++++++++-- mainwindow.h | 1 + 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 02fd3c9..02918c7 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -161,9 +161,40 @@ void MainWindow::onAppendText(QString text, QString suffix) { } Utils::printLineEditInfo(editText.split('\n')); - ui->updateStatus->setPlainText(editText.trimmed()); - ui->updateStatus->setEnabled(true); + scrollDownTextEdit(); +} + +void MainWindow::onReplaceLast(QStringList newTextLines, QString suffix) { + int const s = newTextLines.size(); + if (s > 0) { + QString editText = ui->updateStatus->toPlainText(); + QStringList lines = editText.split('\n'); + QString newText; + if (lines.size() >= s) { + for (int i = 0; i < s; ++i) { + lines.removeLast(); + } + if (lines.size() > 0) { + newText = lines.join('\n'); + newText += '\n'; + } + QStringList newLines; + for (int i = 0; i < s; ++i) { + if (i == 0 && !suffix.isNull() && suffix.size() > 0 && suffix != "\n") { + newLines += Utils::rstrip(newTextLines.at(i).leftJustified(m_width-10) + suffix); + } else { + newLines += Utils::rstrip(newTextLines.at(i).leftJustified(m_width-10)); + } + } + lines += newLines; + newText += newLines.join(' '); + } + + ui->updateStatus->setText(newText); + Utils::printLineEditInfo(lines); + scrollDownTextEdit(); + } } void MainWindow::onReplaceLast(QString text, QString suffix) { diff --git a/mainwindow.h b/mainwindow.h index c217a3a..5dcef5b 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -27,6 +27,7 @@ public: public slots: void onAppendText(QString, QString suffix = ""); + void onReplaceLast(QStringList, QString suffix = ""); void onReplaceLast(QString, QString suffix = ""); void onShowErrorMessage(QString, QString); void onStopStartTimer(); From 56daa84a14651e822f8bbfc1f814357f74dfac0b Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:49:34 +0200 Subject: [PATCH 169/239] Streamlined the connects (without any whitespace) to silence clang. --- mainwindow.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 02918c7..a30b38b 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -43,17 +43,15 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) m_exitTimer->setSingleShot(true); m_exitTimer->start(1800 * 1000); - connect(ui->exit, SIGNAL(clicked()), this, SLOT(onQuit())); - connect(m_worker, SIGNAL(disableExit()), this, SLOT(onDisableExit())); - connect(m_worker, SIGNAL(enableExit()), this, SLOT(onEnableExit())); - connect(m_worker, SIGNAL(stopStartTimer()), this, SLOT(onStopStartTimer())); - connect(m_worker, SIGNAL(restartExitTimer()), this, SLOT(onRestartExitTimer())); - connect(m_worker, SIGNAL(appendText(QString, QString)), this, SLOT(onAppendText(QString, QString))); - connect(m_worker, SIGNAL(showErrorMessage(QString,QString)), this, SLOT(onShowErrorMessage(QString,QString))); - connect(m_worker, SIGNAL(replaceLast(QString, QString)), this, SLOT(onReplaceLast(QString,QString))); - - ui->updateStatus->setText(lst.join('\n')); - ui->updateStatus->setEnabled(true); + connect(ui->exit, SIGNAL(clicked()),this,SLOT(onQuit())); + connect(m_worker, SIGNAL(disableExit()),this,SLOT(onDisableExit())); + connect(m_worker, SIGNAL(enableExit()),this,SLOT(onEnableExit())); + connect(m_worker, SIGNAL(stopStartTimer()),this,SLOT(onStopStartTimer())); + connect(m_worker, SIGNAL(restartExitTimer()),this,SLOT(onRestartExitTimer())); + connect(m_worker, SIGNAL(appendText(QString,QString)),this,SLOT(onAppendText(QString,QString))); + connect(m_worker, SIGNAL(showErrorMessage(QString,QString)),this, SLOT(onShowErrorMessage(QString,QString))); + connect(m_worker, SIGNAL(replaceLast(QString,QString)),this,SLOT(onReplaceLast(QString,QString))); + connect(m_worker, SIGNAL(replaceLast(QStringList,QString)),this, SLOT(onReplaceLast(QStringList,QString))); } MainWindow::~MainWindow() { From bea8242d6f59aca9ab5e51a02d1a62a9f8a4420a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:50:14 +0200 Subject: [PATCH 170/239] Add some changes to the message box (to be chenged anyways later). --- mainwindow.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index a30b38b..a587a62 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -209,16 +209,21 @@ void MainWindow::onReplaceLast(QString text, QString suffix) { } Utils::printLineEditInfo(lines); - ui->updateStatus->setText(lines.join('\n').trimmed()); - ui->updateStatus->setEnabled(true); + scrollDownTextEdit(); } void MainWindow::onShowErrorMessage(QString title, QString text) { text = text.leftJustified(50, ' '); QMessageBox msgBox(QMessageBox::NoIcon, title, text, QMessageBox::Ok, - nullptr, Qt::FramelessWindowHint); + this, Qt::FramelessWindowHint); + msgBox.resize(100, 50); + + // msg.setStyleSheet("background-color: rgb(0, 0, 0);") + // msg.setStyleSheet("text-color: rgb(255, 255, 255);") + + msgBox.setStyleSheet("QMessageBox{border: 1px solid black; background-color:white}"); msgBox.setDefaultButton(QMessageBox::Ok); msgBox.defaultButton()->setVisible(false); @@ -227,6 +232,9 @@ void MainWindow::onShowErrorMessage(QString title, QString text) { t->setSingleShot(true); t->start(5 * 1000); + msgBox.show(); + msgBox.move(0, 0); + if(msgBox.exec() == QMessageBox::Ok) { // do something } else { From 978cc1630480385d67014962265ef19fe242b280 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:51:27 +0200 Subject: [PATCH 171/239] added signal onReplaceLast for QStringList --- worker.h | 1 + 1 file changed, 1 insertion(+) diff --git a/worker.h b/worker.h index c6b8557..8f46a1c 100644 --- a/worker.h +++ b/worker.h @@ -167,6 +167,7 @@ public: signals: void appendText(QString, QString suffix = ""); void replaceLast(QString, QString); + void replaceLast(QStringList, QString); void showErrorMessage(QString title, QString description); void stopStartTimer(); void restartExitTimer(); From 337bdd1bb08ceba1459ae16a419f255b31b8468e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:52:34 +0200 Subject: [PATCH 172/239] Add some debug output when restarting APISM. APISM is give a delay of 20s when restarting. --- worker.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/worker.cpp b/worker.cpp index 4c46673..cbf9dbd 100644 --- a/worker.cpp +++ b/worker.cpp @@ -506,10 +506,25 @@ bool Worker::updateTriggerSet() { emit appendText("\n" CHECK_UPDATE_TRIGGER_SET); QString triggerValue(""); + int const startMs = QTime::currentTime().msecsSinceStartOfDay(); - for (int repeat = 0; repeat < 100; ++repeat) { + for (int repeat = 1; repeat <= 100; ++repeat) { qInfo() << "UPDATE TRIGGER SET -> REPEAT" << repeat; + if (repeat > 1) { + int const durationMs = QTime::currentTime().msecsSinceStartOfDay() - startMs; + qInfo() << "REPEAT" << repeat + << QString("DURATION: %1.%2s").arg(durationMs / 1000).arg(durationMs % 1000); + } + if ((repeat % 10) == 0) { + qInfo() << "CHECK UPDATE TRIGGER. RESTART APISM ..."; + Command c("systemctl restart apism"); + if (c.execute("/tmp")) { + QThread::sleep(20); // give APISM some time to reconnect + qInfo() << "CHECK UPDATE TRIGGER. RESTARTING APISM DONE"; + } + } + startProgressLoop(); if (std::optional result = IsmasClient::sendRequestReceiveResponse( From 631ade1954964c3478aeaf43720516d91fa58040 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 18 Aug 2023 11:53:32 +0200 Subject: [PATCH 173/239] Show the executed opkg-commands in the text edit. --- worker.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/worker.cpp b/worker.cpp index cbf9dbd..d6d4156 100644 --- a/worker.cpp +++ b/worker.cpp @@ -891,14 +891,18 @@ bool Worker::updateFiles(quint8 percent) { if (f.open(QIODevice::ReadOnly)) { QTextStream in(&f); int cmdCount = 0; + QStringList opkgCommands; while (!in.atEnd()) { QString line = in.readLine(); static const QRegularExpression comment("^\\s*#.*$"); if (line.indexOf(comment, 0) == -1) { // found opkg command QString opkgCommand = line.trimmed(); - executeOpkgCommand(opkgCommand); ++cmdCount; + executeOpkgCommand(opkgCommand); + QString cmd = "\n " + opkgCommand; + emit appendText(cmd); + opkgCommands << cmd; m_ismasClient.setProgressInPercent(++percent); m_updateStatus = UpdateStatus(UPDATE_STATUS::EXEC_OPKG_COMMAND, @@ -911,8 +915,10 @@ bool Worker::updateFiles(quint8 percent) { f.close(); if (cmdCount > 0) { m_displayIndex = 1; - emit replaceLast(QString("(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") - + QString(" Update opkg pakets ... "), UPDATE_STEP_DONE); + QString prepend = QString("(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update opkg pakets ... "); + opkgCommands.prepend(prepend); + emit replaceLast(opkgCommands, UPDATE_STEP_DONE); } else { m_displayIndex = 1; emit replaceLast(QString("(") + QString("%1").arg(m_displayIndex).rightJustified(2, ' ') + QString(")") From fff6bd2b49ffb040fbca10e390a99d82599be70a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 09:25:02 +0200 Subject: [PATCH 174/239] Make room for status bar --- mainwindow.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainwindow.ui b/mainwindow.ui index 75739e4..4d081d4 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -25,7 +25,7 @@ 10 10 781 - 461 + 441 From c35390b6d6f65eb43ddb749c0adc433a86ed5797 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 09:27:20 +0200 Subject: [PATCH 175/239] removed commented code plannned for future use -> did not work --- main.cpp | 104 +------------------------------------------------------ 1 file changed, 1 insertion(+), 103 deletions(-) diff --git a/main.cpp b/main.cpp index 471c45b..8b48f51 100644 --- a/main.cpp +++ b/main.cpp @@ -39,93 +39,6 @@ #define SERIAL_PORT "ttyUSB0" #endif -#if 0 -static QGraphicsProxyWidget *createItem(const QSizeF &minimum = QSizeF(100.0, 100.0), - const QSizeF &preferred = QSize(150.0, 100.0), - const QSizeF &maximum = QSizeF(200.0, 100.0), - const QString &name = "0") -{ - QGraphicsProxyWidget *w = new QGraphicsProxyWidget; - w->setWidget(new QPushButton(name)); - w->setData(0, name); - w->setMinimumSize(minimum); - w->setPreferredSize(preferred); - w->setMaximumSize(maximum); - - w->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - return w; -} - -int main(int argc, char **argv) -{ - QApplication app(argc, argv); - - QGraphicsScene scene; - scene.setSceneRect(0, 0, 800, 480); - - QSizeF minSize(30, 100); - QSizeF prefSize(210, 100); - QSizeF maxSize(300, 100); - - QGraphicsProxyWidget *a = createItem(minSize, prefSize, maxSize, "A"); - QGraphicsProxyWidget *b = createItem(minSize, prefSize, maxSize, "B"); - QGraphicsProxyWidget *c = createItem(minSize, prefSize, maxSize, "C"); - QGraphicsProxyWidget *d = createItem(minSize, prefSize, maxSize, "D"); - QGraphicsProxyWidget *e = createItem(minSize, prefSize, maxSize, "E"); - QGraphicsProxyWidget *f = createItem(QSizeF(30, 50), QSizeF(150, 50), maxSize, "F (overflow)"); - QGraphicsProxyWidget *g = createItem(QSizeF(30, 50), QSizeF(30, 100), maxSize, "G (overflow)"); - - QGraphicsAnchorLayout *l = new QGraphicsAnchorLayout; - l->setSpacing(0); - - QGraphicsWidget *w = new QGraphicsWidget(0, Qt::Window); - w->setPos(20, 20); - w->setLayout(l); - - // vertical - l->addAnchor(a, Qt::AnchorTop, l, Qt::AnchorTop); - l->addAnchor(b, Qt::AnchorTop, l, Qt::AnchorTop); - - l->addAnchor(c, Qt::AnchorTop, a, Qt::AnchorBottom); - l->addAnchor(c, Qt::AnchorTop, b, Qt::AnchorBottom); - l->addAnchor(c, Qt::AnchorBottom, d, Qt::AnchorTop); - l->addAnchor(c, Qt::AnchorBottom, e, Qt::AnchorTop); - - l->addAnchor(d, Qt::AnchorBottom, l, Qt::AnchorBottom); - l->addAnchor(e, Qt::AnchorBottom, l, Qt::AnchorBottom); - - l->addAnchor(c, Qt::AnchorTop, f, Qt::AnchorTop); - l->addAnchor(c, Qt::AnchorVerticalCenter, f, Qt::AnchorBottom); - l->addAnchor(f, Qt::AnchorBottom, g, Qt::AnchorTop); - l->addAnchor(c, Qt::AnchorBottom, g, Qt::AnchorBottom); - - // horizontal - l->addAnchor(l, Qt::AnchorLeft, a, Qt::AnchorLeft); - l->addAnchor(l, Qt::AnchorLeft, d, Qt::AnchorLeft); - l->addAnchor(a, Qt::AnchorRight, b, Qt::AnchorLeft); - - l->addAnchor(a, Qt::AnchorRight, c, Qt::AnchorLeft); - l->addAnchor(c, Qt::AnchorRight, e, Qt::AnchorLeft); - - l->addAnchor(b, Qt::AnchorRight, l, Qt::AnchorRight); - l->addAnchor(e, Qt::AnchorRight, l, Qt::AnchorRight); - l->addAnchor(d, Qt::AnchorRight, e, Qt::AnchorLeft); - - l->addAnchor(l, Qt::AnchorLeft, f, Qt::AnchorLeft); - l->addAnchor(l, Qt::AnchorLeft, g, Qt::AnchorLeft); - l->addAnchor(f, Qt::AnchorRight, g, Qt::AnchorRight); - - - scene.addItem(w); - scene.setBackgroundBrush(Qt::darkGreen); - QGraphicsView view(&scene); - - view.show(); - - return app.exec(); -} -#endif - // argv[1]: file to send to dc int main(int argc, char *argv[]) { QByteArray const value = qgetenv("LC_ALL"); @@ -140,22 +53,6 @@ int main(int argc, char *argv[]) { QApplication::setApplicationName("ATBUpdateTool"); QApplication::setApplicationVersion(APP_VERSION); -#if 0 - // vorbereitend - QGraphicsScene scene; - scene.setSceneRect(0, 0, 800, 480); - - QGraphicsAnchorLayout *l = new QGraphicsAnchorLayout; - l->setSpacing(0); - - QGraphicsWidget *w = new QGraphicsWidget(0, Qt::Window); - w->setPos(20, 20); - w->setLayout(l); - - QGraphicsView view(&scene); - // view.show(); -#endif - if (!messageHandlerInstalled()) { // change internal qt-QDebug-handling atbInstallMessageHandler(atbDebugOutput); setDebugLevel(LOG_NOTICE); @@ -230,6 +127,7 @@ int main(int argc, char *argv[]) { QThread::currentThread()->setObjectName("main thread"); qInfo() << "Main thread" << QThread::currentThreadId(); + Worker worker(hw, customerNr, machineNr, From 4307fb96a605b5c3b68634efd030b57f0d75ca3b Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 09:27:59 +0200 Subject: [PATCH 176/239] Add status bar instead of using an message box for displaying error messages. --- mainwindow.cpp | 38 ++++++++++++-------------------------- mainwindow.h | 1 + 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index a587a62..9151fc1 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -16,6 +16,16 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) , m_width(70) , m_progressRunning(false) , m_progressValue(0) { + + + this->setStatusBar(new QStatusBar(this)); + QFont f; + f.setStyleHint(QFont::Monospace); + f.setWeight(QFont::Bold); + f.setFamily("Misc Fixed"); + f.setPixelSize(12); + this->statusBar()->setFont(f); + ui->setupUi(this); ui->updateProgress->setRange(0, 100); @@ -214,30 +224,6 @@ void MainWindow::onReplaceLast(QString text, QString suffix) { } void MainWindow::onShowErrorMessage(QString title, QString text) { - text = text.leftJustified(50, ' '); - QMessageBox msgBox(QMessageBox::NoIcon, title, - text, QMessageBox::Ok, - this, Qt::FramelessWindowHint); - msgBox.resize(100, 50); - - // msg.setStyleSheet("background-color: rgb(0, 0, 0);") - // msg.setStyleSheet("text-color: rgb(255, 255, 255);") - - msgBox.setStyleSheet("QMessageBox{border: 1px solid black; background-color:white}"); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.defaultButton()->setVisible(false); - - QTimer *t = new QTimer(this); - connect(t, SIGNAL(timeout()), msgBox.defaultButton(), SLOT(click())); - t->setSingleShot(true); - t->start(5 * 1000); - - msgBox.show(); - msgBox.move(0, 0); - - if(msgBox.exec() == QMessageBox::Ok) { - // do something - } else { - // do something else - } + this->statusBar()->showMessage( // timeout: 5000 + QString(title + ": " + text).leftJustified(80, ' '), 20000); } diff --git a/mainwindow.h b/mainwindow.h index 5dcef5b..e54b0fc 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -3,6 +3,7 @@ #include #include +#include QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } From 99b941915069731c608f8af6c0bc2e6b026c6fa5 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 12:19:25 +0200 Subject: [PATCH 177/239] Made gitBlob() static and execute in /tmp as this command can be executed for every file not only the files contained in a git repository. --- git/git_client.cpp | 2 +- git/git_client.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index 35475f2..f3c4ae1 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -330,7 +330,7 @@ QString GitClient::gitBlob(QString fileName) { if (fi.exists()) { QString const gitCommand = QString("git hash-object %1").arg(fileName); Command c(gitCommand); - if (c.execute(m_workingDirectory)) { + if (c.execute("/tmp")) { return c.getCommandResult().trimmed(); } } diff --git a/git/git_client.h b/git/git_client.h index 0faca95..63d83e4 100644 --- a/git/git_client.h +++ b/git/git_client.h @@ -51,7 +51,7 @@ class GitClient : public QObject { QString gitLastCommit(QString fileName); QStringList gitShowReason(); - QString gitBlob(QString fileName); + static QString gitBlob(QString fileName); QString gitCommitForBlob(QString blob); bool gitIsFileTracked(QString file2name); }; From 4ebdcf56a06684ded0aae203cc769604451b0532 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 12:21:17 +0200 Subject: [PATCH 178/239] Clear message before showing a new one. --- mainwindow.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 9151fc1..21f73b4 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -224,6 +224,7 @@ void MainWindow::onReplaceLast(QString text, QString suffix) { } void MainWindow::onShowErrorMessage(QString title, QString text) { - this->statusBar()->showMessage( // timeout: 5000 - QString(title + ": " + text).leftJustified(80, ' '), 20000); + this->statusBar()->clearMessage(); + this->statusBar()->showMessage( // timeout: 10000 + QString(title + ": " + text).leftJustified(80, ' '), 10000); } From 1620b73d018bc7c0c1afb3fac0a51ab9fe592f5b Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 12:29:52 +0200 Subject: [PATCH 179/239] Added sameFilesInDirs(): check for two different directories if the contain the same files (comparison by name and by git-blob). --- utils.cpp | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ utils.h | 3 ++ 2 files changed, 86 insertions(+) diff --git a/utils.cpp b/utils.cpp index 92bf620..1d3c7de 100644 --- a/utils.cpp +++ b/utils.cpp @@ -1,5 +1,6 @@ #include "utils.h" #include "message_handler.h" +#include "git/git_client.h" #include #include @@ -86,3 +87,85 @@ QString Utils::rstrip(QString const &str) { } return ""; } + +bool Utils::sameFilesInDirs(QDir const &dir1, QDir const &dir2, + QStringList const &nameFilters) { + if (!dir1.exists()) { + printCriticalErrorMsg(dir1.dirName() + " DOES NOT EXIST"); + return false; + } + if (!dir2.exists()) { + printCriticalErrorMsg(dir2.dirName() + " DOES NOT EXIST"); + return false; + } + if (dir1.absolutePath() == dir2.absolutePath()) { + printCriticalErrorMsg(dir1.dirName() + " AND "+ dir2.dirName() + " HAVE SAME PATH"); + return false; + } + + // files, sorted by name + QFileInfoList const &lst1 = dir1.entryInfoList(nameFilters, QDir::Files, QDir::Name); + QFileInfoList const &lst2 = dir2.entryInfoList(nameFilters, QDir::Files, QDir::Name); + + QStringList fileNameLst1{}; + QStringList fileNameLst2{}; + QListIterator i1(lst1); + while (i1.hasNext()) { + fileNameLst1 << i1.next().fileName(); + } + QListIterator i2(lst2); + while (i2.hasNext()) { + fileNameLst2 << i2.next().fileName(); + } + + if (fileNameLst1.isEmpty()) { + qCritical() << "DIR1" << dir1.dirName() << " DOES NOT CONTAIN EXPECTED FILES"; + return false; + } + if (fileNameLst2.isEmpty()) { + qCritical() << "DIR1" << dir2.dirName() << " DOES NOT CONTAIN EXPECTED FILES"; + return false; + } + if (fileNameLst1 != fileNameLst2) { + printCriticalErrorMsg(dir1.dirName() + " AND " + dir2.dirName() + + " DIFFER: [" + fileNameLst1.join(',') + "],[" + + fileNameLst2.join(',') + "]"); + return false; + } else { + printInfoMsg(dir1.dirName() + " AND " + dir2.dirName() + + " ARE EQUAL: [" + fileNameLst1.join(',') + "]"); + } + + QStringList gitBlobLst1{}; + QStringList gitBlobLst2{}; + QListIterator i3(lst1); + while (i3.hasNext()) { + gitBlobLst1 << GitClient::gitBlob(i3.next().fileName()); + } + QListIterator i4(lst2); + while (i4.hasNext()) { + gitBlobLst2 << GitClient::gitBlob(i4.next().fileName()); + } + + if (gitBlobLst1.isEmpty()) { + qCritical() << "DIR1" << dir1.dirName() << " DOES NOT CONTAIN EXPECTED FILES"; + return false; + } + if (gitBlobLst2.isEmpty()) { + qCritical() << "DIR1" << dir2.dirName() << " DOES NOT CONTAIN EXPECTED FILES"; + return false; + } + + if (gitBlobLst1 != gitBlobLst2) { + printCriticalErrorMsg(dir1.dirName() + " AND " + dir2.dirName() + + " DIFFER: [" + gitBlobLst1.join(',') + "],[" + + gitBlobLst2.join(',') + "]"); + return false; + } else { + printInfoMsg(dir1.dirName() + " AND " + dir2.dirName() + + " CONTAIN SAME GIT-BLOBS FOR FILES: [" + fileNameLst1.join(',') + "]"); + + } + + return true; +} diff --git a/utils.h b/utils.h index 79f838b..7f16616 100644 --- a/utils.h +++ b/utils.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace Utils { int read1stLineOfFile(QString fileName); @@ -16,6 +17,8 @@ namespace Utils { void printLineEditInfo(QStringList const &lines); QString getTariffLoadTime(QString fileName); QString rstrip(QString const &str); + bool sameFilesInDirs(QDir const &dir1, QDir const &dir2, + QStringList const &nameFilters = {"*.json"}); } #endif // UTILS_H_INCLUDED From cbe1fd387dd29f1cc1ee782ee70d41e86aa28239 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 12:31:15 +0200 Subject: [PATCH 180/239] After "rsync", compare etc/psa_tariff and /etc/psa_tariff, if they contain the same traiff-files (as they should). --- worker.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/worker.cpp b/worker.cpp index d6d4156..5de4171 100644 --- a/worker.cpp +++ b/worker.cpp @@ -1001,9 +1001,16 @@ bool Worker::syncCustomerRepositoryAndFS() { progress += 5; setProgress(progress); if (!error) { - setProgress(100); - emit replaceLast(QString("Sync customer environment with filesystem ..."), UPDATE_STEP_DONE); - return true; + // now check tariff-files in etc and /etc/psa_tariff + QDir dir1(QDir::cleanPath(m_customerRepository + QDir::separator() + "etc/psa_tariff")); + QDir dir2("/etc/psa_tariff"); + if (Utils::sameFilesInDirs(dir1, dir2)) { + setProgress(100); + emit replaceLast(QString("Sync customer environment with filesystem ..."), UPDATE_STEP_DONE); + return true; + } else { + // TODO: send message to ISMAS + } } } } From f8fef380090fc61f61852cd37ae5b4dadc6114d7 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 13:47:57 +0200 Subject: [PATCH 181/239] Add last-commit for traiff and add info for opkg_commands. --- ismas/ismas_client.cpp | 13 +++++++++++++ ismas/ismas_client.h | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 1f4dd5e..53f24a7 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -432,6 +432,13 @@ QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { "\"ZONE\" : %d," "\"INFO\" : \"%s\"," "\"BLOB\" : \"%s\"," + "\"LAST-COMMIT\" : \"%s\"," + "\"SIZE\" : %d," + "\"LOADED\" : \"%s\"" + "}," + "\"OPKG_COMMANDS\" : {" + "\"BLOB\" : \"%s\"," + "\"LAST-COMMIT\" : \"%s\"," "\"SIZE\" : %d," "\"LOADED\" : \"%s\"" "}," @@ -639,9 +646,15 @@ QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { psa.tariff.zone, psa.tariff.info.toStdString().c_str(), psa.tariff.blob.toStdString().c_str(), + psa.tariff.lastCommit.toStdString().c_str(), psa.tariff.size, psa.tariff.loadTime.toStdString().c_str(), + psa.opkg.blob.toStdString().c_str(), + psa.opkg.lastCommit.toStdString().c_str(), + psa.opkg.size, + psa.opkg.loadTime.toStdString().c_str(), + psa.cash.blob.toStdString().c_str(), psa.cash.size, psa.conf.blob.toStdString().c_str(), diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index e5cc70f..4ee47d6 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -30,6 +30,13 @@ struct PSAInstalled { QString cpuSerial; } hw; + struct Opkg { + int size; + QString blob; + QString lastCommit; + QString loadTime; + } opkg; + struct DC { QString versionHW; QString versionSW; @@ -79,6 +86,11 @@ struct PSAInstalled { hw.linuxVersion = "N/A"; hw.cpuSerial = "N/A"; + opkg.size = -1; + opkg.blob = "N/A"; + opkg.lastCommit = "N/A"; + opkg.loadTime = "N/A"; + dc.versionHW = "N/A"; dc.versionSW = "N/A"; dc.gitBlob = "N/A"; From afd31f1b27ce1fc438521ad1f80da41cf58a0a65 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 13:49:09 +0200 Subject: [PATCH 182/239] Fill in opkg related info. --- worker.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/worker.cpp b/worker.cpp index 5de4171..abcb378 100644 --- a/worker.cpp +++ b/worker.cpp @@ -1142,6 +1142,10 @@ PSAInstalled Worker::getPSAInstalled() { QString printSysDir("/etc/psa_config"); QString tariffSysDir("/etc/psa_tariff"); QString tariffRepoDir("etc/psa_tariff"); + QString opkgSysDir("/etc/psa_update"); + QString opkgRepoDir("etc/psa_update"); + QString const &absPathNameOpkg = QDir::cleanPath(opkgSysDir + QDir::separator() + "opkg_commands"); + QString const &absPathNameRepositoryOpkg = QDir::cleanPath(opkgRepoDir + QDir::separator() + "opkg_commands"); QString absPathName; QString absPathNameRepository; @@ -1174,6 +1178,11 @@ PSAInstalled Worker::getPSAInstalled() { psaInstalled.hw.linuxVersion = m_osVersion; psaInstalled.hw.cpuSerial = m_cpuSerial; + psaInstalled.opkg.blob = m_gc.gitBlob(absPathNameOpkg); + psaInstalled.opkg.size = getFileSize(absPathNameOpkg); + psaInstalled.opkg.loadTime = Utils::getTariffLoadTime(absPathNameOpkg); + psaInstalled.opkg.lastCommit = m_gc.gitLastCommit(absPathNameRepositoryOpkg); + psaInstalled.dc.versionHW = deviceControllerVersionHW; psaInstalled.dc.versionSW = deviceControllerVersionSW; psaInstalled.dc.gitBlob = "N/A"; From a803907449a9d68e0b1e3c2c6b6d3921fded3f90 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Tue, 22 Aug 2023 13:49:42 +0200 Subject: [PATCH 183/239] Refined information shown in status bar. --- worker.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/worker.cpp b/worker.cpp index abcb378..05f3692 100644 --- a/worker.cpp +++ b/worker.cpp @@ -509,13 +509,18 @@ bool Worker::updateTriggerSet() { int const startMs = QTime::currentTime().msecsSinceStartOfDay(); for (int repeat = 1; repeat <= 100; ++repeat) { + qInfo() << "UPDATE TRIGGER SET -> REPEAT" << repeat; if (repeat > 1) { int const durationMs = QTime::currentTime().msecsSinceStartOfDay() - startMs; - qInfo() << "REPEAT" << repeat - << QString("DURATION: %1.%2s").arg(durationMs / 1000).arg(durationMs % 1000); + QString const &msg = QString("[%1] time: %2.%3s").arg(repeat).arg(durationMs / 1000).arg(durationMs % 1000); + qInfo() << "REPEAT" << msg; + emit showErrorMessage("check update trigger", msg); + } else { + emit showErrorMessage("check update trigger", QString("[%1]").arg(repeat)); } + if ((repeat % 10) == 0) { qInfo() << "CHECK UPDATE TRIGGER. RESTART APISM ..."; Command c("systemctl restart apism"); From a8df026a80c3ead96639e7a23319abdf2207de63 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 23 Aug 2023 16:26:11 +0200 Subject: [PATCH 184/239] Added rsyncFile() method. --- ismas/ismas_client.cpp | 9 +++++++++ ismas/ismas_client.h | 1 + 2 files changed, 10 insertions(+) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 53f24a7..df104d3 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -393,6 +393,15 @@ QString IsmasClient::execOpkgCommand(QString const &info, QString const &version version.toStdString().c_str()); } +QString IsmasClient::rsyncFile(QString const &info, QString const &version) { + return updateNewsToIsmas("U0010", + m_progressInPercent, + RESULT_CODE::SUCCESS, + "RSYNC FILE", + info.toStdString().c_str(), + version.toStdString().c_str()); +} + QString IsmasClient::updateTriggerSet(QString const &info, QString const &version) { return updateNewsToIsmas("U0010", m_progressInPercent, diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index 4ee47d6..2e028eb 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -171,6 +171,7 @@ public: QString errorUpdateTrigger(QString const &info, QString const &version = QString()); QString gitFetch(QString const &info, QString const &version = QString()); QString execOpkgCommand(QString const &info, QString const &version = QString()); + QString rsyncFile(QString const &info, QString const &version = QString()); QString errorGitFetch(int resultCode, QString const &info, QString const &version = QString()); QString updateOfPSAActivated(QString const &version = QString()); // and update accepted From 1509e8619c74580a9691d6493b0ce86953aa4700 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 23 Aug 2023 16:26:55 +0200 Subject: [PATCH 185/239] Send message to ISMAS when rsyncing a traiff-file --- worker.cpp | 34 ++++++++++++++++++++++++++++++---- worker.h | 1 + 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/worker.cpp b/worker.cpp index 05f3692..a3890de 100644 --- a/worker.cpp +++ b/worker.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -514,11 +515,11 @@ bool Worker::updateTriggerSet() { if (repeat > 1) { int const durationMs = QTime::currentTime().msecsSinceStartOfDay() - startMs; - QString const &msg = QString("[%1] time: %2.%3s").arg(repeat).arg(durationMs / 1000).arg(durationMs % 1000); + QString const &msg = QString("elapsed: %1.%2s").arg(durationMs / 1000).arg(durationMs % 1000); qInfo() << "REPEAT" << msg; emit showErrorMessage("check update trigger", msg); } else { - emit showErrorMessage("check update trigger", QString("[%1]").arg(repeat)); + emit showErrorMessage("check update trigger", ""); } if ((repeat % 10) == 0) { @@ -970,7 +971,7 @@ bool Worker::syncCustomerRepositoryAndFS() { } int progress = 10; setProgress(progress); - QString const params("-vv " + QString const params("-vvv " "--recursive " "--progress " "--checksum " @@ -993,8 +994,25 @@ bool Worker::syncCustomerRepositoryAndFS() { qInfo() << "EXECUTING CMD..." << cmd; if (c.execute(m_customerRepository, QStringList() << "-c" << cmd)) { QStringList result = c.getCommandResult().split('\n'); + QString const &p1 = "send_files mapped "; + QString const &p2 = "of size"; for (int i = 0; i < result.size(); ++i) { - qInfo() << result.at(i); + QString line = result.at(i); + qInfo() << line; + + // "send_files mapped etc/psa_tariff/tariff01.json of size 19339" + int sendFilesAtPos = line.indexOf(p1); + int ofSizeAtPos = line.indexOf(p2); + if (sendFilesAtPos != -1 && ofSizeAtPos != -1) { + sendFilesAtPos += p1.length(); + QString const &s = line.mid(sendFilesAtPos, ofSizeAtPos - sendFilesAtPos).trimmed(); + m_updateStatus = UpdateStatus(UPDATE_STATUS::RSYNC_FILE_SUCCESS, + QString("RSYNC FILE ") + s.split("/").last() + + " LAST-COMMIT: " + m_gc.gitLastCommit(s)); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.rsyncFile(m_updateStatus.m_statusDescription, "")); + } } } else { Utils::printCriticalErrorMsg(QString("CMD ") + cmd + " FAILED: " @@ -1345,6 +1363,10 @@ QDebug operator<< (QDebug debug, UpdateStatus status) { debug << QString("UPDATE_STATUS::ISMAS_RESPONSE_RECEIVED: ") << status.m_statusDescription; break; + case UPDATE_STATUS::RSYNC_FILE_SUCCESS: + debug << QString("UPDATE_STATUS::RSYNC_FILE_SUCCESS: ") + << status.m_statusDescription; + break; case UPDATE_STATUS::EXEC_OPKG_COMMANDS: debug << QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS: ") << status.m_statusDescription; @@ -1468,6 +1490,10 @@ QString& operator<< (QString& str, UpdateStatus status) { str = QString("UPDATE_STATUS::EXEC_OPKG_COMMANDS: "); str += status.m_statusDescription; break; + case UPDATE_STATUS::RSYNC_FILE_SUCCESS: + str = QString("UPDATE_STATUS::RSYNC_FILE_SUCCESS: "); + str += status.m_statusDescription; + break; //default:; } return str; diff --git a/worker.h b/worker.h index 8f46a1c..ea67786 100644 --- a/worker.h +++ b/worker.h @@ -41,6 +41,7 @@ enum class UPDATE_STATUS : quint8 { EXEC_OPKG_COMMANDS, RSYNC_UPDATES_FAILURE, RSYNC_UPDATES_SUCCESS, + RSYNC_FILE_SUCCESS, JSON_PARSE_FAILURE, UPDATE_PROCESS_SUCCESS, UPDATE_PROCESS_FAILURE, From be28570d23c98f7b8995b301f1b94e33a1e3f221 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 23 Aug 2023 16:27:47 +0200 Subject: [PATCH 186/239] Minor change for output in status bar. --- mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 21f73b4..1a80fb4 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -226,5 +226,5 @@ void MainWindow::onReplaceLast(QString text, QString suffix) { void MainWindow::onShowErrorMessage(QString title, QString text) { this->statusBar()->clearMessage(); this->statusBar()->showMessage( // timeout: 10000 - QString(title + ": " + text).leftJustified(80, ' '), 10000); + QString(title + " " + text).leftJustified(80, ' '), 10000); } From a45e552d9043459f9456bf266248573d78801481 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Fri, 25 Aug 2023 08:46:29 +0200 Subject: [PATCH 187/239] set version to 1.2.0 --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 2fe6d00..041d97f 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -15,7 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -VERSION=1.1.1 +VERSION=1.2.0 INCLUDEPATH += plugins From 542784497707f6827ebd41ea94829c82e6c24cfd Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:36:28 +0200 Subject: [PATCH 188/239] For each customer repository change the file ChangeLog in branch master as last step. The git commit for this file will be used as output, so this file has always be the last to be checked in. --- git/git_client.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index f3c4ae1..2dcacb3 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -120,7 +120,10 @@ QStringList GitClient::gitShowReason() { // %h: commit (short form) // %s: commit message // %cI: commit date, strict ISO 8601 format - Command c("git show -s --format=\"c=%h m=%s d=%cI\""); + // Note: use master branch. By convention, there is a ChangeLog file + // in the root of the repository, which has to be always the last file + // to be checked in when the customer repository somehow changed. + Command c("git show origin/master -s --format=\"c=%h m=%s d=%cI\""); if (c.execute(m_customerRepository)) { QString const s = c.getCommandResult().trimmed(); int const c = s.indexOf("c="); From 0a28f0d82c2b66b55b64afea2e580c63ef130c2a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:39:37 +0200 Subject: [PATCH 189/239] Add message for current APISM version --- mainwindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mainwindow.cpp b/mainwindow.cpp index 1a80fb4..743a471 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -38,8 +38,10 @@ MainWindow::MainWindow(Worker *worker, QWidget *parent) lst << QString("Machine number : %1 ").arg(m_worker->machineNr()).leftJustified(m_width-3); lst << QString("Customer number : %1 ").arg(m_worker->customerNr()).leftJustified(m_width-3); lst << QString("Zone number : %1 (%2)").arg(m_worker->zoneNr()).arg(Utils::zoneName(m_worker->zoneNr())).leftJustified(m_width-3); + lst << QString("APISM version : %1").arg(m_worker->apismVersion()).leftJustified(m_width-3); lst << QString("").leftJustified(m_width-3, '='); + ui->updateStatus->setText(lst.join('\n')); ui->updateStatus->setEnabled(true); From 7e4b5006eb934e397e433cdf29a7f7e497c9c301 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:41:23 +0200 Subject: [PATCH 190/239] Fixed return string of final result(). --- ismas/ismas_client.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index df104d3..5b224cb 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -829,7 +829,7 @@ QString IsmasClient::updateOfPSAActivated(QString const &version) { // sent ev } QString IsmasClient::updateOfPSASucceeded(QString const &version) { - m_progressInPercent = 0; + m_progressInPercent = 100; return updateNewsToIsmas("U0001", m_progressInPercent, RESULT_CODE::SUCCESS, @@ -857,13 +857,13 @@ QString IsmasClient::jsonParseFailed(int resultCode, QString reason, QString con } std::optional IsmasClient::finalResult(int resultCode, QString reason, QString const &version) { - m_progressInPercent = 0; + m_progressInPercent = 100; if (resultCode == RESULT_CODE::SUCCESS) { return updateNewsToIsmas("U0002", m_progressInPercent, - RESULT_CODE::SUCCESS, + resultCode, "FINAL-UPDATE-RESULT", - "(re-)set WAIT state", + reason.toStdString().c_str(), version.toStdString().c_str()); } if (resultCode == RESULT_CODE::INSTALL_ERROR) { From a84f495d43969a4ca16ec532c8ff8ec204cadfb0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:42:05 +0200 Subject: [PATCH 191/239] Removed output of rauc/opkg-versions, and added output of apism-version. --- ismas/ismas_client.cpp | 8 ++++---- ismas/ismas_client.h | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 5b224cb..0f08d87 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -616,8 +616,9 @@ QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { "}" "}," "\"SOFTWARE\": {" - "\"RAUC\" : \"%s\"," - "\"OPKG\" : \"%s\"," + "\"APISM\" : {" + "\"VERSION\" : \"%s\"" + "}," "\"ATBQT\" : {" "\"VERSION\" : \"%s\"" "}" @@ -745,8 +746,7 @@ QString IsmasClient::updateOfPSASendVersion(PSAInstalled const &psa) { psa.dc.gitBlob.toStdString().c_str(), psa.dc.gitLastCommit.toStdString().c_str(), - psa.sw.raucVersion.toStdString().c_str(), - psa.sw.opkgVersion.toStdString().c_str(), + psa.sw.apismVersion.toStdString().c_str(), psa.sw.atbQTVersion.toStdString().c_str(), psa.pluginVersion.deviceController.toStdString().c_str(), diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index 2e028eb..71beffc 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -46,8 +46,7 @@ struct PSAInstalled { } dc; struct SoftWare { - QString raucVersion; - QString opkgVersion; + QString apismVersion; QString atbQTVersion; } sw; @@ -97,8 +96,7 @@ struct PSAInstalled { dc.gitLastCommit = "N/A"; dc.size = -1; - sw.raucVersion = "N/A"; - sw.opkgVersion = "N/A"; + sw.apismVersion = "N/A"; sw.atbQTVersion = "N/A"; pluginVersion.deviceController = "N/A"; From 12ffa7145520f50446d4922f5bdfb8a5571770fc Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:44:20 +0200 Subject: [PATCH 192/239] Removed rauc/opkg-versions. Added fetching of apism-version. --- worker.cpp | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/worker.cpp b/worker.cpp index a3890de..058fe18 100644 --- a/worker.cpp +++ b/worker.cpp @@ -57,8 +57,6 @@ Worker::Worker(hwinf *hw, , m_osVersion(getOsVersion()) , m_atbqtVersion(getATBQTVersion()) , m_cpuSerial(getCPUSerial()) - , m_raucVersion(getRaucVersion()) - , m_opkgVersion(getOpkgVersion()) , m_pluginVersionATBDeciceController(getPluginVersion("/opt/app/ATBAPP/plugins/libATBDeviceControllerPlugin.so")) , m_pluginVersionIngenicoISelf(getPluginVersion("/opt/app/ATBAPP/plugins/libIngenicoISelf_CCPlugin.so")) , m_pluginVersionMobilisisCalc(getPluginVersion("/opt/app/ATBAPP/plugins/libMOBILISIS_CalculatePricePlugin.so")) @@ -76,6 +74,16 @@ Worker::Worker(hwinf *hw, QDir::setCurrent(m_workingDirectory); + // restart apism to make sure it is running ? + // Command c("systemctl restart apism"); + // if (c.execute("/tmp")) { + // QThread::sleep(10); // give APISM some time to reconnect + // } + + if (std::optional v = getApismVersion()) { + m_apismVersion = v.value(); + } + qInfo() << "CURRENT TIME ..............." << QDateTime::currentDateTime().toString(Qt::ISODate); qInfo() << "OS VERSION ................." << m_osVersion; qInfo() << "ATBQT VERSION .............." << m_atbqtVersion; @@ -88,6 +96,7 @@ Worker::Worker(hwinf *hw, qInfo() << "ZONE_NR ...................." << m_zoneNr; qInfo() << "BRANCH_NAME ................" << m_branchName; qInfo() << "WORKING_DIRECTORY .........." << m_workingDirectory; + qInfo() << "APISM VERSION .............." << m_apismVersion; this->moveToThread(&m_workerThread); m_workerThread.start(); @@ -141,20 +150,8 @@ void Worker::update() { void Worker::privateUpdate() { m_updateProcessRunning = true; - bool sentIsmasLastVersionNotification = false; - //emit appendText("\nRestart APISM ..."); - //startProgressLoop(); - //Command c("systemctl restart apism"); - //if (c.execute("/tmp")) { - // QThread::sleep(10); // give APISM some time to reconnect - // stopProgressLoop(); - // emit replaceLast("Restart APISM ...", UPDATE_STEP_DONE); - //} else { - // stopProgressLoop(); - // emit replaceLast("Restart APISM ...", UPDATE_STEP_FAIL); - //} emit disableExit(); @@ -1212,8 +1209,7 @@ PSAInstalled Worker::getPSAInstalled() { psaInstalled.dc.gitLastCommit = "N/A"; psaInstalled.dc.size = -1; - psaInstalled.sw.raucVersion = m_raucVersion; - psaInstalled.sw.opkgVersion = m_opkgVersion; + psaInstalled.sw.apismVersion = m_apismVersion; psaInstalled.sw.atbQTVersion = m_atbqtVersion; psaInstalled.pluginVersion.deviceController = m_pluginVersionATBDeciceController; From 507586f9dc0d169539f8782d969d8617b4183ede Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:46:00 +0200 Subject: [PATCH 193/239] Added fetching apism-version. Fixed calls to final_result(). --- worker.cpp | 303 +++++++++++++++++++---------------------------------- 1 file changed, 107 insertions(+), 196 deletions(-) diff --git a/worker.cpp b/worker.cpp index 058fe18..17ffc43 100644 --- a/worker.cpp +++ b/worker.cpp @@ -205,122 +205,110 @@ void Worker::privateUpdate() { } } else { m_ismasClient.setProgressInPercent(10); - if (backendConnected()) { + if (updateTriggerSet()) { m_ismasClient.setProgressInPercent(20); - if (updateTriggerSet()) { + if (customerEnvironment()) { m_ismasClient.setProgressInPercent(30); - if (customerEnvironment()) { + if (filesToUpdate()) { + // send message to ISMAS about files which have been + // checked in into git repository + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECK_FILES_TO_UPDATE_SUCCESS, + QString("Files to update: ") + m_filesToUpdate.join(',')); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAContinues("CHECK-FILES-TO-UPDATE", + m_updateStatus.m_statusDescription)); m_ismasClient.setProgressInPercent(40); - if (filesToUpdate()) { - // send message to ISMAS about files which have been - // checked in into git repository - m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECK_FILES_TO_UPDATE_SUCCESS, - QString("Files to update: ") + m_filesToUpdate.join(',')); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateOfPSAContinues("CHECK-FILES-TO-UPDATE", - m_updateStatus.m_statusDescription)); + if (updateFiles(50)) { m_ismasClient.setProgressInPercent(50); - if (updateFiles(50)) { + if (syncCustomerRepositoryAndFS()) { m_ismasClient.setProgressInPercent(60); - if (syncCustomerRepositoryAndFS()) { + if (sendIsmasLastVersionNotification()) { m_ismasClient.setProgressInPercent(70); - if (sendIsmasLastVersionNotification()) { + sentIsmasLastVersionNotification = true; + if (saveLogFile()) { m_ismasClient.setProgressInPercent(80); - sentIsmasLastVersionNotification = true; - if (saveLogFile()) { - m_ismasClient.setProgressInPercent(90); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateOfPSASucceeded("")); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSASucceeded("")); - // mark update as activated -> this resets the WAIT button - m_ismasClient.setProgressInPercent(95); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateOfPSAActivated()); + // mark update as activated -> this resets the WAIT button + m_ismasClient.setProgressInPercent(95); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.updateOfPSAActivated()); - m_returnCode = 0; - } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::SAVE_LOG_FILES_FAILED, - QString("Saving log files failed")); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "SAVE-LOG-FILES", - m_updateStatus.m_statusDescription)); - m_returnCode = -11; - } + m_returnCode = 0; } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_SEND_LAST_VERSION_FAILED, - QString("Sending ISMAS last version failed")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::SAVE_LOG_FILES_FAILED, + QString("Saving log files failed")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "ISMAS-SEND-LAST-VERSION", + "SAVE-LOG-FILES", m_updateStatus.m_statusDescription)); - m_returnCode = -10; + m_returnCode = -11; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::RSYNC_UPDATES_FAILURE, - QString("Syncing files to update failed")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_SEND_LAST_VERSION_FAILED, + QString("Sending ISMAS last version failed")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "RSYNC-UPDATE-FILES", + "ISMAS-SEND-LAST-VERSION", m_updateStatus.m_statusDescription)); - m_returnCode = -9; + m_returnCode = -10; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::PSA_UPDATE_FILES_FAILED, - QString("Updating files failed")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::RSYNC_UPDATES_FAILURE, + QString("Syncing files to update failed")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "UPDATE-FILES", + "RSYNC-UPDATE-FILES", m_updateStatus.m_statusDescription)); - m_returnCode = -8; + m_returnCode = -9; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE, - QString("No files to update")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::PSA_UPDATE_FILES_FAILED, + QString("Updating files failed")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "FETCH-FILES-TO-UPDATE", + "UPDATE-FILES", m_updateStatus.m_statusDescription)); - m_returnCode = -7; + m_returnCode = -8; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH_FAILURE, - QString("Configuring customer environment failed")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_FETCH_UPDATES_REQUEST_FAILURE, + QString("No files to update")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "GIT-CHECKOUT-BRANCH", + "FETCH-FILES-TO-UPDATE", m_updateStatus.m_statusDescription)); - m_returnCode = -6; + m_returnCode = -7; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET_FAILURE, - QString("ISMAS update trigger wrong")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::GIT_CHECKOUT_BRANCH_FAILURE, + QString("Configuring customer environment failed")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "CHECK-UPDATE-TRIGGER", + "GIT-CHECKOUT-BRANCH", m_updateStatus.m_statusDescription)); - m_returnCode = -5; + m_returnCode = -6; } } else { - m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK_FAILURE, - QString("ISMAS backend not available")); + m_updateStatus = UpdateStatus(UPDATE_STATUS::ISMAS_UPDATE_TRIGGER_SET_FAILURE, + QString("ISMAS update trigger wrong")); IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + m_ismasClient.updateOfPSAFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "ISMAS-BACKEND-CHECK", + "CHECK-UPDATE-TRIGGER", m_updateStatus.m_statusDescription)); - m_returnCode = -4; + m_returnCode = -5; } } @@ -334,7 +322,6 @@ void Worker::privateUpdate() { m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_FAILURE, QString("Update process failed")); if (std::optional s = m_ismasClient.finalResult(IsmasClient::RESULT_CODE::INSTALL_ERROR, - "FINAL-UPDATE-RESULT", m_updateStatus.m_statusDescription)) { IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + s.value()); @@ -343,9 +330,8 @@ void Worker::privateUpdate() { emit appendText(QString("UPDATE "), UPDATE_STEP_SUCCESS); m_updateStatus = UpdateStatus(UPDATE_STATUS::UPDATE_PROCESS_SUCCESS, - QString("Update process succeeded")); + QString("Update process succeeded. Reset WAIT.")); if (std::optional s = m_ismasClient.finalResult(IsmasClient::RESULT_CODE::SUCCESS, - "FINAL-UPDATE-RESULT", m_updateStatus.m_statusDescription)) { IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, QString("#M=APISM#C=CMD_EVENT#J=") + s.value()); @@ -362,137 +348,62 @@ void Worker::privateUpdate() { emit restartExitTimer(); } -bool Worker::backendConnected() { - // deactivated: REQ_SELF does not really help. Observation was that even - // id ISMAS is reported as 'true', a following check of the update-trigger - // button has no access to ISMAS. - return true; - - emit appendText("\nConnecting backend ..."); - - if (false) { // so linker removes dead code - for (int repeat = 0; repeat < 100; ++repeat) { - qInfo() << "REPEAT" << repeat << "In backendConnected() -> #M=APISM#C=REQ_SELF#J={}"; - startProgressLoop(); - std::optional result - = IsmasClient::sendRequestReceiveResponse( - IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_SELF#J={}"); - if (result) { - stopProgressLoop(); - int progress = (m_mainWindow->progressValue()/10) + 10; - setProgress(progress); - - QString msg = result.value(); - qInfo() << "In backendConnected() -> APISM response" << msg; - QJsonParseError parseError; - QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); - if (parseError.error != QJsonParseError::NoError) { - qCritical() << "(1) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" - << parseError.error << parseError.errorString(); - setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, - QString("(2) INVALID JSON %1 %2 %3") - .arg(msg) - .arg(parseError.error) - .arg(parseError.errorString())); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - m_updateStatus.m_statusDescription)); - emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); - return false; - } - if (!document.isObject()) { - qCritical() << "FILE IS NOT A JSON OBJECT!"; - setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, - QString("NOT A JSON-OBJECT %1").arg(msg)); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - m_updateStatus.m_statusDescription)); - emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); - return false; - } - - setProgress(progress + 1); - - QJsonObject obj = document.object(); - QStringList keys = obj.keys().filter("CMD_GET_APISMSTATUS_RESPONSE"); - if (keys.size() == 1) { - QString const key = keys.at(0); - QJsonValue v = obj.value(key); - if (v.isObject()) { - obj = v.toObject(); - bool ismas = obj.value("ISMAS").toBool(); - QString status = obj.value("Broker").toString(); - qInfo() << "REPEAT" << repeat << "In backendConnected() Broker=<" - << status << ">, ISMAS=<" << (ismas ? "true>" : "false>"); - if (ismas) { - if (status == "Connected") { - // do not send, as this would result in a corrupted wait button - // but update the user-interface - setProgress(100); - emit replaceLast("Connecting backend ...", UPDATE_STEP_OK); - return true; - } - } - - m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK, - QString ("REPEAT %1 Broker=<").arg(repeat) - + status + ">, ISMAS=<" + (ismas ? "true>" : "false>")); - //IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - // QString("#M=APISM#C=CMD_EVENT#J=") + - // m_ismasClient.updateOfPSAContinues("BACKEND-CHECK", m_updateStatus.m_statusDescription)); - - qInfo() << "BACKEND-CHECK" << m_updateStatus.m_statusDescription; - emit showErrorMessage("Check backend connection", m_updateStatus.m_statusDescription); - QThread::sleep(6); - continue; - } else { - setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK_FAILURE, - "CMD_GET_APISM_STATUS_RESPONSE KEY NOT A JSON-OBJECT"); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - m_updateStatus.m_statusDescription)); - emit showErrorMessage("check backend connection", m_updateStatus.m_statusDescription); - emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); - return false; - } - } else { - setProgress(100); - m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK_FAILURE, - "CMD_GET_APISMSTATUS_RESPONSE KEY NOT AVAILABLE"); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, - m_updateStatus.m_statusDescription)); - emit showErrorMessage("check backend connection", m_updateStatus.m_statusDescription); - emit replaceLast("Connecting backend ...", UPDATE_STEP_FAIL); - return false; - } - - } else { - stopProgressLoop(); - int progress = (m_mainWindow->progressValue()/10) + 10; - setProgress(progress); +std::optional Worker::getApismVersion() { + for (int repeat = 0; repeat < 10; ++repeat) { + qInfo() << "REPEAT" << repeat << "In getApismVersion() -> #M=APISM#C=REQ_SELF#J={}"; + std::optional result + = IsmasClient::sendRequestReceiveResponse( + IsmasClient::APISM::DIRECT_PORT, "#M=APISM#C=REQ_SELF#J={}"); + if (result) { + QString msg = result.value(); + qInfo() << "In getApismVersion() -> APISM response" << msg; + QJsonParseError parseError; + QJsonDocument document(QJsonDocument::fromJson(msg.toUtf8(), &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qCritical() << "(1) INVALID JSON MSG: PARSING FAILED (msg=" << msg << "):" + << parseError.error << parseError.errorString(); + m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, + QString("(2) INVALID JSON %1 %2 %3") + .arg(msg) + .arg(parseError.error) + .arg(parseError.errorString())); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + return std::nullopt; } + if (!document.isObject()) { + qCritical() << "FILE IS NOT A JSON OBJECT!"; + m_updateStatus = UpdateStatus(UPDATE_STATUS::JSON_PARSE_FAILURE, + QString("NOT A JSON-OBJECT %1").arg(msg)); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.jsonParseFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + return std::nullopt; + } + QJsonObject obj = document.object(); + QStringList keys = obj.keys().filter("CMD_GET_APISMSTATUS_RESPONSE"); + if (keys.size() != 1) { + m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_CHECK_FAILURE, + "CMD_GET_APISMSTATUS_RESPONSE KEY NOT AVAILABLE"); + IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, + QString("#M=APISM#C=CMD_EVENT#J=") + + m_ismasClient.sanityCheckFailed(IsmasClient::RESULT_CODE::INSTALL_ERROR, + m_updateStatus.m_statusDescription)); + emit showErrorMessage("apism response", m_updateStatus.m_statusDescription); + return std::nullopt; + } else { + QString const key = keys.at(0); + QJsonValue v = obj.value(key); + return v.toObject().value("Version").toString(); + } + } else { + QThread::sleep(1); } - - setProgress(100); - - emit replaceLast("Connecting backend", UPDATE_STEP_FAIL); - emit showErrorMessage("Error", "Backend not available"); - - m_updateStatus = UpdateStatus(UPDATE_STATUS::BACKEND_NOT_CONNECTED, - QString("NO BACKEND CONNECTION")); - IsmasClient::sendRequestReceiveResponse(IsmasClient::APISM::DB_PORT, - QString("#M=APISM#C=CMD_EVENT#J=") + - m_ismasClient.errorBackendNotConnected(m_updateStatus.m_statusDescription, "")); } - return false; + return std::nullopt; } #define CHECK_UPDATE_TRIGGER_SET "Check update trigger ..." From 01d8312aa8fa2568a8cfe9743ad19114bfc3cb1d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:46:55 +0200 Subject: [PATCH 194/239] Removed rauc/opkg-members. Added m_apismVersion member. --- worker.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/worker.h b/worker.h index ea67786..54d0ddf 100644 --- a/worker.h +++ b/worker.h @@ -9,6 +9,8 @@ #include #include +#include + #include "worker_thread.h" #include "update.h" #include "git/git_client.h" @@ -88,8 +90,6 @@ class Worker : public QObject { QString const m_osVersion; QString const m_atbqtVersion; QString const m_cpuSerial; - QString const m_raucVersion; - QString const m_opkgVersion; QString const m_pluginVersionATBDeciceController; QString const m_pluginVersionIngenicoISelf; QString const m_pluginVersionMobilisisCalc; @@ -111,6 +111,7 @@ class Worker : public QObject { MainWindow *m_mainWindow; int m_progressValue; bool m_withoutIsmasDirectPort; + QString m_apismVersion; bool executeOpkgCommand(QString opkgCommand); QString getOsVersion() const; @@ -155,6 +156,7 @@ public: int machineNr() const { return m_machineNr; } int customerNr() const { return m_customerNr; } int zoneNr() const { return m_zoneNr; } + QString apismVersion() const { return m_apismVersion; } //friend QDebug operator<<(QDebug debug, Worker const &w) { // Q_UNUSED(w); @@ -179,7 +181,6 @@ public slots: void update(); private slots: - bool backendConnected(); bool updateTriggerSet(); bool customerEnvironment(); bool filesToUpdate(); @@ -191,6 +192,7 @@ private slots: private: PSAInstalled getPSAInstalled(); void privateUpdate(); + std::optional getApismVersion(); }; #endif // WORKER_H_INCLUDED From 1ef9853876406500ea75caaa2ac452e09db97762 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 30 Aug 2023 11:47:56 +0200 Subject: [PATCH 195/239] Set version of UpdateTool to 1.3.0. 1.3.0: main change: add fetching info for current apiism-version and send if to ISMAS. --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 041d97f..a074f15 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -15,7 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -VERSION=1.2.0 +VERSION=1.3.0 INCLUDEPATH += plugins From b14b29601157fafd3d807f9ca651079543de3494 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 4 Sep 2023 11:42:12 +0200 Subject: [PATCH 196/239] Added utility getParentName() (name of parent process) --- utils.cpp | 24 ++++++++++++++++++++++++ utils.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/utils.cpp b/utils.cpp index 1d3c7de..7640bf3 100644 --- a/utils.cpp +++ b/utils.cpp @@ -2,6 +2,8 @@ #include "message_handler.h" #include "git/git_client.h" +#include "unistd.h" + #include #include #include @@ -169,3 +171,25 @@ bool Utils::sameFilesInDirs(QDir const &dir1, QDir const &dir2, return true; } + + +QString Utils::getParentName() { // get name of parent process + QString ppid = QString("/proc/%1/status").arg(getppid()); + QFile f(ppid); + if (f.exists()) { + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream in(&f); + in.setCodec("UTF-8"); + while(!in.atEnd()) { + // Name: ATBQT + QStringList line = in.readLine().split(':'); + if (line.size() == 2) { + if (line[0].trimmed() == "Name") { + return line[1].trimmed(); + } + } + } + } + } + return ""; +} diff --git a/utils.h b/utils.h index 7f16616..62f2c38 100644 --- a/utils.h +++ b/utils.h @@ -19,6 +19,9 @@ namespace Utils { QString rstrip(QString const &str); bool sameFilesInDirs(QDir const &dir1, QDir const &dir2, QStringList const &nameFilters = {"*.json"}); + + + QString getParentName(); } #endif // UTILS_H_INCLUDED From 981a2ea13a95e36a38a5874c5a60697daea16843 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 4 Sep 2023 11:45:30 +0200 Subject: [PATCH 197/239] Added method getReasonForSendLastVersion() --- ismas/ismas_client.cpp | 16 ++++++++++++++++ ismas/ismas_client.h | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/ismas/ismas_client.cpp b/ismas/ismas_client.cpp index 0f08d87..0e5ba65 100644 --- a/ismas/ismas_client.cpp +++ b/ismas/ismas_client.cpp @@ -1,4 +1,5 @@ #include "ismas/ismas_client.h" +#include "utils.h" #include #include @@ -886,3 +887,18 @@ QString IsmasClient::updateOfPSAFailed(int resultCode, QString step, reason.toStdString().c_str(), version.toStdString().c_str()); } + +char const *IsmasClient::reason[REASON::ENTRIES] = { + "TIME-TRIGGERED", "SERVICE", "DEV-TEST" +}; + +QString IsmasClient::getReasonForLastSendVersion() { + QString const &parentName = Utils::getParentName(); + if (parentName == "ATBQT") { + return reason[REASON::SERVICE]; + } + if (parentName == "systemd") { + return reason[REASON::TIME_TRIGGERED]; + } + return reason[REASON::DEV_TEST]; +} diff --git a/ismas/ismas_client.h b/ismas/ismas_client.h index 71beffc..a43fe1f 100644 --- a/ismas/ismas_client.h +++ b/ismas/ismas_client.h @@ -139,6 +139,7 @@ public: DIRECT_PORT = 7778 }; + enum RESULT_CODE { SUCCESS=0, NO_UPDATE_NECESSARY=1, @@ -146,9 +147,20 @@ public: WRONG_PACKAGE=3, INSTALL_ERROR=4}; + enum REASON { + TIME_TRIGGERED = 0, + SERVICE, + DEV_TEST, + ENTRIES + }; + + static char const *reason[REASON::ENTRIES]; + static std::optional sendRequestReceiveResponse(int port, QString const &request); + static QString getReasonForLastSendVersion(); + int getProgressInPercent() const {return m_progressInPercent; } void setProgressInPercent(int procent) { m_progressInPercent = procent; } From bb35e985ad238251e53632885b7e1d65a041568c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 4 Sep 2023 11:46:37 +0200 Subject: [PATCH 198/239] Using IsmasClient::getReasonForLastSendVersion() --- git/git_client.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/git/git_client.cpp b/git/git_client.cpp index 2dcacb3..88dbe1a 100644 --- a/git/git_client.cpp +++ b/git/git_client.cpp @@ -129,7 +129,10 @@ QStringList GitClient::gitShowReason() { int const c = s.indexOf("c="); int const m = s.indexOf("m="); int const d = s.indexOf("d="); - QString commit{""}, msg{""}, date{""}; + + QString msg = IsmasClient::getReasonForLastSendVersion(); + QString commit{""}, date{""}; + if (c != -1) { int start = c + 2; if (m >= start) { @@ -139,7 +142,9 @@ QStringList GitClient::gitShowReason() { start = m + 2; if (d >= start) { length = d - start; + msg += " ("; msg = s.mid(start, length).trimmed(); + msg += ")"; start = d + 2; date = s.mid(start); From cef05b7511f9c189a0687fc3be30bf6120e1216f Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 4 Sep 2023 11:48:15 +0200 Subject: [PATCH 199/239] Set version to 1.3.1: extended reason in send-last-version --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index a074f15..e3d1813 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -15,7 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -VERSION=1.3.0 +VERSION=1.3.1 INCLUDEPATH += plugins From c065b57f0c34b38c620287f4378852a67bfae994 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 6 Sep 2023 09:04:43 +0200 Subject: [PATCH 200/239] Remove direct member m_hw, a pointer to the device-controller-plugin. The worker shall not load the plugin, otherwise it would block itself inside an QT slot. --- worker.cpp | 57 +++++++++++++++++++++++++++++++++++++++++------------- worker.h | 12 +++++++++--- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/worker.cpp b/worker.cpp index 17ffc43..6a5c050 100644 --- a/worker.cpp +++ b/worker.cpp @@ -31,28 +31,31 @@ QString const Worker::UPDATE_STEP_DONE(" [done]"); QString const Worker::UPDATE_STEP_FAIL(" [FAIL]"); QString const Worker::UPDATE_STEP_SUCCESS(" [SUCCESS]"); -Worker::Worker(hwinf *hw, - int customerNr, +Worker::Worker(int customerNr, int machineNr, int zoneNr, QString branchName, + QString pluginName, QString workingDirectory, bool dryRun, QObject *parent, char const *serialInterface, char const *baudrate) - : m_hw(hw) - , m_workerThread("workerThread") + : m_workerThread("workerThread") , m_customerNr(customerNr) , m_customerNrStr(QString("customer_") + QString::number(m_customerNr).rightJustified(3, '0')) , m_machineNr(machineNr) , m_zoneNr(zoneNr) + , m_pluginName(pluginName) , m_workingDirectory(workingDirectory) , m_branchName(branchName) , m_customerRepositoryPath(QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(m_customerNrStr)) , m_customerRepository(QDir::cleanPath(m_workingDirectory + QDir::separator() + m_customerNrStr)) - , m_update(new Update(m_hw, this, m_customerRepository, m_customerNrStr, m_branchName, - m_workingDirectory, dryRun, parent, serialInterface, baudrate)) + , m_dryRun(dryRun) + , m_parent(parent) + , m_serialInterface(serialInterface) + , m_baudrate(baudrate) + , m_update(nullptr) , m_gc(m_customerNrStr, m_customerRepository, m_workingDirectory, m_branchName, this) , m_osVersion(getOsVersion()) , m_atbqtVersion(getATBQTVersion()) @@ -70,7 +73,7 @@ Worker::Worker(hwinf *hw, , m_updateProcessRunning(true) , m_returnCode(0) , m_progressValue(0) - , m_withoutIsmasDirectPort(false) /* useful for testing */ { + , m_withoutIsmasDirectPort(true) /* useful for testing */ { QDir::setCurrent(m_workingDirectory); @@ -95,6 +98,7 @@ Worker::Worker(hwinf *hw, qInfo() << "MACHINE_NR ................." << m_machineNr; qInfo() << "ZONE_NR ...................." << m_zoneNr; qInfo() << "BRANCH_NAME ................" << m_branchName; + qInfo() << "PLUGIN_NAME ................" << m_pluginName; qInfo() << "WORKING_DIRECTORY .........." << m_workingDirectory; qInfo() << "APISM VERSION .............." << m_apismVersion; @@ -149,10 +153,14 @@ void Worker::update() { } void Worker::privateUpdate() { + if (!m_mainWindow) { + Utils::printCriticalErrorMsg("m_mainWindow NOT SET"); + return; + } + m_updateProcessRunning = true; bool sentIsmasLastVersionNotification = false; - emit disableExit(); m_returnCode = -1; @@ -845,7 +853,7 @@ bool Worker::updateFiles(quint8 percent) { if (fName.contains("print", Qt::CaseInsensitive)) { filesToDownload << fName; // download printer-config-files } else { - static const QRegularExpression version("^.*dc2c[.][0-9][0-9][.][0-9][0-9][.]bin.*$"); + static const QRegularExpression version("^.*dc2c[.][0-9]{0,1}[0-9][.][0-9][0-9][.]bin.*$"); if (fName.contains(version)) { filesToDownload << fName; // download device controller } @@ -858,7 +866,21 @@ bool Worker::updateFiles(quint8 percent) { if (filesToDownload.size() > 0) { Utils::printInfoMsg(QString("FILES_TO_DOWNLOAD_TO_PSA_HW ") + filesToDownload.join(',')); + + m_update = new Update(m_mainWindow->getPlugin(), + this, + m_customerRepository, + m_customerNrStr, + m_branchName, + m_pluginName, + m_workingDirectory, + m_dryRun, + m_parent, + m_serialInterface.toStdString().c_str(), + m_baudrate.toStdString().c_str()); + ret = m_update->doUpdate(m_displayIndex, filesToDownload); + } else { Utils::printCriticalErrorMsg("NO FILES_TO_DOWNLOAD_TO_PSA_HW"); } @@ -1017,14 +1039,15 @@ QString Worker::getPluginVersion(QString const &pluginFileName) const { QStringList Worker::getDCVersion() const { QStringList lst = (QStringList() << "N/A" << "N/A"); - if (m_hw) { - m_hw->dc_autoRequest(true); // turn auto-request setting on + hwinf *hwi = m_mainWindow->getPlugin(); + if (hwi) { + hwi->dc_autoRequest(true); // turn auto-request setting on QByteArray const cmp(8, char(0)); QByteArray hw(""), sw(""); for (int i=0; i<5; ++i) { - hw = m_hw->dc_getHWversion().toUtf8(); - sw = m_hw->dc_getSWversion().toUtf8(); + hw = hwi->dc_getHWversion().toUtf8(); + sw = hwi->dc_getSWversion().toUtf8(); if (!hw.startsWith(cmp)) { lst.clear(); qInfo() << hw << sw; @@ -1157,6 +1180,14 @@ PSAInstalled Worker::getPSAInstalled() { return psaInstalled; } +hwinf *Worker::getPlugin() { + return m_mainWindow ? m_mainWindow->getPlugin() : nullptr; +} + +hwinf const *Worker::getPlugin() const { + return m_mainWindow ? m_mainWindow->getPlugin() : nullptr; +} + /************************************************************************************************ * operators */ diff --git a/worker.h b/worker.h index 54d0ddf..3907c25 100644 --- a/worker.h +++ b/worker.h @@ -74,16 +74,20 @@ class hwinf; class Worker : public QObject { Q_OBJECT - hwinf *m_hw; WorkerThread m_workerThread; int const m_customerNr; QString const m_customerNrStr; int const m_machineNr; int const m_zoneNr; + QString const m_pluginName; QString const m_workingDirectory; QString const m_branchName; QString const m_customerRepositoryPath; QString const m_customerRepository; + bool const m_dryRun; + QObject *m_parent; + QString const m_serialInterface; + QString const m_baudrate; Update *m_update; IsmasClient m_ismasClient; GitClient m_gc; @@ -130,11 +134,11 @@ public: static const QString UPDATE_STEP_FAIL; static const QString UPDATE_STEP_SUCCESS; - explicit Worker(hwinf *hw, - int customerNr, // 281 + explicit Worker(int customerNr, // 281 int machineNr, int zoneNr, QString branchName, + QString pluginName, QString workingDir = ".", bool dryRun = false, QObject *parent = nullptr, @@ -143,6 +147,8 @@ public: ~Worker(); void setMainWindow(MainWindow *mainWindow) { m_mainWindow = mainWindow; } + hwinf *getPlugin(); + hwinf const *getPlugin() const; void setProgress(int progress); void startProgressLoop(); void stopProgressLoop(); From 9531a08b4aa95610e4e954b98389a7459e35db70 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 6 Sep 2023 09:07:45 +0200 Subject: [PATCH 201/239] Add a pointer to the device-controller-plugin. The main window will always be owned by the GUI thread, and the GUI thread is loading the plugin. Hence the worker-thread does not block itself when inside a QT slot. --- mainwindow.cpp | 5 +++-- mainwindow.h | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 743a471..ed4d2f3 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -3,21 +3,22 @@ #include "worker.h" #include "utils.h" #include "progress_event.h" +#include "plugins/interfaces.h" #include #include #include #include -MainWindow::MainWindow(Worker *worker, QWidget *parent) +MainWindow::MainWindow(hwinf *hw, Worker *worker, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) + , m_hw(hw) , m_worker(worker) , m_width(70) , m_progressRunning(false) , m_progressValue(0) { - this->setStatusBar(new QStatusBar(this)); QFont f; f.setStyleHint(QFont::Monospace); diff --git a/mainwindow.h b/mainwindow.h index e54b0fc..9d4e3cd 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -11,6 +11,7 @@ QT_END_NAMESPACE #include "worker.h" +class hwinf; class MainWindow : public QMainWindow { Q_OBJECT @@ -18,13 +19,15 @@ protected: void customEvent(QEvent *event) override; public: - MainWindow(Worker *worker, QWidget *parent = nullptr); + MainWindow(hwinf *hw, Worker *worker, QWidget *parent = nullptr); ~MainWindow(); static const int START_PROGRESS_LOOP = -1; static const int STOP_PROGRESS_LOOP = -2; int progressValue() const { return m_progressValue; } + hwinf *getPlugin() { return m_hw; } + hwinf const *getPlugin() const { return m_hw; } public slots: void onAppendText(QString, QString suffix = ""); @@ -43,6 +46,7 @@ private: void scrollDownTextEdit(); Ui::MainWindow *ui; + hwinf *m_hw; Worker *m_worker; int m_width; QTimer *m_startTimer; From 22c8997f1e6d9bca377124fa8a0a599c2e093166 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 6 Sep 2023 09:10:14 +0200 Subject: [PATCH 202/239] Set autoRequest to false and pass a pointer to the device-controller-plugin to the main window instead to the worker (thread). --- main.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/main.cpp b/main.cpp index 8b48f51..16db53b 100644 --- a/main.cpp +++ b/main.cpp @@ -116,7 +116,7 @@ int main(int argc, char *argv[]) { #endif hwinf *hw = Update::loadDCPlugin(QDir(plugInDir), plugInName); - // hw->dc_autoRequest(false); + hw->dc_autoRequest(false); int machineNr = Utils::read1stLineOfFile("/etc/machine_nr"); int customerNr = Utils::read1stLineOfFile("/etc/cust_nr"); @@ -127,16 +127,15 @@ int main(int argc, char *argv[]) { QThread::currentThread()->setObjectName("main thread"); qInfo() << "Main thread" << QThread::currentThreadId(); - - Worker worker(hw, - customerNr, + Worker worker(customerNr, machineNr, zoneNr, branchName, + plugInName, workingDir, dryRun); - MainWindow mw(&worker); + MainWindow mw(hw, &worker); worker.setMainWindow(&mw); mw.setWindowFlags(Qt::Window | Qt::FramelessWindowHint); From 6773a7243ae25a26305671abdeb8d3e33da03904 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Wed, 6 Sep 2023 09:12:25 +0200 Subject: [PATCH 203/239] Save name of device-controller-plugin (either libCAmaster or libCAslave) in the Update-object. --- update.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/update.h b/update.h index 24c7d6f..f4dd43b 100644 --- a/update.h +++ b/update.h @@ -27,6 +27,7 @@ class Update : public QObject { QString m_customerRepository; QString m_customerNrStr; QString m_branchName; + QString m_pluginName; QString m_workingDir; bool m_maintenanceMode; bool m_dryRun; @@ -44,6 +45,7 @@ public: QString customerRepository, QString customerNrStr, QString branchName, + QString pluginName, QString workingDir, bool dryRun = false, QObject *parent = nullptr, From e82742a6099f5c3ab388c52ceda3de596bde2116 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:27:50 +0200 Subject: [PATCH 204/239] Add helper class update_dc_event to dend customer messages from the worker-thread to the gui-thread. The gui-thread will then perform bl_rebootDC, bl_startBL, bl_checkBL(), bl_isUp() and bl_stopBL(). --- update_dc_event.cpp | 25 +++++++++++++++++++++++++ update_dc_event.h | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 update_dc_event.cpp create mode 100644 update_dc_event.h diff --git a/update_dc_event.cpp b/update_dc_event.cpp new file mode 100644 index 0000000..a3b8ec9 --- /dev/null +++ b/update_dc_event.cpp @@ -0,0 +1,25 @@ +#include "update_dc_event.h" + +QEvent::Type UpdateDcEvent::customEventType = QEvent::None; + +UpdateDcEvent::UpdateDcEvent(QObject const *sender, + UpdateStep updateStep, + int count, + QDateTime const &sendDateTime) + : QEvent(UpdateDcEvent::type()) + , m_sender(sender) + , m_updateStep(updateStep) + , m_count(count) + , m_sendDateTime(sendDateTime) { +} + +UpdateDcEvent::~UpdateDcEvent() { +} + +QEvent::Type UpdateDcEvent::type() { + if (customEventType == QEvent::None) { + int generatedType = QEvent::registerEventType(); + customEventType = static_cast(generatedType); + } + return customEventType; +} diff --git a/update_dc_event.h b/update_dc_event.h new file mode 100644 index 0000000..869375a --- /dev/null +++ b/update_dc_event.h @@ -0,0 +1,40 @@ +#ifndef UPDATE_DC_EVENT_H_INCLUDED +#define UPDATE_DC_EVENT_H_INCLUDED + +#include +#include + +class UpdateDcEvent : public QEvent { +public: + enum UpdateStep { NONE, DC_REBOOT, BL_START, BL_CHECK, BL_CHECK_AFTER_STOP, BL_IS_UP, BL_IS_DOWN, BL_STOP}; + +private: + QObject const *m_sender; + UpdateStep m_updateStep; + int m_count; + QDateTime m_sendDateTime; + +public: + explicit UpdateDcEvent(QObject const *sender, UpdateStep updateStep, + int count, + QDateTime const &sendDateTime = QDateTime::currentDateTime()); + virtual ~UpdateDcEvent(); + static QEvent::Type type(); + + QObject const *sender() { return m_sender; } + QObject const *sender() const { return m_sender; } + + void setUpdateStep(UpdateStep updateStep) { m_updateStep = updateStep; } + UpdateStep updateStep() { return m_updateStep; } + UpdateStep updateStep() const { return m_updateStep; } + int count() const { return m_count; } + void setCount(int count) { m_count = count; } + QDateTime &sendDateTime() { return m_sendDateTime; } + QDateTime const &sendDateTime() const { return m_sendDateTime; } + +private: + static QEvent::Type customEventType; +}; + + +#endif // PROGRESS_EVENT_H_INCLUDED From ba71728979481187e0dc619f06c328c9ca3b3106 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:30:53 +0200 Subject: [PATCH 205/239] Move the dc-plugin (and the update-object) into the gui-thread instead of the worker thread, so the worker-thread cannot block itself when inside a slot and therefore not able to react to qt-signals. --- main.cpp | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index 16db53b..34d8085 100644 --- a/main.cpp +++ b/main.cpp @@ -32,6 +32,12 @@ #include #include +#include + +#if defined (Q_OS_UNIX) || defined (Q_OS_LINUX) +#include +#include +#endif #ifdef PTU5 #define SERIAL_PORT "ttymxc2" @@ -58,6 +64,24 @@ int main(int argc, char *argv[]) { setDebugLevel(LOG_NOTICE); } +//#if defined (Q_OS_UNIX) || defined (Q_OS_LINUX) +//#ifdef _POSIX_THREAD_PROCESS_SHARED +// errno = 0; +// int res = 0; +// if ((res = sysconf(_SC_THREAD_PROCESS_SHARED)) < 0) { +// if (errno != 0) { +// qCritical() << "_POSIX_THREAD_PROCESS_SHARED NOT SUPPORTED" +// << strerror(errno); +// exit(-1); +// } +// } else { +// if (res == _POSIX_THREAD_PROCESS_SHARED) { +// Utils::printInfoMsg("_POSIX_THREAD_PROCESS_SHARED SUPPORTED"); +// } +// } +//#endif +//#endif + QCommandLineParser parser; parser.setApplicationDescription("Download tool for downloading device controller firmware, printer json-files and executing opkg-commands."); parser.addHelpOption(); @@ -116,7 +140,8 @@ int main(int argc, char *argv[]) { #endif hwinf *hw = Update::loadDCPlugin(QDir(plugInDir), plugInName); - hw->dc_autoRequest(false); + hw->dc_autoRequest(true); + // hw->dc_openSerial(5, "115200", "ttymxc2", 1); int machineNr = Utils::read1stLineOfFile("/etc/machine_nr"); int customerNr = Utils::read1stLineOfFile("/etc/cust_nr"); @@ -135,7 +160,23 @@ int main(int argc, char *argv[]) { workingDir, dryRun); - MainWindow mw(hw, &worker); + QString const customerNrStr( + QString("customer_") + QString::number(customerNr).rightJustified(3, '0')); + + QScopedPointer update( + new Update(hw, + &worker, + QDir::cleanPath(workingDir + QDir::separator() + customerNrStr), + customerNrStr, + branchName, + plugInName, + workingDir, + dryRun, + nullptr, + SERIAL_PORT, + "115200")); + + MainWindow mw(hw, &worker, update.get()); worker.setMainWindow(&mw); mw.setWindowFlags(Qt::Window | Qt::FramelessWindowHint); From 276d65a9d8d34dc0b33e3ee4c9ca2c569fe37076 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:33:13 +0200 Subject: [PATCH 206/239] Add utility isATBQTRunning(). --- utils.cpp | 31 +++++++++++++++++++++++++++++++ utils.h | 1 + 2 files changed, 32 insertions(+) diff --git a/utils.cpp b/utils.cpp index 7640bf3..5496bee 100644 --- a/utils.cpp +++ b/utils.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include int Utils::read1stLineOfFile(QString fileName) { QFile f(fileName); @@ -193,3 +195,32 @@ QString Utils::getParentName() { // get name of parent process } return ""; } + +bool Utils::isATBQTRunning() { + QDirIterator it("/proc", + QStringList() << "status", + QDir::Files, + QDirIterator::Subdirectories); + while (it.hasNext()) { + QString const &nextStatusFile = it.next(); + QFile f(nextStatusFile); + if (f.exists()) { + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream in(&f); + in.setCodec("UTF-8"); + while(!in.atEnd()) { + // Name: ATBQT + QStringList line = in.readLine().split(':'); + if (line.size() == 2) { + if (line[0].trimmed() == "Name") { + if (line[1].trimmed() == "ATBQT") { + return true; + } + } + } + } + } + } + } + return false; +} diff --git a/utils.h b/utils.h index 62f2c38..6dd7dd7 100644 --- a/utils.h +++ b/utils.h @@ -22,6 +22,7 @@ namespace Utils { QString getParentName(); + bool isATBQTRunning(); } #endif // UTILS_H_INCLUDED From 7e96b65c1bb233245551ffb884589c4b730228d8 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:38:53 +0200 Subject: [PATCH 207/239] Move m_update-object to main window. Add signal for showing status messages at status bar of ATBUpdateTool gui. --- worker.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worker.h b/worker.h index 3907c25..2ecadfb 100644 --- a/worker.h +++ b/worker.h @@ -88,7 +88,6 @@ class Worker : public QObject { QObject *m_parent; QString const m_serialInterface; QString const m_baudrate; - Update *m_update; IsmasClient m_ismasClient; GitClient m_gc; QString const m_osVersion; @@ -164,6 +163,9 @@ public: int zoneNr() const { return m_zoneNr; } QString apismVersion() const { return m_apismVersion; } + MainWindow *mainWindow() { return m_mainWindow; } + MainWindow const *mainWindow() const { return m_mainWindow; } + //friend QDebug operator<<(QDebug debug, Worker const &w) { // Q_UNUSED(w); // return debug; @@ -178,6 +180,7 @@ signals: void replaceLast(QString, QString); void replaceLast(QStringList, QString); void showErrorMessage(QString title, QString description); + void showStatusMessage(QString title, QString description); void stopStartTimer(); void restartExitTimer(); void enableExit(); From 64dce44fe12ce42e44061d1ec718631d053ece97 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:40:43 +0200 Subject: [PATCH 208/239] Move update-object into main window. Activate using ISMAS WAIT button. --- worker.cpp | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/worker.cpp b/worker.cpp index 6a5c050..3f8582f 100644 --- a/worker.cpp +++ b/worker.cpp @@ -55,7 +55,6 @@ Worker::Worker(int customerNr, , m_parent(parent) , m_serialInterface(serialInterface) , m_baudrate(baudrate) - , m_update(nullptr) , m_gc(m_customerNrStr, m_customerRepository, m_workingDirectory, m_branchName, this) , m_osVersion(getOsVersion()) , m_atbqtVersion(getATBQTVersion()) @@ -73,8 +72,10 @@ Worker::Worker(int customerNr, , m_updateProcessRunning(true) , m_returnCode(0) , m_progressValue(0) - , m_withoutIsmasDirectPort(true) /* useful for testing */ { + //, m_withoutIsmasDirectPort(true) /* useful for testing */ { + , m_withoutIsmasDirectPort(false) /* useful for testing */ { + this->setObjectName("worker-object"); QDir::setCurrent(m_workingDirectory); // restart apism to make sure it is running ? @@ -126,9 +127,6 @@ Worker::~Worker() { } } } - if (m_update) { - delete m_update; - } } void Worker::setProgress(int progress) { @@ -798,7 +796,6 @@ bool Worker::filesToUpdate() { bool Worker::updateFiles(quint8 percent) { QStringList filesToDownload; m_displayIndex = 0; - startProgressLoop(); for (int i = 0; i < m_filesToUpdate.size(); ++i) { QString const fName = m_filesToUpdate.at(i); @@ -862,30 +859,19 @@ bool Worker::updateFiles(quint8 percent) { stopProgressLoop(); setProgress(100); - bool ret = true; if (filesToDownload.size() > 0) { Utils::printInfoMsg(QString("FILES_TO_DOWNLOAD_TO_PSA_HW ") + filesToDownload.join(',')); - m_update = new Update(m_mainWindow->getPlugin(), - this, - m_customerRepository, - m_customerNrStr, - m_branchName, - m_pluginName, - m_workingDirectory, - m_dryRun, - m_parent, - m_serialInterface.toStdString().c_str(), - m_baudrate.toStdString().c_str()); - - ret = m_update->doUpdate(m_displayIndex, filesToDownload); - + Update *update = m_mainWindow->getUpdate(); + if (update) { + return update->doUpdate(m_displayIndex, filesToDownload); + } } else { Utils::printCriticalErrorMsg("NO FILES_TO_DOWNLOAD_TO_PSA_HW"); } - return ret; + return true; } bool Worker::syncCustomerRepositoryAndFS() { From b2798b349ece00ac387febf49509de21b111a502 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:41:53 +0200 Subject: [PATCH 209/239] Fixed reg-exp for name of device controller firmare version. --- worker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker.cpp b/worker.cpp index 3f8582f..c787c05 100644 --- a/worker.cpp +++ b/worker.cpp @@ -850,7 +850,7 @@ bool Worker::updateFiles(quint8 percent) { if (fName.contains("print", Qt::CaseInsensitive)) { filesToDownload << fName; // download printer-config-files } else { - static const QRegularExpression version("^.*dc2c[.][0-9]{0,1}[0-9][.][0-9][0-9][.]bin.*$"); + static const QRegularExpression version("^.*dc2c[.][0-9]{1,2}[.][0-9]{1,2}[.]bin.*$"); if (fName.contains(version)) { filesToDownload << fName; // download device controller } From 3e925756cf3ff9b2b73cf5119371cf6bb73ba8de Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:43:46 +0200 Subject: [PATCH 210/239] Add getDcSoftAndHardWareVersion() utility. --- update.h | 1 + 1 file changed, 1 insertion(+) diff --git a/update.h b/update.h index f4dd43b..1bfc1d4 100644 --- a/update.h +++ b/update.h @@ -87,6 +87,7 @@ private: bool updateDeviceConf(QString jsFileToSendToDC); bool downloadJson(enum FileTypeJson type, int templateIdx, QString jsFileToSendToDC) const; + QStringList getDcSoftAndHardWareVersion(); private slots: void readyReadStandardOutput(); From 6765b12f0c508b0505f7607fc4f285ed24855e5d Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:52:40 +0200 Subject: [PATCH 211/239] startBootloader(): deprecated. --- update.cpp | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/update.cpp b/update.cpp index 8b20404..38e0d28 100644 --- a/update.cpp +++ b/update.cpp @@ -1,5 +1,8 @@ #include "update.h" #include "worker.h" +#include "utils.h" +#include "update_dc_event.h" +#include "mainwindow.h" #include #include @@ -9,6 +12,7 @@ #include #include #include +#include //#include //#include @@ -81,6 +85,7 @@ Update::Update(hwinf *hw, QString customerRepository, QString customerNrStr, QString branchName, + QString pluginName, QString workingDir, bool dryRun, QObject *parent, @@ -94,8 +99,17 @@ Update::Update(hwinf *hw, , m_customerRepository(customerRepository) , m_customerNrStr(customerNrStr) , m_branchName(branchName) + , m_pluginName(pluginName) , m_workingDir(workingDir) , m_dryRun(dryRun) { + + qInfo() << "UPDATE: m_serialInterface ..." << m_serialInterface; + qInfo() << "UPDATE: m_baudrate ..." << m_baudrate; + qInfo() << "UPDATE: m_customerRepository ..." << m_customerRepository; + qInfo() << "UPDATE: m_customerNr ..........." << m_customerNrStr; + qInfo() << "UPDATE: m_branchName ..........." << m_branchName; + qInfo() << "UPDATE: m_pluginName ..........." << m_pluginName; + qInfo() << "UPDATE: m_workingDirectory ....." << m_workingDir; } Update::~Update() { @@ -221,22 +235,27 @@ Update::DownloadResult Update::dc_downloadBinary(QByteArray const &b) const { return res; } -bool Update::startBootloader() const { - qDebug() << "starting bootloader..."; - int nTry = 5; - while (--nTry >= 0) { +bool Update::startBootloader() const { // deprecated + return false; +#if 0 + int nStartTry = 5; + while (--nStartTry >= 0) { m_hw->bl_startBL(); - QThread::msleep(5000); - m_hw->bl_checkBL(); - if (m_hw->bl_isUp()) { - qInfo() << "starting bootloader...OK"; - QThread::msleep(5000); - return true; - } else { - qCritical() << "bootloader not up (" << nTry << ")"; + QThread::msleep(500); + int nCheckTry = 10; + while (--nCheckTry >= 0) { + m_hw->bl_checkBL(); + QThread::msleep(500); + if (m_hw->bl_isUp()) { + qInfo() << "starting bootloader...OK"; + return true; + } else { + qCritical() << "bootloader not up (" + << nStartTry << "," << nCheckTry << ")" << QThread::currentThread(); + } } } - qCritical() << "starting bootloader...FAILED"; + qCritical() << "starting bootloader...FAILED" << QThread::currentThread(); return false; } From 9df46a1c497300b10bba931bc23484522e401e73 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:53:36 +0200 Subject: [PATCH 212/239] Add some debug output to openSerial(). --- update.cpp | 74 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/update.cpp b/update.cpp index 38e0d28..848cdfe 100644 --- a/update.cpp +++ b/update.cpp @@ -257,31 +257,77 @@ bool Update::startBootloader() const { // deprecated } qCritical() << "starting bootloader...FAILED" << QThread::currentThread(); return false; +#endif } bool Update::stopBootloader() const { - qDebug() << "stopping bootloader..."; - int nTry = 5; - while (--nTry >= 0) { - m_hw->bl_stopBL(); - QThread::msleep(500); - if (!m_hw->bl_isUp()) { - qInfo() << "stopping bootloader...OK"; - return true; + // stop bootloader: this MUST work -> otherwise the PSA has to be restarted + // manually + emit m_worker->showErrorMessage("dc update", "stopping bootloader..."); + + int nTryFinalize = 1; // could do this in an endless loop + + do { + // in principle, any value except BL_STOP will do, as we want to detect + // change to BL_STOP + m_worker->mainWindow()->setUpdateStep(UpdateDcEvent::UpdateStep::BL_CHECK); + + QApplication::postEvent( + m_worker->mainWindow(), + new UpdateDcEvent(m_worker, UpdateDcEvent::UpdateStep::BL_STOP, nTryFinalize)); + + QThread::sleep(1); + + int const cntLimit = 20; + int cnt = 0; + while (++cnt < cntLimit && + m_worker->mainWindow()->updateStep() != UpdateDcEvent::UpdateStep::BL_STOP) { + // wait until bl_stopBL() has been sent + QThread::msleep(500); } - } - qCritical() << "stopping bootloader...FAILED"; - return false; + + QApplication::postEvent( + m_worker->mainWindow(), + new UpdateDcEvent(m_worker, UpdateDcEvent::UpdateStep::BL_CHECK, nTryFinalize)); + QThread::sleep(1); + + QApplication::postEvent( + m_worker->mainWindow(), + new UpdateDcEvent(m_worker, UpdateDcEvent::UpdateStep::BL_IS_UP, nTryFinalize)); + QThread::sleep(1); + + cnt = 0; + while (++cnt < cntLimit && + m_worker->mainWindow()->updateStep() != UpdateDcEvent::UpdateStep::BL_IS_DOWN) { + // wait until done + QThread::msleep(200); + } + + } while (++nTryFinalize <= MainWindow::BL_STOP_COUNT && + m_worker->mainWindow()->updateStep() != UpdateDcEvent::UpdateStep::BL_IS_DOWN); + + return (m_worker->mainWindow()->updateStep() == UpdateDcEvent::UpdateStep::BL_IS_DOWN); } // br is a index into a table, used for historical reasons. bool Update::openSerial(int br, QString baudrate, QString comPort) const { qDebug() << "opening serial" << br << baudrate << comPort << "..."; - if (m_hw->dc_openSerial(br, baudrate, comPort, 1)) { // 1 for connect - qInfo() << "opening serial" << br << baudrate << comPort << "...OK"; + if (m_hw->dc_openSerial(br, baudrate, comPort, 1) == true) { // 1 for connect + Utils::printInfoMsg( + QString("OPENING SERIAL %1").arg(br) + + " " + baudrate + " " + comPort + "...OK"); + + // m_hw->dc_autoRequest(true); + m_hw->dc_autoRequest(false); + QThread::sleep(1); + + Utils::printInfoMsg(QString("IS PORT OPEN %1").arg(m_hw->dc_isPortOpen())); return true; } - qCritical() << "opening serial" << br << baudrate << comPort << "...FAILED"; + + Utils::printCriticalErrorMsg( + QString("OPENING SERIAL %1").arg(br) + + " " + baudrate + " " + comPort + "...FAILED"); return false; } From b6971c1db521f34e119b3108b919c07d35a9297e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:54:12 +0200 Subject: [PATCH 213/239] resetDeviceController(): deprecated. --- update.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/update.cpp b/update.cpp index 848cdfe..68942c3 100644 --- a/update.cpp +++ b/update.cpp @@ -340,13 +340,15 @@ bool Update::isSerialOpen() const { return m_hw->dc_isPortOpen(); } -bool Update::resetDeviceController() const { +bool Update::resetDeviceController() const { // deprecated + return false; +#if 0 qDebug() << "resetting device controller..."; m_hw->bl_rebootDC(); // wait maximally 3 seconds, before starting bootloader - QThread::sleep(1); qInfo() << "resetting device controller...OK"; return true; +#endif } QByteArray Update::loadBinaryDCFile(QString filename) const { From 5158878ce224e2d05553425e07ca96a2c6f752a1 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:54:48 +0200 Subject: [PATCH 214/239] =?UTF-8?q?Extend=20comment=20for:=20USING=20THE?= =?UTF-8?q?=20BOOTLOADER.=20=20Bitte=20geben=20Sie=20eine=20Commit-Beschre?= =?UTF-8?q?ibung=20f=C3=BCr=20Ihre=20=C3=84nderungen=20ein.=20Zeilen,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- update.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/update.cpp b/update.cpp index 68942c3..e54b20c 100644 --- a/update.cpp +++ b/update.cpp @@ -386,17 +386,61 @@ bool Update::downloadBinaryToDC(QString const &bFile) const { } /* - Using the DC bootloader: - 1 : bl_reboot() // send to application, want DC2 to reset (in order to start - // the bootloader) - 2 : bl_startBL(): // send within 4s after DC poewer-on, otherwise bl is left - 3 : bl_check(): // send command to verify if bl is up - 4 : bl_isUp(): // returns true if bl is up and running + /////////////////////////////////////////////////////////////////////////////// + // + // USING THE DC BOOTLOADER + // + /////////////////////////////////////////////////////////////////////////////// + + 1 : bl_reboot() // send to application, want DC2 to reset (in order to + // start the bootloader) + // + // NOTE: this function is NOT reliable !!! Sometimes it + // simply does not work, in which case bl_startBL, + // bl_checkBL and bl_isUp do not work as well. + // Alas, there is no feedback if bl_reboot worked! + // + // NOTE: this function can be called only once per + // minute, because once called again, the controller + // performs some self-checks consuming some time. + // + // NOTE: after a successful bl_reboot(), the device is + // waiting about 4 seconds in the bootloader. To stay in + // the bootloader, we have to send the command + // bl_startBL(), which is kind of a misnomer, as it + // should be bl_doNotLeaveBL(). + // + 2 : bl_startBL(): // send within 4s after DC power-on, otherwise + // bootloader is left. + // + // NOTE: a running bootloader is a MUST for the download + // process of a device controller firmware as it does + // the actual writing of the memory (the bl_reboot() + // from above erases the available memory). + // + 3 : bl_check(): // send command to verify if bl is up + // + // NOTE: this command is kind of a request that we want + // to check if the bootloader is up. The device + // (actually the bootloader) responds with its version. + // + 4 : bl_isUp(): // returns true if bl is up and running + // + // NOTE: we know what the bootloader version actually is + // as the bootloader does not change. By comparing the + // string received in the previous step with this known + // version string we know if the bootloader is up. + // + // NOTE FOR ALL PREVIOUS STEPS: execute them in their + // own slots each to be sure to receive any possible + // responds from the device. + // 5 : bl_sendAddress(blockNumber) // send start address, nr of 64-byte block, start with 0 // will be sent only for following block-numbers: - // 0, 1024, 2048, 3072 and 4096, so basically every 64kByte + // 0, 1024, 2048, 3072 and 4096, so basically every + // 64kByte. // for other addresses nothing happens 6 : bl_wasSendingAddOK() From 67c8b2f4724fc7009e76955ae5544de06b92c004 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:55:48 +0200 Subject: [PATCH 215/239] Extended comment: USING THE BOOTLOADER. --- update.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/update.cpp b/update.cpp index e54b20c..f29c168 100644 --- a/update.cpp +++ b/update.cpp @@ -460,6 +460,13 @@ bool Update::downloadBinaryToDC(QString const &bFile) const { // 10: OK 10 : bl_stopBL() // leave bl and start (the new) application + // + // NOTE: this function MUST work under all conditions. + // Alas, there is no direct result for this command, so + // the only way of knowing it was successful is to ask + // the device if the bootloader is still running. + // There is no problem to repeat this command until the + // bootloader is really not running anymore. */ bool Update::updateBinary(char const *fileToSendToDC) { qInfo() << "UPDATING DEVICE CONTROLLER BINARY" << fileToSendToDC; From cd59a39569d5d10f07179b015c36ae82d307955b Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:57:04 +0200 Subject: [PATCH 216/239] Add some more debug output to updateBinary(). --- update.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/update.cpp b/update.cpp index f29c168..9517510 100644 --- a/update.cpp +++ b/update.cpp @@ -469,20 +469,19 @@ bool Update::downloadBinaryToDC(QString const &bFile) const { // bootloader is really not running anymore. */ bool Update::updateBinary(char const *fileToSendToDC) { - qInfo() << "UPDATING DEVICE CONTROLLER BINARY" << fileToSendToDC; + qInfo() << "UPDATING DEVICE CONTROLLER FIRMWARE BINARY" << fileToSendToDC; QFile fn(fileToSendToDC); bool r; if ((r = fn.exists()) == true) { QFileInfo fi(fn); - qInfo() << " UPDATING BINARY" << fi.fileName() << "(size=" << fi.size() << ")"; if ((r = updateDC(fileToSendToDC)) == true) { - qCritical() << QString(80, '*'); - qInfo() << " UPDATING BINARY" << fi.fileName() << "(size=" << fi.size() << ") DONE"; - qCritical() << QString(80, '*'); + Utils::printInfoMsg( + QString(" UPDATING BINARY ") + fi.fileName() + + QString(" (size=%1").arg(fi.size()) + ") DONE"); } else { - qCritical() << QString(80, '*'); - qCritical() << " UPDATING BINARY" << fi.fileName() << "(size=" << fi.size() << ") FAILED"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg( + QString(" UPDATING BINARY ") + fi.fileName() + + QString(" (size=%1").arg(fi.size()) + ") FAILED"); } } else { qCritical() << QString(80, '*'); From d0eb1d12d87508a32c3a5eece22509ebf33382ca Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:57:49 +0200 Subject: [PATCH 217/239] Add some debug output to updateBinary(). --- update.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/update.cpp b/update.cpp index 9517510..54d74dc 100644 --- a/update.cpp +++ b/update.cpp @@ -484,9 +484,8 @@ bool Update::updateBinary(char const *fileToSendToDC) { + QString(" (size=%1").arg(fi.size()) + ") FAILED"); } } else { - qCritical() << QString(80, '*'); - qCritical() << fileToSendToDC << "does not exist -> NO UPDATE OF DC FIRMWARE"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg( + QString(fileToSendToDC) + " DOES NOT EXIST -> NO UPDATE OF DC FIRMWARE"); } return r; } From a07893ddab6d1782bf847432ca2ff4c0a71667c6 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 14:59:05 +0200 Subject: [PATCH 218/239] doUpdate() and updateDC(): communicate with main window to enter bootloader (i.e. to prepare device for download of device controller firmware), but do not perform an actual download at the moment. --- update.cpp | 256 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 175 insertions(+), 81 deletions(-) diff --git a/update.cpp b/update.cpp index 54d74dc..342b21a 100644 --- a/update.cpp +++ b/update.cpp @@ -491,28 +491,88 @@ bool Update::updateBinary(char const *fileToSendToDC) { } bool Update::updateDC(QString bFile) const { - qDebug() << "updating dc..."; - qDebug() << "updating dc: file to send" << bFile; - if (!resetDeviceController()) { - return false; + qDebug() << "IN UPDATEDC: UPDATING DC: FILE TO SEND" << bFile; + + m_worker->mainWindow()->setUpdateStep(UpdateDcEvent::UpdateStep::NONE); + + QApplication::postEvent( // step 1: reset device controller + m_worker->mainWindow(), + new UpdateDcEvent(m_worker, UpdateDcEvent::UpdateStep::DC_REBOOT, 1)); + QThread::sleep(1); + + for (int i=1; i <= MainWindow::BL_START_COUNT; ++i) { + QApplication::postEvent( // step 2: start bootloader (5x) + m_worker->mainWindow(), + new UpdateDcEvent(m_worker, UpdateDcEvent::UpdateStep::BL_START, i)); + QThread::sleep(1); } - if (!startBootloader()) { - // even when start seems to fail, stopping the boot loader does not harm - stopBootloader(); + + int const cntLimit = 100; // wait until its for sure that bl_startBL() + int cnt = 0; // has been excuted + while (++cnt < cntLimit && + m_worker->mainWindow()->updateStep() != UpdateDcEvent::UpdateStep::BL_START) { + // wait until all bl_startBL() are done + QThread::msleep(200); + } + + if (cnt == cntLimit) { + // start events not received ??? + Utils::printCriticalErrorMsg("BL_START EVENT NOT RECEIVED AFTER 20 SECS"); return false; } - if (!downloadBinaryToDC(bFile)) { - stopBootloader(); - qCritical() << "updating dc: " << bFile << "...FAILED"; + m_worker->mainWindow()->setUpdateStep(UpdateDcEvent::UpdateStep::BL_CHECK); + + for (int i=1; i <= MainWindow::BL_IS_UP_COUNT; ++i) { + QApplication::postEvent(m_worker->mainWindow(), new UpdateDcEvent(m_worker, UpdateDcEvent::UpdateStep::BL_CHECK, i)); + QThread::sleep(1); + QApplication::postEvent(m_worker->mainWindow(), new UpdateDcEvent(m_worker, UpdateDcEvent::UpdateStep::BL_IS_UP, i)); + if (m_worker->mainWindow()->updateStep() == UpdateDcEvent::UpdateStep::BL_IS_UP) { + break; + } + QThread::sleep(1); + } + + cnt = 0; + while (++cnt < cntLimit && + m_worker->mainWindow()->updateStep() != UpdateDcEvent::UpdateStep::BL_IS_UP) { + // wait until all bl_startBL() are done + QThread::msleep(200); + } + + if (cnt == cntLimit) { + // really not up + Utils::printCriticalErrorMsg("BL_IS_UP EVENT NOT RECEIVED AFTER 20 SECS"); + stopBootloader(); // try to stop bootloader whichhas been already started return false; } - qInfo() << "updating dc: " << bFile << "...OK"; - stopBootloader(); - //resetDeviceController(); + if (m_worker->mainWindow()->updateStep() == UpdateDcEvent::UpdateStep::BL_IS_UP) { + // bootloader MUST be running to download device-controller +#if 0 + if (!downloadBinaryToDC(bFile)) { + Utils::printCriticalErrorMsg( + QString("UPDATING DC: ") + bFile + " ...DOWNLOAD FAILED"); + } +#endif - QThread::sleep(3); + } else { + Utils::printCriticalErrorMsg( + QString("UPDATING DC: ") + bFile + " BOOT LOADER NOT RUNNING -> NO DOWNLOAD (" + + QThread::currentThread()->objectName() + ")"); + return false; + } + + // do this unconditionally, even if bootloader is not running at all -> + // the controller possibly tells us nonsense. + if (!stopBootloader()) { + Utils::printCriticalErrorMsg( + QString("UPDATING DC: ") + bFile + " BOOT LOADER STILL RUNNING (" + + QThread::currentThread()->objectName() + ")"); + return false; + } + + Utils::printInfoMsg(QString("UPDATING DC: ") + bFile + " ...OK"); return true; } @@ -634,25 +694,73 @@ void Update::finished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { disconnect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(readyReadStandardError())); } +QStringList Update::getDcSoftAndHardWareVersion() { + m_hw->dc_autoRequest(true); + QThread::sleep(1); // make sure the timer-slots are active + + for (int i=0; i < 3; ++i) { // send explicit reuests to get + // current SW/HW-versions + m_hw->request_DC2_SWversion(); + m_hw->request_DC2_HWversion(); + QThread::sleep(1); + } + + QString const &hwVersion = m_hw->dc_getHWversion().toLower().trimmed(); + QString const &swVersion = m_hw->dc_getSWversion().toLower().trimmed(); + + m_hw->dc_autoRequest(false); + QThread::sleep(1); // make sure the timer-slots are inactive + + if (!hwVersion.isEmpty() && !swVersion.isEmpty()) { + return QStringList() << hwVersion << swVersion; + } + + return QStringList() << "DC HW-version not available" + << "DC SW-version not available"; +} + bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { - // - // ACHTUNG !!! - // - return true; - bool serialOpened = false; - bool serialOpen = false; - if (!serialOpen) { - if (!isSerialOpen()) { // open serial only if not already open - if ((serialOpened = openSerial(baudrateMap.value(m_baudrate), m_baudrate, m_serialInterface)) == false) { - qCritical() << "CANNOT OPEN" << m_serialInterface << "(BAUDRATE=" - << m_baudrate << ")"; - return false; - } + QString const &parentName = Utils::getParentName(); + + if (parentName == "ATBQT" || parentName == "systemd") { + // the tool was not called during 'service' ot during an automatic + // update procedure. and it was called explicitly with libCAmaster.so + if (m_pluginName.contains("master", Qt::CaseInsensitive)) { + Utils::printCriticalErrorMsg(parentName + + " IS MASTER, BUT ATB-UPDATE-TOOL CALLED WITH libCAmaster.so"); + return false; } - serialOpen = true; - qInfo() << "SERIAL OPEN" << m_serialInterface << "(BAUDRATE=" << m_baudrate << ")"; + } else + if (Utils::isATBQTRunning()) { // manual testing + if (m_pluginName.contains("master", Qt::CaseInsensitive)) { + Utils::printCriticalErrorMsg( + "ATBQT IS MASTER, BUT ATB-UPDATE-TOOL CALLED WITH libCAmaster.so"); + return false; + } + } else { + if (m_pluginName.contains("slave", Qt::CaseInsensitive)) { + Utils::printCriticalErrorMsg( + "ATB-UPDATE-TOOL CALLED WITH libCAslave.so ALTHOUGH MASTER"); + return false; + } + + if ((serialOpened = openSerial(baudrateMap.value(m_baudrate), + m_baudrate, + m_serialInterface)) == false) { + Utils::printCriticalErrorMsg( + QString("CANNOT OPEN ") + + m_serialInterface + + "( BAUDRATE=" + m_baudrate + ")"); + return false; + } + + m_hw->dc_autoRequest(false); + + Utils::printInfoMsg( + QString("SERIAL OPEN ") + m_serialInterface + + " (BAUDRATE=" + m_baudrate + ")"); } bool res = false; @@ -662,67 +770,49 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { QString fToWorkOn = (*it).trimmed(); fToWorkOn = QDir::cleanPath(m_customerRepository + QDir::separator() + fToWorkOn); - static const QRegularExpression version("^.*dc2c[.][0-9][0-9][.][0-9][0-9][.]bin.*$"); + static const QRegularExpression version("^.*dc2c[.][0-9]{1,2}[.][0-9]{1,2}[.]bin.*$"); if (fToWorkOn.contains(version)) { - - qInfo() << QString(80, '*'); - qInfo() << "DO-UPDATE FILE-TO-WORK-ON" << fToWorkOn; - qInfo() << QString(80, '*'); - - for (int i=0; i < 3; ++i) { // send explicit reuests to get - // current SW/HW-versions - m_hw->request_DC2_SWversion(); - m_hw->request_DC2_HWversion(); - QThread::sleep(1); - } - - QString const hwVersion = m_hw->dc_getHWversion().toLower(); - QString const fwVersion = m_hw->dc_getSWversion().toLower(); - - qInfo() << "current dc-hardware-version" << hwVersion; - qInfo() << "current dc-firmware-version" << fwVersion; + Utils::printInfoMsg("DO-UPDATE FILE-TO-WORK-ON " + fToWorkOn); QFile fn(fToWorkOn); QFileInfo finfo(fn); if (!fn.exists()) { // check for broken link - qCritical() << QString(80, '*'); - qCritical() << "FILE-TO-WORK-ON" << fn << "DOES NOT EXIST"; - qCritical() << QString(80, '*'); + Utils::printCriticalErrorMsg("DO-UPDATE FILE-TO-WORK-ON " + + fToWorkOn + " DOES NOT EXIST"); res = false; } else { - if (false) { - //if (fwVersion.startsWith(linkTarget.completeBaseName())) { - // qCritical() << "current dc-firmware-version" << fwVersion - // << "already installed"; - // res = false; - } else { - res = true; + bool updateBinaryRes = true; - qInfo() << "DOWNLOADING" << finfo.completeBaseName() << "TO DC"; + qInfo() << "DOWNLOADING" << finfo.completeBaseName() << "TO DC"; #if UPDATE_DC == 1 - m_hw->dc_autoRequest(false);// default: turn auto-request setting off - QThread::sleep(1); // wait to be sure that there are no more - // commands sent to dc-hardware - qInfo() << "SET AUTO-REQUEST=FALSE"; + m_hw->dc_autoRequest(false);// default: turn auto-request setting off + QThread::sleep(1); // wait to be sure that there are no more + // commands sent to dc-hardware + qInfo() << "SET AUTO-REQUEST=FALSE"; - if ((res = updateBinary(fToWorkOn.toStdString().c_str())) == true) { - qCritical() << "downloaded binary" << fToWorkOn; - ++displayIndex; - emit m_worker->appendText(QString("\n(") + QString("%1").arg(displayIndex).rightJustified(2, ' ') + QString(")") - + QString(" Update ") + QFileInfo(fToWorkOn).fileName(), - Worker::UPDATE_STEP_DONE); - } - - m_hw->dc_autoRequest(true); // turn auto-request setting on - qInfo() << "SET AUTO-REQUEST=TRUE"; - qInfo() << "WAIT 10 SECS TO RECEIVE RESPONSES..."; - - QThread::sleep(10); // wait to be sure that responses - // have been received - qInfo() << "updated dc-hardware-version" << m_hw->dc_getHWversion(); - qInfo() << "updated dc-firmware-version" << m_hw->dc_getSWversion(); -#endif + if ((updateBinaryRes = updateBinary(fToWorkOn.toStdString().c_str())) == true) { + qCritical() << "downloaded binary" << fToWorkOn; + ++displayIndex; + emit m_worker->appendText(QString("\n(") + QString("%1").arg(displayIndex).rightJustified(2, ' ') + QString(")") + + QString(" Update ") + QFileInfo(fToWorkOn).fileName(), + Worker::UPDATE_STEP_DONE); } + + m_hw->dc_autoRequest(true); // turn auto-request setting on + qInfo() << "SET AUTO-REQUEST=TRUE"; + + QStringList const &versions = Update::getDcSoftAndHardWareVersion(); + if (versions.size() >= 2) { + if (updateBinaryRes == true) { + qInfo() << "dc-hardware-version (UPDATED)" << versions[0]; + qInfo() << "dc-firmware-version (UPDATED)" << versions[1]; + } else { + qInfo() << "dc-hardware-version (NOT UPDATED)" << versions[0]; + qInfo() << "dc-firmware-version (NOT UPDATED)" << versions[1]; + } + } +#endif + res = updateBinaryRes; } } else if (fToWorkOn.contains("DC2C_print", Qt::CaseInsensitive) && fToWorkOn.endsWith(".json", Qt::CaseInsensitive)) { @@ -791,8 +881,12 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { } } // for (it = openLines.cbegin(); it != openLines.end(); ++it) { - m_hw->dc_autoRequest(true); // ALWAYS turn autoRequest ON - qDebug() << "SET AUTO-REQUEST=TRUE"; + //m_hw->dc_autoRequest(true); // ALWAYS turn autoRequest ON + //qDebug() << "SET AUTO-REQUEST=TRUE"; + + if (serialOpened) { + m_hw->dc_closeSerial(); + } return res; } From 15f28e9ffd2eef95e5b2a9fd9d5c870eef5446dc Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:01:03 +0200 Subject: [PATCH 219/239] Add m_update-object and prepare for doing the bootloader setup for downloading the device controller firmware. --- mainwindow.h | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/mainwindow.h b/mainwindow.h index 9d4e3cd..37f4e03 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -10,6 +10,10 @@ namespace Ui { class MainWindow; } QT_END_NAMESPACE #include "worker.h" +#include "update.h" +#include "update_dc_event.h" + +#define EMERGENCY_LEAVE_BL 0 class hwinf; class MainWindow : public QMainWindow { @@ -19,39 +23,60 @@ protected: void customEvent(QEvent *event) override; public: - MainWindow(hwinf *hw, Worker *worker, QWidget *parent = nullptr); + MainWindow(hwinf *hw, Worker *worker, Update *update, QWidget *parent = nullptr); ~MainWindow(); static const int START_PROGRESS_LOOP = -1; static const int STOP_PROGRESS_LOOP = -2; + static const int BL_START_COUNT = 5; + static const int BL_CHECK_COUNT = 5; + static const int BL_IS_UP_COUNT = 5; + static const int BL_STOP_COUNT = 5; int progressValue() const { return m_progressValue; } hwinf *getPlugin() { return m_hw; } hwinf const *getPlugin() const { return m_hw; } + Update *getUpdate() { return m_update; } + Update const *getUpdate() const { return m_update; } + UpdateDcEvent::UpdateStep updateStep() const { return m_updateStep; } + void setUpdateStep(UpdateDcEvent::UpdateStep updateStep) { m_updateStep = updateStep; } public slots: void onAppendText(QString, QString suffix = ""); void onReplaceLast(QStringList, QString suffix = ""); void onReplaceLast(QString, QString suffix = ""); void onShowErrorMessage(QString, QString); + void onShowStatusMessage(QString, QString); void onStopStartTimer(); void onRestartExitTimer(); void onEnableExit(); void onDisableExit(); +#if EMERGENCY_LEAVE_BL==1 + void emergencyLeaveBL(); +#endif + +signals: + +#if EMERGENCY_LEAVE_BL==1 + void leaveBL(); +#endif private slots: void onQuit(); private: void scrollDownTextEdit(); + void onShowMessage(QString, QString); Ui::MainWindow *ui; hwinf *m_hw; Worker *m_worker; - int m_width; + int const m_width; QTimer *m_startTimer; QTimer *m_exitTimer; bool m_progressRunning; int m_progressValue; + Update *m_update; + UpdateDcEvent::UpdateStep m_updateStep; }; #endif // MAINWINDOW_H From 57b4716e2a68c2ac5594516ab3d9df2539e5222c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:02:45 +0200 Subject: [PATCH 220/239] Add emergency test function: if device stays in bootloader then use it to make the device to leave the bootloader. --- mainwindow.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index ed4d2f3..751bdab 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -3,6 +3,7 @@ #include "worker.h" #include "utils.h" #include "progress_event.h" +#include "update_dc_event.h" #include "plugins/interfaces.h" #include @@ -10,14 +11,102 @@ #include #include -MainWindow::MainWindow(hwinf *hw, Worker *worker, QWidget *parent) + + +#if EMERGENCY_LEAVE_BL==1 +static int step = 0; + +void MainWindow::emergencyLeaveBL() { + // + qCritical() << __func__ << step; + switch(step) { + case 0: + if (m_hw->dc_openSerial(5, "115200", "ttymxc2", 1)) { + qCritical() << __func__ << "open ok"; + step++; + QThread::msleep(2000); + m_hw->dc_autoRequest(false); + emit leaveBL(); + } + break; + case 1: + m_hw->bl_rebootDC(); + QThread::msleep(1000); + qCritical() << __func__ << "reboot ok" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + step++; + emit leaveBL(); + break; + case 2: + case 3: + case 4: + case 5: + case 6: + m_hw->bl_startBL(); + QThread::msleep(1000); + qCritical() << __func__ << "start" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + step++; + emit leaveBL(); + break; + case 7: + case 9: + case 11: + case 13: + case 15: + m_hw->bl_checkBL(); + qCritical() << __func__ << "check" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + QThread::msleep(1500); + ++step; + emit leaveBL(); + break; + case 8: + case 10: + case 12: + case 14: + case 16: + qCritical() << __func__ << "is Up..." << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + if (m_hw->bl_isUp()) { + qCritical() << __func__ << "is Up...OK" << step << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + QThread::msleep(5000); + step = 16; + } else { + qCritical() << __func__ << "is Up...NO" << step << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + } + ++step; + emit leaveBL(); + break; + case 17: + case 18: + case 19: + qCritical() << __func__ << "stop" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + m_hw->bl_stopBL(); + QThread::msleep(1000); + //m_hw->dc_closeSerial(); + ++step; + emit leaveBL(); + break; + } +} +#endif + +MainWindow::MainWindow(hwinf *hw, Worker *worker, Update *update, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_hw(hw) , m_worker(worker) , m_width(70) , m_progressRunning(false) - , m_progressValue(0) { + , m_progressValue(0) + , m_update(update) + , m_updateStep(UpdateDcEvent::UpdateStep::NONE) { + +#if EMERGENCY_LEAVE_BL==1 + QTimer *t = new QTimer(this); + connect(t, SIGNAL(timeout()), this, SLOT(emergencyLeaveBL())); + connect(this, SIGNAL(leaveBL()), this, SLOT(emergencyLeaveBL()), Qt::QueuedConnection); + t->setSingleShot(true); + t->start(1000); + return; +#endif this->setStatusBar(new QStatusBar(this)); QFont f; From 60cc7528198af9446cc4a50dc291ce0ceb2cbee5 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:03:52 +0200 Subject: [PATCH 221/239] Add connect for showning a status message in status bar. --- mainwindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mainwindow.cpp b/mainwindow.cpp index 751bdab..7ba4cd2 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -152,6 +152,7 @@ MainWindow::MainWindow(hwinf *hw, Worker *worker, Update *update, QWidget *paren connect(m_worker, SIGNAL(restartExitTimer()),this,SLOT(onRestartExitTimer())); connect(m_worker, SIGNAL(appendText(QString,QString)),this,SLOT(onAppendText(QString,QString))); connect(m_worker, SIGNAL(showErrorMessage(QString,QString)),this, SLOT(onShowErrorMessage(QString,QString))); + connect(m_worker, SIGNAL(showStatusMessage(QString,QString)),this, SLOT(onShowStatusMessage(QString,QString))); connect(m_worker, SIGNAL(replaceLast(QString,QString)),this,SLOT(onReplaceLast(QString,QString))); connect(m_worker, SIGNAL(replaceLast(QStringList,QString)),this, SLOT(onReplaceLast(QStringList,QString))); } From df0951d671e5916abbb346f7d0b4897a8ed3f69e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:04:41 +0200 Subject: [PATCH 222/239] Add the steps to prepare the bootloader for device-controller-firmware download using cutom-events of type update-dc-event. --- mainwindow.cpp | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/mainwindow.cpp b/mainwindow.cpp index 7ba4cd2..26f6df3 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -211,6 +211,86 @@ void MainWindow::customEvent(QEvent *event) { } else { qCritical() << "!!! UNKNOWN SENDER !!!"; } + } else + if (event->type() == UpdateDcEvent::type()) { + UpdateDcEvent *pevent = (UpdateDcEvent *)event; + UpdateDcEvent::UpdateStep const updateStep = pevent->updateStep(); + QObject const *sender = pevent->sender(); + if (sender == m_worker) { + QDateTime const &recv = QDateTime::currentDateTime(); + QDateTime const &send = pevent->sendDateTime(); + qint64 const delay = recv.toMSecsSinceEpoch() - send.toMSecsSinceEpoch(); + switch(updateStep) { + case UpdateDcEvent::UpdateStep::NONE: + break; + case UpdateDcEvent::UpdateStep::DC_REBOOT: { + m_hw->bl_rebootDC(); + QString msg = QDateTime::currentDateTime().toString(Qt::ISODateWithMs) + + QString(": reset device controller (delay=%1ms").arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + m_updateStep = UpdateDcEvent::UpdateStep::DC_REBOOT; + } break; + case UpdateDcEvent::UpdateStep::BL_START: { + QString const &msg = recv.toString(Qt::ISODateWithMs) + + QString(": start bootloader (%1, delay=%2ms)").arg(pevent->count()).arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + m_hw->bl_startBL(); + if (pevent->count() == BL_START_COUNT) { + m_updateStep = UpdateDcEvent::UpdateStep::BL_START; + } + } break; + case UpdateDcEvent::UpdateStep::BL_CHECK: { + if (m_updateStep != UpdateDcEvent::UpdateStep::BL_IS_UP) { + QString const &msg = recv.toString(Qt::ISODateWithMs) + + QString(": request bootloader version (%1, delay=%2ms)").arg(pevent->count()).arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + m_hw->bl_checkBL(); + //m_updateStep = UpdateDcEvent::UpdateStep::BL_CHECK; + } + } break; + case UpdateDcEvent::UpdateStep::BL_IS_UP: { + QString msg = recv.toString(Qt::ISODateWithMs) + + QString(": check running bootloader (%1, delay=%2ms)").arg(pevent->count()).arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + if (m_updateStep != UpdateDcEvent::UpdateStep::BL_IS_UP) { + if (m_hw->bl_isUp()) { + msg = recv.toString(Qt::ISODateWithMs) + + QString(": bootloader running (%1, delay=%2ms)").arg(pevent->count()).arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + m_updateStep = UpdateDcEvent::UpdateStep::BL_IS_UP; + } else { + msg = recv.toString(Qt::ISODateWithMs) + + QString(": bootloader stop requested (%1, delay=%2ms)").arg(pevent->count()).arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + if (m_updateStep == UpdateDcEvent::UpdateStep::BL_STOP) { + msg = QDateTime::currentDateTime().toString(Qt::ISODateWithMs) + + QString(": bootloader down (%1, delay=%2ms)").arg(pevent->count()).arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + m_updateStep = UpdateDcEvent::UpdateStep::BL_IS_DOWN; + } + } + } + } break; + case UpdateDcEvent::UpdateStep::BL_STOP: { + QString const &msg = QDateTime::currentDateTime().toString(Qt::ISODateWithMs) + + QString(": stop bootloader (%1, delay=%2ms)").arg(pevent->count()).arg(delay); + emit m_worker->showStatusMessage("dc update", msg); + Utils::printInfoMsg(msg.toUpper()); + //if (m_bootLoaderIsUp) { + m_hw->bl_stopBL(); + m_updateStep = UpdateDcEvent::UpdateStep::BL_STOP; + //} + } break; + default: ; + } + } } QThread::yieldCurrentThread(); From 9a65cb44568e909dbc1d8e5a544513f37da7a4b7 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:06:21 +0200 Subject: [PATCH 223/239] Add some debug message when adding content to the text edit of the GUI. --- mainwindow.cpp | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 26f6df3..28995e2 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -320,6 +320,8 @@ void MainWindow::onQuit() { } void MainWindow::scrollDownTextEdit() { + qCritical() << "ON REPLACE LAST CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + ui->updateStatus->setEnabled(true); QTextCursor tmpCursor = ui->updateStatus->textCursor(); @@ -329,6 +331,8 @@ void MainWindow::scrollDownTextEdit() { } void MainWindow::onAppendText(QString text, QString suffix) { + qCritical() << "ON APPEND CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + QString editText = ui->updateStatus->toPlainText(); if (!suffix.isNull() && suffix.size() > 0) { //qInfo() << "TEXT" << text << "SUFFIX" << suffix; @@ -336,21 +340,26 @@ void MainWindow::onAppendText(QString text, QString suffix) { editText += QString("\n").leftJustified(m_width-3, '='); editText += " "; } - editText += (QString("\n") + text).leftJustified(m_width - (2 + suffix.size()) ) + suffix; + QString const &add = (QString("\n") + text).leftJustified(m_width - (2 + suffix.size())) + suffix; + editText += add; } else { - editText += text.leftJustified(m_width-9); + QString const &add = text.leftJustified(m_width-9); + editText += add; } - Utils::printLineEditInfo(editText.split('\n')); - ui->updateStatus->setPlainText(editText.trimmed()); + Utils::printLineEditInfo(editText.split('\n', QString::SplitBehavior::SkipEmptyParts)); + ui->updateStatus->setText(editText.trimmed()); + scrollDownTextEdit(); } void MainWindow::onReplaceLast(QStringList newTextLines, QString suffix) { + qCritical() << "ON REPLACE LAST CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + int const s = newTextLines.size(); if (s > 0) { QString editText = ui->updateStatus->toPlainText(); - QStringList lines = editText.split('\n'); + QStringList lines = editText.split('\n', QString::SplitBehavior::SkipEmptyParts); QString newText; if (lines.size() >= s) { for (int i = 0; i < s; ++i) { @@ -379,15 +388,22 @@ void MainWindow::onReplaceLast(QStringList newTextLines, QString suffix) { } void MainWindow::onReplaceLast(QString text, QString suffix) { - //qInfo() << "REPL TEXT" << text << "SUFFIX" << suffix; + qCritical() << "ON REPLACE LAST CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + QString editText = ui->updateStatus->toPlainText(); - QStringList lines = editText.split('\n'); + QStringList lines = editText.split('\n', QString::SplitBehavior::SkipEmptyParts); if (lines.size() > 0) { lines.removeLast(); if (!suffix.isNull() && suffix.size() > 0 && suffix != "\n") { - lines += text.leftJustified(m_width-10) + suffix; + QString const add = text.leftJustified(m_width-10) + suffix; + if (!add.isEmpty()) { + lines += text.leftJustified(m_width-10) + suffix; + } } else { - lines += text.leftJustified(m_width-10); + QString const add = text.leftJustified(m_width-10); + if (!add.isEmpty()) { + lines += text.leftJustified(m_width-10); + } } } From 838efd394561e804125fda13845cd32e95a5c61c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:06:58 +0200 Subject: [PATCH 224/239] Show status message in the status bar of the GUI. --- mainwindow.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 28995e2..7952aea 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -412,8 +412,16 @@ void MainWindow::onReplaceLast(QString text, QString suffix) { scrollDownTextEdit(); } -void MainWindow::onShowErrorMessage(QString title, QString text) { +void MainWindow::onShowMessage(QString title, QString text) { this->statusBar()->clearMessage(); this->statusBar()->showMessage( // timeout: 10000 QString(title + " " + text).leftJustified(80, ' '), 10000); } + +void MainWindow::onShowErrorMessage(QString title, QString text) { + onShowMessage(title, text); +} + +void MainWindow::onShowStatusMessage(QString title, QString text) { + onShowMessage(title, text); +} From 01cfbddfb1c3cc0e14cf0189e094e807e31b2647 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:08:03 +0200 Subject: [PATCH 225/239] Add update_dc_event --- OnDemandUpdatePTU.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index e3d1813..313d494 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -78,6 +78,7 @@ contains( CONFIG, DesktopLinux ) { SOURCES += \ main.cpp \ progress_event.cpp \ + update_dc_event.cpp \ mainwindow.cpp \ utils.cpp \ update.cpp \ @@ -91,6 +92,7 @@ SOURCES += \ HEADERS += \ update.h \ progress_event.h \ + update_dc_event.h \ utils.h \ mainwindow.h \ git/git_client.h \ From 823e59a582627da1b19057641ab7bcf51805c68e Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:09:59 +0200 Subject: [PATCH 226/239] Set version to 1.3.2 --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 313d494..af639cb 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -15,7 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -VERSION=1.3.1 +VERSION=1.3.2 INCLUDEPATH += plugins From afbce3b4ea094aa1b9879e74a18bf84aee540f36 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sat, 9 Sep 2023 15:10:53 +0200 Subject: [PATCH 227/239] Add new dc-lib --- plugins/libCAmaster.so | Bin 285044 -> 305008 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 plugins/libCAmaster.so diff --git a/plugins/libCAmaster.so b/plugins/libCAmaster.so old mode 100644 new mode 100755 index 6aaaefb8b45f6f0d68ee975d50d70f5d6984a103..eafb6590bd5ee6b2fd6c4bc37c431fcbb5735e1e GIT binary patch literal 305008 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%0A29*a-0&!0q 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�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

_ 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 $j+KH1uVh=Y0X(Cp+ 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 D@Owf3 literal 285044 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 Date: Sun, 10 Sep 2023 16:45:40 +0200 Subject: [PATCH 228/239] Use libCAslave.so as default library to use for downloading the device-controller-firmware. --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index 34d8085..877ebae 100644 --- a/main.cpp +++ b/main.cpp @@ -97,7 +97,7 @@ int main(int argc, char *argv[]) { QCommandLineOption pluginNameOption(QStringList() << "plugin-name" << "plugin-name", QCoreApplication::translate("main", "Name of dc-plugin."), QCoreApplication::translate("main", "directory")); - QString const pluginNameDefault = "libCAmaster.so"; + QString const pluginNameDefault = "libCAslave.so"; pluginNameOption.setDefaultValue(pluginNameDefault); parser.addOption(pluginNameOption); From 103b3f3f9c2ebeffe2d6f95baddc451b24023cb1 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 16:46:59 +0200 Subject: [PATCH 229/239] isATBQTRunning(): Use std::fstream to read /proc//status, as the lines end on '\r', not on '\n'. --- utils.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/utils.cpp b/utils.cpp index 5496bee..ce5cdd4 100644 --- a/utils.cpp +++ b/utils.cpp @@ -9,6 +9,9 @@ #include #include #include +#include + +#include int Utils::read1stLineOfFile(QString fileName) { QFile f(fileName); @@ -203,17 +206,19 @@ bool Utils::isATBQTRunning() { QDirIterator::Subdirectories); while (it.hasNext()) { QString const &nextStatusFile = it.next(); - QFile f(nextStatusFile); - if (f.exists()) { - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - QTextStream in(&f); - in.setCodec("UTF-8"); - while(!in.atEnd()) { - // Name: ATBQT - QStringList line = in.readLine().split(':'); - if (line.size() == 2) { - if (line[0].trimmed() == "Name") { - if (line[1].trimmed() == "ATBQT") { + static const QRegularExpression re("^/proc/[0-9]{1,}/status"); + QRegularExpressionMatch match = re.match(nextStatusFile); + if (match.hasMatch()) { + std::ifstream f(nextStatusFile.toStdString().c_str()); + if (f.is_open()) { + std::string next; + while (std::getline(f, next)) { + QString line = QString(next.c_str()).simplified(); + if (line.startsWith("Name")) { + int const idx = line.indexOf(QChar(':')); + if (idx != -1) { + QString const binary = line.mid(idx+1).trimmed(); + if (binary == "ATBQT") { return true; } } From 38e79f0354bce33bb2b147a51b538a6e80de24e0 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 16:50:19 +0200 Subject: [PATCH 230/239] Use pluginLoader as a dedicated static object. --- update.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/update.h b/update.h index 1bfc1d4..f56cdc7 100644 --- a/update.h +++ b/update.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "plugins/interfaces.h" @@ -32,11 +33,14 @@ class Update : public QObject { bool m_maintenanceMode; bool m_dryRun; + static QPluginLoader pluginLoader; + public: enum class DownloadResult {OK, ERROR, TIMEOUT, NOP}; enum class FileTypeJson {CONFIG=1, DEVICE=2, CASH=3, SERIAL=4, TIME=5, PRINTER=6}; static hwinf *loadDCPlugin(QDir const &plugInDir, QString const &fn); + static bool unloadDCPlugin(); static QStringList split(QString line, QChar sep = ','); From ff418b11a1ee2f8c0194c058ca24503afdb4e84a Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 16:51:07 +0200 Subject: [PATCH 231/239] Use plauginLoader as a dedicated static object. --- update.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/update.cpp b/update.cpp index 342b21a..0ae07e0 100644 --- a/update.cpp +++ b/update.cpp @@ -40,6 +40,8 @@ static const QMap baudrateMap = { {"57600" , 4}, {"115200" , 5} }; +QPluginLoader Update::pluginLoader; + hwinf *Update::loadDCPlugin(QDir const &plugInDir, QString const &fname) { hwinf *hw = nullptr; if (plugInDir.exists()) { @@ -48,7 +50,8 @@ hwinf *Update::loadDCPlugin(QDir const &plugInDir, QString const &fname) { QFileInfo info(pluginLibName); if (info.exists()) { pluginLibName = plugInDir.absoluteFilePath(pluginLibName); - static QPluginLoader pluginLoader(pluginLibName); + pluginLoader.setFileName(pluginLibName); + // static QPluginLoader pluginLoader(pluginLibName); if (!pluginLoader.load()) { qCritical() << "in directory" << plugInDir.absolutePath(); qCritical() << "cannot load plugin" << pluginLoader.fileName(); @@ -80,6 +83,21 @@ hwinf *Update::loadDCPlugin(QDir const &plugInDir, QString const &fname) { return hw; } +bool Update::unloadDCPlugin() { + if (pluginLoader.unload()) { + qCritical() << "unloaded plugin" << pluginLoader.fileName(); + // Note: will re-instantiate the library ! + // QObject *rootObject = pluginLoader.instance(); + // if (rootObject) { + // qCritical() << "reloaded plugin: root object again available"; + // return false; + // } + // qCritical()unloaded plugin: root object gone"; + return true; + } + return false; +} + Update::Update(hwinf *hw, Worker *worker, QString customerRepository, From adfb358e128a918789e2659df19bc60f6d30070c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 16:51:36 +0200 Subject: [PATCH 232/239] Add some output to see if start of the update tool is configured correctly. --- update.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/update.cpp b/update.cpp index 0ae07e0..92865fd 100644 --- a/update.cpp +++ b/update.cpp @@ -750,6 +750,8 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { + " IS MASTER, BUT ATB-UPDATE-TOOL CALLED WITH libCAmaster.so"); return false; } + Utils::printInfoMsg( + QString("ATB-UPDATE-TOOL STARTED AS SLAVE OF ") + parentName); } else if (Utils::isATBQTRunning()) { // manual testing if (m_pluginName.contains("master", Qt::CaseInsensitive)) { @@ -757,12 +759,15 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { "ATBQT IS MASTER, BUT ATB-UPDATE-TOOL CALLED WITH libCAmaster.so"); return false; } + Utils::printInfoMsg( + "ATB-UPDATE-TOOL STARTED AS SLAVE-SIBLING OF ATBQT-MASTER"); } else { if (m_pluginName.contains("slave", Qt::CaseInsensitive)) { Utils::printCriticalErrorMsg( "ATB-UPDATE-TOOL CALLED WITH libCAslave.so ALTHOUGH MASTER"); return false; } + Utils::printInfoMsg("ATB-UPDATE-TOOL STARTED AS MASTER"); if ((serialOpened = openSerial(baudrateMap.value(m_baudrate), m_baudrate, From 6dd8a8c6b3200901b9791a5b59d3e5b306ec99d8 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 16:54:29 +0200 Subject: [PATCH 233/239] did some testing with event filter. not used. --- mainwindow.h | 1 + 1 file changed, 1 insertion(+) diff --git a/mainwindow.h b/mainwindow.h index 37f4e03..88f6ca1 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -21,6 +21,7 @@ class MainWindow : public QMainWindow { protected: void customEvent(QEvent *event) override; + // bool eventFilter(QObject *obj, QEvent *ev) override; public: MainWindow(hwinf *hw, Worker *worker, Update *update, QWidget *parent = nullptr); From 18378afdc581ce872d9e3f550989e9593af5ef82 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 16:54:54 +0200 Subject: [PATCH 234/239] Did some testing with event filters on the main window. not used. --- mainwindow.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 7952aea..42c5d3f 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -131,9 +131,9 @@ MainWindow::MainWindow(hwinf *hw, Worker *worker, Update *update, QWidget *paren lst << QString("APISM version : %1").arg(m_worker->apismVersion()).leftJustified(m_width-3); lst << QString("").leftJustified(m_width-3, '='); - ui->updateStatus->setText(lst.join('\n')); ui->updateStatus->setEnabled(true); + // ui->updateStatus->installEventFilter(this); m_startTimer = new QTimer(this); connect(m_startTimer, SIGNAL(timeout()), m_worker, SLOT(update())); @@ -308,9 +308,18 @@ void MainWindow::onEnableExit() { ui->exit->setEnabled(true); } +//bool MainWindow::eventFilter(QObject *obj, QEvent *ev) { +// if (obj == ui->updateStatus) { +// qCritical() << "REc. event for text edit" << ev->type(); +// } +// return QMainWindow::eventFilter(obj, ev); +//} + void MainWindow::onRestartExitTimer() { m_exitTimer->stop(); m_exitTimer->start(60 * 1000); + + // ui->updateStatus->blockSignals(true); } void MainWindow::onQuit() { From 9c213d0a97dea13849bfb5bd3ebba5f2d3460d7b Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 16:55:32 +0200 Subject: [PATCH 235/239] Added some better debug output in the slots concerned with the text edit of the main window ("update status"). Use insertPlainText() when adding to the text edit to simplify code. --- mainwindow.cpp | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 42c5d3f..d1989cd 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -329,7 +329,8 @@ void MainWindow::onQuit() { } void MainWindow::scrollDownTextEdit() { - qCritical() << "ON REPLACE LAST CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + Utils::printInfoMsg(QString("SCROLL-DOWN-TEXT_EDIT CALLED AT ") + + QDateTime::currentDateTime().toString(Qt::ISODateWithMs)); ui->updateStatus->setEnabled(true); @@ -340,30 +341,37 @@ void MainWindow::scrollDownTextEdit() { } void MainWindow::onAppendText(QString text, QString suffix) { - qCritical() << "ON APPEND CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + Utils::printInfoMsg(QString("ON APPEND CALLED AT ") + + QDateTime::currentDateTime().toString(Qt::ISODateWithMs)); QString editText = ui->updateStatus->toPlainText(); if (!suffix.isNull() && suffix.size() > 0) { //qInfo() << "TEXT" << text << "SUFFIX" << suffix; if (suffix == Worker::UPDATE_STEP_SUCCESS || suffix == Worker::UPDATE_STEP_FAIL) { - editText += QString("\n").leftJustified(m_width-3, '='); - editText += " "; + ui->updateStatus->insertPlainText(QString("\n").leftJustified(m_width-3, '=') + " "); + // editText += QString("\n").leftJustified(m_width-3, '='); + // editText += " "; } QString const &add = (QString("\n") + text).leftJustified(m_width - (2 + suffix.size())) + suffix; - editText += add; + ui->updateStatus->insertPlainText(add); + // editText += add; } else { QString const &add = text.leftJustified(m_width-9); - editText += add; + ui->updateStatus->insertPlainText(add); + //editText += add; } - Utils::printLineEditInfo(editText.split('\n', QString::SplitBehavior::SkipEmptyParts)); - ui->updateStatus->setText(editText.trimmed()); + // debug + // QString editText = ui->updateStatus->toPlainText(); + // Utils::printLineEditInfo(editText.split('\n', QString::SplitBehavior::SkipEmptyParts)); + // ui->updateStatus->setText(editText.trimmed()); scrollDownTextEdit(); } void MainWindow::onReplaceLast(QStringList newTextLines, QString suffix) { - qCritical() << "ON REPLACE LAST CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + Utils::printInfoMsg(QString("ON REPLACE LAST (LIST) CALLED AT ") + + QDateTime::currentDateTime().toString(Qt::ISODateWithMs)); int const s = newTextLines.size(); if (s > 0) { @@ -397,7 +405,8 @@ void MainWindow::onReplaceLast(QStringList newTextLines, QString suffix) { } void MainWindow::onReplaceLast(QString text, QString suffix) { - qCritical() << "ON REPLACE LAST CALLED AT" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs); + Utils::printInfoMsg(QString("ON REPLACE LAST (TEXT) CALLED AT ") + + QDateTime::currentDateTime().toString(Qt::ISODateWithMs)); QString editText = ui->updateStatus->toPlainText(); QStringList lines = editText.split('\n', QString::SplitBehavior::SkipEmptyParts); From 7832ef5d8c067e15d3eb525f28619124d36e3b3c Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Sun, 10 Sep 2023 17:22:49 +0200 Subject: [PATCH 236/239] Set version to 1.3.3. --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index af639cb..18302aa 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -15,7 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -VERSION=1.3.2 +VERSION=1.3.3 INCLUDEPATH += plugins From a03261d04a053ecbfcb17651e135cac0af32cfa3 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 11 Sep 2023 10:12:20 +0200 Subject: [PATCH 237/239] Fixed getParentName() to work analogously to isATBQTRunning(). --- utils.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/utils.cpp b/utils.cpp index ce5cdd4..284c95e 100644 --- a/utils.cpp +++ b/utils.cpp @@ -2,7 +2,10 @@ #include "message_handler.h" #include "git/git_client.h" + +#if defined (Q_OS_UNIX) || defined (Q_OS_LINUX) #include "unistd.h" +#endif #include #include @@ -180,18 +183,15 @@ bool Utils::sameFilesInDirs(QDir const &dir1, QDir const &dir2, QString Utils::getParentName() { // get name of parent process QString ppid = QString("/proc/%1/status").arg(getppid()); - QFile f(ppid); - if (f.exists()) { - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - QTextStream in(&f); - in.setCodec("UTF-8"); - while(!in.atEnd()) { - // Name: ATBQT - QStringList line = in.readLine().split(':'); - if (line.size() == 2) { - if (line[0].trimmed() == "Name") { - return line[1].trimmed(); - } + std::ifstream f(ppid.toStdString().c_str()); + if (f.is_open()) { + std::string next; + while (std::getline(f, next)) { + QString line = QString(next.c_str()).simplified(); + if (line.startsWith("Name")) { + int const idx = line.indexOf(QChar(':')); + if (idx != -1) { + return line.mid(idx+1).trimmed(); } } } From 17a4a69df27437c40dfcbe5113fa84ce98f8d7e2 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 11 Sep 2023 10:14:04 +0200 Subject: [PATCH 238/239] Added some debug output for parent-process-name --- update.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/update.cpp b/update.cpp index 92865fd..66d9770 100644 --- a/update.cpp +++ b/update.cpp @@ -14,9 +14,9 @@ #include #include -//#include -//#include -//#include +#if defined (Q_OS_UNIX) || defined (Q_OS_LINUX) +#include "unistd.h" +#endif #include "plugins/interfaces.h" @@ -742,6 +742,9 @@ bool Update::doUpdate(int &displayIndex, QStringList const &filesToWorkOn) { QString const &parentName = Utils::getParentName(); + Utils::printInfoMsg( + QString("PARENT OF ATB-UPDATE-TOOL (ppid=%1) ").arg(getppid()) + parentName); + if (parentName == "ATBQT" || parentName == "systemd") { // the tool was not called during 'service' ot during an automatic // update procedure. and it was called explicitly with libCAmaster.so From 82751eb1d4b162771403ab12fef3d5e01522e076 Mon Sep 17 00:00:00 2001 From: Gerhard Hoffmann Date: Mon, 11 Sep 2023 10:14:41 +0200 Subject: [PATCH 239/239] Set version to 1.3.4. --- OnDemandUpdatePTU.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnDemandUpdatePTU.pro b/OnDemandUpdatePTU.pro index 18302aa..f58cdce 100644 --- a/OnDemandUpdatePTU.pro +++ b/OnDemandUpdatePTU.pro @@ -15,7 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -VERSION=1.3.3 +VERSION=1.3.4 INCLUDEPATH += plugins