Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Tests

on:
push:
branches:
- "**"
pull_request:

concurrency:
group: tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Build and test (Ubuntu)
runs-on: ubuntu-22.04
timeout-minutes: 30

env:
QT_QPA_PLATFORM: offscreen

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install build and Qt dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build pkg-config qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-multimedia-dev libqt6sql6-sqlite libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libsqlite3-dev libssl-dev

- name: Configure tests
run: |
cmake -S . -B build-tests -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -DENABLE_VTK=OFF

- name: Build tests
run: cmake --build build-tests --parallel 2

- name: Run tests
run: ctest --test-dir build-tests --output-on-failure --timeout 60
8 changes: 7 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ set(MOC_HEADERS
include/multipack/config/SettingsManager.h
include/multipack/system/UsbKeyCheck.h
include/multipack/system/UpdateChecker.h
include/multipack/system/SystemUpdaterClient.h
include/multipack/system/WifiSsidProbe.h
include/multipack/system/AutoUpdater.h
include/multipack/system/UsbMonitor.h
)
Expand Down Expand Up @@ -266,6 +268,8 @@ set(SYSTEM_SOURCES
src/system/UpdateChecker.cpp
src/system/FileOperations.cpp
src/system/RobFileParser.cpp
src/system/SystemUpdaterClient.cpp
src/system/WifiSsidProbe.cpp
src/system/AutoUpdater.cpp
src/system/UsbMonitor.cpp
)
Expand All @@ -274,7 +278,9 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64" AND MULTIPACK_SAFE_ARM64_EMULA
# GCC 11 under Docker/QEMU can ICE/segfault on these translation units at -O2.
# Keep the workaround tightly scoped so the ARM64 release build stays fast.
set_source_files_properties(
src/system/AutoUpdater.cpp
src/system/SystemUpdaterClient.cpp
src/system/WifiSsidProbe.cpp
src/system/AutoUpdater.cpp
src/ui/UIHelpers.cpp
PROPERTIES COMPILE_OPTIONS "-O1")
endif()
Expand Down
25 changes: 25 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,28 @@ available before readiness.
## License

Proprietary - Szaidel Cosmetic GmbH

### Immutable system updates and network status

With `MULTIPACK_IMMUTABLE=1`, the existing update button reads system status and
asks for a published system release VERSION (for example `rpi-hmi-v0.1.0`).
The asynchronous client sends only `status` or `install` plus that validated
identifier to `/run/hmi-updater/control.sock`. The image grants `hmi-update` group
access. The daemon verifies signatures, writes the inactive slot, and reboots;
once accepted, installation cannot be cancelled by closing the UI. Status includes
the active version, boot phase and download/install progress. Network errors can
be retried. No legacy application/USB installation runs in immutable mode.
Without the environment flag the signed legacy updater is retained.

SSID probing uses asynchronous `nmcli` with a deadline and no Wi-Fi rescan.
HMI provisioning installs `network-manager`. Portable bundles require a host
NetworkManager installation for SSID display; when it is absent, the UI explicitly
shows SSID unavailable. The probe uses the C locale so translated nmcli output
does not affect SSID detection. Interface IP status remains usable. Probe processes belong
to the window and are terminated when it closes.

A saved installation failure remains readable system status and allows another
VERSION to be submitted. If a status request is still pending when an update is
confirmed, the UI reports that no request was sent and asks the operator to retry.
Background idle/error observations do not replace operator messages; active
update progress and new installation failures remain visible.
12 changes: 10 additions & 2 deletions include/multipack/config/ConfigDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#define MULTIPACK_CONFIG_CONFIGDEFAULTS_H

#include <QString>
#include <QDir>
#include <QtGlobal>

#ifndef MULTIPACK_APP_VERSION
#define MULTIPACK_APP_VERSION "2.0.0"
Expand Down Expand Up @@ -93,9 +95,15 @@ inline QString defaultDatabasePath()
* @brief Get default settings file path
* @return Settings file path
*/
inline QString dataDirectory()
{
const QString configured = qEnvironmentVariable("MULTIPACK_DATA_DIR").trimmed();
return configured.isEmpty() ? QDir::currentPath() : QDir(configured).absolutePath();
}

inline QString defaultSettingsPath()
{
return "settings.json";
return QDir(dataDirectory()).filePath("settings.json");
}

/**
Expand All @@ -104,7 +112,7 @@ inline QString defaultSettingsPath()
*/
inline QString defaultLogDir()
{
return LOG_DIR;
return QDir(dataDirectory()).filePath(LOG_DIR);
}

} // namespace Defaults
Expand Down
9 changes: 8 additions & 1 deletion include/multipack/system/AutoUpdater.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define MULTIPACK_SYSTEM_AUTOUPDATER_H

#include <QObject>
#include "multipack/system/SystemUpdaterClient.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QFile>
Expand Down Expand Up @@ -77,7 +78,8 @@ class AutoUpdater : public QObject
* @brief Constructor
* @param parent Parent QObject
*/
explicit AutoUpdater(QObject* parent = nullptr);
explicit AutoUpdater(QObject* parent = nullptr,
const QString& systemSocketPath = QStringLiteral("/run/hmi-updater/control.sock"));

/**
* @brief Destructor
Expand Down Expand Up @@ -236,6 +238,11 @@ private slots:
void onSslErrors(const QList<QSslError>& errors);

private:
bool m_immutable = false;
SystemUpdaterClient* m_systemUpdater = nullptr;
QTimer* m_systemPoll = nullptr;
bool m_systemChecking = false;
bool m_systemInstalling = false;
// Core components
QNetworkAccessManager* m_networkManager;
QNetworkReply* m_checkReply = nullptr;
Expand Down
35 changes: 35 additions & 0 deletions include/multipack/system/SystemUpdaterClient.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#ifndef MULTIPACK_SYSTEM_SYSTEMUPDATERCLIENT_H
#define MULTIPACK_SYSTEM_SYSTEMUPDATERCLIENT_H
#include <QObject>
#include <QLocalSocket>
#include <QTimer>
#include <QJsonObject>
namespace multipack::system {
// One bounded asynchronous request at a time. Production always uses the fixed socket.
class SystemUpdaterClient : public QObject {
Q_OBJECT
public:
explicit SystemUpdaterClient(QObject* parent = nullptr,
const QString& socketPath = QStringLiteral("/run/hmi-updater/control.sock"),
int timeoutMs = 4000);
bool status();
bool install(const QString& version);
bool busy() const { return m_busy; }
void cancel(); // Cancels observation only; never cancels a privileged installation.
static bool validVersion(const QString& version);
signals:
void response(const QJsonObject& value);
void error(const QString& message);
private:
bool request(const QJsonObject& value);
void fail(const QString& message);
QLocalSocket* m_socket;
QTimer m_timeout;
QString m_path;
QByteArray m_request, m_buffer;
int m_timeoutMs;
bool m_busy = false;
bool m_statusRequest = false;
};
}
#endif
24 changes: 24 additions & 0 deletions include/multipack/system/WifiSsidProbe.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef MULTIPACK_SYSTEM_WIFISSIDPROBE_H
#define MULTIPACK_SYSTEM_WIFISSIDPROBE_H
#include <QObject>
#include <QProcess>
#include <QTimer>
namespace multipack::system {
class WifiSsidProbe : public QObject {
Q_OBJECT
public:
explicit WifiSsidProbe(QObject* parent = nullptr, const QString& program = "nmcli", int timeoutMs = 1500);
~WifiSsidProbe() override;
void refresh();
static QString parse(const QByteArray& output);
signals:
void result(const QString& ssid);
private:
QProcess* m_process;
QTimer m_timer;
QString m_program;
int m_timeoutMs;
bool m_expired = false;
};
}
#endif
10 changes: 10 additions & 0 deletions include/multipack/ui/MainWindow.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "multipack/system/WifiSsidProbe.h"
/**
* @file MainWindow.h
* @brief Main application window using Qt Designer UI
Expand Down Expand Up @@ -198,6 +199,7 @@ private slots:
void maybeStartRobotStatusMonitor();
void stopRobotStatusMonitor();
void setupStatusTab();
void refreshNetworkStatus();
void updateStatusTab(const robot::RobotStatus& status);
void onStatusDetailsUpdated(const QString& polyscopeVersion,
const QString& serialNumber,
Expand Down Expand Up @@ -264,10 +266,18 @@ private slots:
QWidget* m_centralWidget = nullptr;
QTimer* m_zwischenlageTimer = nullptr;
QTimer* m_paletteClearTimer = nullptr;
system::WifiSsidProbe* m_wifiSsidProbe = nullptr;
QTimer* m_networkStatusTimer = nullptr;
NotificationPopup* m_zwischenlagePopup = nullptr;
QPushButton* m_palette1ClearIndicator = nullptr;
QPushButton* m_palette2ClearIndicator = nullptr;
VisualizationWidget* m_visualizationWidget = nullptr;
QLabel* m_statusHostname = nullptr;
QLabel* m_statusEth0Ip = nullptr;
QLabel* m_statusWlan0Ip = nullptr;
QLabel* m_statusWifiSsid = nullptr;
QLabel* m_statusTailscaleState = nullptr;
QLabel* m_statusTailscaleIp = nullptr;
QLabel* m_statusRobotIp = nullptr;
QLabel* m_statusConnection = nullptr;
QLabel* m_statusRobotMode = nullptr;
Expand Down
6 changes: 6 additions & 0 deletions src/config/SettingsManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ bool SettingsManager::save(const QString& path)

QJsonDocument doc(m_settings);

const QFileInfo targetInfo(filePath);
if (!QDir().mkpath(targetInfo.absolutePath())) {
qWarning() << "Cannot create settings directory:" << targetInfo.absolutePath();
return false;
}

QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "Cannot open settings file for writing:" << filePath;
Expand Down
6 changes: 3 additions & 3 deletions src/core/AppInitializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ QString resolveConfiguredPath(const QString& configuredPath, const QString& fall
{
const QString path = configuredPath.trimmed().isEmpty() ? fallback : configuredPath.trimmed();
QFileInfo info(path);
return info.isAbsolute() ? info.absoluteFilePath() : QDir::current().absoluteFilePath(path);
return info.isAbsolute() ? info.absoluteFilePath() : QDir(config::Defaults::dataDirectory()).absoluteFilePath(path);
}

}
Expand Down Expand Up @@ -209,7 +209,7 @@ bool AppInitializer::initializeLogging()
{
qDebug() << "AppInitializer - initializing logging";

const QString logPath = QDir::currentPath() + "/logs";
const QString logPath = config::Defaults::defaultLogDir();
const bool verboseEnabled = qEnvironmentVariable("MULTIPACK_VERBOSE", "0") == "1";
const config::LogLevel level = verboseEnabled ? config::LogLevel::Debug : config::LogLevel::Info;

Expand All @@ -232,7 +232,7 @@ bool AppInitializer::initializeSettings()
m_settingsManager = std::make_unique<config::SettingsManager>();

// Try to load settings
QString settingsPath = QDir::currentPath() + "/settings.json";
const QString settingsPath = config::Defaults::defaultSettingsPath();
if (QFile::exists(settingsPath)) {
if (!m_settingsManager->load(settingsPath)) {
qWarning() << "Failed to load settings from" << settingsPath;
Expand Down
Loading
Loading