File: WSPRBandHopping.cpp

package info (click to toggle)
wsjtx 2.0.0%2Brepack-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 192,624 kB
  • sloc: cpp: 1,071,838; ansic: 60,751; f90: 25,266; python: 20,318; sh: 10,636; xml: 8,148; cs: 2,121; fortran: 2,051; yacc: 472; asm: 353; makefile: 316; perl: 19
file content (486 lines) | stat: -rw-r--r-- 16,657 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
#include "WSPRBandHopping.hpp"

#include <random>

#include <QPointer>
#include <QSettings>
#include <QBitArray>
#include <QList>
#include <QSet>
#include <QtWidgets>

#include "SettingsGroup.hpp"
#include "Configuration.hpp"
#include "models/Bands.hpp"
#include "models/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_v2::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_v2::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_);
    }
}