js8call/JS8_Transceiver/HRDTransceiver.cpp
Chris-AC9KH fc9d13b510 - Replace Q_FOREACH with C++ for loops. Q_FOREACH is deprecated in Qt 6.11,
use the standard C++17 or later library utility std::as_const which replaces
qAsConst
- Configuration.cpp: the parameters, allow_monitors and show_monitors,
are only used on Linux in a preprocessor guard. Silence the compiler
spam for Mac and Windows which see them as declared but unused parameters
2026-06-21 16:21:31 -05:00

1133 lines
44 KiB
C++

/**
* @file HRD.cpp
* @brief Implementation of the HRD class
* Implements interface with Ham Radio Deluxe
*/
#include "HRDTransceiver.h"
#include "JS8_Network/NetworkServerLookup.h"
#include <QByteArray>
#include <QDir>
#include <QHostAddress>
#include <QLoggingCategory>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTcpSocket>
#include <QThread>
#include <utility>
Q_DECLARE_LOGGING_CATEGORY(hrdtransceiver_js8)
namespace {
char const *const HRD_transceiver_name = "Ham Radio Deluxe";
// some commands require a settling time, particularly "RX A" and
// "RX B" on the Yaesu FTdx3000.
int constexpr yaesu_delay{250};
} // namespace
void HRDTransceiver::register_transceivers(
TransceiverFactory::Transceivers *registry, int id) {
(*registry)[HRD_transceiver_name] = TransceiverFactory::Capabilities(
id, TransceiverFactory::Capabilities::network, true, true /* maybe */);
}
struct HRDMessage {
// Placement style new overload for outgoing messages that does the
// construction too.
static void *operator new(size_t size, QString const &payload) {
size +=
sizeof(QChar) * (payload.size() + 1); // space for terminator too
HRDMessage *storage(reinterpret_cast<HRDMessage *>(new char[size]));
storage->size_ = size;
ushort const *pl(payload.utf16());
std::copy(pl, pl + payload.size() + 1,
storage->payload_); // copy terminator too
storage->magic_1_ = magic_1_value_;
storage->magic_2_ = magic_2_value_;
storage->checksum_ = 0;
return storage;
}
// Placement style new overload for incoming messages that does the
// construction too.
//
// No memory allocation here.
static void *operator new(size_t /* size */, QByteArray const &message) {
// Nasty const_cast here to avoid copying the message buffer.
return const_cast<HRDMessage *>(
reinterpret_cast<HRDMessage const *>(message.data()));
}
void operator delete(void *p, size_t) {
delete[] reinterpret_cast<char *>(
p); // Mirror allocation in operator new above.
}
quint32 size_;
qint32 magic_1_;
qint32 magic_2_;
qint32 checksum_; // Apparently not used.
QChar payload_[0]; // UTF-16 (which is wchar_t on Windows)
static qint32 constexpr magic_1_value_ = 0x1234ABCD;
static qint32 constexpr magic_2_value_ = 0xABCD1234;
};
HRDTransceiver::HRDTransceiver(std::unique_ptr<TransceiverBase> wrapped,
QString const &server, bool use_for_ptt,
TransceiverFactory::TXAudioSource audio_source,
int poll_interval, QObject *parent)
: PollingTransceiver{poll_interval, parent}, wrapped_{std::move(wrapped)},
use_for_ptt_{use_for_ptt}, audio_source_{audio_source}, server_{server},
hrd_{0}, protocol_{none}, current_radio_{0}, vfo_count_{0},
vfo_A_button_{-1}, vfo_B_button_{-1}, vfo_toggle_button_{-1},
mode_A_dropdown_{-1}, mode_B_dropdown_{-1}, data_mode_toggle_button_{-1},
data_mode_on_button_{-1}, data_mode_off_button_{-1},
data_mode_dropdown_{-1}, split_mode_button_{-1}, split_mode_dropdown_{-1},
split_mode_dropdown_write_only_{false}, split_off_button_{-1},
tx_A_button_{-1}, tx_B_button_{-1}, rx_A_button_{-1}, rx_B_button_{-1},
receiver_dropdown_{-1}, ptt_button_{-1}, alt_ptt_button_{-1},
reversed_{false} {}
int HRDTransceiver::do_start() {
TRACE_CAT("HRDTransceiver", "starting");
if (wrapped_)
wrapped_->start(0);
auto server_details = network_server_lookup(server_, 7809u);
if (!hrd_) {
hrd_ = new QTcpSocket{this}; // QObject takes ownership
}
hrd_->connectToHost(std::get<0>(server_details),
std::get<1>(server_details));
if (!hrd_->waitForConnected()) {
TRACE_CAT("HRDTransceiver",
"failed to connect:" << hrd_->errorString());
throw error{tr("Failed to connect to Ham Radio Deluxe\n") +
hrd_->errorString()};
}
if (none == protocol_) {
try {
protocol_ = v5; // try this first (works for v6 too)
send_command("get context", false, false);
} catch (error const &) {
protocol_ = none;
}
}
if (none == protocol_) {
hrd_->close();
protocol_ = v4; // try again with older protocol
hrd_->connectToHost(std::get<0>(server_details),
std::get<1>(server_details));
if (!hrd_->waitForConnected()) {
TRACE_CAT("HRDTransceiver",
"failed to connect:" << hrd_->errorString());
throw error{tr("Failed to connect to Ham Radio Deluxe\n") +
hrd_->errorString()};
}
send_command("get context", false, false);
}
QFile HRD_info_file{QDir{
QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)}
.absoluteFilePath("HRD Interface Information.txt")};
if (!HRD_info_file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
throw error{tr("Failed to open file \"%1\": %2.")
.arg(HRD_info_file.fileName())
.arg(HRD_info_file.errorString())};
}
QTextStream HRD_info{&HRD_info_file};
auto id = send_command("get id", false, false);
auto version = send_command("get version", false, false);
TRACE_CAT("HRDTransceiver", "Id:" << id << "Version:" << version);
HRD_info << "Id: " << id << "\n";
HRD_info << "Version: " << version << "\n";
auto radios = send_command("get radios", false, false)
.trimmed()
.split(',', Qt::SkipEmptyParts);
if (radios.isEmpty()) {
TRACE_CAT("HRDTransceiver", "no rig found");
throw error{tr("Ham Radio Deluxe: no rig found")};
}
HRD_info << "Radios:\n";
for (const auto &radio_str : radios) {
HRD_info << "\t" << radio_str << "\n";
auto entries = radio_str.trimmed().split(':', Qt::SkipEmptyParts);
radios_.push_back(
std::forward_as_tuple(entries[0].toUInt(), entries[1]));
}
TRACE_CAT("HRDTransceiver", "radios:-");
for (const auto &radio : radios_) {
TRACE_CAT("HRDTransceiver",
"\t[" << std::get<0>(radio) << "] " << std::get<1>(radio));
}
auto current_radio_name = send_command("get radio", false, false, true);
HRD_info << "Current radio: " << current_radio_name << "\n";
if (current_radio_name.isEmpty()) {
TRACE_CAT("HRDTransceiver", "no rig found");
throw error{tr("Ham Radio Deluxe: no rig found")};
}
vfo_count_ = send_command("get vfo-count").toUInt();
HRD_info << "VFO count: " << vfo_count_ << "\n";
TRACE_CAT("HRDTransceiver", "vfo count:" << vfo_count_);
buttons_ = send_command("get buttons")
.trimmed()
.split(',', Qt::SkipEmptyParts)
.replaceInStrings(" ", "~");
TRACE_CAT("HRDTransceiver", "HRD Buttons: " << buttons_);
HRD_info << "Buttons: {" << buttons_.join(", ") << "}\n";
dropdown_names_ =
send_command("get dropdowns").trimmed().split(',', Qt::SkipEmptyParts);
TRACE_CAT("HRDTransceiver", "Dropdowns:");
HRD_info << "Dropdowns:\n";
for (auto const &dd : std::as_const(dropdown_names_)) {
auto selections =
send_command("get dropdown-list {" + dd + "}").trimmed().split(',');
TRACE_CAT("HRDTransceiver",
"\t" << dd << ": {" << selections.join(", ") << "}");
HRD_info << "\t" << dd << ": {" << selections.join(", ") << "}\n";
dropdowns_[dd] = selections;
}
slider_names_ = send_command("get sliders")
.trimmed()
.split(',', Qt::SkipEmptyParts)
.replaceInStrings(" ", "~");
TRACE_CAT("HRDTransceiver", "Sliders:-");
HRD_info << "Sliders:\n";
for (auto const &s : std::as_const(slider_names_)) {
auto range =
send_command("get slider-range " + current_radio_name + " " + s)
.trimmed()
.split(',', Qt::SkipEmptyParts);
TRACE_CAT("HRDTransceiver",
"\t" << s << ": {" << range.join(", ") << "}");
HRD_info << "\t" << s << ": {" << range.join(", ") << "}\n";
sliders_[s] = range;
}
// set RX VFO
rx_A_button_ = find_button(QRegularExpression("^(RX~A)$"));
rx_B_button_ = find_button(QRegularExpression("^(RX~B)$"));
// select VFO (sometime set as well)
vfo_A_button_ = find_button(QRegularExpression("^(VFO~A|Main)$"));
vfo_B_button_ = find_button(QRegularExpression("^(VFO~B|Sub)$"));
vfo_toggle_button_ = find_button(QRegularExpression("^(A~/~B)$"));
split_mode_button_ =
find_button(QRegularExpression("^(Spl~On|Spl_On|Split|Split~On)$"));
split_off_button_ =
find_button(QRegularExpression("^(Spl~Off|Spl_Off|Split~Off)$"));
if ((split_mode_dropdown_ =
find_dropdown(QRegularExpression("^(Split)$"))) >= 0) {
split_mode_dropdown_selection_on_ = find_dropdown_selection(
split_mode_dropdown_, QRegularExpression("^(On)$"));
split_mode_dropdown_selection_off_ = find_dropdown_selection(
split_mode_dropdown_, QRegularExpression("^(Off)$"));
} else if ((receiver_dropdown_ =
find_dropdown(QRegularExpression("^Receiver$"))) >= 0) {
rx_A_selection_ = find_dropdown_selection(
receiver_dropdown_, QRegularExpression("^(RX / Off)$"));
rx_B_selection_ = find_dropdown_selection(
receiver_dropdown_, QRegularExpression("^(Mute / RX)$"));
}
tx_A_button_ = find_button(QRegularExpression("^(TX~main|TX~-~A|TX~A)$"));
tx_B_button_ = find_button(QRegularExpression("^(TX~sub|TX~-~B|TX~B)$"));
if ((mode_A_dropdown_ = find_dropdown(
QRegularExpression("^(Main Mode|Mode|Mode A)$"))) >= 0) {
map_modes(mode_A_dropdown_, &mode_A_map_);
} else {
Q_ASSERT(mode_A_dropdown_ <= 0);
}
if ((mode_B_dropdown_ =
find_dropdown(QRegularExpression("^(Sub Mode|Mode B)$"))) >= 0) {
map_modes(mode_B_dropdown_, &mode_B_map_);
}
// Can't do this with ^Data$ as the button name because some Kenwood
// rigs have a "Data" button which is for turning the DSP on and off
// so we must filter by rig name as well
if (current_radio_name.startsWith("IC")) // works with Icom transceivers
{
data_mode_toggle_button_ = find_button(QRegularExpression("^(Data)$"));
}
data_mode_on_button_ =
find_button(QRegularExpression("^(DATA-ON\\(mid\\))$"));
data_mode_off_button_ = find_button(QRegularExpression("^(DATA-OFF)$"));
// Some newer Icoms have a Data drop down with (Off,On,D1,D2,D3)
// Some newer Icoms have a Data drop down with (Off,D1,D2,D3)
// Some newer Icoms (IC-7300) have a Data drop down with
// (Off,,D1-FIL1,D1-FIL2,D1-FIL3) the missing value counts as an
// index value - I think it is a drop down separator line - this
// appears to be an HRD defect and we cannot work around it
if ((data_mode_dropdown_ = find_dropdown(QRegularExpression("^(Data)$"))) >=
0) {
data_mode_dropdown_selection_on_ = find_dropdown_selection(
data_mode_dropdown_, QRegularExpression("^(On|Data1|D1|D1-FIL1)$"));
data_mode_dropdown_selection_off_ = find_dropdown_selection(
data_mode_dropdown_, QRegularExpression("^(Off)$"));
}
ptt_button_ = find_button(QRegularExpression("^(TX)$"));
alt_ptt_button_ = find_button(QRegularExpression("^(TX~Alt|TX~Data)$"));
if (vfo_count_ == 1 && ((vfo_B_button_ >= 0 && vfo_A_button_ >= 0) ||
vfo_toggle_button_ >= 0)) {
// put the rig into a known state for tricky cases like Icoms
auto f = send_command("get frequency").toUInt();
auto m = get_data_mode(
lookup_mode(get_dropdown(mode_A_dropdown_), mode_A_map_));
set_button(vfo_B_button_ >= 0 ? vfo_B_button_ : vfo_toggle_button_);
auto fo = send_command("get frequency").toUInt();
update_other_frequency(fo);
auto mo = get_data_mode(
lookup_mode(get_dropdown(mode_A_dropdown_), mode_A_map_));
set_button(vfo_A_button_ >= 0 ? vfo_A_button_ : vfo_toggle_button_);
if (f != fo || m != mo) {
// we must have started with A/MAIN
update_rx_frequency(f);
update_mode(m);
} else {
update_rx_frequency(send_command("get frequency").toUInt());
update_mode(get_data_mode(
lookup_mode(get_dropdown(mode_A_dropdown_), mode_A_map_)));
}
}
int resolution{0};
auto f = send_command("get frequency").toUInt();
if (f && !(f % 10)) {
auto test_frequency = f - f % 100 + 55;
send_simple_command("set frequency-hz " +
QString::number(test_frequency));
auto new_frequency = send_command("get frequency").toUInt();
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;
send_simple_command("set frequency-hz " +
QString::number(test_frequency));
new_frequency = send_command("get frequency").toUInt();
if (9 == static_cast<Radio::FrequencyDelta>(new_frequency -
test_frequency)) {
resolution = 2; // 20Hz rounded
}
}
send_simple_command("set frequency-hz " + QString::number(f));
}
return resolution;
}
void HRDTransceiver::do_stop() {
if (hrd_) {
hrd_->close();
}
if (wrapped_)
wrapped_->stop();
TRACE_CAT("HRDTransceiver",
"stopped" << state() << "reversed" << reversed_);
}
int HRDTransceiver::find_button(QRegularExpression const &re) const {
return buttons_.indexOf(re);
}
int HRDTransceiver::find_dropdown(QRegularExpression const &re) const {
return dropdown_names_.indexOf(re);
}
std::vector<int>
HRDTransceiver::find_dropdown_selection(int dropdown,
QRegularExpression const &re) const {
std::vector<int> indices; // this will always contain at least a
// -1
auto list = dropdowns_.value(dropdown_names_.value(dropdown));
int index{0};
while (-1 != (index = list.lastIndexOf(re, index - 1))) {
// search backwards because more specialized modes tend to be
// later in list
indices.push_back(index);
if (!index) {
break;
}
}
return indices;
}
void HRDTransceiver::map_modes(int dropdown, ModeMap *map) {
// order matters here (both in the map and in the regexps)
map->push_back(std::forward_as_tuple(
CW, find_dropdown_selection(
dropdown, QRegularExpression("^(CW|CW\\(N\\))|CWL$"))));
map->push_back(std::forward_as_tuple(
CW_R,
find_dropdown_selection(
dropdown, QRegularExpression("^(CW-R|CW-R\\(N\\)|CW|CWU)$"))));
map->push_back(std::forward_as_tuple(
LSB, find_dropdown_selection(
dropdown, QRegularExpression("^(LSB\\(N\\)|LSB)$"))));
map->push_back(std::forward_as_tuple(
USB, find_dropdown_selection(
dropdown, QRegularExpression("^(USB\\(N\\)|USB)$"))));
map->push_back(std::forward_as_tuple(
DIG_U,
find_dropdown_selection(
dropdown,
QRegularExpression("^(DIG|DIGU|DATA-U|PKT-U|DATA|USER-U|USB)$"))));
map->push_back(std::forward_as_tuple(
DIG_L,
find_dropdown_selection(
dropdown, QRegularExpression(
"^(DIG|DIGL|DATA-L|PKT-L|DATA-R|USER-L|LSB)$"))));
map->push_back(std::forward_as_tuple(
FSK, find_dropdown_selection(
dropdown, QRegularExpression("^(DIG|FSK|RTTY|RTTY-LSB)$"))));
map->push_back(std::forward_as_tuple(
FSK_R, find_dropdown_selection(
dropdown,
QRegularExpression("^(DIG|FSK-R|RTTY-R|RTTY|RTTY-USB)$"))));
map->push_back(std::forward_as_tuple(
AM, find_dropdown_selection(dropdown,
QRegularExpression("^(AM|DSB|SAM|DRM)$"))));
map->push_back(std::forward_as_tuple(
FM, find_dropdown_selection(
dropdown, QRegularExpression("^(FM|FM\\(N\\)|FM-N|WFM)$"))));
map->push_back(std::forward_as_tuple(
DIG_FM, find_dropdown_selection(
dropdown, QRegularExpression("^(PKT-FM|PKT|FM)$"))));
TRACE_CAT("HRDTransceiver", "for dropdown" << dropdown_names_[dropdown]);
std::for_each(
map->begin(), map->end(),
[this, dropdown](ModeMap::value_type const &item) {
auto const &rhs = std::get<1>(item);
TRACE_CAT(
"HRDTransceiver",
'\t'
<< std::get<0>(item) << "<->"
<< (rhs.size()
? dropdowns_[dropdown_names_[dropdown]][rhs.front()]
: "None"));
});
}
int HRDTransceiver::lookup_mode(MODE mode, ModeMap const &map) const {
auto it = std::find_if(map.begin(), map.end(),
[mode](ModeMap::value_type const &item) {
return std::get<0>(item) == mode;
});
if (map.end() == it) {
throw error{tr("Ham Radio Deluxe: rig doesn't support mode")};
}
return std::get<1>(*it).front();
}
auto HRDTransceiver::lookup_mode(int mode, ModeMap const &map) const -> MODE {
if (mode < 0) {
return UNK; // no mode dropdown
}
auto it = std::find_if(
map.begin(), map.end(), [mode](ModeMap::value_type const &item) {
auto const &indices = std::get<1>(item);
return indices.cend() !=
std::find(indices.cbegin(), indices.cend(), mode);
});
if (map.end() == it) {
throw error{tr("Ham Radio Deluxe: sent an unrecognised mode")};
}
return std::get<0>(*it);
}
int HRDTransceiver::get_dropdown(int dd, bool no_debug) {
if (dd < 0) {
return -1; // no dropdown to interrogate
}
auto dd_name = dropdown_names_.value(dd);
auto reply = send_command("get dropdown-text {" + dd_name + "}", no_debug);
auto colon_index = reply.indexOf(':');
if (colon_index < 0) {
return -1;
}
Q_ASSERT(reply.left(colon_index).trimmed() == dd_name);
return dropdowns_.value(dropdown_names_.value(dd))
.indexOf(reply.mid(colon_index + 1).trimmed());
}
void HRDTransceiver::set_dropdown(int dd, int value) {
auto dd_name = dropdown_names_.value(dd);
if (value >= 0) {
send_simple_command(
"set dropdown " + dd_name.replace(' ', '~') + ' ' +
dropdowns_.value(dd_name).value(value).replace(' ', '~') + ' ' +
QString::number(value));
} else {
TRACE_CAT("HRDTransceiver",
"item" << value << "not found in" << dd_name);
throw error{tr("Ham Radio Deluxe: item not found in %1 dropdown list")
.arg(dd_name)};
}
}
void HRDTransceiver::do_ptt(bool on) {
TRACE_CAT("HRDTransceiver", on);
if (use_for_ptt_) {
if (alt_ptt_button_ >= 0 &&
TransceiverFactory::TX_audio_source_rear == audio_source_) {
set_button(alt_ptt_button_, on);
} else if (ptt_button_ >= 0) {
set_button(ptt_button_, on);
}
// else
// allow for pathological HRD rig interfaces that don't do
// PTT by simply not even trying
} else {
Q_ASSERT(wrapped_);
TransceiverState new_state{wrapped_->state()};
new_state.ptt(on);
wrapped_->set(new_state, 0);
}
update_PTT(on);
}
void HRDTransceiver::set_button(int button_index, bool checked) {
if (button_index >= 0) {
send_simple_command("set button-select " +
buttons_.value(button_index) +
(checked ? " 1" : " 0"));
if (button_index == rx_A_button_ || button_index == rx_B_button_) {
QThread::msleep(yaesu_delay);
}
} else {
TRACE_CAT("HRDTransceiver", "invalid button");
throw error{tr("Ham Radio Deluxe: button not available")};
}
}
void HRDTransceiver::set_data_mode(MODE m) {
if (data_mode_toggle_button_ >= 0) {
switch (m) {
case DIG_U:
case DIG_L:
case DIG_FM:
set_button(data_mode_toggle_button_, true);
break;
default:
set_button(data_mode_toggle_button_, false);
break;
}
} else if (data_mode_on_button_ >= 0 && data_mode_off_button_ >= 0) {
switch (m) {
case DIG_U:
case DIG_L:
case DIG_FM:
set_button(data_mode_on_button_, true);
break;
default:
set_button(data_mode_off_button_, true);
break;
}
} else if (data_mode_dropdown_ >= 0 &&
data_mode_dropdown_selection_off_.size() &&
data_mode_dropdown_selection_on_.size()) {
switch (m) {
case DIG_U:
case DIG_L:
case DIG_FM:
set_dropdown(data_mode_dropdown_,
data_mode_dropdown_selection_on_.front());
break;
default:
set_dropdown(data_mode_dropdown_,
data_mode_dropdown_selection_off_.front());
break;
}
}
}
auto HRDTransceiver::get_data_mode(MODE m, bool quiet) -> MODE {
if (data_mode_dropdown_ >= 0 && data_mode_dropdown_selection_off_.size()) {
auto selection = get_dropdown(data_mode_dropdown_, quiet);
// can't check for on here as there may be multiple on values so
// we must rely on the initial parse finding valid on values
if (selection >= 0 &&
selection != data_mode_dropdown_selection_off_.front()) {
switch (m) {
case USB:
m = DIG_U;
break;
case LSB:
m = DIG_L;
break;
case FM:
m = DIG_FM;
break;
default:
break;
}
}
}
return m;
}
void HRDTransceiver::do_frequency(Frequency f, MODE m, bool /*no_ignore*/) {
TRACE_CAT("HRDTransceiver", f << "reversed" << reversed_);
if (UNK != m) {
do_mode(m);
}
auto fo_string = QString::number(f);
if (vfo_count_ > 1 && reversed_) {
auto frequencies = send_command("get frequencies")
.trimmed()
.split('-', Qt::SkipEmptyParts);
send_simple_command("set frequencies-hz " +
QString::number(frequencies[0].toUInt()) + ' ' +
fo_string);
} else {
send_simple_command("set frequency-hz " + QString::number(f));
}
update_rx_frequency(f);
}
void HRDTransceiver::do_tx_frequency(Frequency tx, MODE mode,
bool /*no_ignore*/) {
TRACE_CAT("HRDTransceiver", tx << "reversed" << reversed_);
// re-check if reversed VFOs
bool rx_A{true};
bool rx_B{false};
if (receiver_dropdown_ >= 0 && rx_A_selection_.size()) {
auto selection = get_dropdown(receiver_dropdown_);
rx_A = selection == rx_A_selection_.front();
if (!rx_A && rx_B_selection_.size()) {
rx_B = selection == rx_B_selection_.front();
}
} else if (vfo_B_button_ >= 0 || rx_B_button_ >= 0) {
rx_A =
is_button_checked(rx_A_button_ >= 0 ? rx_A_button_ : vfo_A_button_);
if (!rx_A) {
rx_B = is_button_checked(rx_B_button_ >= 0 ? rx_B_button_
: vfo_B_button_);
}
}
reversed_ = rx_B;
bool split{tx != 0};
if (split) {
if (mode != UNK) {
if (!reversed_ && mode_B_dropdown_ >= 0) {
set_dropdown(mode_B_dropdown_, lookup_mode(mode, mode_B_map_));
} else if (reversed_ && mode_B_dropdown_ >= 0) {
set_dropdown(mode_A_dropdown_, lookup_mode(mode, mode_A_map_));
} else {
Q_ASSERT(mode_A_dropdown_ >= 0 &&
((vfo_A_button_ >= 0 && vfo_B_button_ >= 0) ||
vfo_toggle_button_ >= 0));
if (rx_B_button_ >= 0) {
set_button(reversed_ ? rx_A_button_ : rx_B_button_);
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_data_mode(mode);
set_button(reversed_ ? rx_B_button_ : rx_A_button_);
} else if (receiver_dropdown_ >= 0 && rx_A_selection_.size() &&
rx_B_selection_.size()) {
set_dropdown(receiver_dropdown_,
(reversed_ ? rx_A_selection_ : rx_B_selection_)
.front());
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_data_mode(mode);
set_dropdown(receiver_dropdown_,
(reversed_ ? rx_B_selection_ : rx_A_selection_)
.front());
} else if (vfo_count_ > 1 &&
((vfo_A_button_ >= 0 && vfo_B_button_ >= 0) ||
vfo_toggle_button_ >= 0)) {
set_button(vfo_A_button_ >= 0
? (reversed_ ? vfo_A_button_ : vfo_B_button_)
: vfo_toggle_button_);
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_data_mode(mode);
set_button(vfo_A_button_ >= 0
? (reversed_ ? vfo_B_button_ : vfo_A_button_)
: vfo_toggle_button_);
}
// else Tx VFO mode gets set with frequency below or we
// don't have a way of setting it so we assume it is
// always the same as the Rx VFO mode
}
}
auto fo_string = QString::number(tx);
if (reversed_) {
Q_ASSERT(vfo_count_ > 1);
auto frequencies = send_command("get frequencies")
.trimmed()
.split('-', Qt::SkipEmptyParts);
send_simple_command("set frequencies-hz " + fo_string + ' ' +
QString::number(frequencies[1].toUInt()));
} else {
if (vfo_count_ > 1) {
auto frequencies = send_command("get frequencies")
.trimmed()
.split('-', Qt::SkipEmptyParts);
send_simple_command("set frequencies-hz " +
QString::number(frequencies[0].toUInt()) +
' ' + fo_string);
} else if ((vfo_B_button_ >= 0 && vfo_A_button_ >= 0) ||
vfo_toggle_button_ >= 0) {
// we rationalise the modes here as well as the frequencies
set_button(vfo_B_button_ >= 0 ? vfo_B_button_
: vfo_toggle_button_);
send_simple_command("set frequency-hz " + fo_string);
if (mode != UNK && mode_B_dropdown_ < 0) {
// do this here rather than later so we only
// toggle/switch VFOs once
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_data_mode(mode);
}
set_button(vfo_A_button_ >= 0 ? vfo_A_button_
: vfo_toggle_button_);
}
}
}
update_other_frequency(tx);
if (split_mode_button_ >= 0) {
if (split_off_button_ >= 0 && !split) {
set_button(split_off_button_);
} else {
set_button(split_mode_button_, split);
}
} else if (split_mode_dropdown_ >= 0 &&
split_mode_dropdown_selection_off_.size() &&
split_mode_dropdown_selection_on_.size()) {
set_dropdown(split_mode_dropdown_,
split ? split_mode_dropdown_selection_on_.front()
: split_mode_dropdown_selection_off_.front());
} else if (vfo_A_button_ >= 0 && vfo_B_button_ >= 0 && tx_A_button_ >= 0 &&
tx_B_button_ >= 0) {
if (split) {
if (reversed_ != is_button_checked(tx_A_button_)) {
if (rx_A_button_ >= 0 && rx_B_button_ >= 0) {
set_button(reversed_ ? rx_B_button_ : rx_A_button_);
} else if (receiver_dropdown_ >= 0 && rx_A_selection_.size() &&
rx_B_selection_.size()) {
set_dropdown(receiver_dropdown_,
(reversed_ ? rx_B_selection_ : rx_A_selection_)
.front());
} else {
set_button(reversed_ ? vfo_B_button_ : vfo_A_button_);
}
set_button(reversed_ ? tx_A_button_ : tx_B_button_);
}
} else {
if (reversed_ != is_button_checked(tx_B_button_)) {
if (rx_A_button_ >= 0 && rx_B_button_ >= 0) {
set_button(reversed_ ? rx_B_button_ : rx_A_button_);
} else if (receiver_dropdown_ >= 0 && rx_A_selection_.size() &&
rx_B_selection_.size()) {
set_dropdown(receiver_dropdown_,
(reversed_ ? rx_B_selection_ : rx_A_selection_)
.front());
} else {
set_button(reversed_ ? vfo_B_button_ : vfo_A_button_);
}
set_button(reversed_ ? tx_B_button_ : tx_A_button_);
}
}
}
update_split(split);
}
void HRDTransceiver::do_mode(MODE mode) {
TRACE_CAT("HRDTransceiver", mode);
if (reversed_ && mode_B_dropdown_ >= 0) {
set_dropdown(mode_B_dropdown_, lookup_mode(mode, mode_B_map_));
} else {
set_dropdown(mode_A_dropdown_, lookup_mode(mode, mode_A_map_));
}
if (mode != UNK && state().split()) // rationalise mode if split
{
if (reversed_) {
if (mode_B_dropdown_ >= 0) {
set_dropdown(mode_A_dropdown_, lookup_mode(mode, mode_A_map_));
} else {
Q_ASSERT((vfo_B_button_ >= 0 && vfo_A_button_ >= 0) ||
vfo_toggle_button_ >= 0);
if (rx_B_button_ >= 0) {
set_button(rx_A_button_);
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_button(rx_B_button_);
} else if (receiver_dropdown_ >= 0 && rx_A_selection_.size() &&
rx_B_selection_.size()) {
set_dropdown(receiver_dropdown_, rx_A_selection_.front());
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_dropdown(receiver_dropdown_, rx_B_selection_.front());
} else if (vfo_count_ > 1 &&
((vfo_A_button_ >= 0 && vfo_B_button_ >= 0) ||
vfo_toggle_button_ >= 0)) {
set_button(vfo_A_button_ >= 0 ? vfo_A_button_
: vfo_toggle_button_);
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_button(vfo_B_button_ >= 0 ? vfo_B_button_
: vfo_toggle_button_);
}
// else Tx VFO mode gets set when Tx VFO frequency is
// set
if (tx_A_button_ >= 0) {
set_button(tx_A_button_);
}
}
} else {
if (mode_B_dropdown_ >= 0) {
set_dropdown(mode_B_dropdown_, lookup_mode(mode, mode_B_map_));
} else {
Q_ASSERT((vfo_B_button_ >= 0 && vfo_A_button_ >= 0) ||
vfo_toggle_button_ >= 0);
if (rx_B_button_ >= 0) {
set_button(rx_B_button_);
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_button(rx_A_button_);
} else if (receiver_dropdown_ >= 0 && rx_A_selection_.size() &&
rx_B_selection_.size()) {
set_dropdown(receiver_dropdown_, rx_B_selection_.front());
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_dropdown(receiver_dropdown_, rx_A_selection_.front());
} else if (vfo_count_ > 1 &&
((vfo_A_button_ >= 0 && vfo_B_button_ >= 0) ||
vfo_toggle_button_ >= 0)) {
set_button(vfo_B_button_ >= 0 ? vfo_B_button_
: vfo_toggle_button_);
set_dropdown(mode_A_dropdown_,
lookup_mode(mode, mode_A_map_));
set_button(vfo_A_button_ >= 0 ? vfo_A_button_
: vfo_toggle_button_);
}
// else Tx VFO mode gets set when Tx VFO frequency is
// set
if (tx_B_button_ >= 0) {
set_button(tx_B_button_);
}
}
}
}
set_data_mode(mode);
update_mode(mode);
}
bool HRDTransceiver::is_button_checked(int button_index, bool no_debug) {
if (button_index < 0) {
return false;
}
auto reply = send_command(
"get button-select " + buttons_.value(button_index), no_debug);
if ("1" != reply && "0" != reply) {
TRACE_CAT("HRDTransceiver", "bad response");
throw error{tr("Ham Radio Deluxe didn't respond as expected")};
}
return "1" == reply;
}
void HRDTransceiver::poll() {
bool quiet{hrdtransceiver_js8().isDebugEnabled()};
if (!quiet) {
qCDebug(hrdtransceiver_js8) << "+++++++ poll dump +++++++";
qCDebug(hrdtransceiver_js8) << "reversed:" << reversed_;
is_button_checked(vfo_A_button_);
is_button_checked(vfo_B_button_);
is_button_checked(vfo_toggle_button_);
is_button_checked(split_mode_button_);
is_button_checked(split_off_button_);
is_button_checked(rx_A_button_);
is_button_checked(rx_B_button_);
get_dropdown(receiver_dropdown_);
is_button_checked(tx_A_button_);
is_button_checked(tx_B_button_);
is_button_checked(ptt_button_);
is_button_checked(alt_ptt_button_);
get_dropdown(mode_A_dropdown_);
get_dropdown(mode_B_dropdown_);
is_button_checked(data_mode_toggle_button_);
is_button_checked(data_mode_on_button_);
is_button_checked(data_mode_off_button_);
if (data_mode_dropdown_ >= 0 &&
data_mode_dropdown_selection_off_.size() &&
data_mode_dropdown_selection_on_.size()) {
get_dropdown(data_mode_dropdown_);
}
if (!split_mode_dropdown_write_only_) {
get_dropdown(split_mode_dropdown_);
}
qCDebug(hrdtransceiver_js8) << "------- poll dump -------";
}
if (split_off_button_ >= 0) {
// we are probably dealing with an Icom and have to guess SPLIT mode :(
} else if (split_mode_button_ >= 0 &&
!(tx_A_button_ >= 0 && tx_B_button_ >= 0)) {
update_split(is_button_checked(split_mode_button_, quiet));
} else if (split_mode_dropdown_ >= 0) {
if (!split_mode_dropdown_write_only_) {
auto selection = get_dropdown(split_mode_dropdown_, quiet);
if (selection >= 0 && split_mode_dropdown_selection_off_.size()) {
update_split(selection ==
split_mode_dropdown_selection_on_.front());
} else {
// leave split alone as we can't query it - it should be
// correct so long as rig or HRD haven't been changed
split_mode_dropdown_write_only_ = true;
}
}
} else if (vfo_A_button_ >= 0 && vfo_B_button_ >= 0 && tx_A_button_ >= 0 &&
tx_B_button_ >= 0) {
bool rx_A{true}; // no Rx taken as not reversed
bool rx_B{false};
auto tx_A = is_button_checked(tx_A_button_, quiet);
// some rigs have dual Rx, we take VFO A/MAIN receiving as
// normal and only say reversed when only VFO B/SUB is active
// i.e. VFO A/MAIN muted VFO B/SUB active
if (receiver_dropdown_ >= 0 && rx_A_selection_.size()) {
auto selection = get_dropdown(receiver_dropdown_);
rx_A = selection == rx_A_selection_.front();
if (!rx_A && rx_B_selection_.size()) {
rx_B = selection == rx_B_selection_.front();
}
} else if (vfo_B_button_ >= 0 || rx_B_button_ >= 0) {
rx_A = is_button_checked(
rx_A_button_ >= 0 ? rx_A_button_ : vfo_A_button_, quiet);
if (!rx_A) {
rx_B = is_button_checked(
rx_B_button_ >= 0 ? rx_B_button_ : vfo_B_button_, quiet);
}
}
update_split(rx_B == tx_A);
reversed_ = rx_B;
}
if (vfo_count_ > 1) {
auto frequencies = send_command("get frequencies", quiet)
.trimmed()
.split('-', Qt::SkipEmptyParts);
update_rx_frequency(frequencies[reversed_ ? 1 : 0].toUInt());
update_other_frequency(frequencies[reversed_ ? 0 : 1].toUInt());
} else {
// read frequency is unreliable on single VFO addressing rigs
// while transmitting
if (!state().ptt()) {
update_rx_frequency(send_command("get frequency", quiet).toUInt());
}
}
// read mode is unreliable on single VFO addressing rigs while
// transmitting
if (vfo_count_ > 1 || !state().ptt()) {
update_mode(get_data_mode(
lookup_mode(get_dropdown(mode_A_dropdown_, quiet), mode_A_map_),
quiet));
}
}
QString HRDTransceiver::send_command(QString const &cmd, bool no_debug,
bool prepend_context, bool recurse) {
Q_ASSERT(hrd_);
QString result;
if (current_radio_ && prepend_context && vfo_count_ < 2) {
// required on some radios because commands don't get executed
// correctly otherwise (ICOM for example)
QThread::msleep(50);
}
if (!recurse && prepend_context) {
auto radio_name = send_command("get radio", true, current_radio_, true);
auto radio_iter =
std::find_if(radios_.begin(), radios_.end(),
[&radio_name](RadioMap::value_type const &radio) {
return std::get<1>(radio) == radio_name;
});
if (radio_iter == radios_.end()) {
TRACE_CAT("HRDTransceiver", "rig disappeared or changed");
throw error{tr("Ham Radio Deluxe: rig has disappeared or changed")};
}
if (0u == current_radio_ ||
std::get<0>(*radio_iter) != current_radio_) {
current_radio_ = std::get<0>(*radio_iter);
}
}
auto context = '[' + QString::number(current_radio_) + "] ";
if (QTcpSocket::ConnectedState != hrd_->state()) {
TRACE_CAT("HRDTransceiver", cmd << "failed" << hrd_->errorString());
throw error{tr("Ham Radio Deluxe send command \"%1\" failed %2\n")
.arg(cmd)
.arg(hrd_->errorString())};
}
if (v4 == protocol_) {
auto message =
((prepend_context ? context + cmd : cmd) + "\r").toLocal8Bit();
if (!write_to_port(message.constData(), message.size())) {
TRACE_CAT("HRDTransceiver",
"failed to write command" << cmd << "to HRD");
throw error{tr("Ham Radio Deluxe: failed to write command \"%1\"")
.arg(cmd)};
}
} else {
auto string = prepend_context ? context + cmd : cmd;
QScopedPointer<HRDMessage> message{new (string) HRDMessage};
if (!write_to_port(reinterpret_cast<char const *>(message.data()),
message->size_)) {
TRACE_CAT("HRDTransceiver",
"failed to write command" << cmd << "to HRD");
throw error{tr("Ham Radio Deluxe: failed to write command \"%1\"")
.arg(cmd)};
}
}
auto buffer = read_reply(cmd);
if (v4 == protocol_) {
result = QString{buffer}.trimmed();
} else {
HRDMessage const *reply{new (buffer) HRDMessage};
if (reply->magic_1_value_ != reply->magic_1_ &&
reply->magic_2_value_ != reply->magic_2_) {
TRACE_CAT("HRDTransceiver", cmd << "invalid reply");
throw error{tr("Ham Radio Deluxe sent an invalid reply to our "
"command \"%1\"")
.arg(cmd)};
}
// keep reading until expected size arrives
while (buffer.size() - offsetof(HRDMessage, size_) < reply->size_) {
if (!no_debug) {
TRACE_CAT("HRDTransceiver", cmd << "reading more reply data");
}
buffer += read_reply(cmd);
reply = new (buffer) HRDMessage;
}
result =
QString{reply->payload_}; // this is not a memory leak (honest!)
}
if (!no_debug) {
TRACE_CAT("HRDTransceiver", cmd << " ->" << result);
}
return result;
}
bool HRDTransceiver::write_to_port(char const *data, qint64 length) {
qint64 total_bytes_sent{0};
while (total_bytes_sent < length) {
auto bytes_sent =
hrd_->write(data + total_bytes_sent, length - total_bytes_sent);
if (bytes_sent < 0 || !hrd_->waitForBytesWritten()) {
return false;
}
total_bytes_sent += bytes_sent;
}
return true;
}
QByteArray HRDTransceiver::read_reply(QString const &cmd) {
// waitForReadReady appears to be occasionally unreliable on Windows
// timing out when data is waiting so retry a few times
unsigned retries{3};
bool replied{false};
while (!replied && retries--) {
replied = hrd_->waitForReadyRead();
if (!replied && hrd_->error() != hrd_->SocketTimeoutError) {
TRACE_CAT("HRDTransceiver",
cmd << "failed to reply" << hrd_->errorString());
throw error{
tr("Ham Radio Deluxe failed to reply to command \"%1\" %2\n")
.arg(cmd)
.arg(hrd_->errorString())};
}
}
if (!replied) {
TRACE_CAT("HRDTransceiver", cmd << "retries exhausted");
throw error{
tr("Ham Radio Deluxe retries exhausted sending command \"%1\"")
.arg(cmd)};
}
return hrd_->readAll();
}
void HRDTransceiver::send_simple_command(QString const &command,
bool no_debug) {
if ("OK" != send_command(command, no_debug)) {
TRACE_CAT("HRDTransceiver", command << "unexpected response");
throw error{
tr("Ham Radio Deluxe didn't respond to command \"%1\" as expected")
.arg(command)};
}
}
Q_LOGGING_CATEGORY(hrdtransceiver_js8, "hrdtransceiver.js8", QtWarningMsg)