Skip to content
Merged
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
36 changes: 34 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,41 @@ jobs:
env
cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --parallel --target ALL_BUILD --config Release
cp build/Release/pingviewer.exe deploy/pingviewer.exe
if (($env:OPENSSL) -and (Test-Path $env:OPENSSL -pathType container)) {
Copy-Item ${env:OPENSSL}\*.dll -Destination deploy -Force

# Qt 5.15.2 is built against OpenSSL 1.1 and loads libssl-1_1-x64.dll / libcrypto-1_1-x64.dll
# at runtime (QSslSocket, used by the firmware-update HTTPS check). The VS2026 runner no
# longer ships OpenSSL 1.1 at a predictable location (it has OpenSSL 3.x), so the previous
# hardcoded copy silently shipped a build with broken HTTPS. Search common locations, fall
# back to a pinned OpenSSL 1.1.1 download, then fail if the DLLs are still missing.
$opensslDlls = @('libssl-1_1-x64.dll', 'libcrypto-1_1-x64.dll')
$searchRoots = @(
$env:OPENSSL,
'C:\Program Files\OpenSSL-Win64\bin',
'C:\Program Files\OpenSSL\bin',
"$env:QT_ROOT_DIR\..\..\Tools\OpenSSL\Win_x64\bin"
) | Where-Object { $_ -and (Test-Path $_) }
foreach ($dll in $opensslDlls) {
if (Test-Path "deploy\$dll") { continue }
foreach ($root in $searchRoots) {
$hit = Get-ChildItem -Path $root -Filter $dll -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($hit) { Copy-Item $hit.FullName -Destination deploy -Force; break }
}
}
if (-not (Test-Path 'deploy\libssl-1_1-x64.dll') -or -not (Test-Path 'deploy\libcrypto-1_1-x64.dll')) {
curl -L https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1w.zip -o openssl-1.1.zip
Expand-Archive -Path openssl-1.1.zip -DestinationPath openssl-1.1 -Force
foreach ($dll in $opensslDlls) {
$hit = Get-ChildItem -Path openssl-1.1 -Filter $dll -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($hit) { Copy-Item $hit.FullName -Destination deploy -Force }
}
}
foreach ($dll in $opensslDlls) {
if (-not (Test-Path "deploy\$dll")) {
Write-Error "Required OpenSSL 1.1 runtime '$dll' is missing from the deploy directory."
exit 1
}
}

curl -L https://github.com/bluerobotics/stm32flash-code/releases/download/continuous/stm32flash.exe -o deploy/stm32flash.exe
foreach ($I in (${env:SYSTEM32_DLLS} -split ' ')) { copy ${env:SYSTEM32}\$I deploy\ }
windeployqt --qmldir qml --release deploy/pingviewer.exe --verbose=2
Expand Down
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ add_compile_definitions(
QT_USE_FAST_OPERATOR_PLUS
)

# VS2026's MSVC STL removed stdext::make_checked_array_iterator, which Qt 5.15.2's
# qlist.h still calls unconditionally. Force-include a shim that restores it as a
# raw-pointer passthrough on new MSVC so QList-using translation units compile.
# See tools/msvc_stdext_shim.h for details.
if(MSVC)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:/FI${CMAKE_CURRENT_SOURCE_DIR}/tools/msvc_stdext_shim.h>")
endif()

find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS
Charts
Concurrent
Expand Down Expand Up @@ -58,6 +66,11 @@ include_directories(
lib/ping-cpp/ping-cpp/src/message/
)

# The bundled fmt/maddy submodules declare cmake_minimum_required(VERSION < 3.5),
# which CMake >= 4.0 rejects. Allow those older sub-projects to configure until the
# submodules are bumped. Must be set before add_subdirectory(lib/fmt/fmt).
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)

add_subdirectory(lib/fmt/fmt)

add_subdirectory(src)
2 changes: 1 addition & 1 deletion lib/fmt/fmt
Submodule fmt updated 145 files
2 changes: 1 addition & 1 deletion qml/DeviceManagerViewer.qml
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ PingPopup {
if (!connection.isValid())
return DeviceManagerViewer.ConnectionStatus.InvalidConfiguration;

if (connection.deviceType() == PingEnumNamespace.PingDeviceType.PING360 && connection.type() == AbstractLinkNamespace.Udp && connection.isSubnetBroadcast())
if (connection.deviceType() == PingEnumNamespace.PingDeviceType.PING360 && connection.type() == AbstractLinkNamespace.Udp && (connection.isSubnetBroadcast() || !connection.isInSubnet()))
return DeviceManagerViewer.ConnectionStatus.ConfigurationIsRequired;

if (available)
Expand Down
52 changes: 44 additions & 8 deletions src/devicemanager/devicemanager.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <QDateTime>
#include <QQmlEngine>

#include "devicemanager.h"
Expand All @@ -24,6 +25,11 @@ DeviceManager::DeviceManager()
connect(_detector, &ProtocolDetector::availableLinksChanged, this, &DeviceManager::updateAvailableConnections);
connect(Ping360HelperService::self(), &Ping360HelperService::availableLinkFound, this,
&DeviceManager::updateAvailableConnections);

// Periodically expire devices that have not been reported recently instead of dropping them
// on the first missed discovery cycle. The timer only runs while detecting (see
// startDetecting/stopDetecting) so connected devices are not expired after discovery stops.
connect(&_availabilityTimer, &QTimer::timeout, this, &DeviceManager::expireStaleConnections);
}

void DeviceManager::append(const LinkConfiguration& linkConf, const QString& deviceName, const QString& detectorName)
Expand All @@ -34,6 +40,9 @@ void DeviceManager::append(const LinkConfiguration& linkConf, const QString& dev
qCDebug(DEVICEMANAGER) << "Connection configuration already exist for:" << _sensors[Name][i] << linkConf
<< linkConf.argsAsConst();
_sensors[Available][i] = true;
if (i < _lastSeenMs.size()) {
_lastSeenMs[i] = QDateTime::currentMSecsSinceEpoch();
}
const auto indexRow = index(i);
emit dataChanged(indexRow, indexRow, _roles);
return;
Expand All @@ -50,6 +59,7 @@ void DeviceManager::append(const LinkConfiguration& linkConf, const QString& dev
_sensors[Connected].append(false);
_sensors[DetectorName].append(detectorName);
_sensors[Name].append(deviceName);
_lastSeenMs.append(QDateTime::currentMSecsSinceEpoch());

const auto& indexRow = index(line);
endInsertRows();
Expand All @@ -62,6 +72,7 @@ void DeviceManager::startDetecting()
qCDebug(DEVICEMANAGER) << "Start protocol detector service.";
_detectorThread.start();
Ping360HelperService::self()->startBroadcastService();
_availabilityTimer.start(1000);
}

void DeviceManager::stopDetecting()
Expand All @@ -70,6 +81,7 @@ void DeviceManager::stopDetecting()
Ping360HelperService::self()->stopBroadcastService();
_detector->stop();
_detectorThread.quit();
_availabilityTimer.stop();
}

void DeviceManager::connectLink(LinkConfiguration* linkConf)
Expand Down Expand Up @@ -151,19 +163,42 @@ void DeviceManager::updateAvailableConnections(
const QVector<LinkConfiguration>& availableLinkConfigurations, const QString& detector)
{
qCDebug(DEVICEMANAGER) << "Available devices:" << availableLinkConfigurations;
// Make all connections unavailable by default

// Refresh availability using a last-seen timestamp (updated by append) instead of flipping
// every entry for this detector to unavailable on each cycle. That previous approach made the
// list flicker whenever a single discovery cycle missed a reply. Stale entries are dropped by
// expireStaleConnections() once they have not been seen for _availabilityTtlMs.
for (const auto& link : availableLinkConfigurations) {
append(link, PingHelper::nameFromDeviceType(link.deviceType()), detector);
}
}

void DeviceManager::expireStaleConnections()
{
const qint64 now = QDateTime::currentMSecsSinceEpoch();
for (int i {0}; i < _sensors[Available].size(); i++) {
if (!_sensors[Available][i].toBool() || i >= _lastSeenMs.size()) {
continue;
}

// Never expire a device we are actively connected to. Discovery is stopped while
// connected, so its last-seen timestamp would otherwise always go stale.
if (_sensors[Connected][i].toBool()) {
continue;
}

// Keep simulations and directly-connected (non-detected) entries available; only devices
// reported by a detector are subject to the last-seen TTL.
auto linkConf = _sensors[Connection][i].value<QSharedPointer<LinkConfiguration>>();
if (linkConf->isSimulation() || _sensors[DetectorName][i] != detector) {
if (linkConf->isSimulation() || _sensors[DetectorName][i].toString() == QStringLiteral("None")) {
continue;
}
_sensors[Available][i] = false;
const auto indexRow = index(i);
emit dataChanged(indexRow, indexRow, _roles);
}

for (const auto& link : availableLinkConfigurations) {
append(link, PingHelper::nameFromDeviceType(link.deviceType()), detector);
if (now - _lastSeenMs[i] > _availabilityTtlMs) {
_sensors[Available][i] = false;
const auto indexRow = index(i);
emit dataChanged(indexRow, indexRow, _roles);
}
}
}

Expand All @@ -173,6 +208,7 @@ void DeviceManager::clear()
for (const auto category : _roleNames.keys()) {
_sensors[category].clear();
}
_lastSeenMs.clear();
endResetModel();
emit countChanged();
}
Expand Down
15 changes: 15 additions & 0 deletions src/devicemanager/devicemanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <QAbstractListModel>
#include <QLoggingCategory>
#include <QThread>
#include <QTimer>

#include "abstractlinknamespace.h"
#include "ping360helperservice.h"
Expand Down Expand Up @@ -178,6 +179,14 @@ class DeviceManager : public QAbstractListModel {
void updateAvailableConnections(
const QVector<LinkConfiguration>& availableLinkConfigurations, const QString& detectorName);

/**
* @brief Mark detected devices as unavailable once they have not been seen for longer than
* the availability TTL. This replaces flipping every entry to unavailable on each scan
* cycle, which made the device list flicker whenever a single discovery cycle missed a reply.
*
*/
void expireStaleConnections();

// Role and names
enum Roles {
Available = 0,
Expand All @@ -201,4 +210,10 @@ class DeviceManager : public QAbstractListModel {
// Model variables
QVector<int> _roles;
QHash<int, QVector<QVariant>> _sensors;

// Availability bookkeeping: last time (ms since epoch) each row was reported by a detector,
// kept in sync with the model rows (rows are only ever appended or cleared).
QVector<qint64> _lastSeenMs;
QTimer _availabilityTimer;
static constexpr qint64 _availabilityTtlMs {5000};
};
9 changes: 9 additions & 0 deletions src/link/abstractlink.h
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ class AbstractLink : public QObject {
void availableConnectionsChanged();
void configurationChanged();
void nameChanged(const QString& name);

/**
* @brief Emitted when the link fails in a way the user should be told about, with an
* actionable, human-readable message. Unlike transient logging, this is throttled/escalated
* by the link implementation so upper layers can surface it once instead of on every retry.
*
* @param errorMessage
*/
void linkError(const QString& errorMessage);
void autoConnectChanged();
void linkChanged(AbstractLinkNamespace::LinkType link);
void newData(const QByteArray& data);
Expand Down
2 changes: 1 addition & 1 deletion src/link/linkconfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class LinkConfiguration : public QObject {
* @return true
* @return false if IP is not in the same subnet as the computer or is not an ethernet connection
*/
bool isInSubnet() const;
Q_INVOKABLE bool isInSubnet() const;

/**
* @brief Check if IP has a valid and accessible broadcast IP.
Expand Down
40 changes: 37 additions & 3 deletions src/link/udplink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <QNetworkDatagram>

#include "logger.h"
#include "networkmanager.h"
#include "udplink.h"

PING_LOGGING_CATEGORY(PING_PROTOCOL_UDPLINK, "ping.protocol.udplink")
Expand All @@ -17,16 +18,23 @@ UDPLink::UDPLink(QObject* parent)
connect(_udpSocket, &QAbstractSocket::errorOccurred, this,
[this](QAbstractSocket::SocketError /*socketError*/) { printErrorMessage(); });

// Reset the failure counter and backoff once we are actually connected.
connect(_udpSocket, &QAbstractSocket::connected, this, [this] { resetConnectionState(); });

// QUdpSocket fail to emit state signal
// Here we use a timer to check if we are in a connect state, if not we try again
connect(&_stateTimer, &QTimer::timeout, this, [this] {
if (_udpSocket->state() == QAbstractSocket::UnconnectedState) {
printErrorMessage();
handleConnectionFailure();
qDebug(PING_PROTOCOL_UDPLINK) << "Trying to reconnect with host again.";
_udpSocket->connectToHost(_hostAddress, _port);

// Back off the reconnect interval so we stop flooding the log and network while the
// target stays unreachable, capping at _maxReconnectIntervalMs.
_stateTimer.setInterval(qMin(_stateTimer.interval() * 2, _maxReconnectIntervalMs));
}
});
_stateTimer.start(1000);
_stateTimer.start(_baseReconnectIntervalMs);

connect(this, &AbstractLink::sendData, this, [this](const QByteArray& data) { _udpSocket->write(data); });
}
Expand Down Expand Up @@ -73,10 +81,36 @@ bool UDPLink::setConfiguration(const LinkConfiguration& linkConfiguration)
void UDPLink::printErrorMessage()
{
qCWarning(PING_PROTOCOL_UDPLINK) << "An error has occurred with:" << _linkConfiguration;
QString errorMessage = QStringLiteral("Error (%1): %2.").arg(_udpSocket->state()).arg(_udpSocket->errorString());
QString errorMessage = QStringLiteral("Error (%1): %2.").arg(_udpSocket->error()).arg(_udpSocket->errorString());
qCWarning(PING_PROTOCOL_UDPLINK) << errorMessage;
}

void UDPLink::handleConnectionFailure()
{
printErrorMessage();
_connectionErrorCount++;

// Escalate only once, with an actionable message, instead of silently retrying forever.
if (!_errorEscalated && _connectionErrorCount >= _errorEscalationThreshold) {
_errorEscalated = true;

QString message = QStringLiteral("Unable to reach the device at %1:%2.").arg(_hostAddress).arg(_port);
if (!NetworkManager::isAddressInSubnet(_hostAddress)) {
message += QStringLiteral(" It is on a different network than your computer. Set the device IP "
"or your computer's network settings so they share a subnet.");
}
qCWarning(PING_PROTOCOL_UDPLINK) << message;
emit linkError(message);
}
}

void UDPLink::resetConnectionState()
{
_connectionErrorCount = 0;
_errorEscalated = false;
_stateTimer.setInterval(_baseReconnectIntervalMs);
}

bool UDPLink::finishConnection()
{
_udpSocket->close();
Expand Down
20 changes: 20 additions & 0 deletions src/link/udplink.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,28 @@ class UDPLink : public AbstractLink {
*/
void printErrorMessage();

/**
* @brief Handle a failed connection attempt: log it, count consecutive failures, and once the
* escalation threshold is reached emit a single actionable linkError instead of silently
* retrying forever.
*
*/
void handleConnectionFailure();

/**
* @brief Reset the failure counter and reconnect backoff after a successful connection.
*
*/
void resetConnectionState();

QString _hostAddress;
QTimer _stateTimer;
QUdpSocket* _udpSocket;
uint _port;

int _connectionErrorCount {0};
bool _errorEscalated {false};
static constexpr int _baseReconnectIntervalMs {1000};
static constexpr int _maxReconnectIntervalMs {8000};
static constexpr int _errorEscalationThreshold {3};
};
Loading
Loading