mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-16 00:51:56 -05:00
94cec5bdc1
Working frequencies can be for all regions or for individual IARU regions. This allows each mode band tuple to have one or more working frequencies which can include local ones only offered when the user configures their IARU region. Change working frequency default suggestions to better fit in FT8. General rule is FT8 is at JT65 -2kHz except where that is not possible e.g. where that would fall into a segment not allocated for narrow band data modes. For tight bands like top band sqeeze existing JT65 and JT allocations to allow space for FT8. NOTE: this change changes the WSPR frequency on 80m to allow access to JA stations that currently have no allocation where it was placed. ALSO NOTE: the JT65 and JT9 frequencies for 80m move down 6 kHz, again to accommodate region 3 users. Other appliactions not within our control should be asked to move in step when 1.8.0 is released. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@7810 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
487 lines
16 KiB
C++
487 lines
16 KiB
C++
#include "WSPRBandHopping.hpp"
|
|
|
|
#include <random>
|
|
|
|
#include <QPointer>
|
|
#include <QSettings>
|
|
#include <QBitArray>
|
|
#include <QList>
|
|
#include <QSet>
|
|
#include <QtWidgets>
|
|
|
|
#include "SettingsGroup.hpp"
|
|
#include "Configuration.hpp"
|
|
#include "Bands.hpp"
|
|
#include "FrequencyList.hpp"
|
|
#include "WsprTxScheduler.h"
|
|
#include "pimpl_impl.hpp"
|
|
#include "moc_WSPRBandHopping.cpp"
|
|
|
|
extern "C"
|
|
{
|
|
#ifndef CMAKE_BUILD
|
|
#define FC_grayline grayline_
|
|
#else
|
|
#include "FC.h"
|
|
void FC_grayline (int const * year, int const * month, int const * nday, float const * uth, char const * my_grid
|
|
, int const * nduration, int * isun
|
|
, int my_grid_len);
|
|
#endif
|
|
};
|
|
|
|
namespace
|
|
{
|
|
char const * const title = "WSPR Band Hopping";
|
|
char const * const periods[] = {"Sunrise grayline", "Day", "Sunset grayline", "Night", "Tune", "Rx only"};
|
|
size_t constexpr num_periods {sizeof (periods) / sizeof (periods[0])};
|
|
// These 10 bands are globally coordinated
|
|
QList<QString> const coordinated_bands = {"160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m"};
|
|
}
|
|
|
|
//
|
|
// Dialog - maintenance of band hopping options
|
|
//
|
|
class Dialog
|
|
: public QDialog
|
|
{
|
|
public:
|
|
using BandList = QList<QString>;
|
|
|
|
Dialog (QSettings *, Configuration const *, BandList const * WSPT_bands, QBitArray * bands
|
|
, int * gray_line_duration, QWidget * parent = nullptr);
|
|
~Dialog ();
|
|
|
|
Q_SLOT void frequencies_changed ();
|
|
void resize_to_maximum ();
|
|
|
|
private:
|
|
void closeEvent (QCloseEvent *) override;
|
|
void save_window_state ();
|
|
|
|
QSettings * settings_;
|
|
Configuration const * configuration_;
|
|
BandList const * WSPR_bands_;
|
|
QBitArray * bands_;
|
|
int * gray_line_duration_;
|
|
QPointer<QTableWidget> bands_table_;
|
|
QBrush coord_background_brush_;
|
|
QPointer<QSpinBox> gray_line_width_spin_box_;
|
|
static int constexpr band_index_role {Qt::UserRole};
|
|
};
|
|
|
|
Dialog::Dialog (QSettings * settings, Configuration const * configuration, BandList const * WSPR_bands
|
|
, QBitArray * bands, int * gray_line_duration, QWidget * parent)
|
|
: QDialog {parent, Qt::Window | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint}
|
|
, settings_ {settings}
|
|
, configuration_ {configuration}
|
|
, WSPR_bands_ {WSPR_bands}
|
|
, bands_ {bands}
|
|
, gray_line_duration_ {gray_line_duration}
|
|
, bands_table_ {new QTableWidget {this}}
|
|
, coord_background_brush_ {Qt::yellow}
|
|
, gray_line_width_spin_box_ {new QSpinBox {this}}
|
|
{
|
|
setWindowTitle (windowTitle () + ' ' + tr (title));
|
|
{
|
|
SettingsGroup g {settings_, title};
|
|
restoreGeometry (settings_->value ("geometry", saveGeometry ()).toByteArray ());
|
|
}
|
|
|
|
QVBoxLayout * main_layout {new QVBoxLayout};
|
|
|
|
bands_table_->setRowCount (num_periods);
|
|
bands_table_->setVerticalScrollBarPolicy (Qt::ScrollBarAlwaysOff);
|
|
bands_table_->setHorizontalScrollBarPolicy (Qt::ScrollBarAlwaysOff);
|
|
bands_table_->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
frequencies_changed ();
|
|
main_layout->addWidget (bands_table_);
|
|
// recalculate table when frequencies change
|
|
connect (configuration_->frequencies (), &QAbstractItemModel::layoutChanged
|
|
, this, &Dialog::frequencies_changed);
|
|
// handle changes by updating the underlying flags
|
|
connect (bands_table_.data (), &QTableWidget::itemChanged, [this] (QTableWidgetItem * item) {
|
|
auto band_number = item->data (band_index_role).toInt ();
|
|
bands_[item->row ()].setBit (band_number, Qt::Checked == item->checkState ());
|
|
});
|
|
|
|
// set up the gray line duration spin box
|
|
gray_line_width_spin_box_->setRange (1, 60 * 2);
|
|
gray_line_width_spin_box_->setSuffix ("min");
|
|
gray_line_width_spin_box_->setValue (*gray_line_duration_);
|
|
QFormLayout * form_layout = new QFormLayout;
|
|
form_layout->addRow (tr ("Gray time:"), gray_line_width_spin_box_);
|
|
connect (gray_line_width_spin_box_.data ()
|
|
, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged)
|
|
, [this] (int new_value) {*gray_line_duration_ = new_value;});
|
|
|
|
QHBoxLayout * bottom_layout = new QHBoxLayout;
|
|
bottom_layout->addStretch ();
|
|
bottom_layout->addLayout (form_layout);
|
|
main_layout->addLayout (bottom_layout);
|
|
|
|
setLayout (main_layout);
|
|
}
|
|
|
|
Dialog::~Dialog ()
|
|
{
|
|
// do this here too because ESC or parent shutdown closing this
|
|
// window doesn't queue a close event
|
|
save_window_state ();
|
|
}
|
|
|
|
void Dialog::closeEvent (QCloseEvent * e)
|
|
{
|
|
save_window_state ();
|
|
QDialog::closeEvent (e);
|
|
}
|
|
|
|
void Dialog::save_window_state ()
|
|
{
|
|
SettingsGroup g {settings_, title};
|
|
settings_->setValue ("geometry", saveGeometry ());
|
|
}
|
|
|
|
void Dialog::frequencies_changed ()
|
|
{
|
|
bands_table_->setColumnCount (WSPR_bands_->size ());
|
|
// set up and load the table of check boxes
|
|
for (auto row = 0u; row < num_periods; ++row)
|
|
{
|
|
auto vertical_header = new QTableWidgetItem {periods[row]};
|
|
vertical_header->setTextAlignment (Qt::AlignRight | Qt::AlignVCenter);
|
|
bands_table_->setVerticalHeaderItem (row, vertical_header);
|
|
int column {0};
|
|
int band_number {0};
|
|
for (auto const& band : *configuration_->bands ())
|
|
{
|
|
if (WSPR_bands_->contains (band))
|
|
{
|
|
if (0 == row)
|
|
{
|
|
auto horizontal_header = new QTableWidgetItem {band};
|
|
bands_table_->setHorizontalHeaderItem (column, horizontal_header);
|
|
}
|
|
auto item = new QTableWidgetItem;
|
|
item->setFlags (Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
|
|
item->setCheckState (bands_[row].testBit (band_number) ? Qt::Checked : Qt::Unchecked);
|
|
item->setData (band_index_role, band_number);
|
|
if (coordinated_bands.contains (band))
|
|
{
|
|
item->setBackground (coord_background_brush_);
|
|
}
|
|
bands_table_->setItem (row, column, item);
|
|
++column;
|
|
}
|
|
++band_number;
|
|
}
|
|
}
|
|
bands_table_->resizeColumnsToContents ();
|
|
auto is_visible = isVisible ();
|
|
show ();
|
|
resize_to_maximum ();
|
|
adjustSize (); // fix the size
|
|
if (!is_visible)
|
|
{
|
|
hide ();
|
|
}
|
|
}
|
|
|
|
// to get the dialog window exactly the right size to contain the
|
|
// widgets without needing scroll bars we need to measure the size of
|
|
// the table widget and set its minimum size to the measured size
|
|
void Dialog::resize_to_maximum ()
|
|
{
|
|
bands_table_->setMinimumSize ({
|
|
bands_table_->horizontalHeader ()->length ()
|
|
+ bands_table_->verticalHeader ()->width ()
|
|
+ 2 * bands_table_->frameWidth ()
|
|
, bands_table_->verticalHeader ()->length ()
|
|
+ bands_table_->horizontalHeader ()->height ()
|
|
+ 2 * bands_table_->frameWidth ()
|
|
});
|
|
bands_table_->setMaximumSize (bands_table_->minimumSize ());
|
|
}
|
|
|
|
class WSPRBandHopping::impl
|
|
{
|
|
public:
|
|
using BandList = Dialog::BandList;
|
|
|
|
impl (QSettings * settings, Configuration const * configuration, QWidget * parent_widget)
|
|
: settings_ {settings}
|
|
, configuration_ {configuration}
|
|
, tx_percent_ {0}
|
|
, parent_widget_ {parent_widget}
|
|
, last_was_tx_ {false}
|
|
, carry_ {false}
|
|
, seed_ {{rand (), rand (), rand (), rand (), rand (), rand (), rand (), rand ()}}
|
|
, gen_ {seed_}
|
|
, dist_ {1, 100}
|
|
{
|
|
auto num_bands = configuration_->bands ()->rowCount ();
|
|
for (auto& flags : bands_)
|
|
{
|
|
flags.resize (num_bands);
|
|
}
|
|
}
|
|
|
|
bool simple_scheduler ();
|
|
|
|
QSettings * settings_;
|
|
Configuration const * configuration_;
|
|
int tx_percent_;
|
|
BandList WSPR_bands_;
|
|
BandList rx_permutation_;
|
|
BandList tx_permutation_;
|
|
QWidget * parent_widget_;
|
|
|
|
// 5 x 10 bit flags representing each hopping band in each period
|
|
// and tune
|
|
QBitArray bands_[num_periods];
|
|
|
|
int gray_line_duration_;
|
|
QPointer<Dialog> dialog_;
|
|
bool last_was_tx_;
|
|
bool carry_;
|
|
std::seed_seq seed_;
|
|
std::mt19937 gen_;
|
|
std::uniform_int_distribution<int> dist_;
|
|
};
|
|
|
|
bool WSPRBandHopping::impl::simple_scheduler ()
|
|
{
|
|
auto tx = carry_ || tx_percent_ > dist_ (gen_);
|
|
if (carry_)
|
|
{
|
|
carry_ = false;
|
|
}
|
|
else if (tx_percent_ < 40 && last_was_tx_ && tx)
|
|
{
|
|
// if percentage is less than 40 then avoid consecutive tx but
|
|
// always catch up on the next round
|
|
tx = false;
|
|
carry_ = true;
|
|
}
|
|
last_was_tx_ = tx;
|
|
return tx;
|
|
}
|
|
|
|
WSPRBandHopping::WSPRBandHopping (QSettings * settings, Configuration const * configuration, QWidget * parent_widget)
|
|
: m_ {settings, configuration, parent_widget}
|
|
{
|
|
// detect changes to the working frequencies model
|
|
m_->WSPR_bands_ = m_->configuration_->frequencies ()->all_bands (m_->configuration_->region (), Modes::WSPR).toList ();
|
|
connect (m_->configuration_->frequencies (), &QAbstractItemModel::layoutChanged
|
|
, [this] () {
|
|
m_->WSPR_bands_ = m_->configuration_->frequencies ()->all_bands (m_->configuration_->region (), Modes::WSPR).toList ();
|
|
});
|
|
|
|
// load settings
|
|
SettingsGroup g {m_->settings_, title};
|
|
size_t size = m_->settings_->beginReadArray ("phases");
|
|
for (auto i = 0u; i < size; ++i)
|
|
{
|
|
if (i < num_periods)
|
|
{
|
|
m_->settings_->setArrayIndex (i);
|
|
m_->bands_[i] = m_->settings_->value ("bands").toBitArray ();
|
|
}
|
|
}
|
|
m_->settings_->endArray ();
|
|
m_->gray_line_duration_ = m_->settings_->value ("GrayLineDuration", 60).toUInt ();
|
|
}
|
|
|
|
WSPRBandHopping::~WSPRBandHopping ()
|
|
{
|
|
// save settings
|
|
SettingsGroup g {m_->settings_, title};
|
|
m_->settings_->beginWriteArray ("phases");
|
|
for (auto i = 0u; i < num_periods; ++i)
|
|
{
|
|
m_->settings_->setArrayIndex (i);
|
|
m_->settings_->setValue ("bands", m_->bands_[i]);
|
|
}
|
|
m_->settings_->endArray ();
|
|
m_->settings_->setValue ("GrayLineDuration", m_->gray_line_duration_);
|
|
}
|
|
|
|
// pop up the maintenance dialog window
|
|
void WSPRBandHopping::show_dialog (bool /* checked */)
|
|
{
|
|
if (!m_->dialog_)
|
|
{
|
|
m_->dialog_ = new Dialog {m_->settings_, m_->configuration_, &m_->WSPR_bands_, m_->bands_
|
|
, &m_->gray_line_duration_, m_->parent_widget_};
|
|
}
|
|
m_->dialog_->show ();
|
|
m_->dialog_->raise ();
|
|
m_->dialog_->activateWindow ();
|
|
}
|
|
|
|
int WSPRBandHopping::tx_percent () const
|
|
{
|
|
return m_->tx_percent_;
|
|
}
|
|
|
|
void WSPRBandHopping::set_tx_percent (int new_value)
|
|
{
|
|
m_->tx_percent_ = new_value;
|
|
}
|
|
|
|
// determine the parameters of the hop, if any
|
|
auto WSPRBandHopping::next_hop (bool tx_enabled) -> Hop
|
|
{
|
|
auto const& now = QDateTime::currentDateTimeUtc ();
|
|
auto const& date = now.date ();
|
|
auto year = date.year ();
|
|
auto month = date.month ();
|
|
auto day = date.day ();
|
|
auto const& time = now.time ();
|
|
float uth = time.hour () + time.minute () / 60.
|
|
+ (time.second () + .001 * time.msec ()) / 3600.;
|
|
auto my_grid = m_->configuration_->my_grid ();
|
|
int period_index;
|
|
int band_index;
|
|
int tx_next;
|
|
|
|
my_grid = (my_grid + " ").left (6); // hopping doesn't like
|
|
// short grids
|
|
|
|
// look up the period for this time
|
|
FC_grayline (&year, &month, &day, &uth, my_grid.toLatin1 ().constData ()
|
|
, &m_->gray_line_duration_, &period_index
|
|
, my_grid.size ());
|
|
|
|
band_index = next_hopping_band();
|
|
|
|
tx_next = next_is_tx () && tx_enabled;
|
|
|
|
int frequencies_index {-1};
|
|
auto const& frequencies = m_->configuration_->frequencies ();
|
|
auto const& bands = m_->configuration_->bands ();
|
|
auto band_name = bands->data (bands->index (band_index + 3, 0)).toString ();
|
|
if (m_->bands_[period_index].testBit (band_index + 3) // +3 for
|
|
// coordinated bands
|
|
&& m_->WSPR_bands_.contains (band_name))
|
|
{
|
|
// here we have a band that has been enabled in the hopping
|
|
// matrix so check it it has a configured working frequency
|
|
frequencies_index = frequencies->best_working_frequency (band_name);
|
|
}
|
|
|
|
// if we do not have a configured working frequency on the selected
|
|
// coordinated hopping band we next pick from a random permutation
|
|
// of the other enabled bands in the hopping bands matrix
|
|
if (frequencies_index < 0)
|
|
{
|
|
// build sets of available rx and tx bands
|
|
auto target_rx_bands = m_->WSPR_bands_.toSet ();
|
|
auto target_tx_bands = target_rx_bands;
|
|
for (auto i = 0; i < m_->bands_[period_index].size (); ++i)
|
|
{
|
|
auto const& band = bands->data (bands->index (i, 0)).toString ();
|
|
// remove bands that are not enabled for hopping in this phase
|
|
if (!m_->bands_[period_index].testBit (i))
|
|
{
|
|
target_rx_bands.remove (band);
|
|
target_tx_bands.remove (band);
|
|
}
|
|
// remove rx only bands from transmit list and vice versa
|
|
if (m_->bands_[5].testBit (i))
|
|
{
|
|
target_tx_bands.remove (band);
|
|
}
|
|
else
|
|
{
|
|
target_rx_bands.remove (band);
|
|
}
|
|
}
|
|
// if we have some bands to permute
|
|
if (target_rx_bands.size () + target_tx_bands.size ())
|
|
{
|
|
if (!(m_->rx_permutation_.size () + m_->tx_permutation_.size ()) // all used up
|
|
// or rx list contains a band no longer scheduled
|
|
|| !target_rx_bands.contains (m_->rx_permutation_.toSet ())
|
|
// or tx list contains a band no longer scheduled for tx
|
|
|| !target_tx_bands.contains (m_->tx_permutation_.toSet ()))
|
|
{
|
|
// build new random permutations
|
|
m_->rx_permutation_ = target_rx_bands.toList ();
|
|
std::random_shuffle (std::begin (m_->rx_permutation_), std::end (m_->rx_permutation_));
|
|
m_->tx_permutation_ = target_tx_bands.toList ();
|
|
std::random_shuffle (std::begin (m_->tx_permutation_), std::end (m_->tx_permutation_));
|
|
// qDebug () << "New random Rx permutation:" << m_->rx_permutation_
|
|
// << "random Tx permutation:" << m_->tx_permutation_;
|
|
}
|
|
if ((tx_next && m_->tx_permutation_.size ()) || !m_->rx_permutation_.size ())
|
|
{
|
|
Q_ASSERT (m_->tx_permutation_.size ());
|
|
// use one from the current random tx permutation
|
|
band_name = m_->tx_permutation_.takeFirst ();
|
|
}
|
|
else
|
|
{
|
|
Q_ASSERT (m_->rx_permutation_.size ());
|
|
// use one from the current random rx permutation
|
|
band_name = m_->rx_permutation_.takeFirst ();
|
|
}
|
|
// find the first WSPR working frequency for the chosen band
|
|
frequencies_index = frequencies->best_working_frequency (band_name);
|
|
if (frequencies_index >= 0) // should be a redundant check,
|
|
// but to be safe
|
|
{
|
|
// we can use the random choice
|
|
// qDebug () << "random:" << frequencies->data (frequencies->index (frequencies_index, FrequencyList::frequency_column)).toString ();
|
|
band_index = bands->find (band_name);
|
|
if (band_index < 0) // this shouldn't happen
|
|
{
|
|
Q_ASSERT (band_index >= 0);
|
|
frequencies_index = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
band_index += 3;
|
|
// qDebug () << "scheduled:" << frequencies->data (frequencies->index (frequencies_index, FrequencyList::frequency_column)).toString ();
|
|
// remove from random permutations to stop the coordinated bands
|
|
// getting too high a weighting - not perfect but surely helps
|
|
m_->rx_permutation_.removeOne (band_name);
|
|
m_->tx_permutation_.removeOne (band_name);
|
|
}
|
|
|
|
return {
|
|
periods[period_index]
|
|
|
|
, frequencies_index
|
|
|
|
, frequencies_index >= 0 // new band
|
|
&& tx_enabled // transmit is allowed
|
|
&& !tx_next // not going to Tx anyway
|
|
&& m_->bands_[4].testBit (band_index) // tune up required
|
|
&& !m_->bands_[5].testBit (band_index) // not an Rx only band
|
|
|
|
, frequencies_index >= 0 // new band
|
|
&& tx_next // Tx scheduled
|
|
&& !m_->bands_[5].testBit (band_index) // not an Rx only band
|
|
};
|
|
}
|
|
|
|
bool WSPRBandHopping::next_is_tx (bool simple_schedule)
|
|
{
|
|
if (simple_schedule)
|
|
{
|
|
return m_->simple_scheduler ();
|
|
}
|
|
if (100 == m_->tx_percent_)
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// consult scheduler to determine if next period should be a tx interval
|
|
return next_tx_state(m_->tx_percent_);
|
|
}
|
|
}
|