Add WSPR decodes to UDP message protocol

The message_aggregator (MessageAggregator.cpp) has  been updated to do
something with WSPR decodes.

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@6101 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
Bill Somerville 2015-11-15 23:03:11 +00:00
parent 79310954f4
commit 8b3ccfa3b6
8 changed files with 313 additions and 39 deletions

View File

@ -184,17 +184,148 @@ private:
QFont text_font_;
};
//
// Beacons Model - simple data model for all beacon spots
//
// The model is a basic table with uniform row format. Rows consist of
// QStandardItem instances containing the string representation of the
// column data and if the underlying field is not a string then the
// UserRole+1 role contains the underlying data item.
//
// Two slots are provided to add a new decode and remove all spots for
// a client.
//
class BeaconsModel
: public QStandardItemModel
{
Q_OBJECT;
public:
BeaconsModel (QObject * parent = nullptr)
: QStandardItemModel {0, 9, parent}
, text_font_ {"Courier", 10}
{
setHeaderData (0, Qt::Horizontal, tr ("Client"));
setHeaderData (1, Qt::Horizontal, tr ("Time"));
setHeaderData (2, Qt::Horizontal, tr ("Snr"));
setHeaderData (3, Qt::Horizontal, tr ("DT"));
setHeaderData (4, Qt::Horizontal, tr ("Frequency"));
setHeaderData (5, Qt::Horizontal, tr ("Drift"));
setHeaderData (6, Qt::Horizontal, tr ("Callsign"));
setHeaderData (7, Qt::Horizontal, tr ("Grid"));
setHeaderData (8, Qt::Horizontal, tr ("Power"));
}
Q_SLOT void add_beacon_spot (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign, QString const& grid
, qint32 power)
{
if (!is_new)
{
int target_row {-1};
for (auto row = 0; row < rowCount (); ++row)
{
if (data (index (row, 0)).toString () == client_id)
{
auto row_time = item (row, 1)->data ().toTime ();
if (row_time == time
&& item (row, 2)->data ().toInt () == snr
&& item (row, 3)->data ().toFloat () == delta_time
&& item (row, 4)->data ().value<Frequency> () == frequency
&& data (index (row, 5)).toInt () == drift
&& data (index (row, 6)).toString () == callsign
&& data (index (row, 7)).toString () == grid
&& data (index (row, 8)).toInt () == power)
{
return;
}
if (time <= row_time)
{
target_row = row; // last row with same time
}
}
}
if (target_row >= 0)
{
insertRow (target_row + 1, make_row (client_id, time, snr, delta_time, frequency, drift, callsign, grid, power));
return;
}
}
appendRow (make_row (client_id, time, snr, delta_time, frequency, drift, callsign, grid, power));
}
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign
, QString const& grid, qint32 power) const
{
auto time_item = new QStandardItem {time.toString ("hh:mm")};
time_item->setData (time);
time_item->setTextAlignment (Qt::AlignRight);
auto snr_item = new QStandardItem {QString::number (snr)};
snr_item->setData (snr);
snr_item->setTextAlignment (Qt::AlignRight);
auto dt = new QStandardItem {QString::number (delta_time)};
dt->setData (delta_time);
dt->setTextAlignment (Qt::AlignRight);
auto freq = new QStandardItem {Radio::pretty_frequency_MHz_string (frequency)};
freq->setData (frequency);
freq->setTextAlignment (Qt::AlignRight);
auto dri = new QStandardItem {QString::number (drift)};
dri->setData (drift);
dri->setTextAlignment (Qt::AlignRight);
auto gd = new QStandardItem {grid};
gd->setTextAlignment (Qt::AlignRight);
auto pwr = new QStandardItem {QString::number (power)};
pwr->setData (power);
pwr->setTextAlignment (Qt::AlignRight);
QList<QStandardItem *> row {
new QStandardItem {client_id}, time_item, snr_item, dt, freq, dri, new QStandardItem {callsign}, gd, pwr};
Q_FOREACH (auto& item, row)
{
item->setEditable (false);
item->setFont (text_font_);
item->setTextAlignment (item->textAlignment () | Qt::AlignVCenter);
}
return row;
}
Q_SLOT void clear_decodes (QString const& client_id)
{
for (auto row = rowCount () - 1; row >= 0; --row)
{
if (data (index (row, 0)).toString () == client_id)
{
removeRow (row);
}
}
}
private:
QFont text_font_;
};
class ClientWidget
: public QDockWidget
{
Q_OBJECT;
public:
explicit ClientWidget (QAbstractItemModel * decodes_model, QString const& id, QWidget * parent = 0)
explicit ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model
, QString const& id, QWidget * parent = 0)
: QDockWidget {id, parent}
, id_ {id}
, decodes_table_view_ {new QTableView}
, beacons_table_view_ {new QTableView}
, message_line_edit_ {new QLineEdit}
, decodes_stack_ {new QStackedLayout}
, auto_off_button_ {new QPushButton {tr ("&Auto Off")}}
, halt_tx_button_ {new QPushButton {tr ("&Halt Tx")}}
, mode_label_ {new QLabel}
@ -202,19 +333,14 @@ public:
, frequency_label_ {new QLabel}
, report_label_ {new QLabel}
{
auto content_layout = new QVBoxLayout;
content_layout->setContentsMargins (QMargins {2, 2, 2, 2});
// set up table
auto proxy_model = new DecodesFilterModel {id, this};
proxy_model->setSourceModel (decodes_model);
decodes_table_view_->setModel (proxy_model);
// set up widgets
auto decodes_proxy_model = new IdFilterModel {id, this};
decodes_proxy_model->setSourceModel (decodes_model);
decodes_table_view_->setModel (decodes_proxy_model);
decodes_table_view_->verticalHeader ()->hide ();
decodes_table_view_->hideColumn (0);
content_layout->addWidget (decodes_table_view_);
decodes_table_view_->horizontalHeader ()->setStretchLastSection (true);
// set up controls
auto control_layout = new QHBoxLayout;
auto form_layout = new QFormLayout;
form_layout->addRow (tr ("Free text:"), message_line_edit_);
message_line_edit_->setValidator (new QRegExpValidator {message_alphabet, this});
@ -224,16 +350,44 @@ public:
connect (message_line_edit_, &QLineEdit::editingFinished, [this] () {
Q_EMIT do_free_text (id_, message_line_edit_->text (), true);
});
control_layout->addLayout (form_layout);
control_layout->addWidget (auto_off_button_);
control_layout->addWidget (halt_tx_button_);
auto decodes_page = new QWidget;
auto decodes_layout = new QVBoxLayout {decodes_page};
decodes_layout->setContentsMargins (QMargins {2, 2, 2, 2});
decodes_layout->addWidget (decodes_table_view_);
decodes_layout->addLayout (form_layout);
auto beacons_proxy_model = new IdFilterModel {id, this};
beacons_proxy_model->setSourceModel (beacons_model);
beacons_table_view_->setModel (beacons_proxy_model);
beacons_table_view_->verticalHeader ()->hide ();
beacons_table_view_->hideColumn (0);
beacons_table_view_->horizontalHeader ()->setStretchLastSection (true);
auto beacons_page = new QWidget;
auto beacons_layout = new QVBoxLayout {beacons_page};
beacons_layout->setContentsMargins (QMargins {2, 2, 2, 2});
beacons_layout->addWidget (beacons_table_view_);
decodes_stack_->addWidget (decodes_page);
decodes_stack_->addWidget (beacons_page);
// stack alternative views
auto content_layout = new QVBoxLayout;
content_layout->setContentsMargins (QMargins {2, 2, 2, 2});
content_layout->addLayout (decodes_stack_);
// set up controls
auto control_button_box = new QDialogButtonBox;
control_button_box->addButton (auto_off_button_, QDialogButtonBox::ActionRole);
control_button_box->addButton (halt_tx_button_, QDialogButtonBox::ActionRole);
connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, true);
});
connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, false);
});
content_layout->addLayout (control_layout);
content_layout->addWidget (control_button_box);
// set up status area
auto status_bar = new QStatusBar;
@ -254,8 +408,8 @@ public:
setAllowedAreas (Qt::BottomDockWidgetArea);
// connect up table view signals
connect (decodes_table_view_, &QTableView::doubleClicked, this, [this, proxy_model] (QModelIndex const& index) {
Q_EMIT do_reply (proxy_model->mapToSource (index));
connect (decodes_table_view_, &QTableView::doubleClicked, this, [this, decodes_proxy_model] (QModelIndex const& index) {
Q_EMIT do_reply (decodes_proxy_model->mapToSource (index));
});
}
@ -265,7 +419,9 @@ public:
{
if (id == id_)
{
mode_label_->setText (QString {"Mode: %1%2"}.arg (mode).arg (tx_mode.isEmpty () ? tx_mode : '(' + tx_mode + ')'));
mode_label_->setText (QString {"Mode: %1%2"}
.arg (mode)
.arg (tx_mode.isEmpty () || tx_mode == mode ? "" : '(' + tx_mode + ')'));
dx_call_label_->setText ("DX CALL: " + dx_call);
frequency_label_->setText ("QRG: " + Radio::pretty_frequency_MHz_string (f));
report_label_->setText ("SNR: " + report);
@ -277,27 +433,39 @@ public:
}
Q_SLOT void decode_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, quint32 /*delta_frequency*/, QString const& /*mode*/
, QString const& /*message*/)
, float /*delta_time*/, quint32 /*delta_frequency*/, QString const& /*mode*/
, QString const& /*message*/)
{
if (client_id == id_)
{
decodes_stack_->setCurrentIndex (0);
decodes_table_view_->resizeColumnsToContents ();
decodes_table_view_->horizontalHeader ()->setStretchLastSection (true);
decodes_table_view_->scrollToBottom ();
}
}
Q_SLOT void beacon_spot_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, Frequency /*delta_frequency*/, qint32 /*drift*/, QString const& /*callsign*/
, QString const& /*grid*/, qint32 /*power*/)
{
if (client_id == id_)
{
decodes_stack_->setCurrentIndex (1);
beacons_table_view_->resizeColumnsToContents ();
beacons_table_view_->scrollToBottom ();
}
}
Q_SIGNAL void do_reply (QModelIndex const&);
Q_SIGNAL void do_halt_tx (QString const& id, bool auto_only);
Q_SIGNAL void do_free_text (QString const& id, QString const& text, bool);
private:
class DecodesFilterModel final
class IdFilterModel final
: public QSortFilterProxyModel
{
public:
DecodesFilterModel (QString const& id, QObject * parent = nullptr)
IdFilterModel (QString const& id, QObject * parent = nullptr)
: QSortFilterProxyModel {parent}
, id_ {id}
{}
@ -315,7 +483,9 @@ private:
QString id_;
QTableView * decodes_table_view_;
QTableView * beacons_table_view_;
QLineEdit * message_line_edit_;
QStackedLayout * decodes_stack_;
QAbstractButton * auto_off_button_;
QAbstractButton * halt_tx_button_;
QLabel * mode_label_;
@ -333,6 +503,7 @@ public:
MainWindow ()
: log_ {new QStandardItemModel {0, 10, this}}
, decodes_model_ {new DecodesModel {this}}
, beacons_model_ {new BeaconsModel {this}}
, server_ {new MessageServer {this}}
, multicast_group_line_edit_ {new QLineEdit}
, log_table_view_ {new QTableView}
@ -395,8 +566,11 @@ public:
connect (server_, &MessageServer::client_opened, this, &MainWindow::add_client);
connect (server_, &MessageServer::client_closed, this, &MainWindow::remove_client);
connect (server_, &MessageServer::client_closed, decodes_model_, &DecodesModel::clear_decodes);
connect (server_, &MessageServer::client_closed, beacons_model_, &BeaconsModel::clear_decodes);
connect (server_, &MessageServer::decode, decodes_model_, &DecodesModel::add_decode);
connect (server_, &MessageServer::WSPR_decode, beacons_model_, &BeaconsModel::add_beacon_spot);
connect (server_, &MessageServer::clear_decodes, decodes_model_, &DecodesModel::clear_decodes);
connect (server_, &MessageServer::clear_decodes, beacons_model_, &BeaconsModel::clear_decodes);
connect (decodes_model_, &DecodesModel::reply, server_, &MessageServer::reply);
// UI behaviour
@ -435,7 +609,7 @@ public:
private:
void add_client (QString const& id)
{
auto dock = new ClientWidget {decodes_model_, id, this};
auto dock = new ClientWidget {decodes_model_, beacons_model_, id, this};
dock->setAttribute (Qt::WA_DeleteOnClose);
auto view_action = dock->toggleViewAction ();
view_action->setEnabled (true);
@ -443,6 +617,7 @@ private:
addDockWidget (Qt::BottomDockWidgetArea, dock);
connect (server_, &MessageServer::status_update, dock, &ClientWidget::update_status);
connect (server_, &MessageServer::decode, dock, &ClientWidget::decode_added);
connect (server_, &MessageServer::WSPR_decode, dock, &ClientWidget::beacon_spot_added);
connect (dock, &ClientWidget::do_reply, decodes_model_, &DecodesModel::do_reply);
connect (dock, &ClientWidget::do_halt_tx, server_, &MessageServer::halt_tx);
connect (dock, &ClientWidget::do_free_text, server_, &MessageServer::free_text);
@ -464,6 +639,7 @@ private:
QStandardItemModel * log_;
QMenu * view_menu_;
DecodesModel * decodes_model_;
BeaconsModel * beacons_model_;
MessageServer * server_;
QLineEdit * multicast_group_line_edit_;
QTableView * log_table_view_;

View File

@ -272,9 +272,16 @@ MessageClient::MessageClient (QString const& id, QString const& server, port_typ
, m_ {id, server_port, this}
{
connect (&*m_, static_cast<void (impl::*) (impl::SocketError)> (&impl::error)
, [this] (impl::SocketError /* e */)
, [this] (impl::SocketError e)
{
Q_EMIT error (m_->errorString ());
#if defined (Q_OS_WIN) && QT_VERSION >= 0x050500
if (e != impl::NetworkError) // take this out when Qt 5.5
// stops doing this
// spuriously
#endif
{
Q_EMIT error (m_->errorString ());
}
});
set_server (server);
}
@ -340,6 +347,19 @@ void MessageClient::decode (bool is_new, QTime time, qint32 snr, float delta_tim
}
}
void MessageClient::WSPR_decode (bool is_new, QTime time, qint32 snr, float delta_time, Frequency frequency
, qint32 drift, QString const& callsign, QString const& grid, qint32 power)
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::WSPRDecode, m_->id_, m_->schema_};
out << is_new << time << snr << delta_time << frequency << drift << callsign.toUtf8 ()
<< grid.toUtf8 () << power;
m_->send_message (out, message);
}
}
void MessageClient::clear_decodes ()
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())

View File

@ -14,8 +14,8 @@ class QHostAddress;
//
// MessageClient - Manage messages sent and replies received from a
// matching server (MessageServer) at the other end of
// the wire
// matching server (MessageServer) at the other end of
// the wire
//
//
// Each outgoing message type is a Qt slot
@ -50,6 +50,8 @@ public:
, QString const& tx_mode, bool tx_enabled, bool transmitting, bool decoding);
Q_SLOT void decode (bool is_new, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message);
Q_SLOT void WSPR_decode (bool is_new, QTime time, qint32 snr, float delta_time, Frequency
, qint32 drift, QString const& callsign, QString const& grid, qint32 power);
Q_SLOT void clear_decodes ();
Q_SLOT void qso_logged (QDateTime time, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent

View File

@ -223,6 +223,27 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
}
break;
case NetworkMessage::WSPRDecode:
{
// unpack message
bool is_new {true};
QTime time;
qint32 snr;
float delta_time;
Frequency frequency;
qint32 drift;
QByteArray callsign;
QByteArray grid;
qint32 power;
in >> is_new >> time >> snr >> delta_time >> frequency >> drift >> callsign >> grid >> power;
if (check_status (in) != Fail)
{
Q_EMIT self_->WSPR_decode (is_new, id, time, snr, delta_time, frequency, drift
, QString::fromUtf8 (callsign), QString::fromUtf8 (grid), power);
}
}
break;
case NetworkMessage::QSOLogged:
{
QDateTime time;

View File

@ -65,6 +65,8 @@ public:
Q_SIGNAL void client_closed (QString const& id);
Q_SIGNAL void decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message);
Q_SIGNAL void WSPR_decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time, Frequency
, qint32 drift, QString const& callsign, QString const& grid, qint32 power);
Q_SIGNAL void qso_logged (QString const& id, QDateTime time, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power, QString const& comments

View File

@ -137,7 +137,7 @@
* Mode utf8
* Message utf8
*
* The decode message is send when a new decode is completed, in
* The decode message is sent when a new decode is completed, in
* this case the 'New' field is true. It is also used in response
* to a "Replay" message where each old decode in the "Band
* activity" window, that has not been erased, is sent in order
@ -260,6 +260,26 @@
* command to determine the contents of the current free text
* message.
*
* WSPRDecode Out 10 quint32
* Id (unique key) utf8
* New bool
* Time QTime
* snr qint32
* Delta time (S) float (serialized as double)
* Frequency (Hz) quint64
* Drift (Hz) qint32
* Callsign utf8
* Grid utf8
* Power (dBm) qint32
*
* The decode message is sent when a new decode is completed, in
* this case the 'New' field is true. It is also used in response
* to a "Replay" message where each old decode in the "Band
* activity" window, that has not been erased, is sent in order
* as a one of these messages with the 'New' field set to
* false. See the "Replay" message below for details of usage.
*
*
*/
#include <QDataStream>
@ -285,6 +305,7 @@ namespace NetworkMessage
Replay,
HaltTx,
FreeText,
WSPRDecode,
maximum_message_type_ // ONLY add new message types
// immediately before here
};

View File

@ -870,6 +870,10 @@ void MainWindow::dataSink(qint64 frames)
cmnd=t3.mid(0,i1+7) + t3.mid(i1+7);
ui->DecodeButton->setChecked (true);
p1.start(QDir::toNativeSeparators(cmnd));
m_messageClient->status_update (m_dialFreq, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, (m_decoderBusy = true));
}
m_rxDone=true;
}
@ -1012,7 +1016,7 @@ void MainWindow::keyPressEvent( QKeyEvent *e ) //keyPressEvent
switch(e->key())
{
case Qt::Key_D:
if(e->modifiers() & Qt::ShiftModifier) {
if(m_mode != "WSPR-2" && e->modifiers() & Qt::ShiftModifier) {
if(!m_decoderBusy) {
jt9com_.newdat=0;
jt9com_.nagain=0;
@ -1463,7 +1467,7 @@ void MainWindow::on_actionSpecial_mouse_commands_triggered()
void MainWindow::on_DecodeButton_clicked (bool /* checked */) //Decode request
{
if(!m_decoderBusy) {
if(m_mode != "WSPR-2" && !m_decoderBusy) {
jt9com_.newdat=0;
jt9com_.nagain=1;
m_blankLine=false; // don't insert the separator again
@ -3925,17 +3929,27 @@ void MainWindow::replayDecodes ()
{
// we accept this request even if the setting to accept UDP requests
// is not checked
// attempt to parse the decoded text
Q_FOREACH (auto const& message, ui->decodedTextBrowser->toPlainText ().split ('\n', QString::SkipEmptyParts))
{
if (message.size() >= 4 && message.left (4) != "----")
{
auto eom_pos = message.indexOf (' ', 35);
// we always want at least the characters to position 35
if (eom_pos < 35)
auto const& parts = message.split (' ', QString::SkipEmptyParts);
if (parts.size () >= 5 && parts[3].contains ('.')) // WSPR
{
eom_pos = message.size () - 1;
postWSPRDecode (false, parts);
}
else
{
auto eom_pos = message.indexOf (' ', 35);
// we always want at least the characters to position 35
if (eom_pos < 35)
{
eom_pos = message.size () - 1;
}
postDecode (false, message.left (eom_pos + 1));
}
postDecode (false, message.left (eom_pos + 1));
}
}
statusChanged ();
@ -3943,8 +3957,8 @@ void MainWindow::replayDecodes ()
void MainWindow::postDecode (bool is_new, QString const& message)
{
auto decode = message.trimmed ();
auto parts = decode.left (21).split (' ', QString::SkipEmptyParts);
auto const& decode = message.trimmed ();
auto const& parts = decode.left (21).split (' ', QString::SkipEmptyParts);
if (parts.size () >= 5)
{
m_messageClient->decode (is_new, QTime::fromString (parts[0], "hhmm"), parts[1].toInt ()
@ -3952,6 +3966,18 @@ void MainWindow::postDecode (bool is_new, QString const& message)
}
}
void MainWindow::postWSPRDecode (bool is_new, QStringList parts)
{
if (parts.size () < 8)
{
parts.insert (6, "");
}
m_messageClient->WSPR_decode (is_new, QTime::fromString (parts[0], "hhmm"), parts[1].toInt ()
, parts[2].toFloat (), Radio::frequency (parts[3].toFloat (), 6)
, parts[4].toInt (), parts[5].remove ("&lt;").remove ("&gt;")
, parts[6], parts[7].toInt ());
}
void MainWindow::networkError (QString const& e)
{
if (QMessageBox::Retry == QMessageBox::warning (this, tr ("Network Error")
@ -4016,7 +4042,10 @@ void MainWindow::p1ReadFromStdout() //p1readFromStdout
m_RxLog=0;
m_startAnother=m_loopall;
m_blankLine=true;
return;
m_messageClient->status_update (m_dialFreq, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, (m_decoderBusy = false));
} else {
int n=t.length();
@ -4035,6 +4064,7 @@ void MainWindow::p1ReadFromStdout() //p1readFromStdout
.arg(rxFields.at(5).leftJustified (12).replace ('<', "&lt;").replace ('>', "&gt;"))
.arg(rxFields.at(6), -6)
.arg(rxFields.at(7), 3);
postWSPRDecode (true, rxFields);
grid = rxFields.at(6);
} else if ( rxFields.count() == 7 ) { // Type 2 message
rxLine = QString("%1 %2 %3 %4 %5 %6 %7 %8")
@ -4046,6 +4076,7 @@ void MainWindow::p1ReadFromStdout() //p1readFromStdout
.arg(rxFields.at(5).leftJustified (12).replace ('<', "&lt;").replace ('>', "&gt;"))
.arg("", -6)
.arg(rxFields.at(6), 3);
postWSPRDecode (true, rxFields);
} else {
rxLine = t;
}

View File

@ -501,6 +501,7 @@ private:
void replyToCQ (QTime, qint32 snr, float delta_time, quint32 delta_frequency, QString const& mode, QString const& message_text);
void replayDecodes ();
void postDecode (bool is_new, QString const& message);
void postWSPRDecode (bool is_new, QStringList message_parts);
void enable_DXCC_entity (bool on);
void switch_mode (Mode);
void WSPR_scheduling ();