diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a03bc409..559f3ef75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -303,7 +303,7 @@ set (jt9_FSRCS set (wsjtx_CXXSRCS logbook/logbook.cpp - Network/psk_reporter.cpp + Network/PSKReporter.cpp Modulator/Modulator.cpp Detector/Detector.cpp widgets/logqso.cpp diff --git a/Configuration.cpp b/Configuration.cpp index 1ca9ae25a..0cbe744e6 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -604,6 +604,7 @@ private: bool id_after_73_; bool tx_QSY_allowed_; bool spot_to_psk_reporter_; + bool psk_reporter_tcpip_; bool monitor_off_at_startup_; bool monitor_last_used_; bool log_as_RTTY_; @@ -701,6 +702,7 @@ bool Configuration::spot_to_psk_reporter () const // rig must be open and working to spot externally return is_transceiver_online () && m_->spot_to_psk_reporter_; } +bool Configuration::psk_reporter_tcpip () const {return m_->psk_reporter_tcpip_;} bool Configuration::monitor_off_at_startup () const {return m_->monitor_off_at_startup_;} bool Configuration::monitor_last_used () const {return m_->rig_is_dummy_ || m_->monitor_last_used_;} bool Configuration::log_as_RTTY () const {return m_->log_as_RTTY_;} @@ -1241,6 +1243,7 @@ void Configuration::impl::initialize_models () ui_->CW_id_after_73_check_box->setChecked (id_after_73_); ui_->tx_QSY_check_box->setChecked (tx_QSY_allowed_); ui_->psk_reporter_check_box->setChecked (spot_to_psk_reporter_); + ui_->psk_reporter_tcpip_check_box->setChecked (psk_reporter_tcpip_); ui_->monitor_off_check_box->setChecked (monitor_off_at_startup_); ui_->monitor_last_used_check_box->setChecked (monitor_last_used_); ui_->log_as_RTTY_check_box->setChecked (log_as_RTTY_); @@ -1448,6 +1451,7 @@ void Configuration::impl::read_settings () monitor_off_at_startup_ = settings_->value ("MonitorOFF", false).toBool (); monitor_last_used_ = settings_->value ("MonitorLastUsed", false).toBool (); spot_to_psk_reporter_ = settings_->value ("PSKReporter", false).toBool (); + psk_reporter_tcpip_ = settings_->value ("PSKReporterTCPIP", false).toBool (); id_after_73_ = settings_->value ("After73", false).toBool (); tx_QSY_allowed_ = settings_->value ("TxQSYAllowed", false).toBool (); use_dynamic_grid_ = settings_->value ("AutoGrid", false).toBool (); @@ -1587,6 +1591,7 @@ void Configuration::impl::write_settings () settings_->setValue ("MonitorOFF", monitor_off_at_startup_); settings_->setValue ("MonitorLastUsed", monitor_last_used_); settings_->setValue ("PSKReporter", spot_to_psk_reporter_); + settings_->setValue ("PSKReporterTCPIP", psk_reporter_tcpip_); settings_->setValue ("After73", id_after_73_); settings_->setValue ("TxQSYAllowed", tx_QSY_allowed_); settings_->setValue ("Macros", macros_.stringList ()); @@ -2041,6 +2046,7 @@ void Configuration::impl::accept () FD_exchange_= ui_->Field_Day_Exchange->text ().toUpper (); RTTY_exchange_= ui_->RTTY_Exchange->text ().toUpper (); spot_to_psk_reporter_ = ui_->psk_reporter_check_box->isChecked (); + psk_reporter_tcpip_ = ui_->psk_reporter_tcpip_check_box->isChecked (); id_interval_ = ui_->CW_id_interval_spin_box->value (); ntrials_ = ui_->sbNtrials->value (); txDelay_ = ui_->sbTxDelay->value (); diff --git a/Configuration.hpp b/Configuration.hpp index 43267d6ce..882a78629 100644 --- a/Configuration.hpp +++ b/Configuration.hpp @@ -113,6 +113,7 @@ public: bool id_after_73 () const; bool tx_QSY_allowed () const; bool spot_to_psk_reporter () const; + bool psk_reporter_tcpip () const; bool monitor_off_at_startup () const; bool monitor_last_used () const; bool log_as_RTTY () const; diff --git a/Configuration.ui b/Configuration.ui index fa16476e9..16a248223 100644 --- a/Configuration.ui +++ b/Configuration.ui @@ -6,8 +6,8 @@ 0 0 - 557 - 561 + 559 + 553 @@ -1800,20 +1800,27 @@ and DX Grid fields when a 73 or free text message is sent. Network Services - - + + - The program can send your station details and all -decoded signals as spots to the http://pskreporter.info web site. -This is used for reverse beacon analysis which is very useful -for assessing propagation and system performance. + <html><head/><body><p>The program can send your station details and all decoded signals with grid squares as spots to the http://pskreporter.info web site.</p><p>This is used for reverse beacon analysis which is very useful for assessing propagation and system performance.</p></body></html> Enable &PSK Reporter Spotting + + + + <html><head/><body><p>Check this option if a reliable connection is needed</p><p>Most users do not need this, the default uses UDP which is more efficient. Only check this if you have evidence that UDP traffic from you to PSK Reporter is being lost.</p></body></html> + + + Use TCP/IP connection + + + @@ -3108,13 +3115,13 @@ Right click for insert and delete options. + + - - - - - + + + diff --git a/Network/MessageClient.cpp b/Network/MessageClient.cpp index a71beee6e..b545dd44e 100644 --- a/Network/MessageClient.cpp +++ b/Network/MessageClient.cpp @@ -36,6 +36,7 @@ public: impl (QString const& id, QString const& version, QString const& revision, port_type server_port, MessageClient * self) : self_ {self} + , dns_lookup_id_ {0} , enabled_ {false} , id_ {id} , version_ {version} @@ -81,6 +82,7 @@ public: Q_SLOT void host_info_results (QHostInfo); MessageClient * self_; + int dns_lookup_id_; bool enabled_; QString id_; QString version_; @@ -101,6 +103,7 @@ public: void MessageClient::impl::host_info_results (QHostInfo host_info) { + if (host_info.lookupId () != dns_lookup_id_) return; if (QHostInfo::NoError != host_info.error ()) { Q_EMIT self_->error ("UDP server lookup failed:\n" + host_info.errorString ()); @@ -423,25 +426,24 @@ MessageClient::MessageClient (QString const& id, QString const& version, QString : QObject {self} , m_ {id, version, revision, server_port, this} { + connect (&*m_ #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - connect (&*m_, static_cast (&impl::error) - , [this] (impl::SocketError e) { + , static_cast (&impl::error), [this] (impl::SocketError e) #else - connect (&*m_, &impl::errorOccurred, [this] (impl::SocketError e) { + , &impl::errorOccurred, [this] (impl::SocketError e) #endif + { #if defined (Q_OS_WIN) - // take this out when Qt 5.5 stops doing this spuriously - if (e != impl::NetworkError - // not interested in this with UDP socket - && e != impl::ConnectionRefusedError) + if (e != impl::NetworkError // take this out when Qt 5.5 stops doing this spuriously + && e != impl::ConnectionRefusedError) // not interested in this with UDP socket + { #else - Q_UNUSED (e); + { + Q_UNUSED (e); #endif - { - Q_EMIT error (m_->errorString ()); - } - }); - + Q_EMIT error (m_->errorString ()); + } + }); set_server (server); } @@ -459,11 +461,11 @@ void MessageClient::set_server (QString const& server) { m_->server_.clear (); m_->server_string_ = server; - if (!server.isEmpty ()) + if (server.size ()) { // queue a host address lookup TRACE_UDP ("server host DNS lookup:" << server); - QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo))); + m_->dns_lookup_id_ = QHostInfo::lookupHost (server, &*m_, &MessageClient::impl::host_info_results); } } @@ -472,27 +474,6 @@ void MessageClient::set_server_port (port_type server_port) m_->server_port_ = server_port; } -qint64 MessageClient::send_raw_datagram (QByteArray const& message, QHostAddress const& dest_address - , port_type dest_port) -{ - if (dest_port && !dest_address.isNull ()) - { - return m_->writeDatagram (message, dest_address, dest_port); - } - return 0; -} - -void MessageClient::add_blocked_destination (QHostAddress const& a) -{ - m_->blocked_addresses_.push_back (a); - if (a == m_->server_) - { - m_->server_.clear (); - Q_EMIT error ("UDP server blocked, please try another"); - m_->pending_messages_.clear (); // discard - } -} - void MessageClient::enable (bool flag) { m_->enabled_ = flag; diff --git a/Network/MessageClient.hpp b/Network/MessageClient.hpp index 797149fc6..afe361fab 100644 --- a/Network/MessageClient.hpp +++ b/Network/MessageClient.hpp @@ -76,15 +76,6 @@ public: // of record marker Q_SLOT void logged_ADIF (QByteArray const& ADIF_record); - // this may be used to send arbitrary UDP datagrams to and - // destination allowing the underlying socket to be used for general - // UDP messaging if desired - qint64 send_raw_datagram (QByteArray const&, QHostAddress const& dest_address, port_type dest_port); - - // disallowed message destination (does not block datagrams sent - // with send_raw_datagram() above) - Q_SLOT void add_blocked_destination (QHostAddress const&); - // this signal is emitted if the server has requested a decode // window clear action Q_SIGNAL void clear_decodes (quint8 window); diff --git a/Network/PSKReporter.cpp b/Network/PSKReporter.cpp new file mode 100644 index 000000000..56f773896 --- /dev/null +++ b/Network/PSKReporter.cpp @@ -0,0 +1,498 @@ +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +#include +#endif +#include + +#include "Configuration.hpp" +#include "pimpl_impl.hpp" + + +#include "moc_PSKReporter.cpp" + +namespace +{ + constexpr QLatin1String HOST {"report.pskreporter.info"}; + // constexpr QLatin1String HOST {"127.0.0.1"}; + constexpr quint16 SERVICE_PORT {4739}; + // constexpr quint16 SERVICE_PORT {14739}; + constexpr int MIN_SEND_INTERVAL {15}; // in seconds + constexpr int FLUSH_INTERVAL {4 * 5}; // in send intervals + constexpr bool ALIGNMENT_PADDING {true}; + constexpr int MIN_PAYLOAD_LENGTH {508}; + constexpr 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 ();}); + report_timer_.start (MIN_SEND_INTERVAL * 1000); + + // 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 + } + }); + descriptor_timer_.start (1 * 60 * 60 * 1000); // hourly + } + + 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::create (); + *connection = connect (socket_.get (), &QAbstractSocket::disconnected, [this, connection] () { + qDebug () << "PSKReporter::impl::check_connection: disconnected, socket state:" << socket_->state (); + disconnect (*connection); + check_connection (); + }); + // close gracefully + 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 (); + qDebug () << "PSKReporter::impl::handle_socket_error:" << socket_->errorString (); + 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_.get (), QOverload::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); + } + + 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 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 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 ()); + } + + constexpr + 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 (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 + + // qDebug () << "PSKReporter::impl::build_preamble: send_descriptors_:" << send_descriptors_; + 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 ()); + } + } + + // qDebug () << "PSKReporter::impl::build_preamble: send_receiver_data_:" << send_receiver_data_; + // 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) +{ + check_connection (); + // qDebug () << "PSKReporter::impl::send_report: send_residue:" << 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 ()) + { + // qDebug () << "PSKReporter::impl::send_report: building header"; + // Build header, optional descriptors, and receiver information + build_preamble (message); + } + + auto flush = flushing () || send_residue; + // qDebug () << "PSKReporter::impl::send_report: flush:" << flush; + 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) + // qDebug () << "PSKReporter::impl::send_report: set data set header"; + } + + // insert any residue + if (tx_residue_.size ()) + { + tx_out.writeRawData (tx_residue_.constData (), tx_residue_.size ()); + tx_residue_.clear (); + // qDebug () << "PSKReporter::impl::send_report: inserted data residue"; + } + + while (spots_.size () || flush) + { + auto tx_data_size = tx_data_.size (); + if (spots_.size ()) + { + auto const& spot = spots_.dequeue (); + // qDebug () << "PSKReporter::impl::send_report: processing spotted call:" << spot.call_; + + // Sender information + writeUtfString (tx_out, spot.call_); + tx_out + << static_cast (spot.freq_) + << static_cast (spot.snr_); + writeUtfString (tx_out, spot.mode_); + writeUtfString (tx_out, spot.grid_); + tx_out + << quint8 (1u) // REPORTER_SOURCE_AUTOMATIC + << static_cast (spot.time_.toSecsSinceEpoch ()); + } + + 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 (); + // qDebug () << "PSKReporter::impl::send_report: sending short payload:" << 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 (QDateTime::currentDateTime ().toSecsSinceEpoch ()); + + // Send data to PSK Reporter site + socket_->write (payload_); // TODO: handle errors + // qDebug () << "PSKReporter::impl::send_report: sent payload:" << payload_.size () << "observation id:" << observation_id_; + 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 (); + // qDebug () << "PSKReporter::impl::send_report: payload sent residue length:" << tx_residue_.size (); + 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) +{ + 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) +{ + 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 () +{ + m_->send_report (true); +} diff --git a/Network/PSKReporter.hpp b/Network/PSKReporter.hpp new file mode 100644 index 000000000..3582b5850 --- /dev/null +++ b/Network/PSKReporter.hpp @@ -0,0 +1,41 @@ +#ifndef PSK_REPORTER_HPP_ +#define PSK_REPORTER_HPP_ + +#include +#include "Radio.hpp" +#include "pimpl_h.hpp" + +class QString; +class Configuration; + +class PSKReporter final + : public QObject +{ + Q_OBJECT + +public: + explicit PSKReporter (Configuration const *, QString const& program_info); + ~PSKReporter (); + + void reconnect (); + + void setLocalStation (QString const& call, QString const& grid, QString const& antenna); + + // + // Returns false if PSK Reporter connection is not available + // + bool addRemoteStation (QString const& call, QString const& grid, Radio::Frequency freq, QString const& mode, int snr); + + // + // Flush any pending spots to PSK Reporter + // + void sendReport (); + + Q_SIGNAL void errorOccurred (QString const& reason); + +private: + class impl; + pimpl m_; +}; + +#endif diff --git a/Network/psk_reporter.cpp b/Network/psk_reporter.cpp deleted file mode 100644 index f84da5ecd..000000000 --- a/Network/psk_reporter.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// KISS Interface for posting spots to PSK Reporter web site -// Implemented by Edson Pereira PY2SDR -// -// Reports will be sent in batch mode every 5 minutes. - -#include "psk_reporter.h" - -#include -#include -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -#include -#endif - -#include "Network/MessageClient.hpp" - -#include "moc_psk_reporter.cpp" - -namespace -{ - int constexpr MAX_PAYLOAD_LENGTH {1400}; -} - -PSK_Reporter::PSK_Reporter(MessageClient * message_client, QObject *parent) : - QObject {parent}, - m_messageClient {message_client}, - reportTimer {new QTimer {this}}, - m_sequenceNumber {0} -{ - m_header_h = "000Allllttttttttssssssssiiiiiiii"; - - // We use 50E2 and 50E3 for link Id - m_rxInfoDescriptor_h = "0003002C50E200040000" - "8002FFFF0000768F" // 2. Rx Call - "8004FFFF0000768F" // 4. Rx Grid - "8008FFFF0000768F" // 8. Rx Soft - "8009FFFF0000768F" // 9. Rx Antenna - "0000"; - - m_txInfoDescriptor_h = "0002003C50E30007" - "8001FFFF0000768F" // 1. Tx Call - "800500040000768F" // 5. Tx Freq - "800600010000768F" // 6. Tx snr - "800AFFFF0000768F" // 10. Tx Mode - "8003FFFF0000768F" // 3. Tx Grid - "800B00010000768F" // 11. Tx info src - "00960004"; // Report time - - -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - m_randomId_h = QString("%1").arg(qrand(),8,16,QChar('0')); -#else - m_randomId_h = QString("%1").arg(QRandomGenerator::global ()->generate (), 8, 16, QChar('0')); -#endif - - QHostInfo::lookupHost("report.pskreporter.info", this, SLOT(dnsLookupResult(QHostInfo))); - - connect(reportTimer, SIGNAL(timeout()), this, SLOT(sendReport())); - reportTimer->start(5*60*1000); // 5 minutes; -} - -void PSK_Reporter::setLocalStation(QString call, QString gridSquare, QString antenna, QString programInfo) -{ - m_rxCall = call; - m_rxGrid = gridSquare; - m_rxAnt = antenna; - m_progId = programInfo; -} - -void PSK_Reporter::addRemoteStation(QString call, QString grid, QString freq, QString mode, QString snr, QString time ) -{ - QHash spot; - spot["call"] = call; - spot["grid"] = grid; - spot["snr"] = snr; - spot["freq"] = freq; - spot["mode"] = mode; - spot["time"] = time; - m_spotQueue.enqueue(spot); -} - -void PSK_Reporter::sendReport() -{ - while (!m_spotQueue.isEmpty()) { - QString report_h; - - // Header - QString header_h = m_header_h; - header_h.replace("tttttttt", QString("%1").arg(QDateTime::currentDateTime().toTime_t(),8,16,QChar('0'))); - header_h.replace("ssssssss", QString("%1").arg(++m_sequenceNumber,8,16,QChar('0'))); - header_h.replace("iiiiiiii", m_randomId_h); - - // Receiver information - QString rxInfoData_h = "50E2llll"; - rxInfoData_h += QString("%1").arg(m_rxCall.length(),2,16,QChar('0')) + m_rxCall.toUtf8().toHex(); - rxInfoData_h += QString("%1").arg(m_rxGrid.length(),2,16,QChar('0')) + m_rxGrid.toUtf8().toHex(); - rxInfoData_h += QString("%1").arg(m_progId.length(),2,16,QChar('0')) + m_progId.toUtf8().toHex(); - rxInfoData_h += QString("%1").arg(m_rxAnt.length(),2,16,QChar('0')) + m_rxAnt.toUtf8().toHex(); - rxInfoData_h += "0000"; - rxInfoData_h.replace("50E2llll", "50E2" + QString("%1").arg(rxInfoData_h.length()/2,4,16,QChar('0'))); - - // Sender information - QString txInfoData_h = "50E3llll"; - while (!m_spotQueue.isEmpty() - && (header_h.size () + m_rxInfoDescriptor_h.size () + m_txInfoDescriptor_h.size () + rxInfoData_h.size () + txInfoData_h.size ()) / 2 < MAX_PAYLOAD_LENGTH) { - QHash spot = m_spotQueue.dequeue(); - txInfoData_h += QString("%1").arg(spot["call"].length(),2,16,QChar('0')) + spot["call"].toUtf8().toHex(); - txInfoData_h += QString("%1").arg(spot["freq"].toLongLong(),8,16,QChar('0')); - txInfoData_h += QString("%1").arg(spot["snr"].toInt(),8,16,QChar('0')).right(2); - txInfoData_h += QString("%1").arg(spot["mode"].length(),2,16,QChar('0')) + spot["mode"].toUtf8().toHex(); - txInfoData_h += QString("%1").arg(spot["grid"].length(),2,16,QChar('0')) + spot["grid"].toUtf8().toHex(); - txInfoData_h += QString("%1").arg(1,2,16,QChar('0')); // REPORTER_SOURCE_AUTOMATIC - txInfoData_h += QString("%1").arg(spot["time"].toInt(),8,16,QChar('0')); - } - txInfoData_h += "0000"; - txInfoData_h.replace("50E3llll", "50E3" + QString("%1").arg(txInfoData_h.length()/2,4,16,QChar('0'))); - report_h = header_h + m_rxInfoDescriptor_h + m_txInfoDescriptor_h + rxInfoData_h + txInfoData_h; - //qDebug() << "Sending Report TX: "; - - report_h.replace("000Allll", "000A" + QString("%1").arg(report_h.length()/2,4,16,QChar('0'))); - QByteArray report = QByteArray::fromHex(report_h.toUtf8()); - - // Send data to PSK Reporter site - if (!m_pskReporterAddress.isNull()) { - m_messageClient->send_raw_datagram (report, m_pskReporterAddress, 4739); - } - } -} - -void PSK_Reporter::dnsLookupResult(QHostInfo info) -{ - if (!info.addresses().isEmpty()) { - m_pskReporterAddress = info.addresses().at(0); - // qDebug() << "PSK Reporter IP: " << m_pskReporterAddress; - - // deal with miss-configured settings that attempt to set a - // Pskreporter Internet address for the WSJT-X UDP protocol - // server address - m_messageClient->add_blocked_destination (m_pskReporterAddress); - } -} diff --git a/Network/psk_reporter.h b/Network/psk_reporter.h deleted file mode 100644 index 16afcf437..000000000 --- a/Network/psk_reporter.h +++ /dev/null @@ -1,54 +0,0 @@ -// -*- Mode: C++ -*- -#ifndef PSK_REPORTER_H -#define PSK_REPORTER_H - -#include -#include -#include -#include -#include - -class MessageClient; -class QTimer; -class QHostInfo; - -class PSK_Reporter : public QObject -{ - Q_OBJECT -public: - explicit PSK_Reporter(MessageClient *, QObject *parent = nullptr); - void setLocalStation(QString call, QString grid, QString antenna, QString programInfo); - void addRemoteStation(QString call, QString grid, QString freq, QString mode, QString snr, QString time); - -signals: - -public slots: - void sendReport(); - -private slots: - void dnsLookupResult(QHostInfo info); - -private: - QString m_header_h; - QString m_rxInfoDescriptor_h; - QString m_txInfoDescriptor_h; - QString m_randomId_h; - QString m_linkId_h; - - QString m_rxCall; - QString m_rxGrid; - QString m_rxAnt; - QString m_progId; - - QHostAddress m_pskReporterAddress; - - QQueue< QHash > m_spotQueue; - - MessageClient * m_messageClient; - - QTimer *reportTimer; - - int m_sequenceNumber; -}; - -#endif // PSK_REPORTER_H diff --git a/main.cpp b/main.cpp index b3de449f6..22a0ea1e0 100644 --- a/main.cpp +++ b/main.cpp @@ -82,12 +82,12 @@ namespace } catch (std::exception const& e) { - MessageBox::critical_message (nullptr, translate ("main", "Fatal error"), e.what ()); + MessageBox::critical_message (nullptr, "Fatal error", e.what ()); throw; } catch (...) { - MessageBox::critical_message (nullptr, translate ("main", "Unexpected fatal error")); + MessageBox::critical_message (nullptr, "Unexpected fatal error"); throw; } } @@ -438,12 +438,12 @@ int main(int argc, char *argv[]) } catch (std::exception const& e) { - MessageBox::critical_message (nullptr, QApplication::translate ("main", "Fatal error"), e.what ()); + MessageBox::critical_message (nullptr, "Fatal error", e.what ()); std::cerr << "Error: " << e.what () << '\n'; } catch (...) { - MessageBox::critical_message (nullptr, QApplication::translate ("main", "Unexpected fatal error")); + MessageBox::critical_message (nullptr, "Unexpected fatal error"); std::cerr << "Unexpected fatal error\n"; throw; // hoping the runtime might tell us more about the exception } diff --git a/widgets/mainwindow.cpp b/widgets/mainwindow.cpp index d48266e57..dbea24705 100644 --- a/widgets/mainwindow.cpp +++ b/widgets/mainwindow.cpp @@ -412,7 +412,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, version (), revision (), m_config.udp_server_name (), m_config.udp_server_port (), this}}, - psk_Reporter {new PSK_Reporter {m_messageClient, this}}, + m_psk_Reporter {&m_config, QString {"WSJT-X v" + version () + " " + m_revision}.simplified ()}, m_manual {&m_network_manager}, m_block_udp_status_updates {false} { @@ -3481,8 +3481,10 @@ void MainWindow::pskPost (DecodedText const& decodedtext) pskSetLocal (); if(grid.contains (grid_regexp)) { // qDebug() << "To PSKreporter:" << deCall << grid << frequency << msgmode << snr; - psk_Reporter->addRemoteStation(deCall,grid,QString::number(frequency),msgmode, - QString::number(snr),QString::number(QDateTime::currentDateTimeUtc ().toTime_t())); + if (!m_psk_Reporter.addRemoteStation (deCall, grid, frequency, msgmode, snr)) + { + showStatusMessage (tr ("Spotting to PSK Reporter unavailable")); + } } } @@ -6642,7 +6644,7 @@ void MainWindow::band_changed (Frequency f) } m_lastBand.clear (); m_bandEdited = false; - psk_Reporter->sendReport(); // Upload any queued spots before changing band + m_psk_Reporter.sendReport(); // Upload any queued spots before changing band if (!m_transmitting) monitor (true); if ("FreqCal" == m_mode) { @@ -7321,9 +7323,7 @@ void MainWindow::pskSetLocal () , StationList::description_column).data ().toString (); } // qDebug() << "To PSKreporter: local station details"; - psk_Reporter->setLocalStation(m_config.my_callsign (), m_config.my_grid (), - antenna_description, QString {"WSJT-X v" + version() + " " + - m_revision}.simplified ()); + m_psk_Reporter.setLocalStation(m_config.my_callsign (), m_config.my_grid (), antenna_description); } void MainWindow::transmitDisplay (bool transmitting) diff --git a/widgets/mainwindow.h b/widgets/mainwindow.h index f9897e32e..4ad35f618 100644 --- a/widgets/mainwindow.h +++ b/widgets/mainwindow.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -33,7 +34,7 @@ #include "WSPR/WSPRBandHopping.hpp" #include "Transceiver/Transceiver.hpp" #include "DisplayManual.hpp" -#include "Network/psk_reporter.h" +#include "Network/PSKReporter.hpp" #include "logbook/logbook.h" #include "astro.h" #include "MessageBox.hpp" @@ -683,7 +684,7 @@ private: QProgressDialog m_optimizingProgress; QTimer m_heartbeat; MessageClient * m_messageClient; - PSK_Reporter *psk_Reporter; + PSKReporter m_psk_Reporter; DisplayManual m_manual; QHash m_pwrBandTxMemory; // Remembers power level by band QHash m_pwrBandTuneMemory; // Remembers power level by band for tuning diff --git a/wsjtx.pro b/wsjtx.pro index 3248fefd7..0bbe8b565 100644 --- a/wsjtx.pro +++ b/wsjtx.pro @@ -60,7 +60,7 @@ SOURCES += \ PollingTransceiver.cpp EmulateSplitTransceiver.cpp \ HRDTransceiver.cpp DXLabSuiteCommanderTransceiver.cpp \ HamlibTransceiver.cpp FrequencyLineEdit.cpp \ - Configuration.cpp psk_reporter.cpp AudioDevice.cpp \ + Configuration.cpp PSK_Reporter.cpp AudioDevice.cpp \ Modulator.cpp Detector.cpp \ getfile.cpp soundout.cpp soundin.cpp \ WFPalette.cpp \ @@ -77,7 +77,7 @@ HEADERS += qt_helpers.hpp qt_db_helpers.hpp \ soundin.h soundout.h \ WFPalette.hpp getfile.h decodedtext.h \ commons.h sleep.h \ - AudioDevice.hpp Detector.hpp Modulator.hpp psk_reporter.h \ + AudioDevice.hpp Detector.hpp Modulator.hpp PSK_Reporter.hpp \ Transceiver.hpp TransceiverBase.hpp TransceiverFactory.hpp PollingTransceiver.hpp \ EmulateSplitTransceiver.hpp DXLabSuiteCommanderTransceiver.hpp HamlibTransceiver.hpp \ Configuration.hpp wsprnet.h \