mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2026-06-04 23:14:57 -04:00
improve physical structure
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
#include "MessageClient.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
#include <QTimer>
|
||||
#include <QQueue>
|
||||
#include <QByteArray>
|
||||
#include <QHostAddress>
|
||||
#include <QColor>
|
||||
#include <QDebug>
|
||||
|
||||
#include "NetworkMessage.hpp"
|
||||
|
||||
#include "pimpl_impl.hpp"
|
||||
|
||||
#include "moc_MessageClient.cpp"
|
||||
|
||||
// some trace macros
|
||||
#if WSJT_TRACE_UDP
|
||||
#define TRACE_UDP(MSG) qDebug () << QString {"MessageClient::%1:"}.arg (__func__) << MSG
|
||||
#else
|
||||
#define TRACE_UDP(MSG)
|
||||
#endif
|
||||
|
||||
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;
|
||||
TRACE_UDP ("resulting 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))
|
||||
{
|
||||
TRACE_UDP ("message received from:" << sender_address << "port:" << 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))
|
||||
{
|
||||
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;
|
||||
TRACE_UDP ("Reply: time:" << time << "snr:" << snr << "dt:" << delta_time << "df:" << delta_frequency << "mode:" << mode << "message:" << message << "low confidence:" << low_confidence << "modifiers: 0x" << hex << 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::Clear:
|
||||
{
|
||||
quint8 window {0};
|
||||
in >> window;
|
||||
TRACE_UDP ("Clear window:" << window);
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
Q_EMIT self_->clear_decodes (window);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkMessage::Replay:
|
||||
TRACE_UDP ("Replay");
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
last_message_.clear ();
|
||||
Q_EMIT self_->replay ();
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkMessage::HaltTx:
|
||||
{
|
||||
bool auto_only {false};
|
||||
in >> auto_only;
|
||||
TRACE_UDP ("Halt Tx auto_only:" << 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;
|
||||
TRACE_UDP ("FreeText message:" << message << "send:" << send);
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
Q_EMIT self_->free_text (QString::fromUtf8 (message), send);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkMessage::Location:
|
||||
{
|
||||
QByteArray location;
|
||||
in >> location;
|
||||
TRACE_UDP ("Location location:" << 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;
|
||||
TRACE_UDP ("HighlightCallsign call:" << call << "bg:" << bg << "fg:" << fg << "last only:" << 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.
|
||||
if (NetworkMessage::Heartbeat != in.type ())
|
||||
{
|
||||
TRACE_UDP ("ignoring message type:" << in.type ());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TRACE_UDP ("ignored message for id:" << in.id ());
|
||||
}
|
||||
}
|
||||
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))
|
||||
{
|
||||
TRACE_UDP ("schema:" << schema_ << "max schema:" << NetworkMessage::Builder::schema_number << "version:" << version_ << "revision:" << revision_);
|
||||
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))
|
||||
{
|
||||
TRACE_UDP ("");
|
||||
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)
|
||||
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
|
||||
TRACE_UDP ("server host DNS lookup:" << server);
|
||||
QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo)));
|
||||
}
|
||||
}
|
||||
|
||||
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::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, quint8 special_op_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 << special_op_mode;
|
||||
TRACE_UDP ("frequency:" << f << "mode:" << mode << "DX:" << dx_call << "report:" << report << "Tx mode:" << tx_mode << "tx_enabled:" << tx_enabled << "Tx:" << transmitting << "decoding:" << decoding << "Rx df:" << rx_df << "Tx df:" << tx_df << "DE:" << de_call << "DE grid:" << de_grid << "DX grid:" << dx_grid << "w/d t/o:" << watchdog_timeout << "sub_mode:" << sub_mode << "fast mode:" << fast_mode << "spec op mode:" << special_op_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;
|
||||
TRACE_UDP ("new" << is_new << "time:" << time << "snr:" << snr << "dt:" << delta_time << "df:" << delta_frequency << "mode:" << mode << "text:" << message_text << "low conf:" << low_confidence << "off air:" << 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;
|
||||
TRACE_UDP ("new:" << is_new << "time:" << time << "snr:" << snr << "dt:" << delta_time << "frequency:" << frequency << "drift:" << drift << "call:" << callsign << "grid:" << grid << "pwr:" << power << "off air:" << off_air);
|
||||
m_->send_message (out, message);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageClient::decodes_cleared ()
|
||||
{
|
||||
if (m_->server_port_ && !m_->server_string_.isEmpty ())
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::Clear, m_->id_, m_->schema_};
|
||||
TRACE_UDP ("");
|
||||
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, QString const& exchange_sent
|
||||
, QString const& exchange_rcvd)
|
||||
{
|
||||
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 ()
|
||||
<< exchange_sent.toUtf8 () << exchange_rcvd.toUtf8 ();
|
||||
TRACE_UDP ("time off:" << time_off << "DX:" << dx_call << "DX grid:" << dx_grid << "dial:" << dial_frequency << "mode:" << mode << "sent:" << report_sent << "rcvd:" << report_received << "pwr:" << tx_power << "comments:" << comments << "name:" << name << "time on:" << time_on << "op:" << operator_call << "DE:" << my_call << "DE grid:" << my_grid << "exch sent:" << exchange_sent << "exch rcvd:" << exchange_rcvd);
|
||||
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.1.0\n<programid:6>WSJT-X\n<EOH>\n" + ADIF_record + " <EOR>"};
|
||||
out << ADIF;
|
||||
TRACE_UDP ("ADIF:" << ADIF);
|
||||
m_->send_message (out, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#ifndef MESSAGE_CLIENT_HPP__
|
||||
#define MESSAGE_CLIENT_HPP__
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
|
||||
#include "Radio.hpp"
|
||||
#include "pimpl_h.hpp"
|
||||
|
||||
class QByteArray;
|
||||
class QHostAddress;
|
||||
class QColor;
|
||||
|
||||
//
|
||||
// MessageClient - Manage messages sent and replies received from a
|
||||
// matching server (MessageServer) at the other end of
|
||||
// the wire
|
||||
//
|
||||
//
|
||||
// Each outgoing message type is a Qt slot
|
||||
//
|
||||
class MessageClient
|
||||
: public QObject
|
||||
{
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
using Frequency = Radio::Frequency;
|
||||
using port_type = quint16;
|
||||
|
||||
// instantiate and initiate a host lookup on the server
|
||||
//
|
||||
// messages will be silently dropped until a server host lookup is complete
|
||||
MessageClient (QString const& id, QString const& version, QString const& revision,
|
||||
QString const& server, port_type server_port, QObject * parent = nullptr);
|
||||
|
||||
// query server details
|
||||
QHostAddress server_address () const;
|
||||
port_type server_port () const;
|
||||
|
||||
// initiate a new server host lookup or is the server name is empty
|
||||
// the sending of messages is disabled
|
||||
Q_SLOT void set_server (QString const& server = QString {});
|
||||
|
||||
// change the server port messages are sent to
|
||||
Q_SLOT void set_server_port (port_type server_port = 0u);
|
||||
|
||||
// outgoing messages
|
||||
Q_SLOT void status_update (Frequency, 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, quint8 special_op_mode);
|
||||
Q_SLOT void decode (bool is_new, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
|
||||
, QString const& mode, QString const& message, bool low_confidence
|
||||
, bool off_air);
|
||||
Q_SLOT void WSPR_decode (bool is_new, QTime time, qint32 snr, float delta_time, Frequency
|
||||
, qint32 drift, QString const& callsign, QString const& grid, qint32 power
|
||||
, bool off_air);
|
||||
Q_SLOT void decodes_cleared ();
|
||||
Q_SLOT void 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
|
||||
, QString const& exchange_sent, QString const& exchange_rcvd);
|
||||
|
||||
// ADIF_record argument should be valid ADIF excluding any <EOR> end
|
||||
// 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);
|
||||
|
||||
// this signal is emitted if the server sends us a reply, the only
|
||||
// reply supported is reply to a prior CQ or QRZ message
|
||||
Q_SIGNAL void reply (QTime, qint32 snr, float delta_time, quint32 delta_frequency, QString const& mode
|
||||
, QString const& message_text, bool low_confidence, quint8 modifiers);
|
||||
|
||||
// this signal is emitted if the server has requested a replay of
|
||||
// all decodes
|
||||
Q_SIGNAL void replay ();
|
||||
|
||||
// this signal is emitted if the server has requested immediate (or
|
||||
// auto Tx if auto_only is true) transmission to halt
|
||||
Q_SIGNAL void halt_tx (bool auto_only);
|
||||
|
||||
// this signal is emitted if the server has requested a new free
|
||||
// message text
|
||||
Q_SIGNAL void free_text (QString const&, bool send);
|
||||
|
||||
// this signal is emitted if the server has sent a highlight
|
||||
// callsign request for the specified call
|
||||
Q_SIGNAL void highlight_callsign (QString const& callsign, QColor const& bg, QColor const& fg, bool last_only);
|
||||
|
||||
// this signal is emitted when network errors occur or if a host
|
||||
// lookup fails
|
||||
Q_SIGNAL void error (QString const&) const;
|
||||
|
||||
// this signal is emitted if the message obtains a location from a
|
||||
// server. (It doesn't have to be new, could be a periodic location
|
||||
// update)
|
||||
Q_SIGNAL void location (QString const&);
|
||||
|
||||
private:
|
||||
class impl;
|
||||
pimpl<impl> m_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,543 @@
|
||||
#include "MessageServer.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <QNetworkInterface>
|
||||
#include <QUdpSocket>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QHash>
|
||||
|
||||
#include "Radio.hpp"
|
||||
#include "NetworkMessage.hpp"
|
||||
#include "qt_helpers.hpp"
|
||||
|
||||
#include "pimpl_impl.hpp"
|
||||
|
||||
#include "moc_MessageServer.cpp"
|
||||
|
||||
class MessageServer::impl
|
||||
: public QUdpSocket
|
||||
{
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
impl (MessageServer * self, QString const& version, QString const& revision)
|
||||
: self_ {self}
|
||||
, version_ {version}
|
||||
, revision_ {revision}
|
||||
, port_ {0u}
|
||||
, clock_ {new QTimer {this}}
|
||||
{
|
||||
// register the required types with Qt
|
||||
Radio::register_types ();
|
||||
|
||||
connect (this, &QIODevice::readyRead, this, &MessageServer::impl::pending_datagrams);
|
||||
connect (this, static_cast<void (impl::*) (SocketError)> (&impl::error)
|
||||
, [this] (SocketError /* e */)
|
||||
{
|
||||
Q_EMIT self_->error (errorString ());
|
||||
});
|
||||
connect (clock_, &QTimer::timeout, this, &impl::tick);
|
||||
clock_->start (NetworkMessage::pulse * 1000);
|
||||
}
|
||||
|
||||
enum StreamStatus {Fail, Short, OK};
|
||||
|
||||
void leave_multicast_group ();
|
||||
void join_multicast_group ();
|
||||
void parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg);
|
||||
void tick ();
|
||||
void pending_datagrams ();
|
||||
StreamStatus check_status (QDataStream const&) const;
|
||||
void send_message (QDataStream const& out, QByteArray const& message, QHostAddress const& address, port_type port)
|
||||
{
|
||||
if (OK == check_status (out))
|
||||
{
|
||||
writeDatagram (message, address, port);
|
||||
}
|
||||
else
|
||||
{
|
||||
Q_EMIT self_->error ("Error creating UDP message");
|
||||
}
|
||||
}
|
||||
|
||||
MessageServer * self_;
|
||||
QString version_;
|
||||
QString revision_;
|
||||
port_type port_;
|
||||
QHostAddress multicast_group_address_;
|
||||
static BindMode constexpr bind_mode_ = ShareAddress | ReuseAddressHint;
|
||||
struct Client
|
||||
{
|
||||
Client () = default;
|
||||
Client (QHostAddress const& sender_address, port_type const& sender_port)
|
||||
: sender_address_ {sender_address}
|
||||
, sender_port_ {sender_port}
|
||||
, negotiated_schema_number_ {2} // not 1 because it's broken
|
||||
, last_activity_ {QDateTime::currentDateTime ()}
|
||||
{
|
||||
}
|
||||
Client (Client const&) = default;
|
||||
Client& operator= (Client const&) = default;
|
||||
|
||||
QHostAddress sender_address_;
|
||||
port_type sender_port_;
|
||||
quint32 negotiated_schema_number_;
|
||||
QDateTime last_activity_;
|
||||
};
|
||||
QHash<QString, Client> clients_; // maps id to Client
|
||||
QTimer * clock_;
|
||||
};
|
||||
|
||||
MessageServer::impl::BindMode constexpr MessageServer::impl::bind_mode_;
|
||||
|
||||
#include "MessageServer.moc"
|
||||
|
||||
void MessageServer::impl::leave_multicast_group ()
|
||||
{
|
||||
if (!multicast_group_address_.isNull () && BoundState == state ()
|
||||
#if QT_VERSION >= 0x050600
|
||||
&& multicast_group_address_.isMulticast ()
|
||||
#endif
|
||||
)
|
||||
{
|
||||
for (auto const& interface : QNetworkInterface::allInterfaces ())
|
||||
{
|
||||
if (QNetworkInterface::CanMulticast & interface.flags ())
|
||||
{
|
||||
leaveMulticastGroup (multicast_group_address_, interface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::impl::join_multicast_group ()
|
||||
{
|
||||
if (BoundState == state ()
|
||||
&& !multicast_group_address_.isNull ()
|
||||
#if QT_VERSION >= 0x050600
|
||||
&& multicast_group_address_.isMulticast ()
|
||||
#endif
|
||||
)
|
||||
{
|
||||
auto mcast_iface = multicastInterface ();
|
||||
if (IPv4Protocol == multicast_group_address_.protocol ()
|
||||
&& IPv4Protocol != localAddress ().protocol ())
|
||||
{
|
||||
close ();
|
||||
bind (QHostAddress::AnyIPv4, port_, bind_mode_);
|
||||
}
|
||||
bool joined {false};
|
||||
for (auto const& interface : QNetworkInterface::allInterfaces ())
|
||||
{
|
||||
if (QNetworkInterface::CanMulticast & interface.flags ())
|
||||
{
|
||||
// Windows requires outgoing interface to match
|
||||
// interface to be joined while joining, at least for
|
||||
// IPv4 it seems to
|
||||
setMulticastInterface (interface);
|
||||
|
||||
joined |= joinMulticastGroup (multicast_group_address_, interface);
|
||||
}
|
||||
}
|
||||
if (!joined)
|
||||
{
|
||||
multicast_group_address_.clear ();
|
||||
}
|
||||
setMulticastInterface (mcast_iface);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::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 (sender_address, sender_port, datagram);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::impl::parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
//
|
||||
// message format is described in NetworkMessage.hpp
|
||||
//
|
||||
NetworkMessage::Reader in {msg};
|
||||
|
||||
auto id = in.id ();
|
||||
if (OK == check_status (in))
|
||||
{
|
||||
if (!clients_.contains (id))
|
||||
{
|
||||
auto& client = (clients_[id] = {sender, sender_port});
|
||||
QByteArray client_version;
|
||||
QByteArray client_revision;
|
||||
|
||||
if (NetworkMessage::Heartbeat == in.type ())
|
||||
{
|
||||
// negotiate a working schema number
|
||||
in >> client.negotiated_schema_number_;
|
||||
if (OK == check_status (in))
|
||||
{
|
||||
auto sn = NetworkMessage::Builder::schema_number;
|
||||
client.negotiated_schema_number_ = std::min (sn, client.negotiated_schema_number_);
|
||||
|
||||
// reply to the new client informing it of the
|
||||
// negotiated schema number
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder hb {&message, NetworkMessage::Heartbeat, id, client.negotiated_schema_number_};
|
||||
hb << NetworkMessage::Builder::schema_number // maximum schema number accepted
|
||||
<< version_.toUtf8 () << revision_.toUtf8 ();
|
||||
if (impl::OK == check_status (hb))
|
||||
{
|
||||
writeDatagram (message, client.sender_address_, client.sender_port_);
|
||||
}
|
||||
else
|
||||
{
|
||||
Q_EMIT self_->error ("Error creating UDP message");
|
||||
}
|
||||
}
|
||||
// we don't care if this fails to read
|
||||
in >> client_version >> client_revision;
|
||||
}
|
||||
Q_EMIT self_->client_opened (id, QString::fromUtf8 (client_version),
|
||||
QString::fromUtf8 (client_revision));
|
||||
}
|
||||
clients_[id].last_activity_ = QDateTime::currentDateTime ();
|
||||
|
||||
//
|
||||
// message format is described in NetworkMessage.hpp
|
||||
//
|
||||
switch (in.type ())
|
||||
{
|
||||
case NetworkMessage::Heartbeat:
|
||||
//nothing to do here as time out handling deals with lifetime
|
||||
break;
|
||||
|
||||
case NetworkMessage::Clear:
|
||||
Q_EMIT self_->decodes_cleared (id);
|
||||
break;
|
||||
|
||||
case NetworkMessage::Status:
|
||||
{
|
||||
// unpack message
|
||||
Frequency f;
|
||||
QByteArray mode;
|
||||
QByteArray dx_call;
|
||||
QByteArray report;
|
||||
QByteArray tx_mode;
|
||||
bool tx_enabled {false};
|
||||
bool transmitting {false};
|
||||
bool decoding {false};
|
||||
qint32 rx_df {-1};
|
||||
qint32 tx_df {-1};
|
||||
QByteArray de_call;
|
||||
QByteArray de_grid;
|
||||
QByteArray dx_grid;
|
||||
bool watchdog_timeout {false};
|
||||
QByteArray sub_mode;
|
||||
bool fast_mode {false};
|
||||
quint8 special_op_mode {0};
|
||||
in >> f >> mode >> dx_call >> report >> tx_mode >> tx_enabled >> transmitting >> decoding
|
||||
>> rx_df >> tx_df >> de_call >> de_grid >> dx_grid >> watchdog_timeout >> sub_mode
|
||||
>> fast_mode >> special_op_mode;
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
Q_EMIT self_->status_update (id, f, QString::fromUtf8 (mode), QString::fromUtf8 (dx_call)
|
||||
, QString::fromUtf8 (report), QString::fromUtf8 (tx_mode)
|
||||
, tx_enabled, transmitting, decoding, rx_df, tx_df
|
||||
, QString::fromUtf8 (de_call), QString::fromUtf8 (de_grid)
|
||||
, QString::fromUtf8 (dx_grid), watchdog_timeout
|
||||
, QString::fromUtf8 (sub_mode), fast_mode
|
||||
, special_op_mode);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkMessage::Decode:
|
||||
{
|
||||
// unpack message
|
||||
bool is_new {true};
|
||||
QTime time;
|
||||
qint32 snr;
|
||||
float delta_time;
|
||||
quint32 delta_frequency;
|
||||
QByteArray mode;
|
||||
QByteArray message;
|
||||
bool low_confidence {false};
|
||||
bool off_air {false};
|
||||
in >> is_new >> time >> snr >> delta_time >> delta_frequency >> mode
|
||||
>> message >> low_confidence >> off_air;
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
Q_EMIT self_->decode (is_new, id, time, snr, delta_time, delta_frequency
|
||||
, QString::fromUtf8 (mode), QString::fromUtf8 (message)
|
||||
, low_confidence, off_air);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkMessage::WSPRDecode:
|
||||
{
|
||||
// unpack message
|
||||
bool is_new {true};
|
||||
QTime time;
|
||||
qint32 snr;
|
||||
float delta_time;
|
||||
Frequency frequency;
|
||||
qint32 drift;
|
||||
QByteArray callsign;
|
||||
QByteArray grid;
|
||||
qint32 power;
|
||||
bool off_air {false};
|
||||
in >> is_new >> time >> snr >> delta_time >> frequency >> drift >> callsign >> grid >> power
|
||||
>> off_air;
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
Q_EMIT self_->WSPR_decode (is_new, id, time, snr, delta_time, frequency, drift
|
||||
, QString::fromUtf8 (callsign), QString::fromUtf8 (grid)
|
||||
, power, off_air);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkMessage::QSOLogged:
|
||||
{
|
||||
QDateTime time_off;
|
||||
QByteArray dx_call;
|
||||
QByteArray dx_grid;
|
||||
Frequency dial_frequency;
|
||||
QByteArray mode;
|
||||
QByteArray report_sent;
|
||||
QByteArray report_received;
|
||||
QByteArray tx_power;
|
||||
QByteArray comments;
|
||||
QByteArray name;
|
||||
QDateTime time_on; // Note: LOTW uses TIME_ON for their +/- 30-minute time window
|
||||
QByteArray operator_call;
|
||||
QByteArray my_call;
|
||||
QByteArray my_grid;
|
||||
QByteArray exchange_sent;
|
||||
QByteArray exchange_rcvd;
|
||||
in >> time_off >> dx_call >> dx_grid >> dial_frequency >> mode >> report_sent >> report_received
|
||||
>> tx_power >> comments >> name >> time_on >> operator_call >> my_call >> my_grid
|
||||
>> exchange_sent >> exchange_rcvd;
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
Q_EMIT self_->qso_logged (id, time_off, QString::fromUtf8 (dx_call), QString::fromUtf8 (dx_grid)
|
||||
, dial_frequency, QString::fromUtf8 (mode), QString::fromUtf8 (report_sent)
|
||||
, QString::fromUtf8 (report_received), QString::fromUtf8 (tx_power)
|
||||
, QString::fromUtf8 (comments), QString::fromUtf8 (name), time_on
|
||||
, QString::fromUtf8 (operator_call), QString::fromUtf8 (my_call)
|
||||
, QString::fromUtf8 (my_grid), QString::fromUtf8 (exchange_sent)
|
||||
, QString::fromUtf8 (exchange_rcvd));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkMessage::Close:
|
||||
Q_EMIT self_->client_closed (id);
|
||||
clients_.remove (id);
|
||||
break;
|
||||
|
||||
case NetworkMessage::LoggedADIF:
|
||||
{
|
||||
QByteArray ADIF;
|
||||
in >> ADIF;
|
||||
if (check_status (in) != Fail)
|
||||
{
|
||||
Q_EMIT self_->logged_ADIF (id, ADIF);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Q_EMIT self_->error ("MessageServer warning: invalid UDP message received");
|
||||
}
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
Q_EMIT self_->error (QString {"MessageServer exception: %1"}.arg (e.what ()));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Q_EMIT self_->error ("Unexpected exception in MessageServer");
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::impl::tick ()
|
||||
{
|
||||
auto now = QDateTime::currentDateTime ();
|
||||
auto iter = std::begin (clients_);
|
||||
while (iter != std::end (clients_))
|
||||
{
|
||||
if (now > (*iter).last_activity_.addSecs (NetworkMessage::pulse))
|
||||
{
|
||||
Q_EMIT self_->clear_decodes (iter.key ());
|
||||
Q_EMIT self_->client_closed (iter.key ());
|
||||
iter = clients_.erase (iter); // safe while iterating as doesn't rehash
|
||||
}
|
||||
else
|
||||
{
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto MessageServer::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;
|
||||
}
|
||||
|
||||
MessageServer::MessageServer (QObject * parent, QString const& version, QString const& revision)
|
||||
: QObject {parent}
|
||||
, m_ {this, version, revision}
|
||||
{
|
||||
}
|
||||
|
||||
void MessageServer::start (port_type port, QHostAddress const& multicast_group_address)
|
||||
{
|
||||
if (port != m_->port_
|
||||
|| multicast_group_address != m_->multicast_group_address_)
|
||||
{
|
||||
m_->leave_multicast_group ();
|
||||
if (impl::BoundState == m_->state ())
|
||||
{
|
||||
m_->close ();
|
||||
}
|
||||
m_->multicast_group_address_ = multicast_group_address;
|
||||
auto address = m_->multicast_group_address_.isNull ()
|
||||
|| impl::IPv4Protocol != m_->multicast_group_address_.protocol () ? QHostAddress::Any : QHostAddress::AnyIPv4;
|
||||
if (port && m_->bind (address, port, m_->bind_mode_))
|
||||
{
|
||||
m_->port_ = port;
|
||||
m_->join_multicast_group ();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_->port_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::clear_decodes (QString const& id, quint8 window)
|
||||
{
|
||||
auto iter = m_->clients_.find (id);
|
||||
if (iter != std::end (m_->clients_))
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::Clear, id, (*iter).negotiated_schema_number_};
|
||||
out << window;
|
||||
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::reply (QString const& id, QTime time, qint32 snr, float delta_time
|
||||
, quint32 delta_frequency, QString const& mode
|
||||
, QString const& message_text, bool low_confidence, quint8 modifiers)
|
||||
{
|
||||
auto iter = m_->clients_.find (id);
|
||||
if (iter != std::end (m_->clients_))
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::Reply, id, (*iter).negotiated_schema_number_};
|
||||
out << time << snr << delta_time << delta_frequency << mode.toUtf8 ()
|
||||
<< message_text.toUtf8 () << low_confidence << modifiers;
|
||||
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::replay (QString const& id)
|
||||
{
|
||||
auto iter = m_->clients_.find (id);
|
||||
if (iter != std::end (m_->clients_))
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::Replay, id, (*iter).negotiated_schema_number_};
|
||||
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::halt_tx (QString const& id, bool auto_only)
|
||||
{
|
||||
auto iter = m_->clients_.find (id);
|
||||
if (iter != std::end (m_->clients_))
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::HaltTx, id, (*iter).negotiated_schema_number_};
|
||||
out << auto_only;
|
||||
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::free_text (QString const& id, QString const& text, bool send)
|
||||
{
|
||||
auto iter = m_->clients_.find (id);
|
||||
if (iter != std::end (m_->clients_))
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::FreeText, id, (*iter).negotiated_schema_number_};
|
||||
out << text.toUtf8 () << send;
|
||||
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::location (QString const& id, QString const& loc)
|
||||
{
|
||||
auto iter = m_->clients_.find (id);
|
||||
if (iter != std::end (m_->clients_))
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::Location, id, (*iter).negotiated_schema_number_};
|
||||
out << loc.toUtf8 ();
|
||||
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageServer::highlight_callsign (QString const& id, QString const& callsign
|
||||
, QColor const& bg, QColor const& fg, bool last_only)
|
||||
{
|
||||
auto iter = m_->clients_.find (id);
|
||||
if (iter != std::end (m_->clients_))
|
||||
{
|
||||
QByteArray message;
|
||||
NetworkMessage::Builder out {&message, NetworkMessage::HighlightCallsign, id, (*iter).negotiated_schema_number_};
|
||||
out << callsign.toUtf8 () << bg << fg << last_only;
|
||||
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#ifndef MESSAGE_SERVER_HPP__
|
||||
#define MESSAGE_SERVER_HPP__
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
#include <QDateTime>
|
||||
#include <QHostAddress>
|
||||
#include <QColor>
|
||||
|
||||
#include "udp_export.h"
|
||||
#include "Radio.hpp"
|
||||
|
||||
#include "pimpl_h.hpp"
|
||||
|
||||
class QString;
|
||||
|
||||
//
|
||||
// MessageServer - a reference implementation of a message server
|
||||
// matching the MessageClient class at the other end
|
||||
// of the wire
|
||||
//
|
||||
// This class is fully functioning and suitable for use in C++
|
||||
// applications that use the Qt framework. Other applications should
|
||||
// use this classes' implementation as a reference implementation.
|
||||
//
|
||||
class UDP_EXPORT MessageServer
|
||||
: public QObject
|
||||
{
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
using port_type = quint16;
|
||||
using Frequency = Radio::Frequency;
|
||||
|
||||
MessageServer (QObject * parent = nullptr,
|
||||
QString const& version = QString {}, QString const& revision = QString {});
|
||||
|
||||
// start or restart the server, if the multicast_group_address
|
||||
// argument is given it is assumed to be a multicast group address
|
||||
// which the server will join
|
||||
Q_SLOT void start (port_type port,
|
||||
QHostAddress const& multicast_group_address = QHostAddress {});
|
||||
|
||||
// ask the client to clear one or both of the decode windows
|
||||
Q_SLOT void clear_decodes (QString const& id, quint8 window = 0);
|
||||
|
||||
// ask the client with identification 'id' to make the same action
|
||||
// as a double click on the decode would
|
||||
//
|
||||
// note that the client is not obliged to take any action and only
|
||||
// takes any action if the decode is present and is a CQ or QRZ message
|
||||
Q_SLOT void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
|
||||
, QString const& mode, QString const& message, bool low_confidence, quint8 modifiers);
|
||||
|
||||
// ask the client with identification 'id' to replay all decodes
|
||||
Q_SLOT void replay (QString const& id);
|
||||
|
||||
// ask the client with identification 'id' to halt transmitting
|
||||
// auto_only just disables auto Tx, otherwise halt is immediate
|
||||
Q_SLOT void halt_tx (QString const& id, bool auto_only);
|
||||
|
||||
// ask the client with identification 'id' to set the free text
|
||||
// message and optionally send it ASAP
|
||||
Q_SLOT void free_text (QString const& id, QString const& text, bool send);
|
||||
|
||||
// ask the client with identification 'id' to set the location provided
|
||||
Q_SLOT void location (QString const& id, QString const& location);
|
||||
|
||||
// ask the client with identification 'id' to highlight the callsign
|
||||
// specified with the given colors
|
||||
Q_SLOT void highlight_callsign (QString const& id, QString const& callsign
|
||||
, QColor const& bg = QColor {}, QColor const& fg = QColor {}
|
||||
, bool last_only = false);
|
||||
|
||||
// the following signals are emitted when a client broadcasts the
|
||||
// matching message
|
||||
Q_SIGNAL void client_opened (QString const& id, QString const& version, QString const& revision);
|
||||
Q_SIGNAL void status_update (QString const& id, Frequency, 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
|
||||
, quint8 special_op_mode);
|
||||
Q_SIGNAL void client_closed (QString const& id);
|
||||
Q_SIGNAL void decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time
|
||||
, quint32 delta_frequency, QString const& mode, QString const& message
|
||||
, bool low_confidence, bool off_air);
|
||||
Q_SIGNAL void WSPR_decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time, Frequency
|
||||
, qint32 drift, QString const& callsign, QString const& grid, qint32 power
|
||||
, bool off_air);
|
||||
Q_SIGNAL void qso_logged (QString const& id, 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
|
||||
, QString const& exchange_sent, QString const& exchange_rcvd);
|
||||
Q_SIGNAL void decodes_cleared (QString const& id);
|
||||
Q_SIGNAL void logged_ADIF (QString const& id, QByteArray const& ADIF);
|
||||
|
||||
// this signal is emitted when a network error occurs
|
||||
Q_SIGNAL void error (QString const&) const;
|
||||
|
||||
private:
|
||||
class UDP_NO_EXPORT impl;
|
||||
pimpl<impl> m_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,76 @@
|
||||
#ifndef NETWORK_ACCESS_MANAGER_HPP__
|
||||
#define NETWORK_ACCESS_MANAGER_HPP__
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QList>
|
||||
#include <QSslError>
|
||||
#include <QNetworkReply>
|
||||
#include <QString>
|
||||
|
||||
#include "widgets/MessageBox.hpp"
|
||||
|
||||
class QNetworkRequest;
|
||||
class QIODevice;
|
||||
class QWidget;
|
||||
|
||||
// sub-class QNAM to keep a list of accepted SSL errors and allow
|
||||
// them in future replies
|
||||
class NetworkAccessManager
|
||||
: public QNetworkAccessManager
|
||||
{
|
||||
public:
|
||||
NetworkAccessManager (QWidget * parent)
|
||||
: QNetworkAccessManager (parent)
|
||||
{
|
||||
// handle SSL errors that have not been cached as allowed
|
||||
// exceptions and offer them to the user to add to the ignored
|
||||
// exception cache
|
||||
connect (this, &QNetworkAccessManager::sslErrors, [this, &parent] (QNetworkReply * reply, QList<QSslError> const& errors) {
|
||||
QString message;
|
||||
QList<QSslError> new_errors;
|
||||
for (auto const& error: errors)
|
||||
{
|
||||
if (!allowed_ssl_errors_.contains (error))
|
||||
{
|
||||
new_errors << error;
|
||||
message += '\n' + reply->request ().url ().toDisplayString () + ": "
|
||||
+ error.errorString ();
|
||||
}
|
||||
}
|
||||
if (new_errors.size ())
|
||||
{
|
||||
QString certs;
|
||||
for (auto const& cert : reply->sslConfiguration ().peerCertificateChain ())
|
||||
{
|
||||
certs += cert.toText () + '\n';
|
||||
}
|
||||
if (MessageBox::Ignore == MessageBox::query_message (parent, tr ("Network SSL Errors"), message, certs, MessageBox::Abort | MessageBox::Ignore))
|
||||
{
|
||||
// accumulate new SSL error exceptions that have been allowed
|
||||
allowed_ssl_errors_.append (new_errors);
|
||||
reply->ignoreSslErrors (allowed_ssl_errors_);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// no new exceptions so silently ignore the ones already allowed
|
||||
reply->ignoreSslErrors (allowed_ssl_errors_);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
QNetworkReply * createRequest (Operation operation, QNetworkRequest const& request, QIODevice * outgoing_data = nullptr) override
|
||||
{
|
||||
auto reply = QNetworkAccessManager::createRequest (operation, request, outgoing_data);
|
||||
// errors are usually certificate specific so passing all cached
|
||||
// exceptions here is ok
|
||||
reply->ignoreSslErrors (allowed_ssl_errors_);
|
||||
return reply;
|
||||
}
|
||||
|
||||
private:
|
||||
QList<QSslError> allowed_ssl_errors_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "NetworkMessage.hpp"
|
||||
|
||||
#include <exception>
|
||||
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
|
||||
#include "pimpl_impl.hpp"
|
||||
|
||||
namespace NetworkMessage
|
||||
{
|
||||
Builder::Builder (QIODevice * device, Type type, QString const& id, quint32 schema)
|
||||
: QDataStream {device}
|
||||
{
|
||||
common_initialization (type, id, schema);
|
||||
}
|
||||
|
||||
Builder::Builder (QByteArray * a, Type type, QString const& id, quint32 schema)
|
||||
: QDataStream {a, QIODevice::WriteOnly}
|
||||
{
|
||||
common_initialization (type, id, schema);
|
||||
}
|
||||
|
||||
void Builder::common_initialization (Type type, QString const& id, quint32 schema)
|
||||
{
|
||||
if (schema <= 1)
|
||||
{
|
||||
setVersion (QDataStream::Qt_5_0); // Qt schema version
|
||||
}
|
||||
#if QT_VERSION >= 0x050200
|
||||
else if (schema <= 2)
|
||||
{
|
||||
setVersion (QDataStream::Qt_5_2); // Qt schema version
|
||||
}
|
||||
#endif
|
||||
#if QT_VERSION >= 0x050400
|
||||
else if (schema <= 3)
|
||||
{
|
||||
setVersion (QDataStream::Qt_5_4); // Qt schema version
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
throw std::runtime_error {"Unrecognized message schema"};
|
||||
}
|
||||
|
||||
// the following two items assume that the quint32 encoding is
|
||||
// unchanged over QDataStream versions
|
||||
*this << magic;
|
||||
*this << schema;
|
||||
|
||||
*this << static_cast<quint32> (type) << id.toUtf8 ();
|
||||
}
|
||||
|
||||
class Reader::impl
|
||||
{
|
||||
public:
|
||||
void common_initialization (Reader * parent)
|
||||
{
|
||||
quint32 magic;
|
||||
*parent >> magic;
|
||||
if (magic != Builder::magic)
|
||||
{
|
||||
throw std::runtime_error {"Invalid message format"};
|
||||
}
|
||||
*parent >> schema_;
|
||||
if (schema_ > Builder::schema_number)
|
||||
{
|
||||
throw std::runtime_error {"Unrecognized message schema"};
|
||||
}
|
||||
if (schema_ <= 1)
|
||||
{
|
||||
parent->setVersion (QDataStream::Qt_5_0);
|
||||
}
|
||||
#if QT_VERSION >= 0x050200
|
||||
else if (schema_ <= 2)
|
||||
{
|
||||
parent->setVersion (QDataStream::Qt_5_2);
|
||||
}
|
||||
#endif
|
||||
#if QT_VERSION >= 0x050400
|
||||
else if (schema_ <= 3)
|
||||
{
|
||||
parent->setVersion (QDataStream::Qt_5_4);
|
||||
}
|
||||
#endif
|
||||
quint32 type;
|
||||
*parent >> type >> id_;
|
||||
if (type >= maximum_message_type_)
|
||||
{
|
||||
qDebug () << "Unrecognized message type:" << type << "from id:" << id_;
|
||||
type_ = maximum_message_type_;
|
||||
}
|
||||
else
|
||||
{
|
||||
type_ = static_cast<Type> (type);
|
||||
}
|
||||
}
|
||||
|
||||
quint32 schema_;
|
||||
Type type_;
|
||||
QByteArray id_;
|
||||
};
|
||||
|
||||
Reader::Reader (QIODevice * device)
|
||||
: QDataStream {device}
|
||||
{
|
||||
m_->common_initialization (this);
|
||||
}
|
||||
|
||||
Reader::Reader (QByteArray const& a)
|
||||
: QDataStream {a}
|
||||
{
|
||||
m_->common_initialization (this);
|
||||
}
|
||||
|
||||
Reader::~Reader ()
|
||||
{
|
||||
}
|
||||
|
||||
quint32 Reader::schema () const
|
||||
{
|
||||
return m_->schema_;
|
||||
}
|
||||
|
||||
Type Reader::type () const
|
||||
{
|
||||
return static_cast<Type> (m_->type_);
|
||||
}
|
||||
|
||||
QString Reader::id () const
|
||||
{
|
||||
return QString::fromUtf8 (m_->id_);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
#ifndef NETWORK_MESSAGE_HPP__
|
||||
#define NETWORK_MESSAGE_HPP__
|
||||
|
||||
/*
|
||||
* WSJT-X Message Formats
|
||||
* ======================
|
||||
*
|
||||
* All messages are written or read using the QDataStream derivatives
|
||||
* defined below, note that we are using the default for floating
|
||||
* point precision which means all are double precision i.e. 64-bit
|
||||
* IEEE format.
|
||||
*
|
||||
* Message is big endian format
|
||||
*
|
||||
* Header format:
|
||||
*
|
||||
* 32-bit unsigned integer magic number 0xadbccbda
|
||||
* 32-bit unsigned integer schema number
|
||||
*
|
||||
* Payload format:
|
||||
*
|
||||
* As per the QDataStream format, see below for version used and
|
||||
* here:
|
||||
*
|
||||
* http://doc.qt.io/qt-5/datastreamformat.html
|
||||
*
|
||||
* for the serialization details for each type, at the time of
|
||||
* writing the above document is for Qt_5_0 format which is buggy
|
||||
* so we use Qt_5_4 format, differences are:
|
||||
*
|
||||
* QDateTime:
|
||||
* QDate qint64 Julian day number
|
||||
* QTime quint32 Milli-seconds since midnight
|
||||
* timespec quint8 0=local, 1=UTC, 2=Offset from UTC
|
||||
* (seconds)
|
||||
* 3=time zone
|
||||
* offset qint32 only present if timespec=2
|
||||
* timezone several-fields only present if timespec=3
|
||||
*
|
||||
* we will avoid using QDateTime fields with time zones for simplicity.
|
||||
*
|
||||
* Type utf8 is a utf-8 byte string formatted as a QByteArray for
|
||||
* serialization purposes (currently a quint32 size followed by size
|
||||
* bytes, no terminator is present or counted).
|
||||
*
|
||||
* The QDataStream format document linked above is not complete for
|
||||
* the QByteArray serialization format, it is similar to the QString
|
||||
* serialization format in that it differentiates between empty
|
||||
* strings and null strings. Empty strings have a length of zero
|
||||
* whereas null strings have a length field of 0xffffffff.
|
||||
*
|
||||
* Schema Negotiation
|
||||
* ------------------
|
||||
*
|
||||
* The NetworkMessage::Builder class specifies a schema number which
|
||||
* may be incremented from time to time. It represents a version of
|
||||
* the underlying encoding schemes used to store data items. Since the
|
||||
* underlying encoding is defined by the Qt project in it's
|
||||
* QDataStream stream operators, it is essential that clients and
|
||||
* servers of this protocol can agree on a common scheme. The
|
||||
* NetworkMessage utility classes below exchange the schema number
|
||||
* actually used. The handling of the schema is backwards compatible
|
||||
* to an extent, so long as clients and servers are written
|
||||
* correctly. For example a server written to any particular schema
|
||||
* version can communicate with a client written to a later schema.
|
||||
*
|
||||
* Schema Version 1:- this schema used the QDataStream::Qt_5_0 version
|
||||
* which is broken.
|
||||
*
|
||||
* Schema Version 2:- this schema uses the QDataStream::Qt_5_2 version.
|
||||
*
|
||||
* Schema Version 3:- this schema uses the QDataStream::Qt_5_4 version.
|
||||
*
|
||||
*
|
||||
*
|
||||
* Message Direction Value Type
|
||||
* ------------- --------- ---------------------- -----------
|
||||
* Heartbeat Out/In 0 quint32
|
||||
* Id (unique key) utf8
|
||||
* Maximum schema number quint32
|
||||
* version utf8
|
||||
* revision utf8
|
||||
*
|
||||
* The heartbeat message shall be sent on a periodic basis every
|
||||
* NetworkMessage::pulse seconds (see below), the WSJT-X
|
||||
* application does that using the MessageClient class. This
|
||||
* message is intended to be used by servers to detect the presence
|
||||
* of a client and also the unexpected disappearance of a client
|
||||
* and by clients to learn the schema negotiated by the server
|
||||
* after it receives the initial heartbeat message from a client.
|
||||
* The message_aggregator reference server does just that using the
|
||||
* MessageServer class. Upon initial startup a client must send a
|
||||
* heartbeat message as soon as is practical, this message is used
|
||||
* to negotiate the maximum schema number common to the client and
|
||||
* server. Note that the server may not be able to support the
|
||||
* client's requested maximum schema number, in which case the
|
||||
* first message received from the server will specify a lower
|
||||
* schema number (never a higher one as that is not allowed). If a
|
||||
* server replies with a lower schema number then no higher than
|
||||
* that number shall be used for all further outgoing messages from
|
||||
* either clients or the server itself.
|
||||
*
|
||||
* Note: the "Maximum schema number" field was introduced at the
|
||||
* same time as schema 3, therefore servers and clients must assume
|
||||
* schema 2 is the highest schema number supported if the Heartbeat
|
||||
* message does not contain the "Maximum schema number" field.
|
||||
*
|
||||
*
|
||||
* Status Out 1 quint32
|
||||
* Id (unique key) utf8
|
||||
* Dial Frequency (Hz) quint64
|
||||
* Mode utf8
|
||||
* DX call utf8
|
||||
* Report utf8
|
||||
* Tx Mode utf8
|
||||
* Tx Enabled bool
|
||||
* Transmitting bool
|
||||
* Decoding bool
|
||||
* Rx DF qint32
|
||||
* Tx DF qint32
|
||||
* DE call utf8
|
||||
* DE grid utf8
|
||||
* DX grid utf8
|
||||
* Tx Watchdog bool
|
||||
* Sub-mode utf8
|
||||
* Fast mode bool
|
||||
* Special operation mode quint8
|
||||
*
|
||||
* WSJT-X sends this status message when various internal state
|
||||
* changes to allow the server to track the relevant state of each
|
||||
* client without the need for polling commands. The current state
|
||||
* changes that generate status messages are:
|
||||
*
|
||||
* Application start up,
|
||||
* "Enable Tx" button status changes,
|
||||
* Dial frequency changes,
|
||||
* Changes to the "DX Call" field,
|
||||
* Operating mode, sub-mode or fast mode changes,
|
||||
* Transmit mode changed (in dual JT9+JT65 mode),
|
||||
* Changes to the "Rpt" spinner,
|
||||
* After an old decodes replay sequence (see Replay below),
|
||||
* When switching between Tx and Rx mode,
|
||||
* At the start and end of decoding,
|
||||
* When the Rx DF changes,
|
||||
* When the Tx DF changes,
|
||||
* When settings are exited,
|
||||
* When the DX call or grid changes,
|
||||
* When the Tx watchdog is set or reset.
|
||||
*
|
||||
* The Special operation mode is an enumeration that indicates the
|
||||
* setting selected in the WSJT-X "Settings->Advanced->Special
|
||||
* operating activity" panel. The values are as follows:
|
||||
*
|
||||
* 0 -> NONE
|
||||
* 1 -> NA VHF
|
||||
* 2 -> EU VHF
|
||||
* 3 -> FIELD DAY
|
||||
* 4 -> RTTY RU
|
||||
* 5 -> FOX
|
||||
* 6 -> HOUND
|
||||
*
|
||||
*
|
||||
* Decode Out 2 quint32
|
||||
* Id (unique key) utf8
|
||||
* New bool
|
||||
* Time QTime
|
||||
* snr qint32
|
||||
* Delta time (S) float (serialized as double)
|
||||
* Delta frequency (Hz) quint32
|
||||
* Mode utf8
|
||||
* Message utf8
|
||||
* Low confidence bool
|
||||
* Off air bool
|
||||
*
|
||||
* The decode message is sent when a new decode is completed, in
|
||||
* this case the 'New' field is true. It is also used in response
|
||||
* to a "Replay" message where each old decode in the "Band
|
||||
* activity" window, that has not been erased, is sent in order
|
||||
* as a one of these messages with the 'New' field set to false.
|
||||
* See the "Replay" message below for details of usage. Low
|
||||
* confidence decodes are flagged in protocols where the decoder
|
||||
* has knows that a decode has a higher than normal probability
|
||||
* of being false, they should not be reported on publicly
|
||||
* accessible services without some attached warning or further
|
||||
* validation. Off air decodes are those that result from playing
|
||||
* back a .WAV file.
|
||||
*
|
||||
*
|
||||
* Clear Out/In 3 quint32
|
||||
* Id (unique key) utf8
|
||||
* Window quint8 (In only)
|
||||
*
|
||||
* This message is send when all prior "Decode" messages in the
|
||||
* "Band Activity" window have been discarded and therefore are
|
||||
* no long available for actioning with a "Reply" message. It is
|
||||
* sent when the user erases the "Band activity" window and when
|
||||
* WSJT-X closes down normally. The server should discard all
|
||||
* decode messages upon receipt of this message.
|
||||
*
|
||||
* It may also be sent to a WSJT-X instance in which case it
|
||||
* clears one or both of the "Band Activity" and "Rx Frequency"
|
||||
* windows. The Window argument can be one of the following
|
||||
* values:
|
||||
*
|
||||
* 0 - clear the "Band Activity" window (default)
|
||||
* 1 - clear the "Rx Frequency" window
|
||||
* 2 - clear both "Band Activity" and "Rx Frequency" windows
|
||||
*
|
||||
*
|
||||
* Reply In 4 quint32
|
||||
* Id (target unique key) utf8
|
||||
* Time QTime
|
||||
* snr qint32
|
||||
* Delta time (S) float (serialized as double)
|
||||
* Delta frequency (Hz) quint32
|
||||
* Mode utf8
|
||||
* Message utf8
|
||||
* Low confidence bool
|
||||
* Modifiers quint8
|
||||
*
|
||||
* In order for a server to provide a useful cooperative service
|
||||
* to WSJT-X it is possible for it to initiate a QSO by sending
|
||||
* this message to a client. WSJT-X filters this message and only
|
||||
* acts upon it if the message exactly describes a prior decode
|
||||
* and that decode is a CQ or QRZ message. The action taken is
|
||||
* exactly equivalent to the user double clicking the message in
|
||||
* the "Band activity" window. The intent of this message is for
|
||||
* servers to be able to provide an advanced look up of potential
|
||||
* QSO partners, for example determining if they have been worked
|
||||
* before or if working them may advance some objective like
|
||||
* award progress. The intention is not to provide a secondary
|
||||
* user interface for WSJT-X, it is expected that after QSO
|
||||
* initiation the rest of the QSO is carried out manually using
|
||||
* the normal WSJT-X user interface.
|
||||
*
|
||||
* The Modifiers field allows the equivalent of keyboard
|
||||
* modifiers to be sent "as if" those modifier keys where pressed
|
||||
* while double-clicking the specified decoded message. The
|
||||
* modifier values (hexadecimal) are as follows:
|
||||
*
|
||||
* no modifier 0x00
|
||||
* SHIFT 0x02
|
||||
* CTRL 0x04 CMD on Mac
|
||||
* ALT 0x08
|
||||
* META 0x10 Windows key on MS Windows
|
||||
* KEYPAD 0x20 Keypad or arrows
|
||||
* Group switch 0x40 X11 only
|
||||
*
|
||||
*
|
||||
* QSO Logged Out 5 quint32
|
||||
* Id (unique key) utf8
|
||||
* Date & Time Off QDateTime
|
||||
* DX call utf8
|
||||
* DX grid utf8
|
||||
* Tx frequency (Hz) quint64
|
||||
* Mode utf8
|
||||
* Report sent utf8
|
||||
* Report received utf8
|
||||
* Tx power utf8
|
||||
* Comments utf8
|
||||
* Name utf8
|
||||
* Date & Time On QDateTime
|
||||
* Operator call utf8
|
||||
* My call utf8
|
||||
* My grid utf8
|
||||
* Exchange sent utf8
|
||||
* Exchange received utf8
|
||||
*
|
||||
* The QSO logged message is sent to the server(s) when the
|
||||
* WSJT-X user accepts the "Log QSO" dialog by clicking the "OK"
|
||||
* button.
|
||||
*
|
||||
*
|
||||
* Close Out 6 quint32
|
||||
* Id (unique key) utf8
|
||||
*
|
||||
* Close is sent by a client immediately prior to it shutting
|
||||
* down gracefully.
|
||||
*
|
||||
*
|
||||
* Replay In 7 quint32
|
||||
* Id (unique key) utf8
|
||||
*
|
||||
* When a server starts it may be useful for it to determine the
|
||||
* state of preexisting clients. Sending this message to each
|
||||
* client as it is discovered will cause that client (WSJT-X) to
|
||||
* send a "Decode" message for each decode currently in its "Band
|
||||
* activity" window. Each "Decode" message sent will have the
|
||||
* "New" flag set to false so that they can be distinguished from
|
||||
* new decodes. After all the old decodes have been broadcast a
|
||||
* "Status" message is also broadcast. If the server wishes to
|
||||
* determine the status of a newly discovered client; this
|
||||
* message should be used.
|
||||
*
|
||||
*
|
||||
* Halt Tx In 8
|
||||
* Id (unique key) utf8
|
||||
* Auto Tx Only bool
|
||||
*
|
||||
* The server may stop a client from transmitting messages either
|
||||
* immediately or at the end of the current transmission period
|
||||
* using this message.
|
||||
*
|
||||
*
|
||||
* Free Text In 9
|
||||
* Id (unique key) utf8
|
||||
* Text utf8
|
||||
* Send bool
|
||||
*
|
||||
* This message allows the server to set the current free text
|
||||
* message content. Sending this message with a non-empty "Text"
|
||||
* field is equivalent to typing a new message (old contents are
|
||||
* discarded) in to the WSJT-X free text message field or "Tx5"
|
||||
* field (both are updated) and if the "Send" flag is set then
|
||||
* clicking the "Now" radio button for the "Tx5" field if tab one
|
||||
* is current or clicking the "Free msg" radio button if tab two
|
||||
* is current.
|
||||
*
|
||||
* It is the responsibility of the sender to limit the length of
|
||||
* the message text and to limit it to legal message
|
||||
* characters. Despite this, it may be difficult for the sender
|
||||
* to determine the maximum message length without reimplementing
|
||||
* the complete message encoding protocol. Because of this is may
|
||||
* be better to allow any reasonable message length and to let
|
||||
* the WSJT-X application encode and possibly truncate the actual
|
||||
* on-air message.
|
||||
*
|
||||
* If the message text is empty the meaning of the message is
|
||||
* refined to send the current free text unchanged when the
|
||||
* "Send" flag is set or to clear the current free text when the
|
||||
* "Send" flag is unset. Note that this API does not include a
|
||||
* command to determine the contents of the current free text
|
||||
* message.
|
||||
*
|
||||
*
|
||||
* WSPRDecode Out 10 quint32
|
||||
* Id (unique key) utf8
|
||||
* New bool
|
||||
* Time QTime
|
||||
* snr qint32
|
||||
* Delta time (S) float (serialized as double)
|
||||
* Frequency (Hz) quint64
|
||||
* Drift (Hz) qint32
|
||||
* Callsign utf8
|
||||
* Grid utf8
|
||||
* Power (dBm) qint32
|
||||
* Off air bool
|
||||
*
|
||||
* The decode message is sent when a new decode is completed, in
|
||||
* this case the 'New' field is true. It is also used in response
|
||||
* to a "Replay" message where each old decode in the "Band
|
||||
* activity" window, that has not been erased, is sent in order
|
||||
* as a one of these messages with the 'New' field set to
|
||||
* false. See the "Replay" message below for details of
|
||||
* usage. The off air field indicates that the decode was decoded
|
||||
* from a played back recording.
|
||||
*
|
||||
*
|
||||
* Location In 11
|
||||
* Id (unique key) utf8
|
||||
* Location utf8
|
||||
*
|
||||
* This message allows the server to set the current current
|
||||
* geographical location of operation. The supplied location is
|
||||
* not persistent but is used as a session lifetime replacement
|
||||
* loction that overrides the Maidenhead grid locater set in the
|
||||
* application settings. The intent is to allow an external
|
||||
* application to update the operating location dynamically
|
||||
* during a mobile period of operation.
|
||||
*
|
||||
* Currently only Maidenhead grid squares or sub-squares are
|
||||
* accepted, i.e. 4- or 6-digit locators. Other formats may be
|
||||
* accepted in future.
|
||||
*
|
||||
*
|
||||
* Logged ADIF Out 12 quint32
|
||||
* Id (unique key) utf8
|
||||
* ADIF text utf8
|
||||
*
|
||||
* The logged ADIF message is sent to the server(s) when the
|
||||
* WSJT-X user accepts the "Log QSO" dialog by clicking the "OK"
|
||||
* button. The "ADIF text" field consists of a valid ADIF file
|
||||
* such that the WSJT-X UDP header information is encapsulated
|
||||
* into a valid ADIF header. E.g.:
|
||||
*
|
||||
* <magic-number><schema-number><type><id><32-bit-count> # binary encoded fields
|
||||
* # the remainder is the contents of the ADIF text field
|
||||
* <adif_ver:5>3.0.7
|
||||
* <programid:6>WSJT-X
|
||||
* <EOH>
|
||||
* ADIF log data fields ...<EOR>
|
||||
*
|
||||
* Note that receiving applications can treat the whole message
|
||||
* as a valid ADIF file with one record without special parsing.
|
||||
*
|
||||
*
|
||||
* Highlight Callsign In 13 quint32
|
||||
* Id (unique key) utf8
|
||||
* Callsign utf8
|
||||
* Background Color QColor
|
||||
* Foreground Color QColor
|
||||
* Highlight last bool
|
||||
*
|
||||
* The server may send this message at any time. The message
|
||||
* specifies the background and foreground color that will be
|
||||
* used to highlight the specified callsign in the decoded
|
||||
* messages printed in the Band Activity panel. The WSJT-X
|
||||
* clients maintain a list of such instructions and apply them to
|
||||
* all decoded messages in the band activity window. To clear
|
||||
* highlighting send an invalid QColor value for either or both
|
||||
* of the background and foreground fields.
|
||||
*
|
||||
* The "Highlight last" field allows the sender to request that
|
||||
* the last instance only instead of all instances of the
|
||||
* specified call be highlighted or have it's highlighting
|
||||
* cleared.
|
||||
*/
|
||||
|
||||
#include <QDataStream>
|
||||
|
||||
#include "pimpl_h.hpp"
|
||||
|
||||
class QIODevice;
|
||||
class QByteArray;
|
||||
class QString;
|
||||
|
||||
namespace NetworkMessage
|
||||
{
|
||||
// NEVER DELETE MESSAGE TYPES
|
||||
enum Type
|
||||
{
|
||||
Heartbeat,
|
||||
Status,
|
||||
Decode,
|
||||
Clear,
|
||||
Reply,
|
||||
QSOLogged,
|
||||
Close,
|
||||
Replay,
|
||||
HaltTx,
|
||||
FreeText,
|
||||
WSPRDecode,
|
||||
Location,
|
||||
LoggedADIF,
|
||||
HighlightCallsign,
|
||||
maximum_message_type_ // ONLY add new message types
|
||||
// immediately before here
|
||||
};
|
||||
|
||||
quint32 constexpr pulse {15}; // seconds
|
||||
|
||||
//
|
||||
// NetworkMessage::Builder - build a message containing serialized Qt types
|
||||
//
|
||||
class Builder
|
||||
: public QDataStream
|
||||
{
|
||||
public:
|
||||
static quint32 constexpr magic {0xadbccbda}; // never change this
|
||||
|
||||
// increment this if a newer Qt schema is required and add decode
|
||||
// logic to the Builder and Reader class implementations
|
||||
#if QT_VERSION >= 0x050400
|
||||
static quint32 constexpr schema_number {3};
|
||||
#elif QT_VERSION >= 0x050200
|
||||
static quint32 constexpr schema_number {2};
|
||||
#else
|
||||
// Schema 1 (Qt_5_0) is broken
|
||||
#error "Qt version 5.2 or greater required"
|
||||
#endif
|
||||
|
||||
explicit Builder (QIODevice *, Type, QString const& id, quint32 schema);
|
||||
explicit Builder (QByteArray *, Type, QString const& id, quint32 schema);
|
||||
Builder (Builder const&) = delete;
|
||||
Builder& operator = (Builder const&) = delete;
|
||||
|
||||
private:
|
||||
void common_initialization (Type type, QString const& id, quint32 schema);
|
||||
};
|
||||
|
||||
//
|
||||
// NetworkMessage::Reader - read a message containing serialized Qt types
|
||||
//
|
||||
// Message is as per NetworkMessage::Builder above, the schema()
|
||||
// member may be used to determine the schema of the original
|
||||
// message.
|
||||
//
|
||||
class Reader
|
||||
: public QDataStream
|
||||
{
|
||||
public:
|
||||
explicit Reader (QIODevice *);
|
||||
explicit Reader (QByteArray const&);
|
||||
Reader (Reader const&) = delete;
|
||||
Reader& operator = (Reader const&) = delete;
|
||||
~Reader ();
|
||||
|
||||
quint32 schema () const;
|
||||
Type type () const;
|
||||
QString id () const;
|
||||
|
||||
private:
|
||||
class impl;
|
||||
pimpl<impl> m_;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "NetworkServerLookup.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <QHostInfo>
|
||||
#include <QString>
|
||||
|
||||
std::tuple<QHostAddress, quint16>
|
||||
network_server_lookup (QString query
|
||||
, quint16 default_service_port
|
||||
, QHostAddress default_host_address
|
||||
, QAbstractSocket::NetworkLayerProtocol required_protocol)
|
||||
{
|
||||
query = query.trimmed ();
|
||||
|
||||
QHostAddress host_address {default_host_address};
|
||||
quint16 service_port {default_service_port};
|
||||
|
||||
QString host_name;
|
||||
if (!query.isEmpty ())
|
||||
{
|
||||
int port_colon_index {-1};
|
||||
|
||||
if ('[' == query[0])
|
||||
{
|
||||
// assume IPv6 combined address/port syntax [<address>]:<port>
|
||||
auto close_bracket_index = query.lastIndexOf (']');
|
||||
host_name = query.mid (1, close_bracket_index - 1);
|
||||
port_colon_index = query.indexOf (':', close_bracket_index);
|
||||
}
|
||||
else
|
||||
{
|
||||
port_colon_index = query.lastIndexOf (':');
|
||||
host_name = query.left (port_colon_index);
|
||||
}
|
||||
host_name = host_name.trimmed ();
|
||||
|
||||
if (port_colon_index >= 0)
|
||||
{
|
||||
bool ok;
|
||||
service_port = query.mid (port_colon_index + 1).trimmed ().toUShort (&ok);
|
||||
if (!ok)
|
||||
{
|
||||
throw std::runtime_error {"network server lookup error: invalid port"};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!host_name.isEmpty ())
|
||||
{
|
||||
auto host_info = QHostInfo::fromName (host_name);
|
||||
if (host_info.addresses ().isEmpty ())
|
||||
{
|
||||
throw std::runtime_error {"network server lookup error: host name lookup failed"};
|
||||
}
|
||||
|
||||
bool found {false};
|
||||
for (int i {0}; i < host_info.addresses ().size () && !found; ++i)
|
||||
{
|
||||
host_address = host_info.addresses ().at (i);
|
||||
switch (required_protocol)
|
||||
{
|
||||
case QAbstractSocket::IPv4Protocol:
|
||||
case QAbstractSocket::IPv6Protocol:
|
||||
if (required_protocol != host_address.protocol ())
|
||||
{
|
||||
break;
|
||||
}
|
||||
// drop through
|
||||
|
||||
case QAbstractSocket::AnyIPProtocol:
|
||||
found = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw std::runtime_error {"network server lookup error: invalid required protocol"};
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
throw std::runtime_error {"network server lookup error: no suitable host address found"};
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_tuple (host_address, service_port);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef NETWORK_SERVER_LOOKUP_HPP__
|
||||
#define NETWORK_SERVER_LOOKUP_HPP__
|
||||
|
||||
#include <tuple>
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QAbstractSocket>
|
||||
|
||||
class QString;
|
||||
|
||||
//
|
||||
// Do a blocking DNS lookup using query as a destination host address
|
||||
// and port.
|
||||
//
|
||||
// query can be one of:
|
||||
//
|
||||
// 1) "" (empty string) - use defaults
|
||||
// 2) ":nnnnn" - override default service port with port nnnnn
|
||||
// 3) "<valid-host-name>" - override default host address with DNS lookup
|
||||
// 4) "nnn.nnn.nnn.nnn" - override default host address with the IPv4 address given by nnn.nnn.nnn.nnn
|
||||
// 5) "[<valid-IPv6-address]" - override default host address with the given IPv6 address
|
||||
// 6) "<valid-host-name>:nnnnn" - use as per (3) & (2)
|
||||
// 7) "nnn.nnn.nnn.nnn:nnnnn" - use as per (4) & (2)
|
||||
// 8) "[<valid-IPv6-address]:nnnnn" - use as per (5) & (2)
|
||||
//
|
||||
// The first host address matching the protocol and the service port
|
||||
// number are returned.
|
||||
//
|
||||
// If no suitable host address is found QHostAddress::Null will be
|
||||
// returned in the first member of the result tuple.
|
||||
//
|
||||
std::tuple<QHostAddress, quint16>
|
||||
network_server_lookup (QString query
|
||||
, quint16 default_service_port
|
||||
, QHostAddress default_host_address = QHostAddress::LocalHost
|
||||
, QAbstractSocket::NetworkLayerProtocol protocol = QAbstractSocket::AnyIPProtocol);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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 <QHostInfo>
|
||||
#include <QTimer>
|
||||
|
||||
#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
|
||||
|
||||
|
||||
m_randomId_h = QString("%1").arg(qrand(),8,16,QChar('0'));
|
||||
|
||||
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<QString,QString> 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<QString,QString> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// -*- Mode: C++ -*-
|
||||
#ifndef PSK_REPORTER_H
|
||||
#define PSK_REPORTER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QHostAddress>
|
||||
#include <QQueue>
|
||||
#include <QHash>
|
||||
|
||||
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<QString,QString> > m_spotQueue;
|
||||
|
||||
MessageClient * m_messageClient;
|
||||
|
||||
QTimer *reportTimer;
|
||||
|
||||
int m_sequenceNumber;
|
||||
};
|
||||
|
||||
#endif // PSK_REPORTER_H
|
||||
Reference in New Issue
Block a user