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
38 changes: 38 additions & 0 deletions src/gui/RadioSetupDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,44 @@ QWidget* RadioSetupDialog::buildRadioTab()
kInfoRightLabelWidth),
3, 1);

auto* rebootBtn = new QPushButton(QStringLiteral("Reboot Radio"));
AetherSDR::ThemeManager::instance().applyStyleSheet(rebootBtn,
"QPushButton { background: #3a1a1a; color: #ffb080; border: 1px solid #6e3030;"
" border-radius: 3px; font-size: 11px; font-weight: bold; padding: 3px 10px; }"
"QPushButton:hover { background: #4a2020; }"
"QPushButton:disabled { background: {{color.background.1}}; color: {{color.meter.bar.fill}}; border-color: {{color.background.2}}; }");
// Only enable when actually connected; subscribe so disconnect/reconnect
// disables/re-enables the button without the user having to reopen the
// dialog. rebootRadio() also early-returns on disconnected, but the
// disabled state makes the affordance discoverable rather than silent.
rebootBtn->setEnabled(m_model->isConnected());
connect(m_model, &RadioModel::connectionStateChanged,
rebootBtn, &QPushButton::setEnabled);
connect(rebootBtn, &QPushButton::clicked, this, [this] {
const bool wan = m_model->isWan();
const QString body = wan
? QStringLiteral("Reboot the connected radio now?\n\n"
"AetherSDR will disconnect. SmartLink/WAN sessions "
"do not auto-reconnect today — you will need to "
"reconnect manually once the radio finishes booting.")
: QStringLiteral("Reboot the connected radio now?\n\n"
"AetherSDR will disconnect and automatically reconnect "
"once the radio finishes booting.");
const auto ret = QMessageBox::warning(
this,
QStringLiteral("Reboot Radio"),
body,
QMessageBox::Ok | QMessageBox::Cancel,
QMessageBox::Cancel);
if (ret == QMessageBox::Ok) {
m_model->rebootRadio();
close();
}
});
grid->addWidget(makeInfoField(QStringLiteral("Reboot:"), rebootBtn,
kInfoLeftLabelWidth),
3, 0);

connect(m_model, &RadioModel::infoChanged, this, [this] {
if (m_serialLabel) {
m_serialLabel->setText(radioSerialNumber(m_model));
Expand Down
40 changes: 39 additions & 1 deletion src/models/RadioModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,9 @@ void RadioModel::connectToRadio(const RadioInfo& info)
m_lastInfo = info;
m_intentionalDisconnect = false;
m_forcedDisconnectInProgress = false;
// Note: m_rebootInProgress is NOT cleared here — connectToRadio() runs
// again from the reconnect timer during a reboot, and we want to keep
// suppressing toasts until onConnected() actually fires.
m_announcedClientConnections.clear();
m_reconnectTimer.stop();
m_name = info.name;
Expand Down Expand Up @@ -972,6 +975,9 @@ void RadioModel::connectViaWan(WanConnection* wan, const QString& publicIp, quin
m_wanUdpPort = udpPort;
m_intentionalDisconnect = false;
m_forcedDisconnectInProgress = false;
// Note: m_rebootInProgress is NOT cleared here — connectToRadio() runs
// again from the reconnect timer during a reboot, and we want to keep
// suppressing toasts until onConnected() actually fires.
m_announcedClientConnections.clear();
m_reconnectTimer.stop();

Expand Down Expand Up @@ -1161,6 +1167,7 @@ void RadioModel::announceClientConnection(quint32 handle,
void RadioModel::disconnectFromRadio()
{
m_intentionalDisconnect = true;
m_rebootInProgress = false;
m_reconnectTimer.stop();
m_pingTimer.stop();
if (m_wanConn) {
Expand Down Expand Up @@ -1219,6 +1226,34 @@ void RadioModel::forceDisconnect()
}
}

void RadioModel::rebootRadio()
{
// Gate on isConnected() (which already covers WAN/SmartLink sessions), not
// the LAN socket alone — sendCommand() already routes through m_wanConn
// for WAN, so a SmartLink user clicking Reboot should send the command
// and tear the link down the same way as a LAN user.
if (!isConnected()) {
return;
}
m_rebootInProgress = true;
sendCommand(QStringLiteral("radio reboot"));
// Give the TCP write a brief moment to flush before tearing down the
// socket, then drop into the unexpected-disconnect path so the existing
// reconnect timer brings us back when the radio is up again.
QTimer::singleShot(250, this, &RadioModel::forceDisconnect);
// Fail-open safety: if the reboot wedges the radio's network stack, the
// reconnect timer keeps firing "connection refused" forever and the user
// sees no toasts at all because m_rebootInProgress is gating them. Time
// the suppression out after 60s so a stuck radio surfaces real errors
// instead of silently retrying forever. 60s comfortably covers a healthy
// 6000/8600 boot.
QTimer::singleShot(60'000, this, [this] {
if (m_rebootInProgress) {
m_rebootInProgress = false;
}
});
}

void RadioModel::setTransmit(bool tx, TransmitModel::PttSource source)
{
if (tx) {
Expand Down Expand Up @@ -1774,6 +1809,7 @@ void RadioModel::onConnected()
{
qCDebug(lcProtocol) << "RadioModel: connected";
m_reconnectTimer.stop();
m_rebootInProgress = false;
armClientConnectionNoticeSuppression();
setActivePanResized(false);

Expand Down Expand Up @@ -2508,7 +2544,9 @@ void RadioModel::onDisconnected()
void RadioModel::onConnectionError(const QString& msg)
{
qCWarning(lcProtocol) << "RadioModel: connection error:" << msg;
emit connectionError(msg);
if (!m_rebootInProgress) {
emit connectionError(msg);
}
// A refused connect may never emit disconnected, but the radio can recover
// after expiring a stale session. Keep retrying the same discovered radio.
if (!m_wanConn && !m_intentionalDisconnect && !m_lastInfo.address.isNull()
Expand Down
8 changes: 8 additions & 0 deletions src/models/RadioModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ class RadioModel : public QObject {
void cancelMultiFlexConflict();
void disconnectFromRadio();
void forceDisconnect(); // Close TCP but allow auto-reconnect
// Send `radio reboot` (FlexLib Radio.cs:2575), surface a notification to
// the operator, then trigger forceDisconnect so the standard reconnect
// timer brings the link back when the radio finishes booting.
void rebootRadio();
bool isWan() const { return m_wanConn != nullptr; }

// Phase 2 of GHSA-wfx7-w6p8-4jr2 (#2951): forward the user's
Expand Down Expand Up @@ -827,6 +831,10 @@ private slots:
RadioInfo m_lastInfo; // stored for auto-reconnect
bool m_intentionalDisconnect{false};
bool m_forcedDisconnectInProgress{false};
// Suppress connection-error toasts between rebootRadio() and the next
// successful reconnect — multiple `Connection refused` retries fire
// while the radio is still booting and would otherwise spam the UI.
bool m_rebootInProgress{false};
QTimer m_reconnectTimer;

// ── Network quality monitor ──
Expand Down
Loading