mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
700 lines
26 KiB
C++
700 lines
26 KiB
C++
/**
|
|
* @brief WSJT-X compatible message client for JS8Call
|
|
*
|
|
* This class implements the WSJT-X UDP binary protocol for inter-application
|
|
* communication. It allows JS8Call to send and receive messages compatible
|
|
* with WSJT-X and other applications that support the WSJT-X protocol.
|
|
*
|
|
* Based on WSJT-X MessageClient but adapted for JS8Call.
|
|
*/
|
|
|
|
#include "WSJTXMessageClient.h"
|
|
#include "JS8_Include/pimpl_impl.h"
|
|
#include "JS8_Main/qt_helpers.h"
|
|
#include "NetworkMessage.h"
|
|
|
|
#include <QByteArray>
|
|
#include <QColor>
|
|
#include <QDebug>
|
|
#include <QHostInfo>
|
|
#include <QLoggingCategory>
|
|
#include <QNetworkInterface>
|
|
#include <QQueue>
|
|
#include <QTextStream>
|
|
#include <QTimer>
|
|
#include <QUdpSocket>
|
|
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
#include "moc_WSJTXMessageClient.cpp"
|
|
|
|
Q_LOGGING_CATEGORY(wsjtx_js8, "wsjtx.js8", QtWarningMsg)
|
|
|
|
// some trace macros
|
|
#define TRACE_UDP(MSG)
|
|
|
|
/**
|
|
* @brief Helper function to dump binary payload in human-readable format
|
|
*
|
|
* Creates a hex dump of the message payload for debugging purposes.
|
|
* Shows the magic number, message size, and first 64 bytes in hex and ASCII.
|
|
*
|
|
* @param message Binary message to dump
|
|
* @return Formatted string with hex dump
|
|
*/
|
|
static QString dump_payload(QByteArray const &message) {
|
|
QString result;
|
|
QTextStream stream(&result);
|
|
|
|
// Show first 4 bytes as magic number (if present)
|
|
if (message.size() >= 4) {
|
|
quint32 magic = 0;
|
|
// Read magic number in big-endian format
|
|
magic = (static_cast<quint8>(message[0]) << 24) |
|
|
(static_cast<quint8>(message[1]) << 16) |
|
|
(static_cast<quint8>(message[2]) << 8) |
|
|
static_cast<quint8>(message[3]);
|
|
stream << QString("Magic: 0x%1 ").arg(magic, 8, 16, QChar('0'));
|
|
}
|
|
|
|
// Show size and hex dump of first 64 bytes
|
|
stream << QString("Size: %1 bytes\n").arg(message.size());
|
|
stream << "Hex dump (first 64 bytes):\n";
|
|
int dump_size = qMin(64, message.size());
|
|
for (int i = 0; i < dump_size; i += 16) {
|
|
stream << QString("%1: ").arg(i, 4, 16, QChar('0'));
|
|
for (int j = 0; j < 16 && (i + j) < dump_size; ++j) {
|
|
stream << QString("%1 ").arg(static_cast<quint8>(message[i + j]), 2,
|
|
16, QChar('0'));
|
|
}
|
|
stream << " | ";
|
|
for (int j = 0; j < 16 && (i + j) < dump_size; ++j) {
|
|
char c = message[i + j];
|
|
stream << (QChar::isPrint(c) ? QChar(c) : QChar('.'));
|
|
}
|
|
stream << "\n";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
class WSJTXMessageClient::impl : public QUdpSocket {
|
|
Q_OBJECT;
|
|
|
|
public:
|
|
impl(QString const &id, QString const &version, QString const &revision,
|
|
port_type server_port, int TTL, WSJTXMessageClient *self)
|
|
: self_{self}, enabled_{false}, id_{id}, version_{version},
|
|
revision_{revision}, dns_lookup_id_{-1}, server_port_{server_port},
|
|
TTL_{TTL},
|
|
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);
|
|
}
|
|
|
|
~impl() {
|
|
closedown();
|
|
if (dns_lookup_id_ != -1) {
|
|
QHostInfo::abortHostLookup(dns_lookup_id_);
|
|
}
|
|
}
|
|
|
|
enum StreamStatus { Fail, Short, OK };
|
|
|
|
void set_server(QString const &server_name,
|
|
QStringList const &network_interface_names);
|
|
Q_SLOT void host_info_results(QHostInfo);
|
|
void start();
|
|
void parse_message(QByteArray const &);
|
|
void pending_datagrams();
|
|
void heartbeat();
|
|
void closedown();
|
|
StreamStatus check_status(QDataStream const &) const;
|
|
void send_message(QByteArray const &, bool queue_if_pending = true,
|
|
bool allow_duplicates = false);
|
|
void send_message(QDataStream const &out, QByteArray const &message,
|
|
bool queue_if_pending = true,
|
|
bool allow_duplicates = false) {
|
|
if (OK == check_status(out)) {
|
|
send_message(message, queue_if_pending, allow_duplicates);
|
|
} else {
|
|
Q_EMIT self_->error("Error creating UDP message");
|
|
}
|
|
}
|
|
|
|
WSJTXMessageClient *self_;
|
|
bool enabled_;
|
|
QString id_;
|
|
QString version_;
|
|
QString revision_;
|
|
int dns_lookup_id_;
|
|
QHostAddress server_;
|
|
port_type server_port_;
|
|
int TTL_;
|
|
std::vector<QNetworkInterface> network_interfaces_;
|
|
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 "WSJTXMessageClient.moc"
|
|
|
|
void WSJTXMessageClient::impl::set_server(
|
|
QString const &server_name, QStringList const &network_interface_names) {
|
|
server_.setAddress(server_name);
|
|
network_interfaces_.clear();
|
|
for (auto const &net_if_name : network_interface_names) {
|
|
network_interfaces_.push_back(
|
|
QNetworkInterface::interfaceFromName(net_if_name));
|
|
}
|
|
|
|
if (server_.isNull() && server_name.size()) // DNS lookup required
|
|
{
|
|
// queue a host address lookup
|
|
dns_lookup_id_ = QHostInfo::lookupHost(server_name, this, &WSJTXMessageClient::impl::host_info_results);
|
|
|
|
} else {
|
|
start();
|
|
}
|
|
}
|
|
|
|
void WSJTXMessageClient::impl::host_info_results(QHostInfo host_info) {
|
|
if (host_info.lookupId() != dns_lookup_id_)
|
|
return;
|
|
dns_lookup_id_ = -1;
|
|
if (QHostInfo::NoError != host_info.error()) {
|
|
Q_EMIT self_->error("UDP server DNS lookup failed: " +
|
|
host_info.errorString());
|
|
return;
|
|
} else {
|
|
auto const &server_addresses = host_info.addresses();
|
|
if (server_addresses.size()) {
|
|
server_ = server_addresses[0];
|
|
}
|
|
}
|
|
start();
|
|
}
|
|
|
|
void WSJTXMessageClient::impl::start() {
|
|
if (server_.isNull()) {
|
|
Q_EMIT self_->close();
|
|
pending_messages_.clear(); // discard
|
|
return;
|
|
}
|
|
|
|
if (is_broadcast_address(server_)) {
|
|
Q_EMIT self_->error(
|
|
"IPv4 broadcast not supported, please specify the loop-back "
|
|
"address, a server host address, or multicast group address");
|
|
pending_messages_.clear(); // discard
|
|
return;
|
|
}
|
|
|
|
if (blocked_addresses_.end() != std::find(blocked_addresses_.begin(),
|
|
blocked_addresses_.end(),
|
|
server_)) {
|
|
Q_EMIT self_->error("UDP server blocked, please try another");
|
|
pending_messages_.clear(); // discard
|
|
return;
|
|
}
|
|
|
|
TRACE_UDP("Trying server:" << server_.toString());
|
|
QHostAddress interface_addr{IPv6Protocol == server_.protocol()
|
|
? QHostAddress::AnyIPv6
|
|
: QHostAddress::AnyIPv4};
|
|
|
|
if (localAddress() != interface_addr) {
|
|
if (UnconnectedState != state() || state()) {
|
|
close();
|
|
}
|
|
// bind to an ephemeral port on the selected interface and set
|
|
// up for sending datagrams
|
|
bind(interface_addr);
|
|
// set multicast TTL to limit scope when sending to multicast
|
|
// group addresses
|
|
setSocketOption(MulticastTtlOption, TTL_);
|
|
}
|
|
|
|
// send initial heartbeat which allows schema negotiation
|
|
heartbeat();
|
|
|
|
// clear any backlog
|
|
while (pending_messages_.size()) {
|
|
send_message(pending_messages_.dequeue(), true, false);
|
|
}
|
|
}
|
|
|
|
void WSJTXMessageClient::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 WSJTXMessageClient::impl::parse_message(QByteArray const &msg) {
|
|
try {
|
|
NetworkMessage::Reader in{msg};
|
|
if (OK == check_status(in)) {
|
|
if (schema_ < in.schema()) // one time record of server's
|
|
// negotiated schema
|
|
{
|
|
schema_ = in.schema();
|
|
}
|
|
|
|
if (!enabled_) {
|
|
TRACE_UDP("message processing disabled for id:" << in.id());
|
|
return;
|
|
}
|
|
|
|
switch (in.type()) {
|
|
case NetworkMessage::Reply: {
|
|
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::Clear: {
|
|
quint8 window{0};
|
|
in >> window;
|
|
if (check_status(in) != Fail) {
|
|
Q_EMIT self_->clear_decodes(window);
|
|
}
|
|
} break;
|
|
|
|
case NetworkMessage::Close:
|
|
if (check_status(in) != Fail) {
|
|
last_message_.clear();
|
|
Q_EMIT self_->close();
|
|
}
|
|
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;
|
|
|
|
default:
|
|
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{"WSJTXMessageClient exception: %1"}.arg(e.what()));
|
|
} catch (...) {
|
|
Q_EMIT self_->error("Unexpected exception in WSJTXMessageClient");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a Heartbeat message
|
|
*
|
|
* Periodically sends Heartbeat messages to announce this application's
|
|
* presence and capabilities to WSJT-X protocol clients.
|
|
*/
|
|
void WSJTXMessageClient::impl::heartbeat() {
|
|
if (server_port_ && !server_.isNull()) {
|
|
QByteArray message;
|
|
NetworkMessage::Builder out{&message, NetworkMessage::Heartbeat, id_,
|
|
schema_};
|
|
out << NetworkMessage::Builder::schema_number // maximum schema number
|
|
// accepted
|
|
<< version_.toUtf8() << revision_.toUtf8();
|
|
qCDebug(wsjtx_js8) << "WSJT-X: Sending Heartbeat message"
|
|
<< "id:" << id_ << "schema:" << schema_
|
|
<< "version:" << version_ << "revision:" << revision_
|
|
<< "to:" << server_.toString()
|
|
<< "port:" << server_port_;
|
|
qCDebug(wsjtx_js8) << dump_payload(message);
|
|
send_message(out, message, false, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a Close message and clean up
|
|
*
|
|
* Sends a Close message to notify clients that this application is shutting
|
|
* down.
|
|
*/
|
|
void WSJTXMessageClient::impl::closedown() {
|
|
if (server_port_ && !server_.isNull()) {
|
|
QByteArray message;
|
|
NetworkMessage::Builder out{&message, NetworkMessage::Close, id_,
|
|
schema_};
|
|
qCDebug(wsjtx_js8) << "WSJT-X: Sending Close message"
|
|
<< "id:" << id_ << "schema:" << schema_
|
|
<< "to:" << server_.toString()
|
|
<< "port:" << server_port_;
|
|
qCDebug(wsjtx_js8) << dump_payload(message);
|
|
send_message(out, message, false);
|
|
}
|
|
}
|
|
|
|
void WSJTXMessageClient::impl::send_message(QByteArray const &message,
|
|
bool queue_if_pending,
|
|
bool allow_duplicates) {
|
|
if (server_port_) {
|
|
if (!server_.isNull()) {
|
|
if (allow_duplicates ||
|
|
message != last_message_) // avoid duplicates
|
|
{
|
|
if (is_multicast_address(server_)) {
|
|
// send datagram on each selected network interface
|
|
std::for_each(
|
|
network_interfaces_.begin(), network_interfaces_.end(),
|
|
[&](QNetworkInterface const &net_if) {
|
|
setMulticastInterface(net_if);
|
|
writeDatagram(message, server_, server_port_);
|
|
});
|
|
} else {
|
|
writeDatagram(message, server_, server_port_);
|
|
}
|
|
last_message_ = message;
|
|
}
|
|
} else if (queue_if_pending) {
|
|
pending_messages_.enqueue(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto WSJTXMessageClient::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;
|
|
}
|
|
|
|
WSJTXMessageClient::WSJTXMessageClient(
|
|
QString const &id, QString const &version, QString const &revision,
|
|
QString const &server_name, port_type server_port,
|
|
QStringList const &network_interface_names, int TTL, QObject *self)
|
|
: QObject{self}, m_{id, version, revision, server_port, TTL, this} {
|
|
connect(&*m_
|
|
, &impl::errorOccurred, [this] (impl::SocketError e)
|
|
{
|
|
#if defined(Q_OS_WIN)
|
|
if (e != impl::NetworkError &&
|
|
e != impl::ConnectionRefusedError) {
|
|
#else
|
|
{
|
|
Q_UNUSED (e);
|
|
#endif
|
|
Q_EMIT error(m_->errorString());
|
|
}
|
|
});
|
|
m_->set_server(server_name, network_interface_names);
|
|
}
|
|
|
|
QHostAddress WSJTXMessageClient::server_address() const { return m_->server_; }
|
|
|
|
auto WSJTXMessageClient::server_port() const -> port_type {
|
|
return m_->server_port_;
|
|
}
|
|
|
|
void WSJTXMessageClient::set_server(
|
|
QString const &server_name, QStringList const &network_interface_names) {
|
|
m_->set_server(server_name, network_interface_names);
|
|
}
|
|
|
|
void WSJTXMessageClient::set_server_port(port_type server_port) {
|
|
m_->server_port_ = server_port;
|
|
}
|
|
|
|
void WSJTXMessageClient::set_TTL(int TTL) {
|
|
m_->TTL_ = TTL;
|
|
m_->setSocketOption(QAbstractSocket::MulticastTtlOption, m_->TTL_);
|
|
}
|
|
|
|
/**
|
|
* @brief Enable or disable incoming message processing
|
|
* @param flag true to enable, false to disable
|
|
*/
|
|
void WSJTXMessageClient::enable(bool flag) { m_->enabled_ = flag; }
|
|
|
|
/**
|
|
* @brief Send a Status message
|
|
*
|
|
* Sends the current station status including frequency, mode, callsigns,
|
|
* and operational state to WSJT-X protocol clients.
|
|
*
|
|
* @param f Operating frequency (Hz)
|
|
* @param mode Operating mode string
|
|
* @param dx_call DX station callsign (selected station)
|
|
* @param report Report string
|
|
* @param tx_mode TX mode string
|
|
* @param tx_enabled Whether TX is enabled
|
|
* @param transmitting Whether currently transmitting
|
|
* @param decoding Whether currently decoding
|
|
* @param rx_df Receive frequency offset (Hz)
|
|
* @param tx_df Transmit frequency offset (Hz)
|
|
* @param de_call My callsign
|
|
* @param de_grid My grid square
|
|
* @param dx_grid DX station grid square
|
|
* @param watchdog_timeout Whether watchdog has timed out
|
|
* @param sub_mode Sub-mode string
|
|
* @param fast_mode Whether fast mode is enabled
|
|
* @param special_op_mode Special operating mode
|
|
* @param frequency_tolerance Frequency tolerance (Hz)
|
|
* @param tr_period Transmit/receive period (seconds)
|
|
* @param configuration_name Configuration name
|
|
* @param tx_message Current TX message text
|
|
*/
|
|
void WSJTXMessageClient::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, quint32 rx_df, quint32 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, quint32 frequency_tolerance, quint32 tr_period,
|
|
QString const &configuration_name, QString const &tx_message) {
|
|
if (m_->server_port_ && !m_->server_.isNull()) {
|
|
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 << frequency_tolerance << tr_period
|
|
<< configuration_name.toUtf8() << tx_message.toUtf8();
|
|
qCDebug(wsjtx_js8) << "WSJT-X: Sending Status message"
|
|
<< "freq:" << f << "mode:" << mode
|
|
<< "dx_call:" << dx_call
|
|
<< "tx_enabled:" << tx_enabled
|
|
<< "transmitting:" << transmitting
|
|
<< "decoding:" << decoding << "de_call:" << de_call
|
|
<< "de_grid:" << de_grid << "dx_grid:" << dx_grid
|
|
<< "sub_mode:" << sub_mode
|
|
<< "to:" << m_->server_.toString()
|
|
<< "port:" << m_->server_port_;
|
|
qCDebug(wsjtx_js8) << dump_payload(message);
|
|
m_->send_message(out, message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a Decode message
|
|
*
|
|
* Sends information about a decoded message to WSJT-X protocol clients.
|
|
*
|
|
* @param is_new Whether this is a new decode
|
|
* @param time Decode time
|
|
* @param snr Signal-to-noise ratio (dB)
|
|
* @param delta_time Time offset from expected time (seconds)
|
|
* @param delta_frequency Frequency offset from nominal (Hz)
|
|
* @param mode Operating mode string
|
|
* @param message_text Decoded message text
|
|
* @param low_confidence Whether decode confidence is low
|
|
* @param off_air Whether this is an off-air decode
|
|
*/
|
|
void WSJTXMessageClient::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_.isNull()) {
|
|
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;
|
|
qCDebug(wsjtx_js8) << "WSJT-X: Sending Decode message"
|
|
<< "is_new:" << is_new
|
|
<< "time:" << time.toString("hh:mm:ss")
|
|
<< "snr:" << snr << "delta_time:" << delta_time
|
|
<< "delta_frequency:" << delta_frequency
|
|
<< "mode:" << mode << "message:" << message_text
|
|
<< "low_confidence:" << low_confidence
|
|
<< "off_air:" << off_air
|
|
<< "to:" << m_->server_.toString()
|
|
<< "port:" << m_->server_port_;
|
|
qCDebug(wsjtx_js8) << dump_payload(message);
|
|
m_->send_message(out, message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a Clear Decodes message
|
|
*
|
|
* Notifies WSJT-X protocol clients to clear the decode window.
|
|
*/
|
|
void WSJTXMessageClient::decodes_cleared() {
|
|
if (m_->server_port_ && !m_->server_.isNull()) {
|
|
QByteArray message;
|
|
NetworkMessage::Builder out{&message, NetworkMessage::Clear, m_->id_,
|
|
m_->schema_};
|
|
qCDebug(wsjtx_js8) << "WSJT-X: Sending Clear message"
|
|
<< "id:" << m_->id_ << "schema:" << m_->schema_
|
|
<< "to:" << m_->server_.toString()
|
|
<< "port:" << m_->server_port_;
|
|
qCDebug(wsjtx_js8) << dump_payload(message);
|
|
m_->send_message(out, message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a QSO Logged message
|
|
*
|
|
* Sends information about a logged QSO to WSJT-X protocol clients.
|
|
*
|
|
* @param time_off QSO end time
|
|
* @param dx_call DX station callsign
|
|
* @param dx_grid DX station grid square
|
|
* @param dial_frequency Dial frequency (Hz)
|
|
* @param mode Operating mode
|
|
* @param report_sent Report sent to DX station
|
|
* @param report_received Report received from DX station
|
|
* @param tx_power Transmit power
|
|
* @param comments QSO comments
|
|
* @param name DX station name
|
|
* @param time_on QSO start time
|
|
* @param operator_call Operator callsign
|
|
* @param my_call My callsign
|
|
* @param my_grid My grid square
|
|
* @param exchange_sent Exchange sent
|
|
* @param exchange_rcvd Exchange received
|
|
* @param propmode Propagation mode
|
|
*/
|
|
void WSJTXMessageClient::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, QString const &propmode) {
|
|
if (m_->server_port_ && !m_->server_.isNull()) {
|
|
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()
|
|
<< propmode.toUtf8();
|
|
qCDebug(wsjtx_js8) << "WSJT-X: Sending QSOLogged message"
|
|
<< "time_off:" << time_off.toString(Qt::ISODate)
|
|
<< "dx_call:" << dx_call << "dx_grid:" << dx_grid
|
|
<< "dial_frequency:" << dial_frequency
|
|
<< "mode:" << mode << "report_sent:" << report_sent
|
|
<< "report_received:" << report_received
|
|
<< "my_call:" << my_call << "my_grid:" << my_grid
|
|
<< "to:" << m_->server_.toString()
|
|
<< "port:" << m_->server_port_;
|
|
qCDebug(wsjtx_js8) << dump_payload(message);
|
|
m_->send_message(out, message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a Logged ADIF message
|
|
*
|
|
* Sends the ADIF record for a logged QSO. This is sent in addition to
|
|
* the QSOLogged message and uses the WSJT-X LoggedADIF message format
|
|
* (type 12). The ADIF record is formatted with a header that includes
|
|
* the ADIF version and program ID for compatibility with WSJT-X clients.
|
|
*
|
|
* @param ADIF_record ADIF formatted record for the logged QSO (without header)
|
|
*/
|
|
void WSJTXMessageClient::logged_ADIF(QByteArray const &ADIF_record) {
|
|
if (m_->server_port_ && !m_->server_.isNull()) {
|
|
QByteArray message;
|
|
NetworkMessage::Builder out{&message, NetworkMessage::LoggedADIF,
|
|
m_->id_, m_->schema_};
|
|
// Format ADIF with header like WSJT-X does
|
|
// Use WSJT-X as programid for compatibility with clients expecting
|
|
// WSJT-X format
|
|
QByteArray ADIF{"\n<adif_ver:5>3.1.0\n<programid:6>WSJT-X\n<EOH>\n" +
|
|
ADIF_record + " <EOR>"};
|
|
out << ADIF;
|
|
qCDebug(wsjtx_js8) << "WSJT-X: Sending LoggedADIF message"
|
|
<< "ADIF length:" << ADIF.length()
|
|
<< "to:" << m_->server_.toString()
|
|
<< "port:" << m_->server_port_;
|
|
qCDebug(wsjtx_js8) << dump_payload(message);
|
|
m_->send_message(out, message);
|
|
}
|
|
}
|