2026-01-09 12:48:09 -05:00
|
|
|
/**
|
|
|
|
|
* @file SpotClient.cpp
|
|
|
|
|
* @brief Implementation of SpotClient class
|
|
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
#include "SpotClient.h"
|
|
|
|
|
#include "JS8_Include/pimpl_impl.h"
|
|
|
|
|
#include "JS8_Main/Message.h"
|
2026-01-18 12:53:55 -06:00
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
#include <QHostInfo>
|
|
|
|
|
#include <QLoggingCategory>
|
|
|
|
|
#include <QNetworkDatagram>
|
|
|
|
|
#include <QQueue>
|
|
|
|
|
#include <QTimer>
|
|
|
|
|
#include <QUdpSocket>
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
#include "moc_SpotClient.cpp"
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
Q_DECLARE_LOGGING_CATEGORY(spotclient_js8)
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
// Constants
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
constexpr auto SEND_INTERVAL = std::chrono::seconds(60);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
// Local Utilities
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
template <typename T> bool changeValue(T &stored, T const &update) {
|
|
|
|
|
if (stored == update)
|
|
|
|
|
return false;
|
|
|
|
|
stored = update;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
// Private Implementation
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2026-01-09 12:48:09 -05:00
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* @brief Private implementation of the SpotClient class.
|
2026-01-18 12:53:55 -06:00
|
|
|
*
|
2026-01-09 12:48:09 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
class SpotClient::impl final : public QUdpSocket {
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
// Constructor
|
|
|
|
|
|
|
|
|
|
impl(QString const &name, quint16 const port, QString const &version,
|
|
|
|
|
SpotClient *self)
|
|
|
|
|
: QUdpSocket{self}, self_{self}, name_{name}, port_{port},
|
|
|
|
|
version_{version}, send_{new QTimer{this}} {}
|
|
|
|
|
|
|
|
|
|
// Intended to be called on the thread that starts us, which can be
|
|
|
|
|
// the main thread, if we're not going to be moved to a background
|
|
|
|
|
// thread.
|
|
|
|
|
|
|
|
|
|
void start() {
|
|
|
|
|
// Note that With UDP, error reporting is not guaranteed, which is not
|
|
|
|
|
// the same as a guarantee of no error reporting. Typically, a packet
|
|
|
|
|
// arriving on a port where there is no listener will trigger an ICMP
|
|
|
|
|
// Port Unreachable message back to the sender, and some implementations
|
|
|
|
|
// e.g., Windows, will report that to the application on the next
|
|
|
|
|
// attempt to transmit to the same destination.
|
|
|
|
|
|
|
|
|
|
connect(this, &QUdpSocket::errorOccurred, [this](SocketError const e) {
|
|
|
|
|
if (e != ConnectionRefusedError) {
|
|
|
|
|
Q_EMIT self_->error(errorString());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Start a host lookup for the name we were provided. If it succeeds,
|
|
|
|
|
// use the first address in the list. If it fails, then we've missed
|
|
|
|
|
// what was our one and only shot at this.
|
|
|
|
|
|
|
|
|
|
QHostInfo::lookupHost(name_, this, [this](QHostInfo const &info) {
|
|
|
|
|
if (auto const &list = info.addresses(); !list.isEmpty()) {
|
|
|
|
|
host_ = list.first();
|
|
|
|
|
|
|
|
|
|
qCDebug(spotclient_js8)
|
|
|
|
|
<< "SpotClient Host:" << name_ << host_.toString();
|
|
|
|
|
|
|
|
|
|
bind(host_.protocol() == IPv6Protocol ? QHostAddress::AnyIPv6
|
|
|
|
|
: QHostAddress::AnyIPv4);
|
|
|
|
|
|
|
|
|
|
send_->start(SEND_INTERVAL);
|
|
|
|
|
} else {
|
|
|
|
|
Q_EMIT self_->error(
|
|
|
|
|
QString{"Host lookup failed: %1"}.arg(info.errorString()));
|
|
|
|
|
valid_ = false;
|
|
|
|
|
queue_.clear();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Empty the queue every time our timer goes off.
|
|
|
|
|
|
|
|
|
|
connect(send_, &QTimer::timeout, this, [this]() {
|
|
|
|
|
while (!queue_.isEmpty()) {
|
|
|
|
|
writeDatagram(queue_.dequeue().toJson(), host_, port_);
|
|
|
|
|
}
|
|
|
|
|
sent_++;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sent as the "BY" value on command and spot sends; contains the call
|
|
|
|
|
// sign and grid of the local station, as set by setLocalStation().
|
|
|
|
|
|
|
|
|
|
QVariantMap by() {
|
|
|
|
|
return {
|
|
|
|
|
{"CALLSIGN", QVariant(call_)},
|
|
|
|
|
{"GRID", QVariant(grid_)},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Data members
|
|
|
|
|
|
|
|
|
|
SpotClient *self_;
|
|
|
|
|
QString name_;
|
|
|
|
|
quint16 port_;
|
|
|
|
|
QString version_;
|
|
|
|
|
QTimer *send_;
|
|
|
|
|
QHostAddress host_;
|
|
|
|
|
QQueue<Message> queue_;
|
|
|
|
|
bool valid_ = true;
|
|
|
|
|
bool once_ = false;
|
|
|
|
|
int sent_ = 0;
|
|
|
|
|
QString call_;
|
|
|
|
|
QString grid_;
|
|
|
|
|
QString info_;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
// Implementation
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
|
|
#include "SpotClient.moc"
|
|
|
|
|
|
|
|
|
|
// Constructor
|
|
|
|
|
|
2026-01-09 12:48:09 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Constructs a SpotClient object.
|
2026-01-18 12:53:55 -06:00
|
|
|
*
|
2026-01-09 12:48:09 -05:00
|
|
|
* @param name The hostname of the spot server.
|
|
|
|
|
* @param port The port number of the spot server.
|
|
|
|
|
* @param version The version string of the client.
|
|
|
|
|
* @param parent The parent QObject.
|
|
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
SpotClient::SpotClient(QString const &name, quint16 const port,
|
|
|
|
|
QString const &version, QObject *parent)
|
|
|
|
|
: QObject{parent}, m_{name, port, version, this} {}
|
|
|
|
|
|
|
|
|
|
void SpotClient::start() {
|
|
|
|
|
if (!m_->once_) {
|
|
|
|
|
m_->once_ = true;
|
|
|
|
|
m_->start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 12:48:09 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Sets the local station information.
|
2026-01-18 12:53:55 -06:00
|
|
|
*
|
2026-01-09 12:48:09 -05:00
|
|
|
* @param callsign The callsign of the local station.
|
|
|
|
|
* @param grid The grid locator of the local station.
|
|
|
|
|
* @param info Additional info about the local station.
|
|
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
void SpotClient::setLocalStation(QString const &callsign, QString const &grid,
|
|
|
|
|
QString const &info) {
|
|
|
|
|
qCDebug(spotclient_js8) << "SpotClient Set Local Station:" << callsign
|
|
|
|
|
<< "grid:" << grid << "info:" << info;
|
|
|
|
|
|
|
|
|
|
auto const changed = changeValue(m_->call_, callsign) +
|
|
|
|
|
changeValue(m_->grid_, grid) +
|
|
|
|
|
changeValue(m_->info_, info);
|
|
|
|
|
|
|
|
|
|
// Send local information to network on change, or once every 15 minutes.
|
|
|
|
|
|
|
|
|
|
if (m_->valid_ && (changed || m_->sent_ % 15 == 0)) {
|
|
|
|
|
m_->queue_.enqueue({"RX.LOCAL",
|
|
|
|
|
"",
|
|
|
|
|
{{"CALLSIGN", QVariant(callsign)},
|
|
|
|
|
{"GRID", QVariant(grid)},
|
|
|
|
|
{"INFO", QVariant(info)},
|
|
|
|
|
{"VERSION", QVariant(m_->version_)}}});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 12:48:09 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Enqueues a command to be sent to the spot server.
|
2026-01-18 12:53:55 -06:00
|
|
|
*
|
2026-01-09 12:48:09 -05:00
|
|
|
* @param cmd The command string.
|
|
|
|
|
* @param from The sender callsign.
|
|
|
|
|
* @param to The recipient callsign.
|
|
|
|
|
* @param relayPath The relay path.
|
|
|
|
|
* @param text The message text.
|
|
|
|
|
* @param grid The grid locator.
|
|
|
|
|
* @param extra Additional information.
|
|
|
|
|
* @param submode The submode identifier.
|
|
|
|
|
* @param dial The dial frequency.
|
|
|
|
|
* @param offset The frequency offset.
|
|
|
|
|
* @param snr The signal-to-noise ratio.
|
|
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
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) {
|
|
|
|
|
if (m_->valid_) {
|
|
|
|
|
m_->queue_.enqueue({"RX.DIRECTED",
|
|
|
|
|
"",
|
|
|
|
|
{{"BY", QVariant(m_->by())},
|
|
|
|
|
{"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)}}});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 12:48:09 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Enqueues a spot to be sent to the spot server.
|
2026-01-18 12:53:55 -06:00
|
|
|
*
|
2026-01-09 12:48:09 -05:00
|
|
|
* @param callsign The callsign of the spotted station.
|
|
|
|
|
* @param grid The grid locator of the spotted station.
|
|
|
|
|
* @param submode The submode identifier.
|
|
|
|
|
* @param dial The dial frequency.
|
|
|
|
|
* @param offset The frequency offset.
|
|
|
|
|
* @param snr The signal-to-noise ratio.
|
|
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
void SpotClient::enqueueSpot(QString const &callsign, QString const &grid,
|
|
|
|
|
int const submode, int const dial,
|
|
|
|
|
int const offset, int const snr) {
|
|
|
|
|
if (m_->valid_) {
|
|
|
|
|
m_->queue_.enqueue({"RX.SPOT",
|
|
|
|
|
"",
|
|
|
|
|
{{"BY", QVariant(m_->by())},
|
|
|
|
|
{"CALLSIGN", QVariant(callsign)},
|
|
|
|
|
{"GRID", QVariant(grid)},
|
|
|
|
|
{"FREQ", QVariant(dial + offset)},
|
|
|
|
|
{"DIAL", QVariant(dial)},
|
|
|
|
|
{"OFFSET", QVariant(offset)},
|
|
|
|
|
{"SNR", QVariant(snr)},
|
|
|
|
|
{"SPEED", QVariant(submode)}}});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
|
|
Q_LOGGING_CATEGORY(spotclient_js8, "spotclient.js8", QtWarningMsg)
|