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
50 changes: 38 additions & 12 deletions src/core/DvkWavTransfer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ DvkWavTransfer::DvkWavTransfer(RadioModel* model, QObject* parent)
finish(false, "Timed out waiting for radio connection", true);
} else if (m_direction == Upload && m_client &&
m_client->state() != QAbstractSocket::ConnectedState) {
if (retryUploadOnDefaultPort(m_connectTimeoutGeneration)) {
return;
}
finish(false, "Timed out connecting to radio upload port", false);
}
});
Expand Down Expand Up @@ -256,8 +259,11 @@ void DvkWavTransfer::handleNewConnection(quint64 generation, QTcpServer* server)
// Anyone on the LAN can reach the listening port; only the radio may
// write the operator's file. Others are dropped and the wait continues.
if (m_model && !isRadioPeer(pending->peerAddress(), m_model->radioAddress())) {
qWarning() << "DvkWavTransfer: rejected export connection from"
<< pending->peerAddress().toString();
const QString peer = pending->peerAddress().toString();
qWarning() << "DvkWavTransfer: rejected export connection from" << peer
<< "expected" << m_model->radioAddress().toString();
emit statusChanged(QString("Ignored a connection from %1; waiting for the radio at %2…")
.arg(peer, m_model->radioAddress().toString()));
pending->abort();
pending->deleteLater();
return;
Expand Down Expand Up @@ -383,6 +389,12 @@ void DvkWavTransfer::upload(int slotId, const QString& filePath)
emit finished(false, "The radio connection was lost before the transfer could start.");
return;
}
// Checked here, not only when the menu opened: the file dialog is modal,
// and the DVK may have started recording or playing while it was up.
if (m_dvk && !m_dvk->canStartOperation()) {
emit finished(false, "The voice keyer is busy; stop it before importing");
return;
}

QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
Expand Down Expand Up @@ -488,6 +500,8 @@ void DvkWavTransfer::handleUploadPortReceived(quint64 generation, quint64 reques
bool ok = false;
int port = body.trimmed().toInt(&ok);
if (!ok || port <= 0 || port > 65535) {
qWarning() << "DvkWavTransfer: no usable port in file upload reply" << body.trimmed()
<< "- using" << DEFAULT_UPLOAD_PORT;
port = DEFAULT_UPLOAD_PORT;
}

Expand Down Expand Up @@ -635,22 +649,32 @@ void DvkWavTransfer::handleUploadBytesWritten(quint64 generation, QTcpSocket* so
sendNextChunk(generation, socket);
}

bool DvkWavTransfer::retryUploadOnDefaultPort(quint64 generation)
{
if (!isCurrent(generation) || m_uploadFallbackTried || m_uploadConnected
|| m_uploadPort == 0 || m_uploadPort == DEFAULT_UPLOAD_PORT) {
return false;
}
m_uploadFallbackTried = true;
if (m_client) {
m_client->disconnect(this);
m_client->abort();
m_client->deleteLater();
m_client = nullptr;
}
stopConnectTimeout();
openUploadSocket(generation, DEFAULT_UPLOAD_PORT);
return true;
}

void DvkWavTransfer::handleUploadError(quint64 generation, QTcpSocket* socket)
{
if (!isCurrentSocket(generation, socket)) return;

// As FirmwareUploader: a port the radio named but would not connect gets
// one retry on the file server's usual 42607. An error after connecting is
// a failed upload, not a wrong port.
if (!m_uploadFallbackTried && !m_uploadConnected && m_uploadPort != 0
&& m_uploadPort != DEFAULT_UPLOAD_PORT) {
m_uploadFallbackTried = true;
m_client->disconnect(this);
m_client->abort();
m_client->deleteLater();
m_client = nullptr;
stopConnectTimeout();
openUploadSocket(generation, DEFAULT_UPLOAD_PORT);
if (retryUploadOnDefaultPort(generation)) {
return;
}

Expand Down Expand Up @@ -704,7 +728,9 @@ void DvkWavTransfer::cleanup(bool discardDownload)
// already queued for this transfer must not match a replacement started
// from the finished() handler below.
invalidateOperation();
if (m_direction == Upload) {
// Only an upload that reached the file server leaves it busy; a refused
// `dvk upload` or a port that never connected must not mask the next try.
if (m_direction == Upload && m_uploadConnected) {
m_sinceUpload.start();
}
m_transferring = false;
Expand Down
3 changes: 3 additions & 0 deletions src/core/DvkWavTransfer.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class DvkWavTransfer : public QObject {
quint64 requestId);
void handleUploadSlotAccepted(quint64 generation, quint64 requestId, int code);
void openUploadSocket(quint64 generation, int port);
// One retry on 42607 for a named port that refused or never answered,
// before any connection was made. False when it does not apply.
bool retryUploadOnDefaultPort(quint64 generation);
// A refusal on any transfer leg also reaches the DVK license latch.
void reportRefusal(int code);
std::function<void(int, const QString&)> makeUploadPortCallback(quint64 generation,
Expand Down
7 changes: 4 additions & 3 deletions src/core/backends/flex/CommandParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ QMap<QString, QString> CommandParser::parseKVs(const QString& body)
QMap<QString, QString> result;
// Body may look like: "freq=14.225000 mode=USB filter_lo=-1500 filter_hi=1500"
// A quoted value may contain spaces (`dvk … name="CQ Contest"`) and is kept
// whole, quotes included, up to the next token holding a quote. A quote
// left open with no later quote on the line splits as before.
// whole, quotes included, up to the token that closes it. A quote left
// open (no closing token before the next `key="` or the end of the line)
// splits as before, so a malformed value cannot swallow the keys after it.
const QStringList tokens = body.split(' ', Qt::SkipEmptyParts);
for (qsizetype i = 0; i < tokens.size(); ++i) {
QString token = tokens[i];
Expand All @@ -20,7 +21,7 @@ QMap<QString, QString> CommandParser::parseKVs(const QString& body)
while (end < tokens.size() && !tokens[end].contains('"')) {
++end;
}
if (end < tokens.size()) {
if (end < tokens.size() && !tokens[end].contains(QLatin1String("=\""))) {
for (qsizetype j = i + 1; j <= end; ++j) {
token += QLatin1Char(' ') + tokens[j];
}
Expand Down
30 changes: 25 additions & 5 deletions src/gui/DvkPanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ DvkPanel::DvkPanel(DvkModel* model, QWidget* parent)
stopActiveOperation();
});

m_announceTimer = new QTimer(this);
m_announceTimer->setSingleShot(true);
m_announceTimer->setInterval(150);
connect(m_announceTimer, &QTimer::timeout, this, &DvkPanel::flushSlotAnnouncement);

// Elapsed timer for recording/playback/preview progress
m_elapsedTimer = new QTimer(this);
m_elapsedTimer->setInterval(100);
Expand Down Expand Up @@ -401,12 +406,27 @@ void DvkPanel::updateSlotAccessibility(int id, const QString& name, int duration
play->setAccessibleDescription(durationMs > 0
? QString("%1 recording. Transmits on the transmit slice (F%2).").arg(length).arg(id)
: QStringLiteral("Empty slot, nothing to play."));
// Announce a real change the operator can see; slot loads and resets on
// connect and disconnect would otherwise be a burst of a dozen.
if (changed && play->isVisible()) {
QAccessibleEvent ev(play, QAccessible::NameChanged);
QAccessible::updateAccessibility(&ev);
// Announce a real change the operator can see, one slot at a time. A
// load or reset changes many slots at once; flushSlotAnnouncement() stays
// silent for those instead of reading a dozen names back to back.
if (changed && m_announceTimer) {
if (!m_announceSlots.contains(id)) {
m_announceSlots.append(id);
}
m_announceTimer->start();
}
}

void DvkPanel::flushSlotAnnouncement()
{
if (m_announceSlots.size() == 1) {
QPushButton* play = m_fkeyBtns[m_announceSlots.first() - 1];
if (play->isVisible()) {
QAccessibleEvent ev(play, QAccessible::NameChanged);
QAccessible::updateAccessibility(&ev);
}
}
m_announceSlots.clear();
}

void DvkPanel::announceStatus(const QString& text, bool error)
Expand Down
6 changes: 6 additions & 0 deletions src/gui/DvkPanel.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ private slots:
// (#2464, #2582).
QVector<QShortcut*> m_shortcuts;

// F-key name changes collected for one announcement; a burst (slot load,
// disconnect) of more than one is not announced.
QVector<int> m_announceSlots;
QTimer* m_announceTimer{nullptr};
void flushSlotAnnouncement();

void selectSlot(int id);
// Row and F-key names carry the slot's name and length for screen readers.
void updateSlotAccessibility(int id, const QString& name, int durationMs);
Expand Down
24 changes: 16 additions & 8 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10301,17 +10301,19 @@ void MainWindow::updateToolsMenuState()

if (m_swrScanAction) {
m_swrScanAction->setEnabled(txReady);
m_swrScanAction->setToolTip(txReady ? QString()
const QString reason = txReady ? QString()
: tr("Requires an idle, TX-capable radio with this client holding "
"the interlock"));
"the interlock");
m_swrScanAction->setToolTip(reason);
m_swrScanAction->setStatusTip(reason); // a tooltip is never announced
}

// Every disabling condition names itself. A greyed control with no stated
// reason reads as broken (#5510), and Pre-tune now has five of them.
if (m_preTuneAction) {
m_preTuneAction->setEnabled(txReady && memories && tx.memoriesEnabled()
&& hasTxApplet);
m_preTuneAction->setToolTip(
const QString reason =
!caps.hasTunerMemories
? tr("ATU memory controls are unavailable for this radio")
: tgxlOperate
Expand All @@ -10323,11 +10325,13 @@ void MainWindow::updateToolsMenuState()
: !txReady
? tr("Requires an idle, TX-capable radio with this client "
"holding the interlock")
: QString());
: QString();
m_preTuneAction->setToolTip(reason);
m_preTuneAction->setStatusTip(reason);
}
if (m_clearAtuAction) {
m_clearAtuAction->setEnabled(connected && memories && hasTxApplet);
m_clearAtuAction->setToolTip(
const QString reason =
!caps.hasTunerMemories
? tr("ATU memory controls are unavailable for this radio")
: tgxlOperate
Expand All @@ -10336,7 +10340,9 @@ void MainWindow::updateToolsMenuState()
? tr("The transmit applet is unavailable in this build")
: !connected
? tr("Connect to a radio first")
: QString());
: QString();
m_clearAtuAction->setToolTip(reason);
m_clearAtuAction->setStatusTip(reason);
}

if (m_gpsDashboardAction) {
Expand All @@ -10349,15 +10355,17 @@ void MainWindow::updateToolsMenuState()
const bool externalRx = active
&& active->externalReceiveReplacementActive();
m_agcTCalibrationMenuAction->setEnabled(connected && active && !externalRx);
m_agcTCalibrationMenuAction->setToolTip(
const QString reason =
!connected
? tr("Connect to a radio first")
: !active
? tr("No active receiver to calibrate")
: externalRx
? tr("Not available while an external receive source "
"replaces this slice's RX")
: QString());
: QString();
m_agcTCalibrationMenuAction->setToolTip(reason);
m_agcTCalibrationMenuAction->setStatusTip(reason);
}
}

Expand Down
24 changes: 20 additions & 4 deletions src/models/DvkModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,36 @@ DvkModel::DvkModel(QObject* parent) : QObject(parent) {}

void DvkModel::recStart(int id)
{
m_pendingId = id;
setPending(Recording);
emit replyCommandReady(QString("dvk rec_start id=%1").arg(id), "rec_start", id);
}
void DvkModel::recStop()
{
emit replyCommandReady(QStringLiteral("dvk rec_stop"), "rec_stop", m_activeId);
emit replyCommandReady(QStringLiteral("dvk rec_stop"), "rec_stop",
m_activeId > 0 ? m_activeId : m_pendingId);
}
void DvkModel::previewStart(int id)
{
m_pendingId = id;
setPending(Preview);
emit replyCommandReady(QString("dvk preview_start id=%1").arg(id), "preview_start", id);
}
void DvkModel::previewStop()
{
emit replyCommandReady(QStringLiteral("dvk preview_stop"), "preview_stop", m_activeId);
emit replyCommandReady(QStringLiteral("dvk preview_stop"), "preview_stop",
m_activeId > 0 ? m_activeId : m_pendingId);
}
void DvkModel::playbackStart(int id)
{
m_pendingId = id;
setPending(Playback);
emit replyCommandReady(QString("dvk playback_start id=%1").arg(id), "playback_start", id);
}
void DvkModel::playbackStop()
{
emit replyCommandReady(QStringLiteral("dvk playback_stop"), "playback_stop", m_activeId);
emit replyCommandReady(QStringLiteral("dvk playback_stop"), "playback_stop",
m_activeId > 0 ? m_activeId : m_pendingId);
}
void DvkModel::clear(int id)
{
Expand Down Expand Up @@ -131,6 +137,14 @@ void DvkModel::setTransferActive(bool active)
emit admissionChanged();
}

void DvkModel::clearRefusal()
{
if (m_licenseRefused) {
m_licenseRefused = false;
emit licenseRefusedChanged(false);
}
}

void DvkModel::noteRefusal(uint code)
{
if (code == kNotLicensed && !m_licenseRefused) {
Expand Down Expand Up @@ -254,7 +268,9 @@ void DvkModel::reset()
m_enabled = false;
m_licenseRefused = false;
m_pending = Unknown;
m_transferActive = false;
m_pendingId = -1;
// m_transferActive belongs to DvkWavTransfer, which clears it when its own
// transfer ends; a transfer can outlive the connection by its timeout.
const QVector<DvkRecording> gone = std::exchange(m_recordings, {});
emit statusChanged(m_status, m_activeId);
for (const DvkRecording& r : gone) {
Expand Down
7 changes: 7 additions & 0 deletions src/models/DvkModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <QObject>
#include <QString>
#include <QVector>
#include <QLatin1String>
#include <QMap>

namespace AetherSDR {
Expand Down Expand Up @@ -41,6 +42,11 @@ class DvkModel : public QObject {
bool licenseRefused() const { return m_licenseRefused; }
// Latches a 50004001 refusal from any dvk path, including WAV transfers.
void noteRefusal(uint code);
// The radio later reported the feature licensed (its status outranks an
// earlier refusal, e.g. a subscription activated mid-session).
void clearRefusal();
// The radio's name for the DVK entitlement in `license feature` status.
static constexpr QLatin1String kLicenseFeature{"digital_voice_keyer"};
const QVector<DvkRecording>& recordings() const { return m_recordings; }

static QString defaultName(int id);
Expand Down Expand Up @@ -99,6 +105,7 @@ class DvkModel : public QObject {
bool m_enabled{false};
bool m_licenseRefused{false};
Status m_pending{Unknown};
int m_pendingId{-1};
bool m_transferActive{false};

void setPending(Status pending);
Expand Down
4 changes: 4 additions & 0 deletions src/models/RadioModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10952,6 +10952,10 @@ void RadioModel::onStatusReceived(const QString& object,
m_licenseFeatures.insert(name, next);
emit licenseFeaturesChanged();
}
// The radio's licensed status outranks an earlier 50004001 refusal.
if (next.enabled && name == DvkModel::kLicenseFeature) {
m_dvkModel.clearRefusal();
}
emit infoChanged();
return;
}
Expand Down
Loading
Loading