mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-08-24 15:16:36 -04:00
UDP servers can request that WSJT-X clients highlight a specified callsign in the Band Activity decodes window. Either the last occurrence of the callsign may be highlighted or all past and future occurrences can be highlighted. The latter case WSJT-X will remember the callsign and requested highlighting options so that future occurrences can be correctly highlighted. Either or both of the text background color and the text foreground color may be specified. A further UDP message may be sent to change the persistent color highlighting for a given callsign, including reseting persistent highlighting by passing an invalid color value. Thanks to Alex, VE3NEA, for this contribution. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@8589 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
488 lines
15 KiB
C++
488 lines
15 KiB
C++
#include "MessageClient.hpp"
|
|
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
|
|
#include <QUdpSocket>
|
|
#include <QHostInfo>
|
|
#include <QTimer>
|
|
#include <QQueue>
|
|
#include <QByteArray>
|
|
#include <QHostAddress>
|
|
#include <QColor>
|
|
|
|
#include "NetworkMessage.hpp"
|
|
|
|
#include "pimpl_impl.hpp"
|
|
|
|
#include "moc_MessageClient.cpp"
|
|
|
|
class MessageClient::impl
|
|
: public QUdpSocket
|
|
{
|
|
Q_OBJECT;
|
|
|
|
public:
|
|
impl (QString const& id, QString const& version, QString const& revision,
|
|
port_type server_port, MessageClient * self)
|
|
: self_ {self}
|
|
, id_ {id}
|
|
, version_ {version}
|
|
, revision_ {revision}
|
|
, server_port_ {server_port}
|
|
, schema_ {2} // use 2 prior to negotiation not 1 which is broken
|
|
, heartbeat_timer_ {new QTimer {this}}
|
|
{
|
|
connect (heartbeat_timer_, &QTimer::timeout, this, &impl::heartbeat);
|
|
connect (this, &QIODevice::readyRead, this, &impl::pending_datagrams);
|
|
|
|
heartbeat_timer_->start (NetworkMessage::pulse * 1000);
|
|
|
|
// bind to an ephemeral port
|
|
bind ();
|
|
}
|
|
|
|
~impl ()
|
|
{
|
|
closedown ();
|
|
}
|
|
|
|
enum StreamStatus {Fail, Short, OK};
|
|
|
|
void parse_message (QByteArray const& msg);
|
|
void pending_datagrams ();
|
|
void heartbeat ();
|
|
void closedown ();
|
|
StreamStatus check_status (QDataStream const&) const;
|
|
void send_message (QByteArray const&);
|
|
void send_message (QDataStream const& out, QByteArray const& message)
|
|
{
|
|
if (OK == check_status (out))
|
|
{
|
|
send_message (message);
|
|
}
|
|
else
|
|
{
|
|
Q_EMIT self_->error ("Error creating UDP message");
|
|
}
|
|
}
|
|
|
|
Q_SLOT void host_info_results (QHostInfo);
|
|
|
|
MessageClient * self_;
|
|
QString id_;
|
|
QString version_;
|
|
QString revision_;
|
|
QString server_string_;
|
|
port_type server_port_;
|
|
QHostAddress server_;
|
|
quint32 schema_;
|
|
QTimer * heartbeat_timer_;
|
|
std::vector<QHostAddress> blocked_addresses_;
|
|
|
|
// hold messages sent before host lookup completes asynchronously
|
|
QQueue<QByteArray> pending_messages_;
|
|
QByteArray last_message_;
|
|
};
|
|
|
|
#include "MessageClient.moc"
|
|
|
|
void MessageClient::impl::host_info_results (QHostInfo host_info)
|
|
{
|
|
if (QHostInfo::NoError != host_info.error ())
|
|
{
|
|
Q_EMIT self_->error ("UDP server lookup failed:\n" + host_info.errorString ());
|
|
pending_messages_.clear (); // discard
|
|
}
|
|
else if (host_info.addresses ().size ())
|
|
{
|
|
auto server = host_info.addresses ()[0];
|
|
if (blocked_addresses_.end () == std::find (blocked_addresses_.begin (), blocked_addresses_.end (), server))
|
|
{
|
|
server_ = server;
|
|
|
|
// send initial heartbeat which allows schema negotiation
|
|
heartbeat ();
|
|
|
|
// clear any backlog
|
|
while (pending_messages_.size ())
|
|
{
|
|
send_message (pending_messages_.dequeue ());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Q_EMIT self_->error ("UDP server blocked, please try another");
|
|
pending_messages_.clear (); // discard
|
|
}
|
|
}
|
|
}
|
|
|
|
void MessageClient::impl::pending_datagrams ()
|
|
{
|
|
while (hasPendingDatagrams ())
|
|
{
|
|
QByteArray datagram;
|
|
datagram.resize (pendingDatagramSize ());
|
|
QHostAddress sender_address;
|
|
port_type sender_port;
|
|
if (0 <= readDatagram (datagram.data (), datagram.size (), &sender_address, &sender_port))
|
|
{
|
|
parse_message (datagram);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MessageClient::impl::parse_message (QByteArray const& msg)
|
|
{
|
|
try
|
|
{
|
|
//
|
|
// message format is described in NetworkMessage.hpp
|
|
//
|
|
NetworkMessage::Reader in {msg};
|
|
if (OK == check_status (in) && id_ == in.id ()) // OK and for us
|
|
{
|
|
if (schema_ < in.schema ()) // one time record of server's
|
|
// negotiated schema
|
|
{
|
|
schema_ = in.schema ();
|
|
}
|
|
|
|
//
|
|
// message format is described in NetworkMessage.hpp
|
|
//
|
|
switch (in.type ())
|
|
{
|
|
case NetworkMessage::Reply:
|
|
{
|
|
// unpack message
|
|
QTime time;
|
|
qint32 snr;
|
|
float delta_time;
|
|
quint32 delta_frequency;
|
|
QByteArray mode;
|
|
QByteArray message;
|
|
bool low_confidence {false};
|
|
quint8 modifiers {0};
|
|
in >> time >> snr >> delta_time >> delta_frequency >> mode >> message
|
|
>> low_confidence >> modifiers;
|
|
if (check_status (in) != Fail)
|
|
{
|
|
Q_EMIT self_->reply (time, snr, delta_time, delta_frequency
|
|
, QString::fromUtf8 (mode), QString::fromUtf8 (message)
|
|
, low_confidence, modifiers);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NetworkMessage::Replay:
|
|
if (check_status (in) != Fail)
|
|
{
|
|
last_message_.clear ();
|
|
Q_EMIT self_->replay ();
|
|
}
|
|
break;
|
|
|
|
case NetworkMessage::HaltTx:
|
|
{
|
|
bool auto_only {false};
|
|
in >> auto_only;
|
|
if (check_status (in) != Fail)
|
|
{
|
|
Q_EMIT self_->halt_tx (auto_only);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NetworkMessage::FreeText:
|
|
{
|
|
QByteArray message;
|
|
bool send {true};
|
|
in >> message >> send;
|
|
if (check_status (in) != Fail)
|
|
{
|
|
Q_EMIT self_->free_text (QString::fromUtf8 (message), send);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NetworkMessage::Location:
|
|
{
|
|
QByteArray location;
|
|
in >> location;
|
|
if (check_status (in) != Fail)
|
|
{
|
|
Q_EMIT self_->location (QString::fromUtf8 (location));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NetworkMessage::HighlightCallsign:
|
|
{
|
|
QByteArray call;
|
|
QColor bg; // default invalid color
|
|
QColor fg; // default invalid color
|
|
bool last_only {false};
|
|
in >> call >> bg >> fg >> last_only;
|
|
if (check_status (in) != Fail && call.size ())
|
|
{
|
|
Q_EMIT self_->highlight_callsign (QString::fromUtf8 (call), bg, fg, last_only);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Ignore
|
|
//
|
|
// Note that although server heartbeat messages are not
|
|
// parsed here they are still partially parsed in the
|
|
// message reader class to negotiate the maximum schema
|
|
// number being used on the network.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
Q_EMIT self_->error (QString {"MessageClient exception: %1"}.arg (e.what ()));
|
|
}
|
|
catch (...)
|
|
{
|
|
Q_EMIT self_->error ("Unexpected exception in MessageClient");
|
|
}
|
|
}
|
|
|
|
void MessageClient::impl::heartbeat ()
|
|
{
|
|
if (server_port_ && !server_.isNull ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder hb {&message, NetworkMessage::Heartbeat, id_, schema_};
|
|
hb << NetworkMessage::Builder::schema_number // maximum schema number accepted
|
|
<< version_.toUtf8 () << revision_.toUtf8 ();
|
|
if (OK == check_status (hb))
|
|
{
|
|
writeDatagram (message, server_, server_port_);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MessageClient::impl::closedown ()
|
|
{
|
|
if (server_port_ && !server_.isNull ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder out {&message, NetworkMessage::Close, id_, schema_};
|
|
if (OK == check_status (out))
|
|
{
|
|
writeDatagram (message, server_, server_port_);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MessageClient::impl::send_message (QByteArray const& message)
|
|
{
|
|
if (server_port_)
|
|
{
|
|
if (!server_.isNull ())
|
|
{
|
|
if (message != last_message_) // avoid duplicates
|
|
{
|
|
writeDatagram (message, server_, server_port_);
|
|
last_message_ = message;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pending_messages_.enqueue (message);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto MessageClient::impl::check_status (QDataStream const& stream) const -> StreamStatus
|
|
{
|
|
auto stat = stream.status ();
|
|
StreamStatus result {Fail};
|
|
switch (stat)
|
|
{
|
|
case QDataStream::ReadPastEnd:
|
|
result = Short;
|
|
break;
|
|
|
|
case QDataStream::ReadCorruptData:
|
|
Q_EMIT self_->error ("Message serialization error: read corrupt data");
|
|
break;
|
|
|
|
case QDataStream::WriteFailed:
|
|
Q_EMIT self_->error ("Message serialization error: write error");
|
|
break;
|
|
|
|
default:
|
|
result = OK;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
MessageClient::MessageClient (QString const& id, QString const& version, QString const& revision,
|
|
QString const& server, port_type server_port, QObject * self)
|
|
: QObject {self}
|
|
, m_ {id, version, revision, server_port, this}
|
|
{
|
|
connect (&*m_, static_cast<void (impl::*) (impl::SocketError)> (&impl::error)
|
|
, [this] (impl::SocketError e)
|
|
{
|
|
#if defined (Q_OS_WIN) && QT_VERSION >= 0x050500
|
|
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);
|
|
#endif
|
|
{
|
|
Q_EMIT error (m_->errorString ());
|
|
}
|
|
});
|
|
set_server (server);
|
|
}
|
|
|
|
QHostAddress MessageClient::server_address () const
|
|
{
|
|
return m_->server_;
|
|
}
|
|
|
|
auto MessageClient::server_port () const -> port_type
|
|
{
|
|
return m_->server_port_;
|
|
}
|
|
|
|
void MessageClient::set_server (QString const& server)
|
|
{
|
|
m_->server_.clear ();
|
|
m_->server_string_ = server;
|
|
if (!server.isEmpty ())
|
|
{
|
|
// queue a host address lookup
|
|
QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo)));
|
|
}
|
|
}
|
|
|
|
void MessageClient::set_server_port (port_type server_port)
|
|
{
|
|
m_->server_port_ = server_port;
|
|
}
|
|
|
|
void MessageClient::send_raw_datagram (QByteArray const& message, QHostAddress const& dest_address
|
|
, port_type dest_port)
|
|
{
|
|
if (dest_port && !dest_address.isNull ())
|
|
{
|
|
m_->writeDatagram (message, dest_address, dest_port);
|
|
}
|
|
}
|
|
|
|
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::status_update (Frequency f, QString const& mode, QString const& dx_call
|
|
, QString const& report, QString const& tx_mode
|
|
, bool tx_enabled, bool transmitting, bool decoding
|
|
, qint32 rx_df, qint32 tx_df, QString const& de_call
|
|
, QString const& de_grid, QString const& dx_grid
|
|
, bool watchdog_timeout, QString const& sub_mode
|
|
, bool fast_mode)
|
|
{
|
|
if (m_->server_port_ && !m_->server_string_.isEmpty ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder out {&message, NetworkMessage::Status, m_->id_, m_->schema_};
|
|
out << f << mode.toUtf8 () << dx_call.toUtf8 () << report.toUtf8 () << tx_mode.toUtf8 ()
|
|
<< tx_enabled << transmitting << decoding << rx_df << tx_df << de_call.toUtf8 ()
|
|
<< de_grid.toUtf8 () << dx_grid.toUtf8 () << watchdog_timeout << sub_mode.toUtf8 ()
|
|
<< fast_mode;
|
|
m_->send_message (out, message);
|
|
}
|
|
}
|
|
|
|
void MessageClient::decode (bool is_new, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
|
|
, QString const& mode, QString const& message_text, bool low_confidence
|
|
, bool off_air)
|
|
{
|
|
if (m_->server_port_ && !m_->server_string_.isEmpty ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder out {&message, NetworkMessage::Decode, m_->id_, m_->schema_};
|
|
out << is_new << time << snr << delta_time << delta_frequency << mode.toUtf8 ()
|
|
<< message_text.toUtf8 () << low_confidence << off_air;
|
|
m_->send_message (out, message);
|
|
}
|
|
}
|
|
|
|
void MessageClient::WSPR_decode (bool is_new, QTime time, qint32 snr, float delta_time, Frequency frequency
|
|
, qint32 drift, QString const& callsign, QString const& grid, qint32 power
|
|
, bool off_air)
|
|
{
|
|
if (m_->server_port_ && !m_->server_string_.isEmpty ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder out {&message, NetworkMessage::WSPRDecode, m_->id_, m_->schema_};
|
|
out << is_new << time << snr << delta_time << frequency << drift << callsign.toUtf8 ()
|
|
<< grid.toUtf8 () << power << off_air;
|
|
m_->send_message (out, message);
|
|
}
|
|
}
|
|
|
|
void MessageClient::clear_decodes ()
|
|
{
|
|
if (m_->server_port_ && !m_->server_string_.isEmpty ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder out {&message, NetworkMessage::Clear, m_->id_, m_->schema_};
|
|
m_->send_message (out, message);
|
|
}
|
|
}
|
|
|
|
void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QString const& dx_grid
|
|
, Frequency dial_frequency, QString const& mode, QString const& report_sent
|
|
, QString const& report_received, QString const& tx_power
|
|
, QString const& comments, QString const& name, QDateTime time_on
|
|
, QString const& operator_call, QString const& my_call
|
|
, QString const& my_grid)
|
|
{
|
|
if (m_->server_port_ && !m_->server_string_.isEmpty ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder out {&message, NetworkMessage::QSOLogged, m_->id_, m_->schema_};
|
|
out << time_off << dx_call.toUtf8 () << dx_grid.toUtf8 () << dial_frequency << mode.toUtf8 ()
|
|
<< report_sent.toUtf8 () << report_received.toUtf8 () << tx_power.toUtf8 () << comments.toUtf8 ()
|
|
<< name.toUtf8 () << time_on << operator_call.toUtf8 () << my_call.toUtf8 () << my_grid.toUtf8 ();
|
|
m_->send_message (out, message);
|
|
}
|
|
}
|
|
|
|
void MessageClient::logged_ADIF (QByteArray const& ADIF_record)
|
|
{
|
|
if (m_->server_port_ && !m_->server_string_.isEmpty ())
|
|
{
|
|
QByteArray message;
|
|
NetworkMessage::Builder out {&message, NetworkMessage::LoggedADIF, m_->id_, m_->schema_};
|
|
QByteArray ADIF {"\n<adif_ver:5>3.0.7\n<programid:6>WSJT-X\n<EOH>\n" + ADIF_record + " <EOR>"};
|
|
out << ADIF;
|
|
m_->send_message (out, message);
|
|
}
|
|
}
|