mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-09-08 22:36:40 -04:00
522 lines
17 KiB
C++
522 lines
17 KiB
C++
#include "PSKReporter.hpp"
|
|
|
|
// Interface for posting spots to PSK Reporter web site
|
|
// Implemented by Edson Pereira PY2SDR
|
|
// Updated by Bill Somerville, G4WJS
|
|
//
|
|
// Reports will be sent in batch mode every 5 minutes.
|
|
|
|
#include <cmath>
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include <QDateTime>
|
|
#include <QSharedPointer>
|
|
#include <QUdpSocket>
|
|
#include <QTcpSocket>
|
|
#include <QHostInfo>
|
|
#include <QQueue>
|
|
#include <QByteArray>
|
|
#include <QDataStream>
|
|
#include <QTimer>
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
#include <QRandomGenerator>
|
|
#endif
|
|
|
|
#include "Configuration.hpp"
|
|
#include "pimpl_impl.hpp"
|
|
|
|
|
|
#include "moc_PSKReporter.cpp"
|
|
|
|
namespace
|
|
{
|
|
QLatin1String HOST {"report.pskreporter.info"};
|
|
// QLatin1String HOST {"127.0.0.1"};
|
|
quint16 SERVICE_PORT {4739};
|
|
// quint16 SERVICE_PORT {14739};
|
|
int MIN_SEND_INTERVAL {15}; // in seconds
|
|
int FLUSH_INTERVAL {4 * 5}; // in send intervals
|
|
bool ALIGNMENT_PADDING {true};
|
|
int MIN_PAYLOAD_LENGTH {508};
|
|
int MAX_PAYLOAD_LENGTH {1400};
|
|
}
|
|
|
|
class PSKReporter::impl final
|
|
: public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
impl (PSKReporter * self, Configuration const * config, QString const& program_info)
|
|
: self_ {self}
|
|
, config_ {config}
|
|
, sequence_number_ {0u}
|
|
, send_descriptors_ {0}
|
|
, send_receiver_data_ {0}
|
|
, flush_counter_ {0u}
|
|
, prog_id_ {program_info}
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
|
|
observation_id_ = qrand();
|
|
#else
|
|
observation_id_ = QRandomGenerator::global ()->generate ();
|
|
#endif
|
|
|
|
// This timer sets the interval to check for spots to send.
|
|
connect (&report_timer_, &QTimer::timeout, [this] () {send_report ();});
|
|
|
|
// This timer repeats the sending of IPFIX templates and receiver
|
|
// information if we are using UDP, in case server has been
|
|
// restarted ans lost cached information.
|
|
connect (&descriptor_timer_, &QTimer::timeout, [this] () {
|
|
if (socket_
|
|
&& QAbstractSocket::UdpSocket == socket_->socketType ())
|
|
{
|
|
// send templates again
|
|
send_descriptors_ = 3; // three times
|
|
// send receiver data set again
|
|
send_receiver_data_ = 3; // three times
|
|
}
|
|
});
|
|
}
|
|
|
|
void check_connection ()
|
|
{
|
|
if (!socket_
|
|
|| QAbstractSocket::UnconnectedState == socket_->state ()
|
|
|| (socket_->socketType () != config_->psk_reporter_tcpip () ? QAbstractSocket::TcpSocket : QAbstractSocket::UdpSocket))
|
|
{
|
|
// we need to create the appropriate socket
|
|
if (socket_
|
|
&& QAbstractSocket::UnconnectedState != socket_->state ()
|
|
&& QAbstractSocket::ClosingState != socket_->state ())
|
|
{
|
|
// handle re-opening asynchronously
|
|
auto connection = QSharedPointer<QMetaObject::Connection>::create ();
|
|
*connection = connect (socket_.data (), &QAbstractSocket::disconnected, [this, connection] () {
|
|
disconnect (*connection);
|
|
check_connection ();
|
|
});
|
|
// close gracefully
|
|
send_report (true);
|
|
socket_->close ();
|
|
}
|
|
else
|
|
{
|
|
reconnect ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void handle_socket_error (QAbstractSocket::SocketError e)
|
|
{
|
|
switch (e)
|
|
{
|
|
case QAbstractSocket::RemoteHostClosedError:
|
|
socket_->disconnectFromHost ();
|
|
break;
|
|
|
|
case QAbstractSocket::TemporaryError:
|
|
break;
|
|
|
|
default:
|
|
spots_.clear ();
|
|
Q_EMIT self_->errorOccurred (socket_->errorString ());
|
|
break;
|
|
}
|
|
}
|
|
|
|
void reconnect ()
|
|
{
|
|
// Using deleteLater for the deleter as we may eventually
|
|
// be called from the disconnected handler above.
|
|
if (config_->psk_reporter_tcpip ())
|
|
{
|
|
socket_.reset (new QTcpSocket, &QObject::deleteLater);
|
|
send_descriptors_ = 1;
|
|
send_receiver_data_ = 1;
|
|
}
|
|
else
|
|
{
|
|
socket_.reset (new QUdpSocket, &QObject::deleteLater);
|
|
send_descriptors_ = 3;
|
|
send_receiver_data_ = 3;
|
|
}
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
connect (socket_.get (), &QAbstractSocket::errorOccurred, this, &PSKReporter::impl::handle_socket_error);
|
|
#else
|
|
connect (socket_.data (), QOverload<QAbstractSocket::SocketError>::of (&QAbstractSocket::error), this, &PSKReporter::impl::handle_socket_error);
|
|
#endif
|
|
|
|
// use this for pseudo connection with UDP, allows us to use
|
|
// QIODevice::write() instead of QUDPSocket::writeDatagram()
|
|
socket_->connectToHost (HOST, SERVICE_PORT, QAbstractSocket::WriteOnly);
|
|
|
|
if (!report_timer_.isActive ())
|
|
{
|
|
report_timer_.start (MIN_SEND_INTERVAL * 1000);
|
|
}
|
|
if (!descriptor_timer_.isActive ())
|
|
{
|
|
descriptor_timer_.start (1 * 60 * 60 * 1000); // hourly
|
|
}
|
|
}
|
|
|
|
void stop ()
|
|
{
|
|
if (socket_)
|
|
{
|
|
socket_->disconnectFromHost ();
|
|
}
|
|
descriptor_timer_.stop ();
|
|
report_timer_.stop ();
|
|
}
|
|
|
|
void send_report (bool send_residue = false);
|
|
void build_preamble (QDataStream&);
|
|
|
|
bool flushing ()
|
|
{
|
|
return FLUSH_INTERVAL && !(++flush_counter_ % FLUSH_INTERVAL);
|
|
}
|
|
|
|
PSKReporter * self_;
|
|
Configuration const * config_;
|
|
QSharedPointer<QAbstractSocket> socket_;
|
|
int dns_lookup_id_;
|
|
QByteArray payload_;
|
|
quint32 sequence_number_;
|
|
int send_descriptors_;
|
|
|
|
// Currently PSK Reporter requires that a receiver data set is sent
|
|
// in every data flow. This memeber variable can be used to only
|
|
// send that information at session start (3 times for UDP), when it
|
|
// changes (3 times for UDP), or once per hour (3 times) if using
|
|
// UDP. Uncomment the relevant code to enable that fuctionality.
|
|
int send_receiver_data_;
|
|
|
|
unsigned flush_counter_;
|
|
quint32 observation_id_;
|
|
QString rx_call_;
|
|
QString rx_grid_;
|
|
QString rx_ant_;
|
|
QString prog_id_;
|
|
QByteArray tx_data_;
|
|
QByteArray tx_residue_;
|
|
struct Spot
|
|
{
|
|
bool operator == (Spot const& rhs)
|
|
{
|
|
return
|
|
call_ == rhs.call_
|
|
&& grid_ == rhs.grid_
|
|
&& mode_ == rhs.mode_
|
|
&& std::abs (Radio::FrequencyDelta (freq_ - rhs.freq_)) < 50;
|
|
}
|
|
|
|
QString call_;
|
|
QString grid_;
|
|
int snr_;
|
|
Radio::Frequency freq_;
|
|
QString mode_;
|
|
QDateTime time_;
|
|
};
|
|
QQueue<Spot> spots_;
|
|
QTimer report_timer_;
|
|
QTimer descriptor_timer_;
|
|
};
|
|
|
|
#include "PSKReporter.moc"
|
|
|
|
namespace
|
|
{
|
|
void writeUtfString (QDataStream& out, QString const& s)
|
|
{
|
|
auto const& utf = s.toUtf8 ().left (254);
|
|
out << quint8 (utf.size ());
|
|
out.writeRawData (utf, utf.size ());
|
|
}
|
|
|
|
int num_pad_bytes (int len)
|
|
{
|
|
return ALIGNMENT_PADDING ? (4 - len % 4) % 4 : 0;
|
|
}
|
|
|
|
void set_length (QDataStream& out, QByteArray& b)
|
|
{
|
|
// pad with nulls modulo 4
|
|
auto pad_len = num_pad_bytes (b.size ());
|
|
out.writeRawData (QByteArray {pad_len, '\0'}.constData (), pad_len);
|
|
auto pos = out.device ()->pos ();
|
|
out.device ()->seek (sizeof (quint16));
|
|
// insert length
|
|
out << static_cast<quint16> (b.size ());
|
|
out.device ()->seek (pos);
|
|
}
|
|
}
|
|
|
|
void PSKReporter::impl::build_preamble (QDataStream& message)
|
|
{
|
|
// Message Header
|
|
message
|
|
<< quint16 (10u) // Version Number
|
|
<< quint16 (0u) // Length (place-holder filled in later)
|
|
<< quint32 (0u) // Export Time (place-holder filled in later)
|
|
<< ++sequence_number_ // Sequence Number
|
|
<< observation_id_; // Observation Domain ID
|
|
|
|
if (send_descriptors_)
|
|
{
|
|
--send_descriptors_;
|
|
{
|
|
// Sender Information descriptor
|
|
QByteArray descriptor;
|
|
QDataStream out {&descriptor, QIODevice::WriteOnly};
|
|
out
|
|
<< quint16 (2u) // Template Set ID
|
|
<< quint16 (0u) // Length (place-holder)
|
|
<< quint16 (0x50e3) // Link ID
|
|
<< quint16 (7u) // Field Count
|
|
<< quint16 (0x8000 + 1u) // Option 1 Information Element ID (senderCallsign)
|
|
<< quint16 (0xffff) // Option 1 Field Length (variable)
|
|
<< quint32 (30351u) // Option 1 Enterprise Number
|
|
<< quint16 (0x8000 + 5u) // Option 2 Information Element ID (frequency)
|
|
<< quint16 (4u) // Option 2 Field Length
|
|
<< quint32 (30351u) // Option 2 Enterprise Number
|
|
<< quint16 (0x8000 + 6u) // Option 3 Information Element ID (sNR)
|
|
<< quint16 (1u) // Option 3 Field Length
|
|
<< quint32 (30351u) // Option 3 Enterprise Number
|
|
<< quint16 (0x8000 + 10u) // Option 4 Information Element ID (mode)
|
|
<< quint16 (0xffff) // Option 4 Field Length (variable)
|
|
<< quint32 (30351u) // Option 4 Enterprise Number
|
|
<< quint16 (0x8000 + 3u) // Option 5 Information Element ID (senderLocator)
|
|
<< quint16 (0xffff) // Option 5 Field Length (variable)
|
|
<< quint32 (30351u) // Option 5 Enterprise Number
|
|
<< quint16 (0x8000 + 11u) // Option 6 Information Element ID (informationSource)
|
|
<< quint16 (1u) // Option 6 Field Length
|
|
<< quint32 (30351u) // Option 6 Enterprise Number
|
|
<< quint16 (150u) // Option 7 Information Element ID (dateTimeSeconds)
|
|
<< quint16 (4u); // Option 7 Field Length
|
|
// insert Length and move to payload
|
|
set_length (out, descriptor);
|
|
message.writeRawData (descriptor.constData (), descriptor.size ());
|
|
}
|
|
{
|
|
// Receiver Information descriptor
|
|
QByteArray descriptor;
|
|
QDataStream out {&descriptor, QIODevice::WriteOnly};
|
|
out
|
|
<< quint16 (3u) // Options Template Set ID
|
|
<< quint16 (0u) // Length (place-holder)
|
|
<< quint16 (0x50e2) // Link ID
|
|
<< quint16 (4u) // Field Count
|
|
<< quint16 (0u) // Scope Field Count
|
|
<< quint16 (0x8000 + 2u) // Option 1 Information Element ID (receiverCallsign)
|
|
<< quint16 (0xffff) // Option 1 Field Length (variable)
|
|
<< quint32 (30351u) // Option 1 Enterprise Number
|
|
<< quint16 (0x8000 + 4u) // Option 2 Information Element ID (receiverLocator)
|
|
<< quint16 (0xffff) // Option 2 Field Length (variable)
|
|
<< quint32 (30351u) // Option 2 Enterprise Number
|
|
<< quint16 (0x8000 + 8u) // Option 3 Information Element ID (decodingSoftware)
|
|
<< quint16 (0xffff) // Option 3 Field Length (variable)
|
|
<< quint32 (30351u) // Option 3 Enterprise Number
|
|
<< quint16 (0x8000 + 9u) // Option 4 Information Element ID (antennaInformation)
|
|
<< quint16 (0xffff) // Option 4 Field Length (variable)
|
|
<< quint32 (30351u); // Option 4 Enterprise Number
|
|
// insert Length
|
|
set_length (out, descriptor);
|
|
message.writeRawData (descriptor.constData (), descriptor.size ());
|
|
}
|
|
}
|
|
|
|
// if (send_receiver_data_)
|
|
{
|
|
// --send_receiver_data_;
|
|
|
|
// Receiver information
|
|
QByteArray data;
|
|
QDataStream out {&data, QIODevice::WriteOnly};
|
|
|
|
// Set Header
|
|
out
|
|
<< quint16 (0x50e2) // Template ID
|
|
<< quint16 (0u); // Length (place-holder)
|
|
|
|
// Set data
|
|
writeUtfString (out, rx_call_);
|
|
writeUtfString (out, rx_grid_);
|
|
writeUtfString (out, prog_id_);
|
|
writeUtfString (out, rx_ant_);
|
|
|
|
// insert Length and move to payload
|
|
set_length (out, data);
|
|
message.writeRawData (data.constData (), data.size ());
|
|
}
|
|
}
|
|
|
|
void PSKReporter::impl::send_report (bool send_residue)
|
|
{
|
|
if (QAbstractSocket::ConnectedState != socket_->state ()) return;
|
|
|
|
QDataStream message {&payload_, QIODevice::WriteOnly | QIODevice::Append};
|
|
QDataStream tx_out {&tx_data_, QIODevice::WriteOnly | QIODevice::Append};
|
|
|
|
if (!payload_.size ())
|
|
{
|
|
// Build header, optional descriptors, and receiver information
|
|
build_preamble (message);
|
|
}
|
|
|
|
auto flush = flushing () || send_residue;
|
|
while (spots_.size () || flush)
|
|
{
|
|
if (!payload_.size ())
|
|
{
|
|
// Build header, optional descriptors, and receiver information
|
|
build_preamble (message);
|
|
}
|
|
|
|
if (!tx_data_.size () && (spots_.size () || tx_residue_.size ()))
|
|
{
|
|
// Set Header
|
|
tx_out
|
|
<< quint16 (0x50e3) // Template ID
|
|
<< quint16 (0u); // Length (place-holder)
|
|
}
|
|
|
|
// insert any residue
|
|
if (tx_residue_.size ())
|
|
{
|
|
tx_out.writeRawData (tx_residue_.constData (), tx_residue_.size ());
|
|
tx_residue_.clear ();
|
|
}
|
|
|
|
while (spots_.size () || flush)
|
|
{
|
|
auto tx_data_size = tx_data_.size ();
|
|
if (spots_.size ())
|
|
{
|
|
auto const& spot = spots_.dequeue ();
|
|
|
|
// Sender information
|
|
writeUtfString (tx_out, spot.call_);
|
|
tx_out
|
|
<< static_cast<quint32> (spot.freq_)
|
|
<< static_cast<qint8> (spot.snr_);
|
|
writeUtfString (tx_out, spot.mode_);
|
|
writeUtfString (tx_out, spot.grid_);
|
|
tx_out
|
|
<< quint8 (1u) // REPORTER_SOURCE_AUTOMATIC
|
|
<< static_cast<quint32> (
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
|
|
spot.time_.toSecsSinceEpoch ()
|
|
#else
|
|
spot.time_.toMSecsSinceEpoch () / 1000
|
|
#endif
|
|
);
|
|
}
|
|
|
|
auto len = payload_.size () + tx_data_.size ();
|
|
len += num_pad_bytes (tx_data_.size ());
|
|
len += num_pad_bytes (len);
|
|
if (len > MAX_PAYLOAD_LENGTH // our upper datagram size limit
|
|
|| (!spots_.size () && len > MIN_PAYLOAD_LENGTH) // spots drained and above lower datagram size limit
|
|
|| (flush && !spots_.size ())) // send what we have, possibly no spots
|
|
{
|
|
if (tx_data_.size ())
|
|
{
|
|
if (len <= MAX_PAYLOAD_LENGTH)
|
|
{
|
|
tx_data_size = tx_data_.size ();
|
|
}
|
|
QByteArray tx {tx_data_.left (tx_data_size)};
|
|
QDataStream out {&tx, QIODevice::WriteOnly | QIODevice::Append};
|
|
// insert Length
|
|
set_length (out, tx);
|
|
message.writeRawData (tx.constData (), tx.size ());
|
|
}
|
|
|
|
// insert Length and Export Time
|
|
set_length (message, payload_);
|
|
message.device ()->seek (2 * sizeof (quint16));
|
|
message << static_cast<quint32> (
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
|
|
QDateTime::currentDateTime ().toSecsSinceEpoch ()
|
|
#else
|
|
QDateTime::currentDateTime ().toMSecsSinceEpoch () / 1000
|
|
#endif
|
|
);
|
|
|
|
// Send data to PSK Reporter site
|
|
socket_->write (payload_); // TODO: handle errors
|
|
flush = false; // break loop
|
|
message.device ()->seek (0u);
|
|
payload_.clear (); // Fresh message
|
|
// Save unsent spots
|
|
tx_residue_ = tx_data_.right (tx_data_.size () - tx_data_size);
|
|
tx_out.device ()->seek (0u);
|
|
tx_data_.clear ();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PSKReporter::PSKReporter (Configuration const * config, QString const& program_info)
|
|
: m_ {this, config, program_info}
|
|
{
|
|
}
|
|
|
|
PSKReporter::~PSKReporter ()
|
|
{
|
|
// m_->send_report (true); // send any pending spots
|
|
}
|
|
|
|
void PSKReporter::reconnect ()
|
|
{
|
|
m_->reconnect ();
|
|
}
|
|
|
|
void PSKReporter::setLocalStation (QString const& call, QString const& gridSquare, QString const& antenna)
|
|
{
|
|
m_->check_connection ();
|
|
if (call != m_->rx_call_ || gridSquare != m_->rx_grid_ || antenna != m_->rx_ant_)
|
|
{
|
|
m_->send_receiver_data_ = m_->socket_
|
|
&& QAbstractSocket::UdpSocket == m_->socket_->socketType () ? 3 : 1;
|
|
m_->rx_call_ = call;
|
|
m_->rx_grid_ = gridSquare;
|
|
m_->rx_ant_ = antenna;
|
|
}
|
|
}
|
|
|
|
bool PSKReporter::addRemoteStation (QString const& call, QString const& grid, Radio::Frequency freq
|
|
, QString const& mode, int snr)
|
|
{
|
|
m_->check_connection ();
|
|
if (m_->socket_ && m_->socket_->isValid ())
|
|
{
|
|
if (QAbstractSocket::UnconnectedState == m_->socket_->state ())
|
|
{
|
|
reconnect ();
|
|
}
|
|
m_->spots_.enqueue ({call, grid, snr, freq, mode, QDateTime::currentDateTimeUtc ()});
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void PSKReporter::sendReport (bool last)
|
|
{
|
|
m_->check_connection ();
|
|
if (m_->socket_ && QAbstractSocket::ConnectedState == m_->socket_->state ())
|
|
{
|
|
m_->send_report (true);
|
|
}
|
|
if (last)
|
|
{
|
|
m_->stop ();
|
|
}
|
|
}
|