From 080494f602332990ad7c710d7403aaac5161daeb Mon Sep 17 00:00:00 2001 From: Allan Bazinet Date: Tue, 5 Nov 2024 06:36:43 -0800 Subject: [PATCH] Decouple spot client from message client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decouple under-the-covers usage of the message client’s socket by the spot client, in order to facilitate future multicast operations by the message client. --- MessageClient.cpp | 16 --- MessageClient.hpp | 9 +- SpotClient.cpp | 358 +++++++++++++++++++++++++++++++--------------- SpotClient.h | 61 ++++---- mainwindow.cpp | 4 +- 5 files changed, 280 insertions(+), 168 deletions(-) diff --git a/MessageClient.cpp b/MessageClient.cpp index a234c8d6..b6325fae 100644 --- a/MessageClient.cpp +++ b/MessageClient.cpp @@ -280,20 +280,4 @@ MessageClient::send(Message const & message) } } -// Send a raw datagram to the host and port specified, if they've got sane -// values. We're just leveraging the fact that we've got a socket to play -// with; this is, in short, just a convenient way to obtain general UDP -// messaging, unrelated to everything above. - -void -MessageClient::send_raw_datagram(QByteArray const & datagram, - QHostAddress const & host, - quint16 const port) -{ - if (port && !host.isNull()) - { - m_->writeDatagram(datagram, host, port); - } -} - /******************************************************************************/ diff --git a/MessageClient.hpp b/MessageClient.hpp index 8f877711..afd4d41f 100644 --- a/MessageClient.hpp +++ b/MessageClient.hpp @@ -26,7 +26,9 @@ public: // instantiate and initiate a host lookup on the server; // messages will be queued until a server host lookup is complete - MessageClient (QString const& server, quint16 server_port, QObject * parent = nullptr); + MessageClient (QString const & server_name, + quint16 server_port, + QObject * parent = nullptr); // query server details QHostAddress server_host() const; @@ -42,11 +44,6 @@ public: // this slot is used to send an arbitrary message Q_SLOT void send (Message const &message); - // this slot may be used to send arbitrary UDP datagrams to and - // destination allowing the underlying socket to be used for general - // UDP messaging if desired - Q_SLOT void send_raw_datagram (QByteArray const&, QHostAddress const& dest_address, quint16 dest_port); - // this signal is emitted when a message is received Q_SIGNAL void message (Message const &message); diff --git a/SpotClient.cpp b/SpotClient.cpp index 830e9cc8..dd4c8e4b 100644 --- a/SpotClient.cpp +++ b/SpotClient.cpp @@ -1,134 +1,256 @@ #include "SpotClient.h" +#include +#include +#include +#include +#include #include "Message.hpp" - +#include "pimpl_impl.hpp" #include "moc_SpotClient.cpp" -SpotClient::SpotClient(MessageClient *client, QObject *parent): - QObject(parent), - m_client { client } -{ - prepare(); +/******************************************************************************/ +// Constants +/******************************************************************************/ - connect(&m_timer, &QTimer::timeout, this, &SpotClient::processSpots); - m_timer.setInterval(60 * 1000); - m_timer.setSingleShot(false); - m_timer.start(); +namespace +{ + constexpr auto SEND_INTERVAL = std::chrono::seconds(60); +} + +/******************************************************************************/ +// Private Implementation +/******************************************************************************/ + +class SpotClient::impl final : public QUdpSocket +{ + Q_OBJECT + +public: + + // Constructor + + explicit impl(quint16 const port, + SpotClient * self) + : self_ {self} + , port_ {port} + , send_ {new QTimer {this}} + { + connect(send_, &QTimer::timeout, this, [this]() + { + if (!host_.isNull()) + { + while (!queue_.isEmpty()) + { + writeDatagram(queue_.dequeue().toJson(), host_, port_); + } + } + + sent_++; + }); + + send_->start(SEND_INTERVAL); + } + + // Destructor + + ~impl() + { + abort_host_lookup(); + } + + // If we've got a host lookup in flight, but not yet completed, abort it + // and indicate that we no longer have one in flight. + + void + abort_host_lookup() + { + if (hostLookupId_ != -1) + { + QHostInfo::abortHostLookup(hostLookupId_); + hostLookupId_ = -1; + } + } + + // Abort any current host lookup that might be in flight, and start a new + // host lookup for the provided server name, noting that we have a lookup + // in flight. + // + // If, at the time of host lookup completion, we find ourselves to be the + // active host lookup, and we were able to look up addresses, then use the + // first address associated with the server as our host address. + + void + queue_host_lookup(QString const & name) + { + abort_host_lookup(); + + hostLookupId_ = QHostInfo::lookupHost(name, + this, + [this](QHostInfo const & info) + { + // This functor is always called in the context of the thread that + // made the call to lookupHost(), so we're safe to modify anything + // that we were safe to modify outside. + + if (info.lookupId() == hostLookupId_) + { + hostLookupId_ = -1; + + if (auto const & list = info.addresses(); + !list.isEmpty()) + { + host_ = list.first(); + + qDebug() << "SpotClient Host:" << host_.toString();; + + if (state() != UnconnectedState) close(); + + bind(host_.protocol() == IPv6Protocol ? QHostAddress::AnyIPv6 + : QHostAddress::AnyIPv4); + } + else + { + Q_EMIT self_->error (QString {"Host lookup failed: %1"}.arg(info.errorString())); + } + } + }); + } + + // Data members + + SpotClient * self_; + quint16 port_; + QTimer * send_; + QHostAddress host_; + int hostLookupId_ = -1; + int sent_ = 0; + QString call_; + QString grid_; + QString info_; + QString version_; + QQueue queue_; +}; + + +/******************************************************************************/ +// Implementation +/******************************************************************************/ + +#include "SpotClient.moc" + +// Constructor +// +// On Windows, Qt will seemingly emit spurious 'connection refused' errors +// for UDP sockets; ignore them if they appear. + +SpotClient::SpotClient(QString const & name, + quint16 const port, + QObject * parent) + : QObject {parent} + , m_ {port, this} +{ + connect(&*m_, &impl::errorOccurred, [this](impl::SocketError e) + { +#if defined (Q_OS_WIN) + if (e != impl::NetworkError && + e != impl::ConnectionRefusedError) +#else + Q_UNUSED (e); +#endif + { + Q_EMIT error (m_->errorString()); + } + }); + + m_->queue_host_lookup(name); } void -SpotClient::prepare() +SpotClient::setLocalStation(QString const & callsign, + QString const & grid, + QString const & info, + QString const & version) { - QHostInfo::lookupHost("spot.js8call.com", - this, - [this](QHostInfo const & info) - { - if (info.addresses().isEmpty()) - { - qDebug() << "SpotClient Error:" << info.errorString(); - return; - } + auto const valueChanged = [](QString & oldValue, + QString const & newValue) + { + if (oldValue == newValue) return false; + oldValue = newValue; + return true; + }; - m_address = info.addresses().at(0); - qDebug() << "SpotClient Resolve:" << m_address.toString(); - }); + bool const changed = valueChanged(m_->call_, callsign) + | valueChanged(m_->grid_, grid) + | valueChanged(m_->info_, info) + | valueChanged(m_->version_, version); + + // Send local information to network on change, or once every 15 minutes. + + if (changed || m_->sent_ % 15 == 0) + { + m_->queue_.enqueue({"RX.LOCAL", "", { + {"CALLSIGN", QVariant(callsign)}, + {"GRID", QVariant(grid) }, + {"INFO", QVariant(info) }, + {"VERSION", QVariant(version) } + }}); + } } -void SpotClient::setLocalStation(QString callsign, QString grid, QString info, QString version){ - bool changed = false; - - if(m_call != callsign){ - m_call = callsign; - changed = true; - } - - if(m_grid != grid){ - m_grid = grid; - changed = true; - } - - if(m_info != info){ - m_info = info; - changed = true; - } - - if(m_version != version){ - m_version = version; - changed = true; - } - - // send local information to network on change, or once every 15 minutes - if(changed || m_seq % 15 == 0){ - enqueueLocalSpot(callsign, grid, info, version); - } +void +SpotClient::enqueueCmd(QString const & cmd, + QString const & from, + QString const & to, + QString const & relayPath, + QString const & text, + QString const & grid, + QString const & extra, + int const submode, + int const dial, + int const offset, + int const snr) +{ + m_->queue_.enqueue({"RX.DIRECTED", "", { + {"BY", QVariant(QVariantMap { + {"CALLSIGN", QVariant(m_->call_)}, + {"GRID", QVariant(m_->grid_)}, + })}, + {"CMD", QVariant(cmd) }, + {"FROM", QVariant(from) }, + {"TO", QVariant(to) }, + {"PATH", QVariant(relayPath) }, + {"TEXT", QVariant(text) }, + {"GRID", QVariant(grid) }, + {"EXTRA", QVariant(extra) }, + {"FREQ", QVariant(dial + offset)}, + {"DIAL", QVariant(dial) }, + {"OFFSET", QVariant(offset) }, + {"SNR", QVariant(snr) }, + {"SPEED", QVariant(submode) } + }}); } -void SpotClient::enqueueLocalSpot(QString callsign, QString grid, QString info, QString version){ - auto m = Message("RX.LOCAL", "", { - {"CALLSIGN", QVariant(callsign)}, - {"GRID", QVariant(grid)}, - {"INFO", QVariant(info)}, - {"VERSION", QVariant(version)}, - }); - - m_queue.enqueue(m.toJson()); +void +SpotClient::enqueueSpot(QString const & callsign, + QString const & grid, + int const submode, + int const dial, + int const offset, + int const snr) +{ + m_->queue_.enqueue({"RX.SPOT", "", { + {"BY", QVariant(QVariantMap { + {"CALLSIGN", QVariant(m_->call_)}, + {"GRID", QVariant(m_->grid_)}, + })}, + {"CALLSIGN", QVariant(callsign) }, + {"GRID", QVariant(grid) }, + {"FREQ", QVariant(dial + offset)}, + {"DIAL", QVariant(dial) }, + {"OFFSET", QVariant(offset) }, + {"SNR", QVariant(snr) }, + {"SPEED", QVariant(submode) } + }}); } -void SpotClient::enqueueSpot(QString callsign, QString grid, int submode, int dial, int offset, int snr){ - auto m = Message("RX.SPOT", "", { - {"BY", QVariant(QVariantMap{ - {"CALLSIGN", QVariant(m_call)}, - {"GRID", QVariant(m_grid)}, - })}, - {"CALLSIGN", QVariant(callsign)}, - {"GRID", QVariant(grid)}, - {"FREQ", QVariant(dial+offset)}, - {"DIAL", QVariant(dial)}, - {"OFFSET", QVariant(offset)}, - {"SNR", QVariant(snr)}, - {"SPEED", QVariant(submode)}, - }); - - m_queue.enqueue(m.toJson()); -} - -void SpotClient::enqueueCmd(QString cmd, QString from, QString to, QString relayPath, QString text, QString grid, QString extra, int submode, int dial, int offset, int snr){ - auto m = Message("RX.DIRECTED", "", { - {"BY", QVariant(QVariantMap{ - {"CALLSIGN", QVariant(m_call)}, - {"GRID", QVariant(m_grid)}, - })}, - {"CMD", QVariant(cmd)}, - {"FROM", QVariant(from)}, - {"TO", QVariant(to)}, - {"PATH", QVariant(relayPath)}, - {"TEXT", QVariant(text)}, - {"GRID", QVariant(grid)}, - {"EXTRA", QVariant(extra)}, - {"FREQ", QVariant(dial+offset)}, - {"DIAL", QVariant(dial)}, - {"OFFSET", QVariant(offset)}, - {"SNR", QVariant(snr)}, - {"SPEED", QVariant(submode)}, - }); - - m_queue.enqueue(m.toJson()); -} - -void SpotClient::processSpots(){ - if(m_address.isNull()){ - prepare(); - return; - } - - while(!m_queue.isEmpty()){ - sendRawSpot(m_queue.dequeue()); - } - - m_seq++; -} - -void SpotClient::sendRawSpot(QByteArray payload){ - if(!m_address.isNull()){ - m_client->send_raw_datagram(payload, m_address, 50000); - } -} +/******************************************************************************/ diff --git a/SpotClient.h b/SpotClient.h index e34a88fe..2f7bbbbb 100644 --- a/SpotClient.h +++ b/SpotClient.h @@ -1,40 +1,49 @@ #ifndef JS8SPOTCLIENT_H #define JS8SPOTCLIENT_H -#include "MessageClient.hpp" - #include -#include -#include -#include +#include +#include "pimpl_h.hpp" -class SpotClient : public QObject +class SpotClient final : public QObject { - Q_OBJECT + Q_OBJECT + public: - SpotClient(MessageClient *client, QObject *parent = nullptr); - void prepare(); - void setLocalStation(QString callsign, QString grid, QString info, QString version); - void enqueueLocalSpot(QString callsign, QString grid, QString info, QString version); - void enqueueCmd(QString cmd, QString from, QString to, QString relayPath, QString text, QString grid, QString extra, int submode, int dial, int offset, int snr); - void enqueueSpot(QString callsign, QString grid, int submode, int dial, int offset, int snr); - void sendRawSpot(QByteArray payload); + SpotClient(QString const & host, + quint16 port, + QObject * parent = nullptr); -public slots: - void processSpots(); + void setLocalStation(QString const & callsign, + QString const & grid, + QString const & info, + QString const & version); + + void enqueueCmd(QString const & cmd, + QString const & from, + QString const & to, + QString const & relayPath, + QString const & text, + QString const & grid, + QString const & extra, + int submode, + int dial, + int offset, + int snr); + + void enqueueSpot(QString const & callsign, + QString const & grid, + int submode, + int dial, + int offset, + int snr); + + Q_SIGNAL void error (QString const &) const; private: - int m_seq; - QString m_call; - QString m_grid; - QString m_info; - QString m_version; - - QHostAddress m_address; - MessageClient *m_client; - QTimer m_timer; - QQueue m_queue; + class impl; + pimpl m_; }; #endif // JS8SPOTCLIENT_H diff --git a/mainwindow.cpp b/mainwindow.cpp index e9055f59..46bbb3d2 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -415,8 +415,8 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, m_messageServer {new MessageServer()}, m_n3fjpClient {new TCPClient{this}}, m_psk_Reporter {&m_config, QString {"JS8Call v" + version() }.simplified ()}, // UR - m_spotClient {new SpotClient{m_messageClient, this}}, - m_aprsClient {new APRSISClient{"rotate.aprs2.net", 14580}}, + m_spotClient {new SpotClient {"spot.js8call.com", 50000, this}}, + m_aprsClient {new APRSISClient {"rotate.aprs2.net", 14580, this}}, m_manual {&m_network_manager} { ui->setupUi(this);