From f018f765a77e6eaf37f90a07bf897ce9f2de9c02 Mon Sep 17 00:00:00 2001 From: John Date: Sun, 4 Oct 2026 17:36:42 +0300 Subject: [PATCH] Rig: Raw CAT command after tuning to a spot (rig profile) A rig profile can name a raw CAT command that QLog sends to the rig after it has tuned it to a spot from the DX cluster, the alert window or the bandmap. The field sits next to "DX Spots to Rig" in the rig profile and is empty by default. The case it was made for: the Yaesu FTDX101 has a second receiver. Its CAT command "SY2;" switches SYNC on and copies frequency and mode of the main receiver to the sub receiver, while antenna selection and the other sub receiver settings stay as they are. With "SY2;" in the profile both receivers listen to the clicked DX, each on its own antenna. The A=B copy would be one command less but copies the antenna too. On a direct link the driver sends the bytes with rig_send_raw. Behind rigctld that is not possible, because that socket speaks the rigctld protocol, so the driver opens a short second connection to rigctld and forwards the command with rigctld's "send_cmd_rx" and 0 reply bytes. It returns at once and leaves the Hamlib link untouched. --- core/Migration.h | 2 +- data/RigProfile.cpp | 13 ++++++--- data/RigProfile.h | 1 + res/res.qrc | 1 + res/sql/migration_042.sql | 1 + rig/Rig.cpp | 24 +++++++++++++++ rig/Rig.h | 2 ++ rig/drivers/GenericRigDrv.cpp | 9 ++++++ rig/drivers/GenericRigDrv.h | 1 + rig/drivers/HamlibRigDrv.cpp | 55 +++++++++++++++++++++++++++++++++++ rig/drivers/HamlibRigDrv.h | 1 + ui/NewContactWidget.cpp | 8 +++++ ui/SettingsDialog.cpp | 2 ++ ui/SettingsDialog.ui | 17 +++++++++++ 14 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 res/sql/migration_042.sql diff --git a/core/Migration.h b/core/Migration.h index 163b32c9..ac969be8 100644 --- a/core/Migration.h +++ b/core/Migration.h @@ -14,7 +14,7 @@ class DBSchemaMigration : public QObject bool run(bool force = false); static bool backupAllQSOsToADX(bool force = false); - static constexpr int latestVersion = 41; + static constexpr int latestVersion = 42; private: bool functionMigration(int version); diff --git a/data/RigProfile.cpp b/data/RigProfile.cpp index 0d0c2356..7d7e53d2 100644 --- a/data/RigProfile.cpp +++ b/data/RigProfile.cpp @@ -23,7 +23,7 @@ QDataStream& operator<<(QDataStream& out, const RigProfile& v) << v.getKeySpeed << v.assignedCWKey << v.keySpeedSync << v.driver << v.dxSpot2Rig << v.pttType << v.pttPortPath << v.rts << v.dtr << v.civAddr - << v.shareRigctld << v.rigctldPort << v.rigctldPath << v.rigctldArgs << v.getSplitInfo; + << v.shareRigctld << v.rigctldPort << v.rigctldPath << v.rigctldArgs << v.getSplitInfo << v.spotTuneCommand; return out; } @@ -69,6 +69,7 @@ QDataStream& operator>>(QDataStream& in, RigProfile& v) in >> v.rigctldPath; in >> v.rigctldArgs; in >> v.getSplitInfo; + in >> v.spotTuneCommand; return in; } @@ -88,7 +89,8 @@ RigProfilesManager::RigProfilesManager() : "key_speed_sync, driver, dxspot2rig, ptt_type, ptt_port_pathname, " "IFNULL(rts, '%0'), IFNULL(dtr, '%0'), IFNULL(civaddr, -1), " "IFNULL(share_rigctld, 0), IFNULL(rigctld_port, 4532), " - "IFNULL(rigctld_path, ''), IFNULL(rigctld_args, ''), get_split " + "IFNULL(rigctld_path, ''), IFNULL(rigctld_args, ''), get_split, " + "IFNULL(spot_tune_command, '') " "FROM rig_profiles").arg(SerialPort::SERIAL_SIGNAL_NONE))) { qWarning()<< "Cannot prepare select"; @@ -138,6 +140,7 @@ RigProfilesManager::RigProfilesManager() : profileDB.rigctldPath = profileQuery.value(36).toString(); profileDB.rigctldArgs = profileQuery.value(37).toString(); profileDB.getSplitInfo = profileQuery.value(38).toBool(); + profileDB.spotTuneCommand = profileQuery.value(39).toString(); addProfile(profileDB.profileName, profileDB); } @@ -166,13 +169,13 @@ void RigProfilesManager::save() "txfreq_end, get_freq, get_mode, get_vfo, get_pwr, rit_offset, xit_offset, get_rit, " "get_xit, default_pwr, get_ptt, qsy_wiping, get_key_speed, assigned_cw_key, key_speed_sync, " "driver, dxSpot2Rig, ptt_type, ptt_port_pathname, rts, dtr, civaddr, " - "share_rigctld, rigctld_port, rigctld_path, rigctld_args, get_split ) " + "share_rigctld, rigctld_port, rigctld_path, rigctld_args, get_split, spot_tune_command ) " "VALUES (:profile_name, :model, :port_pathname, :hostname, :netport, " ":baudrate, :databits, :stopbits, :flowcontrol, :parity, :pollinterval, :txfreq_start, " ":txfreq_end, :get_freq, :get_mode, :get_vfo, :get_pwr, :rit_offset, :xit_offset, :get_rit, " ":get_xit, :default_pwr, :get_ptt, :qsy_wiping, :get_key_speed, :assigned_cw_key, :key_speed_sync, " ":driver, :dxSpot2Rig, :ptt_type, :ptt_port_pathname, :rts, :dtr, :civaddr, " - ":share_rigctld, :rigctld_port, :rigctld_path, :rigctld_args, :get_split )") ) + ":share_rigctld, :rigctld_port, :rigctld_path, :rigctld_args, :get_split, :spot_tune_command )") ) { qWarning() << "cannot prepare Insert statement"; return; @@ -224,6 +227,7 @@ void RigProfilesManager::save() insertQuery.bindValue(":rigctld_path", rigProfile.rigctldPath); insertQuery.bindValue(":rigctld_args", rigProfile.rigctldArgs); insertQuery.bindValue(":get_split", rigProfile.getSplitInfo); + insertQuery.bindValue(":spot_tune_command", rigProfile.spotTuneCommand); if ( ! insertQuery.exec() ) { @@ -280,6 +284,7 @@ bool RigProfile::operator==(const RigProfile &profile) && profile.rigctldPath == this->rigctldPath && profile.rigctldArgs == this->rigctldArgs && profile.getSplitInfo == this->getSplitInfo + && profile.spotTuneCommand == this->spotTuneCommand ); } diff --git a/data/RigProfile.h b/data/RigProfile.h index d8d83af7..5da0d808 100644 --- a/data/RigProfile.h +++ b/data/RigProfile.h @@ -102,6 +102,7 @@ class RigProfile bool dxSpot2Rig; bool shareRigctld; bool getSplitInfo; + QString spotTuneCommand; bool operator== (const RigProfile &profile); bool operator!= (const RigProfile &profile); diff --git a/res/res.qrc b/res/res.qrc index 58a291ea..b36d73b5 100644 --- a/res/res.qrc +++ b/res/res.qrc @@ -56,5 +56,6 @@ sql/migration_039.sql sql/migration_040.sql sql/migration_041.sql + sql/migration_042.sql diff --git a/res/sql/migration_042.sql b/res/sql/migration_042.sql new file mode 100644 index 00000000..8a9d89b3 --- /dev/null +++ b/res/sql/migration_042.sql @@ -0,0 +1 @@ +ALTER TABLE rig_profiles ADD COLUMN spot_tune_command TEXT; diff --git a/rig/Rig.cpp b/rig/Rig.cpp index 9fb9426e..24ddd78e 100644 --- a/rig/Rig.cpp +++ b/rig/Rig.cpp @@ -742,6 +742,30 @@ void Rig::sendDXSpotImpl(const DxSpot &spot) rigDriver->sendDXSpot(spot); } +void Rig::sendRawCommand(const QString &command) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << command; + + QMetaObject::invokeMethod(this, "sendRawCommandImpl", Qt::QueuedConnection, + Q_ARG(QString, command)); +} + +void Rig::sendRawCommandImpl(const QString &command) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << command; + + MUTEXLOCKER; + + if ( ! rigDriver ) + return; + + rigDriver->sendRawCommand(command); +} + GenericRigDrv *Rig::getDriver(const RigProfile &profile, qint32 controlledRigModel) { FCT_IDENTIFICATION; diff --git a/rig/Rig.h b/rig/Rig.h index ba62eaee..ed9d1cf2 100644 --- a/rig/Rig.h +++ b/rig/Rig.h @@ -106,6 +106,7 @@ public slots: void stopMorse(); void sendState(); void sendDXSpot(DxSpot spot); + void sendRawCommand(const QString &command); signals: void frequencyChanged(VFOID, double, double, double); @@ -142,6 +143,7 @@ private slots: void stopMorseImpl(); void sendStateImpl(); void sendDXSpotImpl(const DxSpot &spot); + void sendRawCommandImpl(const QString &command); void sendHeartBeat(); private: diff --git a/rig/drivers/GenericRigDrv.cpp b/rig/drivers/GenericRigDrv.cpp index e28e1986..4b0fd8d5 100644 --- a/rig/drivers/GenericRigDrv.cpp +++ b/rig/drivers/GenericRigDrv.cpp @@ -44,3 +44,12 @@ void GenericRigDrv::setSplit(bool) // Default: do nothing — driver does not support split } + +void GenericRigDrv::sendRawCommand(const QString &command) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << command; + + // Default: do nothing - driver has no raw command channel +} diff --git a/rig/drivers/GenericRigDrv.h b/rig/drivers/GenericRigDrv.h index 37024064..b9ddf9e2 100644 --- a/rig/drivers/GenericRigDrv.h +++ b/rig/drivers/GenericRigDrv.h @@ -41,6 +41,7 @@ class GenericRigDrv : public QObject virtual void sendState() = 0; virtual void stopTimers() = 0; virtual void sendDXSpot(const DxSpot &spot) = 0; + virtual void sendRawCommand(const QString &command); signals: // STATE Signals diff --git a/rig/drivers/HamlibRigDrv.cpp b/rig/drivers/HamlibRigDrv.cpp index 5189c785..76f55749 100644 --- a/rig/drivers/HamlibRigDrv.cpp +++ b/rig/drivers/HamlibRigDrv.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "HamlibRigDrv.h" @@ -462,6 +463,60 @@ void HamlibRigDrv::setSplit(bool enabled) commandSleep(); } +void HamlibRigDrv::sendRawCommand(const QString &command) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << command; + + MUTEXLOCKER; + + if ( !rig ) + { + qCWarning(runtime) << "Rig is not active"; + return; + } + + if ( rigProfile.model == RIG_MODEL_NETRIGCTL ) + { + // The Hamlib link speaks the rigctld protocol, so raw bytes cannot go + // down that socket. rigctld takes further clients: a short second + // connection forwards the command with rigctld's "send_cmd_rx" and + // 0 reply bytes, so a set command returns at once and the protocol + // state of the Hamlib link is not touched. + QTcpSocket socket; + + socket.connectToHost(rigProfile.hostname, rigProfile.netport); + + if ( !socket.waitForConnected(1000) ) + { + qCWarning(runtime) << "Raw command failed, rigctld not reachable" << command << socket.errorString(); + return; + } + + socket.write(QString("W %1 0\n").arg(command).toLatin1()); + socket.waitForBytesWritten(1000); + socket.waitForReadyRead(1000); // rigctld's answer line, nothing to evaluate for a set command + socket.disconnectFromHost(); + } + else + { +#if HAMLIB_VERSION >= HAMLIB_VERSION_CHECK(4,5,0) + const QByteArray bytes = command.toLatin1(); + const int status = rig_send_raw(rig, + reinterpret_cast(bytes.constData()), bytes.size(), + nullptr, 0, nullptr); + + if ( status < 0 ) + qCWarning(runtime) << "Raw command failed" << command << rigerror(status); +#else + qCWarning(runtime) << "Raw CAT commands need Hamlib 4.5 or newer" << command; +#endif + } + + commandSleep(); +} + void HamlibRigDrv::setRawMode(const QString &rawMode) { FCT_IDENTIFICATION; diff --git a/rig/drivers/HamlibRigDrv.h b/rig/drivers/HamlibRigDrv.h index 91dda611..5e15d814 100644 --- a/rig/drivers/HamlibRigDrv.h +++ b/rig/drivers/HamlibRigDrv.h @@ -41,6 +41,7 @@ class HamlibRigDrv : public GenericRigDrv virtual void sendState() override; virtual void stopTimers() override; virtual void sendDXSpot(const DxSpot &spot) override; + virtual void sendRawCommand(const QString &command) override; private slots: void checkRigStateChange(); diff --git a/ui/NewContactWidget.cpp b/ui/NewContactWidget.cpp index 456d0ddb..4f5110a2 100644 --- a/ui/NewContactWidget.cpp +++ b/ui/NewContactWidget.cpp @@ -3550,6 +3550,14 @@ void NewContactWidget::tuneDx(const DxSpot &spot) } emit userModeChanged(VFO1, QString(), mode, subMode, bandwidthFilter); } + + // The rig profile may name a raw CAT command for the end of a spot + // tune, e.g. "SY2;" lets a Yaesu FTDX101 take its second receiver + // along to the same frequency and mode. + const QString &spotTuneCommand = RigProfilesManager::instance()->getCurProfile1().spotTuneCommand; + + if ( !spotTuneCommand.isEmpty() ) + rig->sendRawCommand(spotTuneCommand); } resetContact(); diff --git a/ui/SettingsDialog.cpp b/ui/SettingsDialog.cpp index aeb58b17..0407b943 100644 --- a/ui/SettingsDialog.cpp +++ b/ui/SettingsDialog.cpp @@ -998,6 +998,7 @@ void SettingsDialog::addRigProfile() profile.getKeySpeed = ui->rigGetKeySpeedCheckBox->isChecked(); profile.keySpeedSync = ui->rigKeySpeedSyncCheckBox->isChecked(); profile.dxSpot2Rig = ui->rigDXSpots2RigCheckBox->isChecked(); + profile.spotTuneCommand = ui->rigSpotTuneCommandEdit->text().trimmed(); profile.getSplitInfo = ui->rigGetSplitCheckBox->isChecked(); // Rigctld sharing settings @@ -1076,6 +1077,7 @@ void SettingsDialog::doubleClickRigProfile(QModelIndex i) ui->rigGetKeySpeedCheckBox->setChecked(profile.getKeySpeed); ui->rigKeySpeedSyncCheckBox->setChecked(profile.keySpeedSync); ui->rigDXSpots2RigCheckBox->setChecked(profile.dxSpot2Rig); + ui->rigSpotTuneCommandEdit->setText(profile.spotTuneCommand); ui->rigGetSplitCheckBox->setChecked(profile.getSplitInfo); setComboByData(ui->rigFlowControlSelect, profile.flowcontrol.toLower()); diff --git a/ui/SettingsDialog.ui b/ui/SettingsDialog.ui index 8903b712..1f0e0605 100644 --- a/ui/SettingsDialog.ui +++ b/ui/SettingsDialog.ui @@ -1990,6 +1990,23 @@ + + + + CAT after spot tuning: + + + + + + + Raw CAT command sent to the rig after QLog has tuned it to a spot. Example for the Yaesu FTDX101: SY2; synchronizes the second receiver (frequency and mode) with the main receiver. Leave empty if not needed. + + + e.g. SY2; + + +