js8call/JS8_Transceiver/DXLabSuiteCommanderTransceiver.cpp

507 lines
19 KiB
C++
Raw Normal View History

2026-01-18 12:53:55 -06:00
/**
* @file DXLabSuiteCommanderTransceiver.cpp
* @brief Implementation of the DXLabSuiteCommanderTransceiver class
*/
#include "DXLabSuiteCommanderTransceiver.h"
2026-01-18 12:53:55 -06:00
#include "JS8_Main/DriftingDateTime.h"
#include "JS8_Network/NetworkServerLookup.h"
#include <QDateTime>
#include <QLocale>
#include <QLoggingCategory>
#include <QRegularExpression>
#include <QTcpSocket>
#include <QThread>
#include "moc_DXLabSuiteCommanderTransceiver.cpp"
Q_DECLARE_LOGGING_CATEGORY(dxlabsuitecommandertransceiver_js8)
namespace {
char const *const commander_transceiver_name{"DX Lab Suite Commander"};
QString map_mode(Transceiver::MODE mode) {
switch (mode) {
case Transceiver::AM:
return "AM";
case Transceiver::CW:
return "CW";
case Transceiver::CW_R:
return "CW-R";
case Transceiver::USB:
return "USB";
case Transceiver::LSB:
return "LSB";
case Transceiver::FSK:
return "RTTY";
case Transceiver::FSK_R:
return "RTTY-R";
case Transceiver::DIG_L:
return "DATA-L";
case Transceiver::DIG_U:
return "DATA-U";
case Transceiver::FM:
case Transceiver::DIG_FM:
return "FM";
default:
break;
}
return "USB";
}
} // namespace
void DXLabSuiteCommanderTransceiver::register_transceivers(
TransceiverFactory::Transceivers *registry, int id) {
(*registry)[commander_transceiver_name] = TransceiverFactory::Capabilities{
id, TransceiverFactory::Capabilities::network, true};
}
DXLabSuiteCommanderTransceiver::DXLabSuiteCommanderTransceiver(
std::unique_ptr<TransceiverBase> wrapped, QString const &address,
bool use_for_ptt, int poll_interval, QObject *parent)
: PollingTransceiver{poll_interval, parent}, wrapped_{std::move(wrapped)},
use_for_ptt_{use_for_ptt}, server_{address}, commander_{nullptr} {}
int DXLabSuiteCommanderTransceiver::do_start() {
TRACE_CAT("DXLabSuiteCommanderTransceiver", "starting");
if (wrapped_)
wrapped_->start(0);
auto server_details =
network_server_lookup(server_, 52002u, QHostAddress::LocalHost,
QAbstractSocket::IPv4Protocol);
if (!commander_) {
commander_ = new QTcpSocket{this}; // QObject takes ownership
}
commander_->connectToHost(std::get<0>(server_details),
std::get<1>(server_details));
if (!commander_->waitForConnected()) {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
"failed to connect" << commander_->errorString());
throw error{tr("Failed to connect to DX Lab Suite Commander\n") +
commander_->errorString()};
}
// sleeps here are to ensure Commander has actually queried the rig
// rather than returning cached data which maybe stale or simply
// read backs of not yet committed values, the 2s delays are
// arbitrary but hopefully enough as the actual time required is rig
// and Commander setting dependent
int resolution{0};
QThread::msleep(2000);
auto reply = command_with_reply("<command:10>CmdGetFreq<parameters:0>");
if (0 == reply.indexOf("<CmdFreq:")) {
auto f = string_to_frequency(reply.mid(reply.indexOf('>') + 1));
if (f && !(f % 10)) {
auto test_frequency = f - f % 100 + 55;
auto f_string = frequency_to_string(test_frequency);
auto params = ("<xcvrfreq:%1>" + f_string).arg(f_string.size());
simple_command(("<command:10>CmdSetFreq<parameters:%1>" + params)
.arg(params.size()));
QThread::msleep(2000);
reply = command_with_reply("<command:10>CmdGetFreq<parameters:0>");
if (0 == reply.indexOf("<CmdFreq:")) {
auto new_frequency =
string_to_frequency(reply.mid(reply.indexOf('>') + 1));
switch (static_cast<Radio::FrequencyDelta>(new_frequency -
test_frequency)) {
case -5:
resolution = -1;
break; // 10Hz truncated
case 5:
resolution = 1;
break; // 10Hz rounded
case -15:
resolution = -2;
break; // 20Hz truncated
case -55:
resolution = -2;
break; // 100Hz truncated
case 45:
resolution = 2;
break; // 100Hz rounded
}
if (1 == resolution) // may be 20Hz rounded
{
test_frequency = f - f % 100 + 51;
f_string = frequency_to_string(test_frequency);
params = ("<xcvrfreq:%1>" + f_string).arg(f_string.size());
simple_command(
("<command:10>CmdSetFreq<parameters:%1>" + params)
.arg(params.size()));
QThread::msleep(2000);
reply = command_with_reply(
"<command:10>CmdGetFreq<parameters:0>");
new_frequency =
string_to_frequency(reply.mid(reply.indexOf('>') + 1));
if (9 == static_cast<Radio::FrequencyDelta>(
new_frequency - test_frequency)) {
resolution = 2; // 20Hz rounded
}
}
f_string = frequency_to_string(f);
params = ("<xcvrfreq:%1>" + f_string).arg(f_string.size());
simple_command(
("<command:10>CmdSetFreq<parameters:%1>" + params)
.arg(params.size()));
}
}
} else {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
"get frequency unexpected response" << reply);
throw error{tr("DX Lab Suite Commander didn't respond correctly "
"reading frequency: ") +
reply};
}
poll();
return resolution;
}
void DXLabSuiteCommanderTransceiver::do_stop() {
if (commander_) {
commander_->close();
delete commander_, commander_ = nullptr;
}
if (wrapped_)
wrapped_->stop();
TRACE_CAT("DXLabSuiteCommanderTransceiver", "stopped");
}
void DXLabSuiteCommanderTransceiver::do_ptt(bool on) {
TRACE_CAT("DXLabSuiteCommanderTransceiver", on << state());
if (use_for_ptt_) {
simple_command(on ? "<command:5>CmdTX<parameters:0>"
: "<command:5>CmdRX<parameters:0>");
bool tx{!on};
auto start = DriftingDateTime::currentMSecsSinceEpoch();
// we must now wait for Tx on the rig, we will wait a short while
// before bailing out
while (tx != on &&
DriftingDateTime::currentMSecsSinceEpoch() - start < 1000) {
auto reply =
command_with_reply("<command:9>CmdSendTx<parameters:0>");
if (0 == reply.indexOf("<CmdTX:")) {
auto state = reply.mid(reply.indexOf('>') + 1);
if ("ON" == state) {
tx = true;
} else if ("OFF" == state) {
tx = false;
} else {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
"unexpected TX state" << state);
throw error{tr("DX Lab Suite Commander sent an "
"unrecognised TX state: ") +
state};
}
} else {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
"get TX unexpected response" << reply);
throw error{tr("DX Lab Suite Commander didn't respond "
"correctly polling TX status: ") +
reply};
}
if (tx != on)
QThread::msleep(10); // don't thrash Commander
}
update_PTT(tx);
if (tx != on) {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
"rig failed to respond to PTT: " << on);
throw error{
tr("DX Lab Suite Commander rig did not respond to PTT: ") +
(on ? "ON" : "OFF")};
}
} else {
Q_ASSERT(wrapped_);
TransceiverState new_state{wrapped_->state()};
new_state.ptt(on);
wrapped_->set(new_state, 0);
update_PTT(on);
}
}
void DXLabSuiteCommanderTransceiver::do_frequency(Frequency f, MODE m,
bool /*no_ignore*/) {
TRACE_CAT("DXLabSuiteCommanderTransceiver", f << state());
auto f_string = frequency_to_string(f);
if (UNK != m && m != get_mode()) {
auto m_string = map_mode(m);
auto params = ("<xcvrfreq:%1>" + f_string + "<xcvrmode:%2>" + m_string +
"<preservesplitanddual:1>Y")
.arg(f_string.size())
.arg(m_string.size());
simple_command(("<command:14>CmdSetFreqMode<parameters:%1>" + params)
.arg(params.size()));
update_mode(m);
} else {
auto params = ("<xcvrfreq:%1>" + f_string).arg(f_string.size());
simple_command(("<command:10>CmdSetFreq<parameters:%1>" + params)
.arg(params.size()));
}
update_rx_frequency(f);
}
void DXLabSuiteCommanderTransceiver::do_tx_frequency(Frequency tx, MODE mode,
bool /*no_ignore*/) {
TRACE_CAT("DXLabSuiteCommanderTransceiver", tx << state());
if (tx) {
auto f_string = frequency_to_string(tx);
auto params = ("<xcvrfreq:%1>" + f_string + "<SuppressDual:1>Y")
.arg(f_string.size());
if (UNK == mode) {
params += "<SuppressModeChange:1>Y";
}
simple_command(("<command:11>CmdQSXSplit<parameters:%1>" + params)
.arg(params.size()));
} else {
simple_command("<command:8>CmdSplit<parameters:8><1:3>off");
}
update_split(tx);
update_other_frequency(tx);
}
void DXLabSuiteCommanderTransceiver::do_mode(MODE m) {
TRACE_CAT("DXLabSuiteCommanderTransceiver", m << state());
auto m_string = map_mode(m);
auto params = ("<1:%1>" + m_string).arg(m_string.size());
simple_command(
("<command:10>CmdSetMode<parameters:%1>" + params).arg(params.size()));
update_mode(m);
}
void DXLabSuiteCommanderTransceiver::poll() {
bool quiet{!transceiverbase_js8().isDebugEnabled()};
auto reply =
command_with_reply("<command:10>CmdGetFreq<parameters:0>", quiet);
if (0 == reply.indexOf("<CmdFreq:")) {
auto f = string_to_frequency(reply.mid(reply.indexOf('>') + 1));
if (f) {
if (!state().ptt()) // Commander is not reliable on frequency
// polls while transmitting
{
update_rx_frequency(f);
}
}
} else {
TRACE_CAT_POLL("DXLabSuiteCommanderTransceiver",
"get frequency unexpected response" << reply);
throw error{tr("DX Lab Suite Commander didn't respond correctly "
"polling frequency: ") +
reply};
}
if (state().split()) {
reply =
command_with_reply("<command:12>CmdGetTXFreq<parameters:0>", quiet);
if (0 == reply.indexOf("<CmdTXFreq:")) {
auto f = string_to_frequency(reply.mid(reply.indexOf('>') + 1));
if (f) {
if (!state().ptt()) // Commander is not reliable on frequency
// polls while transmitting
{
update_other_frequency(f);
}
}
} else {
TRACE_CAT_POLL("DXLabSuiteCommanderTransceiver",
"get tx frequency unexpected response" << reply);
throw error{tr("DX Lab Suite Commander didn't respond correctly "
"polling TX frequency: ") +
reply};
}
}
reply = command_with_reply("<command:12>CmdSendSplit<parameters:0>", quiet);
if (0 == reply.indexOf("<CmdSplit:")) {
auto split = reply.mid(reply.indexOf('>') + 1);
if ("ON" == split) {
update_split(true);
} else if ("OFF" == split) {
update_split(false);
} else {
TRACE_CAT_POLL("DXLabSuiteCommanderTransceiver",
"unexpected split state" << split);
throw error{tr("DX Lab Suite Commander sent an unrecognised split "
"state: ") +
split};
}
} else {
TRACE_CAT_POLL("DXLabSuiteCommanderTransceiver",
"get split mode unexpected response" << reply);
throw error{tr("DX Lab Suite Commander didn't respond correctly "
"polling split status: ") +
reply};
}
get_mode(quiet);
}
auto DXLabSuiteCommanderTransceiver::get_mode(bool no_debug) -> MODE {
MODE m{UNK};
auto reply =
command_with_reply("<command:11>CmdSendMode<parameters:0>", no_debug);
if (0 == reply.indexOf("<CmdMode:")) {
auto mode = reply.mid(reply.indexOf('>') + 1);
if ("AM" == mode) {
m = AM;
} else if ("CW" == mode) {
m = CW;
} else if ("CW-R" == mode) {
m = CW_R;
} else if ("FM" == mode || "WBFM" == mode) {
m = FM;
} else if ("LSB" == mode) {
m = LSB;
} else if ("USB" == mode) {
m = USB;
} else if ("RTTY" == mode) {
m = FSK;
} else if ("RTTY-R" == mode) {
m = FSK_R;
} else if ("PKT" == mode || "DATA-L" == mode || "Data-L" == mode ||
"DIGL" == mode) {
m = DIG_L;
} else if ("PKT-R" == mode || "DATA-U" == mode || "Data-U" == mode ||
"DIGU" == mode) {
m = DIG_U;
} else {
TRACE_CAT_POLL("DXLabSuiteCommanderTransceiver",
"unexpected mode name" << mode);
throw error{
tr("DX Lab Suite Commander sent an unrecognised mode: \"") +
mode + '"'};
}
update_mode(m);
} else {
TRACE_CAT_POLL("DXLabSuiteCommanderTransceiver",
"unexpected response" << reply);
throw error{tr("DX Lab Suite Commander didn't respond correctly "
"polling mode: ") +
reply};
}
return m;
}
void DXLabSuiteCommanderTransceiver::simple_command(QString const &cmd,
bool no_debug) {
Q_ASSERT(commander_);
if (!no_debug) {
TRACE_CAT("DXLabSuiteCommanderTransceiver", cmd);
}
if (!write_to_port(cmd)) {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
"failed:" << commander_->errorString());
throw error{tr("DX Lab Suite Commander send command failed\n") +
commander_->errorString()};
}
}
QString DXLabSuiteCommanderTransceiver::command_with_reply(QString const &cmd,
bool no_debug) {
Q_ASSERT(commander_);
if (!write_to_port(cmd)) {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
"failed to send command:" << commander_->errorString());
throw error{
tr("DX Lab Suite Commander failed to send command \"%1\": %2\n")
.arg(cmd)
.arg(commander_->errorString())};
}
// waitForReadReady appears to be unreliable on Windows timing out
// when data is waiting so retry a few times
unsigned retries{5};
bool replied{false};
while (!replied && --retries) {
replied = commander_->waitForReadyRead();
if (!replied && commander_->error() != commander_->SocketTimeoutError) {
TRACE_CAT("DXLabSuiteCommanderTransceiver",
cmd << "failed to read reply:"
<< commander_->errorString());
throw error{tr("DX Lab Suite Commander send command \"%1\" read "
"reply failed: %2\n")
.arg(cmd)
.arg(commander_->errorString())};
}
}
if (!replied) {
TRACE_CAT("DXLabSuiteCommanderTransceiver", cmd << "retries exhausted");
throw error{tr("DX Lab Suite Commander retries exhausted sending "
"command \"%1\"")
.arg(cmd)};
}
auto result = commander_->readAll();
// qCDebug (dxlabsuitecommandertransceiver_js8) << "result: " << result;
// for (int i = 0; i < result.size (); ++i)
// {
// qCDebug (dxlabsuitecommandertransceiver_js8) << i << ":" << hex <<
// int (result[i]);
// }
if (!no_debug) {
TRACE_CAT("DXLabSuiteCommanderTransceiver", cmd << "->" << result);
}
return result; // converting raw UTF-8 bytes to QString
}
bool DXLabSuiteCommanderTransceiver::write_to_port(QString const &s) {
auto data = s.toLocal8Bit();
auto to_send = data.constData();
auto length = data.size();
qint64 total_bytes_sent{0};
while (total_bytes_sent < length) {
auto bytes_sent = commander_->write(to_send + total_bytes_sent,
length - total_bytes_sent);
if (bytes_sent < 0 || !commander_->waitForBytesWritten()) {
return false;
}
total_bytes_sent += bytes_sent;
}
return true;
}
QString DXLabSuiteCommanderTransceiver::frequency_to_string(Frequency f) const {
// number is localized and in kHz, avoid floating point translation
// errors by adding a small number (0.1Hz)
return QString{"%L1"}.arg(f / 1e3 + 1e-4, 10, 'f', 3);
}
auto DXLabSuiteCommanderTransceiver::string_to_frequency(QString s) const
-> Frequency {
// temporary hack because Commander is returning invalid UTF-8 bytes
s.replace(QChar{QChar::ReplacementCharacter}, locale_.groupSeparator());
// remove DP - relies on n.nnn kHz format so we can do ulonglong
// conversion to Hz
bool ok;
// auto f = locale_.toDouble (s, &ok); // use when CmdSendFreq and
// CmdSendTxFreq reinstated
auto f = QLocale::c().toDouble(s, &ok); // temporary fix
if (!ok) {
throw error{
tr("DX Lab Suite Commander sent an unrecognized frequency")};
}
return (f + 1e-4) * 1e3;
}
Q_LOGGING_CATEGORY(dxlabsuitecommandertransceiver_js8,
"dxlabsuitecommandertransceiver.js8", QtWarningMsg)