Environment
OS: Windows 11 (observed in debug build)
AetherSDR version: 0.8.22
Radio: FLEX-6600, firmware 4.1.5 (SmartSDR protocol v1.4.0.0)
Platform: x64, Qt 6.11.0 debug build, D3D11 backend
Bug description
When the radio TCP connection drops (intentional or not), a fatal Qt assertion fires on Windows. The application continues running in the session but the assert dialog is raised:
ASSERT failure in QCoreApplication::sendEvent:
"Cannot send events to objects owned by a different thread.
Current thread QThread(... name = "Qt mainThread").
Receiver 'QNativeSocketEngine(...)' was created in thread
QThread(... name = "SpotClients")"
file: qcoreapplication.cpp, line 542
The same log also shows several QRingBuffer assertion failures earlier in the session that may share the same root cause:
ASSERT: "bytes <= bufferSize" qringbuffer.cpp:76
ASSERT: "size() - offset > 0" qringbuffer_p.h:95
ASSERT: "size() + offset > 0" qringbuffer_p.h:101
These do not reproduce on Linux.
Steps to reproduce
Launch AetherSDR on Windows with a DX cluster configured to auto-connect on startup.
Wait for the radio to fully connect and all panadapters to initialize.
Either disconnect from the radio manually, or allow the radio to drop the TCP connection.
Observe the Qt assert dialog for QCoreApplication::sendEvent.
Expected behavior
Radio disconnect is handled cleanly. No cross-thread violations. Spot clients remain connected and auto-reconnect when the radio comes back.
Root cause analysis
DxClusterClient (and likely RbnClient) creates its QTcpSocket* m_socket as a child object in the constructor, which runs on the main thread:
// src/core/DxClusterClient.cpp:14
DxClusterClient::DxClusterClient(QObject* parent)
: QObject(parent)
, m_socket(new QTcpSocket(this)) // ← created on main thread
, m_reconnectTimer(new QTimer(this)) // ← same
The object is then moved to the SpotClients thread after construction:
// src/gui/MainWindow.cpp:1041
m_dxCluster->moveToThread(m_spotThread);
moveToThread() transfers the object's thread affinity, but on Windows, QTcpSocket internally creates a QSocketNotifier that registers with the Win32 message loop of the thread where the socket was constructed (the main thread's QEventDispatcherWin32). When a socket event fires on Windows — specifically during the cascade of connection teardown triggered by the radio dropping TCP — Win32 delivers the socket notification via the main thread's message pump, which then calls QCoreApplication::sendEvent targeting the QNativeSocketEngine that now lives in SpotClients. Qt's cross-thread guard fires.
On Linux this is not observed because epoll-based socket notifiers have no such thread affinity.
A secondary violation occurs at shutdown in closeEvent():
// src/gui/MainWindow.cpp:3425-3428
m_spotThread->quit();
m_spotThread->wait(3000);
delete m_dxCluster; // destructor calls m_socket->abort() from main thread
The destructor runs on the main thread while m_socket still has SpotClients affinity. Safe on Linux (epoll is thread-agnostic), but another assertion risk on Windows.
Proposed fix
Move socket and timer construction into a dedicated initialize() slot that runs inside the SpotClients thread after moveToThread():
// DxClusterClient.h — add:
public slots:
void initialize(); // called via QMetaObject::invokeMethod after moveToThread
private:
QTcpSocket* m_socket{nullptr};
QTimer* m_reconnectTimer{nullptr};
// DxClusterClient.cpp
DxClusterClient::DxClusterClient(QObject* parent)
: QObject(parent)
{
// socket and timer created in initialize(), not here
}
void DxClusterClient::initialize()
{
m_socket = new QTcpSocket(this);
m_reconnectTimer = new QTimer(this);
// move all connect() calls here from constructor
}
// MainWindow.cpp — after moveToThread():
m_dxCluster->moveToThread(m_spotThread);
QMetaObject::invokeMethod(m_dxCluster, &DxClusterClient::initialize,
Qt::QueuedConnection);
The same pattern applies to RbnClient and any other spot clients that own sockets.
For the shutdown path, queue the socket cleanup into the SpotClients thread before calling quit()/wait(), or use deleteLater() before stopping the thread so Qt cleans up the objects in the correct thread.
Affected files
src/core/DxClusterClient.h
src/core/DxClusterClient.cpp
src/gui/MainWindow.cpp (spot thread setup ~line 1039, closeEvent ~line 3425)
Labels: bug, windows, threading, spot
Environment
OS: Windows 11 (observed in debug build)
AetherSDR version: 0.8.22
Radio: FLEX-6600, firmware 4.1.5 (SmartSDR protocol v1.4.0.0)
Platform: x64, Qt 6.11.0 debug build, D3D11 backend
Bug description
When the radio TCP connection drops (intentional or not), a fatal Qt assertion fires on Windows. The application continues running in the session but the assert dialog is raised:
ASSERT failure in QCoreApplication::sendEvent:
"Cannot send events to objects owned by a different thread.
Current thread QThread(... name = "Qt mainThread").
Receiver 'QNativeSocketEngine(...)' was created in thread
QThread(... name = "SpotClients")"
file: qcoreapplication.cpp, line 542
The same log also shows several QRingBuffer assertion failures earlier in the session that may share the same root cause:
ASSERT: "bytes <= bufferSize" qringbuffer.cpp:76
ASSERT: "size() - offset > 0" qringbuffer_p.h:95
ASSERT: "size() + offset > 0" qringbuffer_p.h:101
These do not reproduce on Linux.
Steps to reproduce
Launch AetherSDR on Windows with a DX cluster configured to auto-connect on startup.
Wait for the radio to fully connect and all panadapters to initialize.
Either disconnect from the radio manually, or allow the radio to drop the TCP connection.
Observe the Qt assert dialog for QCoreApplication::sendEvent.
Expected behavior
Radio disconnect is handled cleanly. No cross-thread violations. Spot clients remain connected and auto-reconnect when the radio comes back.
Root cause analysis
DxClusterClient (and likely RbnClient) creates its QTcpSocket* m_socket as a child object in the constructor, which runs on the main thread:
// src/core/DxClusterClient.cpp:14
DxClusterClient::DxClusterClient(QObject* parent)
: QObject(parent)
, m_socket(new QTcpSocket(this)) // ← created on main thread
, m_reconnectTimer(new QTimer(this)) // ← same
The object is then moved to the SpotClients thread after construction:
// src/gui/MainWindow.cpp:1041
m_dxCluster->moveToThread(m_spotThread);
moveToThread() transfers the object's thread affinity, but on Windows, QTcpSocket internally creates a QSocketNotifier that registers with the Win32 message loop of the thread where the socket was constructed (the main thread's QEventDispatcherWin32). When a socket event fires on Windows — specifically during the cascade of connection teardown triggered by the radio dropping TCP — Win32 delivers the socket notification via the main thread's message pump, which then calls QCoreApplication::sendEvent targeting the QNativeSocketEngine that now lives in SpotClients. Qt's cross-thread guard fires.
On Linux this is not observed because epoll-based socket notifiers have no such thread affinity.
A secondary violation occurs at shutdown in closeEvent():
// src/gui/MainWindow.cpp:3425-3428
m_spotThread->quit();
m_spotThread->wait(3000);
delete m_dxCluster; // destructor calls m_socket->abort() from main thread
The destructor runs on the main thread while m_socket still has SpotClients affinity. Safe on Linux (epoll is thread-agnostic), but another assertion risk on Windows.
Proposed fix
Move socket and timer construction into a dedicated initialize() slot that runs inside the SpotClients thread after moveToThread():
// DxClusterClient.h — add:
public slots:
void initialize(); // called via QMetaObject::invokeMethod after moveToThread
private:
QTcpSocket* m_socket{nullptr};
QTimer* m_reconnectTimer{nullptr};
// DxClusterClient.cpp
DxClusterClient::DxClusterClient(QObject* parent)
: QObject(parent)
{
// socket and timer created in initialize(), not here
}
void DxClusterClient::initialize()
{
m_socket = new QTcpSocket(this);
m_reconnectTimer = new QTimer(this);
// move all connect() calls here from constructor
}
// MainWindow.cpp — after moveToThread():
m_dxCluster->moveToThread(m_spotThread);
QMetaObject::invokeMethod(m_dxCluster, &DxClusterClient::initialize,
Qt::QueuedConnection);
The same pattern applies to RbnClient and any other spot clients that own sockets.
For the shutdown path, queue the socket cleanup into the SpotClients thread before calling quit()/wait(), or use deleteLater() before stopping the thread so Qt cleans up the objects in the correct thread.
Affected files
src/core/DxClusterClient.h
src/core/DxClusterClient.cpp
src/gui/MainWindow.cpp (spot thread setup ~line 1039, closeEvent ~line 3425)
Labels: bug, windows, threading, spot