code:
#ifndef NETWORK_MESSAGE_HPP__
#define NETWORK_MESSAGE_HPP__

/*
 * WSJT-X Message Formats
 * ======================
 *
 * All messages are written or  read using the QDataStream derivatives
 * defined below, note that we are using the default for floating
 * point precision which means all are double precision i.e. 64-bit
 * IEEE format.
 *
 *  Message is big endian format
 *
 *   Header format:
 *
 *      32-bit unsigned integer magic number 0xadbccbda
 *      32-bit unsigned integer schema number
 *
 *   Payload format:
 *
 *      As per  the QDataStream format,  see below for version  used and
 *      here:
 *
 *        http://doc.qt.io/qt-5/datastreamformat.html
 *
 *      for the serialization details for each type, at the time of
 *      writing the above document is for Qt_5_0 format which is buggy
 *      so we use Qt_5_4 format, differences are:
 *
 *      QDateTime:
 *           QDate      qint64    Julian day number
 *           QTime      quint32   Milli-seconds since midnight
 *           timespec   quint8    0=local, 1=UTC, 2=Offset from UTC
 *                                                 (seconds)
 *                                3=time zone
 *           offset     qint32    only present if timespec=2
 *           timezone   several-fields only present if timespec=3
 *
 *      we will avoid using QDateTime fields with time zones for simplicity.
 *
 * Type utf8  is a  utf-8 byte  string formatted  as a  QByteArray for
 * serialization purposes  (currently a quint32 size  followed by size
 * bytes, no terminator is present or counted).
 *
 * The QDataStream format document linked above is not complete for
 * the QByteArray serialization format, it is similar to the QString
 * serialization format in that it differentiates between empty
 * strings and null strings. Empty strings have a length of zero
 * whereas null strings have a length field of 0xffffffff.
 *
 * Schema Negotiation
 * ------------------
 *
 * The NetworkMessage::Builder  class specifies a schema  number which
 * may be  incremented from time to  time. It represents a  version of
 * the underlying encoding schemes used to store data items. Since the
 * underlying  encoding  is   defined  by  the  Qt   project  in  it's
 * QDataStream  stream operators,  it  is essential  that clients  and
 * servers  of  this protocol  can  agree  on  a common  scheme.   The
 * NetworkMessage  utility classes  below exchange  the schema  number
 * actually used.  The handling of  the schema is backwards compatible
 * to  an  extent,  so  long   as  clients  and  servers  are  written
 * correctly. For  example a server  written to any  particular schema
 * version can communicate with a client written to a later schema.
 *
 * Schema Version 1:- this schema used the QDataStream::Qt_5_0 version
 *  which is broken.
 *
 * Schema Version 2:- this schema uses the QDataStream::Qt_5_2 version.
 *
 * Schema Version 3:- this schema uses the QDataStream::Qt_5_4 version.
 *
 *
 *
 * Message       Direction Value                  Type
 * ------------- --------- ---------------------- -----------
 * Heartbeat     Out/In    0                      quint32
 *                         Id (unique key)        utf8
 *                         Maximum schema number  quint32
 *                         version                utf8
 *                         revision               utf8
 *
 *    The heartbeat  message shall be  sent on a periodic  basis every
 *    NetworkMessage::pulse   seconds   (see    below),   the   WSJT-X
 *    application  does  that  using the  MessageClient  class.   This
 *    message is intended to be used by servers to detect the presence
 *    of a  client and also  the unexpected disappearance of  a client
 *    and  by clients  to learn  the schema  negotiated by  the server
 *    after it receives  the initial heartbeat message  from a client.
 *    The message_aggregator reference server does just that using the
 *    MessageServer class. Upon  initial startup a client  must send a
 *    heartbeat message as soon as  is practical, this message is used
 *    to negotiate the maximum schema  number common to the client and
 *    server. Note  that the  server may  not be  able to  support the
 *    client's  requested maximum  schema  number, in  which case  the
 *    first  message received  from the  server will  specify a  lower
 *    schema number (never a higher one  as that is not allowed). If a
 *    server replies  with a lower  schema number then no  higher than
 *    that number shall be used for all further outgoing messages from
 *    either clients or the server itself.
 *
 *    Note: the  "Maximum schema number"  field was introduced  at the
 *    same time as schema 3, therefore servers and clients must assume
 *    schema 2 is the highest schema number supported if the Heartbeat
 *    message does not contain the "Maximum schema number" field.
 *
 *
 * Status        Out       1                      quint32
 *                         Id (unique key)        utf8
 *                         Dial Frequency (Hz)    quint64
 *                         Mode                   utf8
 *                         DX call                utf8
 *                         Report                 utf8
 *                         Tx Mode                utf8
 *                         Tx Enabled             bool
 *                         Transmitting           bool
 *                         Decoding               bool
 *                         Rx DF                  qint32
 *                         Tx DF                  qint32
 *                         DE call                utf8
 *                         DE grid                utf8
 *                         DX grid                utf8
 *                         Tx Watchdog            bool
 *                         Sub-mode               utf8
 *                         Fast mode              bool
 *
 *    WSJT-X  sends this  status message  when various  internal state
 *    changes to allow the server to  track the relevant state of each
 *    client without the need for  polling commands. The current state
 *    changes that generate status messages are:
 *
 *      Application start up,
 *      "Enable Tx" button status changes,
 *      Dial frequency changes,
 *      Changes to the "DX Call" field,
 *      Operating mode, sub-mode or fast mode changes,
 *      Transmit mode changed (in dual JT9+JT65 mode),
 *      Changes to the "Rpt" spinner,
 *      After an old decodes replay sequence (see Replay below),
 *      When switching between Tx and Rx mode,
 *      At the start and end of decoding,
 *      When the Rx DF changes,
 *      When the Tx DF changes,
 *      When the DE call or grid changes (currently when settings are exited),
 *      When the DX call or grid changes,
 *      When the Tx watchdog is set or reset.
 *
 *
 * Decode        Out       2                      quint32
 *                         Id (unique key)        utf8
 *                         New                    bool
 *                         Time                   QTime
 *                         snr                    qint32
 *                         Delta time (S)         float (serialized as double)
 *                         Delta frequency (Hz)   quint32
 *                         Mode                   utf8
 *                         Message                utf8
 *                         Low confidence         bool
 *                         Off air                bool
 *
 *      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.   Low
 *      confidence decodes are flagged  in protocols where the decoder
 *      has knows that  a decode has a higher  than normal probability
 *      of  being  false, they  should  not  be reported  on  publicly
 *      accessible services  without some attached warning  or further
 *      validation. Off air decodes are those that result from playing
 *      back a .WAV file.
 *
 *
 * Clear         Out       3                      quint32
 *                         Id (unique key)        utf8
 *
 *      This message is  send when all prior "Decode"  messages in the
 *      "Band activity"  window have been discarded  and therefore are
 *      no long available for actioning  with a "Reply" message. It is
 *      sent when the user erases  the "Band activity" window and when
 *      WSJT-X  closes down  normally. The  server should  discard all
 *      decode messages upon receipt of this message.
 *
 *
 * Reply         In        4                      quint32
 *                         Id (target unique key) utf8
 *                         Time                   QTime
 *                         snr                    qint32
 *                         Delta time (S)         float (serialized as double)
 *                         Delta frequency (Hz)   quint32
 *                         Mode                   utf8
 *                         Message                utf8
 *                         Low confidence         bool
 *                         Modifiers              quint8
 *
 *      In order for a server  to provide a useful cooperative service
 *      to WSJT-X it  is possible for it to initiate  a QSO by sending
 *      this message to a client. WSJT-X filters this message and only
 *      acts upon it  if the message exactly describes  a prior decode
 *      and that decode  is a CQ or QRZ message.   The action taken is
 *      exactly equivalent to the user  double clicking the message in
 *      the "Band activity" window. The  intent of this message is for
 *      servers to be able to provide an advanced look up of potential
 *      QSO partners, for example determining if they have been worked
 *      before  or if  working them  may advance  some objective  like
 *      award progress.  The  intention is not to  provide a secondary
 *      user  interface for  WSJT-X,  it is  expected  that after  QSO
 *      initiation the rest  of the QSO is carried  out manually using
 *      the normal WSJT-X user interface.
 *
 *      The  Modifiers   field  allows  the  equivalent   of  keyboard
 *      modifiers to be sent "as if" those modifier keys where pressed
 *      while  double-clicking  the  specified  decoded  message.  The
 *      modifier values (hexadecimal) are as follows:
 *
 *          no modifier     0x00
 *          SHIFT           0x02
 *          CTRL            0x04  CMD on Mac
 *          ALT             0x08
 *          META            0x10  Windows key on MS Windows
 *          KEYPAD          0x20  Keypad or arrows
 *          Group switch    0x40  X11 only
 *
 *
 * QSO Logged    Out       5                      quint32
 *                         Id (unique key)        utf8
 *                         Date & Time Off        QDateTime
 *                         DX call                utf8
 *                         DX grid                utf8
 *                         Tx frequency (Hz)      quint64
 *                         Mode                   utf8
 *                         Report send            utf8
 *                         Report received        utf8
 *                         Tx power               utf8
 *                         Comments               utf8
 *                         Name                   utf8
 *                         Date & Time On         QDateTime
 *                         Operator call          utf8
 *                         My call                utf8
 *                         My grid                utf8
 *
 *      The  QSO logged  message is  sent  to the  server(s) when  the
 *      WSJT-X user accepts the "Log  QSO" dialog by clicking the "OK"
 *      button.
 *
 *
 * Close         Out       6                      quint32
 *                         Id (unique key)        utf8
 *
 *      Close is sent by a client immediately prior to it shutting
 *      down gracefully.
 *
 *
 * Replay        In        7                      quint32
 *                         Id (unique key)        utf8
 *
 *      When a server starts it may  be useful for it to determine the
 *      state  of preexisting  clients. Sending  this message  to each
 *      client as it is discovered  will cause that client (WSJT-X) to
 *      send a "Decode" message for each decode currently in its "Band
 *      activity"  window. Each  "Decode" message  sent will  have the
 *      "New" flag set to false so that they can be distinguished from
 *      new decodes. After  all the old decodes have  been broadcast a
 *      "Status" message  is also broadcast.  If the server  wishes to
 *      determine  the  status  of  a newly  discovered  client;  this
 *      message should be used.
 *
 *
 * Halt Tx       In        8
 *                         Id (unique key)        utf8
 *                         Auto Tx Only           bool
 *
 *      The server may stop a client from transmitting messages either
 *      immediately or at  the end of the  current transmission period
 *      using this message.
 *
 *
 * Free Text     In        9
 *                         Id (unique key)        utf8
 *                         Text                   utf8
 *                         Send                   bool
 *
 *      This message  allows the server  to set the current  free text
 *      message content. Sending this  message with a non-empty "Text"
 *      field is equivalent to typing  a new message (old contents are
 *      discarded) in to  the WSJT-X free text message  field or "Tx5"
 *      field (both  are updated) and if  the "Send" flag is  set then
 *      clicking the "Now" radio button for the "Tx5" field if tab one
 *      is current or clicking the "Free  msg" radio button if tab two
 *      is current.
 *
 *      It is the responsibility of the  sender to limit the length of
 *      the  message   text  and   to  limit   it  to   legal  message
 *      characters. Despite this,  it may be difficult  for the sender
 *      to determine the maximum message length without reimplementing
 *      the complete message encoding protocol. Because of this is may
 *      be better  to allow any  reasonable message length and  to let
 *      the WSJT-X application encode and possibly truncate the actual
 *      on-air message.
 *
 *      If the  message text is  empty the  meaning of the  message is
 *      refined  to send  the  current free  text  unchanged when  the
 *      "Send" flag is set or to  clear the current free text when the
 *      "Send" flag is  unset.  Note that this API does  not include a
 *      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
 *                         Off air                bool
 *
 *      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. The off air field indicates that the decode was decoded
 *      from a played back recording.
 *
 *
 * Location       In       11
 *                         Id (unique key)        utf8
 *                         Location               utf8
 *
 *      This  message allows  the server  to set  the current  current
 *      geographical location  of operation. The supplied  location is
 *      not persistent but  is used as a  session lifetime replacement
 *      loction that overrides the Maidenhead  grid locater set in the
 *      application  settings.  The  intent  is to  allow an  external
 *      application  to  update  the  operating  location  dynamically
 *      during a mobile period of operation.
 *
 *      Currently  only Maidenhead  grid  squares  or sub-squares  are
 *      accepted, i.e.  4- or 6-digit  locators. Other formats  may be
 *      accepted in future.
 *
 *
 * Logged ADIF    Out      12                     quint32
 *                         Id (unique key)        utf8
 *                         ADIF text              utf8
 *
 *      The  logged ADIF  message is  sent to  the server(s)  when the
 *      WSJT-X user accepts the "Log  QSO" dialog by clicking the "OK"
 *      button. The  "ADIF text" field  consists of a valid  ADIF file
 *      such that  the WSJT-X  UDP header information  is encapsulated
 *      into a valid ADIF header. E.g.:
 *
 *          <magic-number><schema-number><type><id><32-bit-count>  # binary encoded fields
 *          # the remainder is the contents of the ADIF text field
 *          <adif_ver:5>3.0.7
 *          <programid:6>WSJT-X
 *          <EOH>
 *          ADIF log data fields ...<EOR>
 *
 *      Note that  receiving applications can treat  the whole message
 *      as a valid ADIF file with one record without special parsing.
 *
 *
 * Highlight Callsign In   13                     quint32
 *                         Id (unique key)        utf8
 *                         Callsign               utf8
 *                         Background Color       QColor
 *                         Foreground Color       QColor
 *                         Highlight last         bool
 *
 *      The server  may send  this message at  any time.   The message
 *      specifies  the background  and foreground  color that  will be
 *      used  to  highlight  the  specified callsign  in  the  decoded
 *      messages  printed  in  the  Band Activity  panel.  The  WSJT-X
 *      clients maintain a list of such instructions and apply them to
 *      all decoded  messages in the  band activity window.   To clear
 *      highlighting send an  invalid QColor value for  either or both
 *      of the background and foreground fields.
 *
 *      The "Highlight last"  field allows the sender  to request that
 *      the  last  instance  only  instead of  all  instances  of  the
 *      specified  call  be  highlighted  or  have  it's  highlighting
 *      cleared.
 */

#include <QDataStream>

#include "pimpl_h.hpp"

class QIODevice;
class QByteArray;
class QString;

namespace NetworkMessage
{
  // NEVER DELETE MESSAGE TYPES
  enum Type
    {
      Heartbeat,
      Status,
      Decode,
      Clear,
      Reply,
      QSOLogged,
      Close,
      Replay,
      HaltTx,
      FreeText,
      WSPRDecode,
      Location,
      LoggedADIF,
      HighlightCallsign,
      maximum_message_type_     // ONLY add new message types
                                // immediately before here
    };

  quint32 constexpr pulse {15}; // seconds

  //
  // NetworkMessage::Builder - build a message containing serialized Qt types
  //
  class Builder
    : public QDataStream
  {
  public:
    static quint32 constexpr magic {0xadbccbda}; // never change this

    // increment this if a newer Qt schema is required and add decode
    // logic to the Builder and Reader class implementations
#if QT_VERSION >= 0x050400
    static quint32 constexpr schema_number {3};
#elif QT_VERSION >= 0x050200
    static quint32 constexpr schema_number {2};
#else
    // Schema 1 (Qt_5_0) is broken
#error "Qt version 5.2 or greater required"
#endif

    explicit Builder (QIODevice *, Type, QString const& id, quint32 schema);
    explicit Builder (QByteArray *, Type, QString const& id, quint32 schema);
    Builder (Builder const&) = delete;
    Builder& operator = (Builder const&) = delete;

  private:
    void common_initialization (Type type, QString const& id, quint32 schema);
  };

  //
  // NetworkMessage::Reader - read a message containing serialized Qt types
  //
  // Message  is as  per NetworkMessage::Builder  above, the  schema()
  // member  may be  used  to  determine the  schema  of the  original
  // message.
  //
  class Reader
    : public QDataStream
  {
  public:
    explicit Reader (QIODevice *);
    explicit Reader (QByteArray const&);
    Reader (Reader const&) = delete;
    Reader& operator = (Reader const&) = delete;
    ~Reader ();

    quint32 schema () const;
    Type type () const;
    QString id () const;

  private:
    class impl;
    pimpl<impl> m_;
  };
}

#endif


code:
#include "NetworkServerLookup.hpp"

#include <stdexcept>

#include <QHostInfo>
#include <QString>

std::tuple<QHostAddress, quint16>
network_server_lookup (QString query
		       , quint16 default_service_port
		       , QHostAddress default_host_address
		       , QAbstractSocket::NetworkLayerProtocol required_protocol)
{
  query = query.trimmed ();

  QHostAddress host_address {default_host_address};
  quint16 service_port {default_service_port};

  QString host_name;
  if (!query.isEmpty ())
    {
      int port_colon_index {-1};

      if ('[' == query[0])
        {
          // assume IPv6 combined address/port syntax [<address>]:<port>
          auto close_bracket_index = query.lastIndexOf (']');
          host_name = query.mid (1, close_bracket_index - 1);
          port_colon_index = query.indexOf (':', close_bracket_index);
        }
      else
        {
          port_colon_index = query.lastIndexOf (':');
          host_name = query.left (port_colon_index);
        }
      host_name = host_name.trimmed ();

      if (port_colon_index >= 0)
        {
          bool ok;
          service_port = query.mid (port_colon_index + 1).trimmed ().toUShort (&ok);
          if (!ok)
            {
              throw std::runtime_error {"network server lookup error: invalid port"};
            }
        }
    }

  if (!host_name.isEmpty ())
    {
      auto host_info = QHostInfo::fromName (host_name);
      if (host_info.addresses ().isEmpty ())
        {
          throw std::runtime_error {"network server lookup error: host name lookup failed"};
        }

      bool found {false};
      for (int i {0}; i < host_info.addresses ().size () && !found; ++i)
        {
          host_address = host_info.addresses ().at (i);
          switch (required_protocol)
            {
            case QAbstractSocket::IPv4Protocol:
            case QAbstractSocket::IPv6Protocol:
              if (required_protocol != host_address.protocol ())
                {
                  break;
                }
              // drop through

            case QAbstractSocket::AnyIPProtocol:
              found = true;
              break;

            default:
              throw std::runtime_error {"network server lookup error: invalid required protocol"};
            }
        }
      if (!found)
        {
          throw std::runtime_error {"network server lookup error: no suitable host address found"};
        }
    }

  return std::make_tuple (host_address, service_port);
}

license: mit


code:
#ifndef NETWORK_SERVER_LOOKUP_HPP__
#define NETWORK_SERVER_LOOKUP_HPP__

#include <tuple>

#include <QHostAddress>
#include <QAbstractSocket>

class QString;

//
// Do a blocking DNS lookup using query as a destination host address
// and port.
//
// query can be one of:
//
// 1) "" (empty string) - use defaults
// 2) ":nnnnn" - override default service port with port nnnnn
// 3) "<valid-host-name>" - override default host address with DNS lookup
// 4) "nnn.nnn.nnn.nnn" - override default host address with the IPv4 address given by nnn.nnn.nnn.nnn
// 5) "[<valid-IPv6-address]" - override default host address with the given IPv6 address
// 6) "<valid-host-name>:nnnnn" - use as per (3) & (2)
// 7) "nnn.nnn.nnn.nnn:nnnnn" - use as per (4) & (2)
// 8) "[<valid-IPv6-address]:nnnnn" - use as per (5) & (2)
//
// The first host address matching the protocol and the service port
// number are returned.
//
// If no suitable host address is found QHostAddress::Null will be
// returned in the first member of the result tuple.
//
std::tuple<QHostAddress, quint16>
network_server_lookup (QString query
		       , quint16 default_service_port
		       , QHostAddress default_host_address = QHostAddress::LocalHost
		       , QAbstractSocket::NetworkLayerProtocol protocol = QAbstractSocket::AnyIPProtocol);

#endif


code:
#include "OmniRigTransceiver.hpp"

#include <QTimer>
#include <QDebug>
#include <objbase.h>
#include <QThread>

#include "qt_helpers.hpP"

#include "moc_OmniRigTransceiver.cpp"

namespace
{
  auto constexpr OmniRig_transceiver_one_name = "OmniRig Rig 1";
  auto constexpr OmniRig_transceiver_two_name = "OmniRig Rig 2";
}

auto OmniRigTransceiver::map_mode (OmniRig::RigParamX param) -> MODE
{
  if (param & OmniRig::PM_CW_U)
    {
      return CW_R;
    }
  else if (param & OmniRig::PM_CW_L)
    {
      return CW;
    }
  else if (param & OmniRig::PM_SSB_U)
    {
      return USB;
    }
  else if (param & OmniRig::PM_SSB_L)
    {
      return LSB;
    }
  else if (param & OmniRig::PM_DIG_U)
    {
      return DIG_U;
    }
  else if (param & OmniRig::PM_DIG_L)
    {
      return DIG_L;
    }
  else if (param & OmniRig::PM_AM)
    {
      return AM;
    }
  else if (param & OmniRig::PM_FM)
    {
      return FM;
    }
  TRACE_CAT ("OmniRigTransceiver", "unrecognized mode");
  throw_qstring (tr ("OmniRig: unrecognized mode"));
  return UNK;
}

OmniRig::RigParamX OmniRigTransceiver::map_mode (MODE mode)
{
  switch (mode)
    {
    case AM: return OmniRig::PM_AM;
    case CW: return OmniRig::PM_CW_L;
    case CW_R: return OmniRig::PM_CW_U;
    case USB: return OmniRig::PM_SSB_U;
    case LSB: return OmniRig::PM_SSB_L;
    case FSK: return OmniRig::PM_DIG_L;
    case FSK_R: return OmniRig::PM_DIG_U;
    case DIG_L: return OmniRig::PM_DIG_L;
    case DIG_U: return OmniRig::PM_DIG_U;
    case FM: return OmniRig::PM_FM;
    case DIG_FM: return OmniRig::PM_FM;
    default: break;
    }
  return OmniRig::PM_SSB_U; // quieten compiler grumble
}

void OmniRigTransceiver::register_transceivers (TransceiverFactory::Transceivers * registry, int id1, int id2)
{
  (*registry)[OmniRig_transceiver_one_name] = TransceiverFactory::Capabilities {
    id1
    , TransceiverFactory::Capabilities::none // COM isn't serial or network
    , true             // does PTT
    , false            // doesn't select mic/data (use OmniRig config file)
    , true             // can remote control RTS nd DTR
    , true             // asynchronous interface
  };
  (*registry)[OmniRig_transceiver_two_name] = TransceiverFactory::Capabilities {
    id2
    , TransceiverFactory::Capabilities::none // COM isn't serial or network
    , true             // does PTT
    , false            // doesn't select mic/data (use OmniRig config file)
    , true             // can remote control RTS nd DTR
    , true             // asynchronous interface
  };
}

OmniRigTransceiver::OmniRigTransceiver (std::unique_ptr<TransceiverBase> wrapped,
                                        RigNumber n, TransceiverFactory::PTTMethod ptt_type,
                                        QString const& ptt_port, QObject * parent)
  : TransceiverBase {parent}
  , wrapped_ {std::move (wrapped)}
  , use_for_ptt_ {TransceiverFactory::PTT_method_CAT == ptt_type || ("CAT" == ptt_port && (TransceiverFactory::PTT_method_RTS == ptt_type || TransceiverFactory::PTT_method_DTR == ptt_type))}
  , ptt_type_ {ptt_type}
  , rig_number_ {n}
  , readable_params_ {0}
  , writable_params_ {0}
  , send_update_signal_ {false}
  , reversed_ {false}
{
}

int OmniRigTransceiver::do_start ()
{
  TRACE_CAT ("OmniRigTransceiver", "starting");
  if (wrapped_) wrapped_->start (0);

  CoInitializeEx (nullptr, 0 /*COINIT_APARTMENTTHREADED*/); // required because Qt only does this for GUI thread

  omni_rig_.reset (new OmniRig::OmniRigX {this});
  if (omni_rig_->isNull ())
    {
      TRACE_CAT ("OmniRigTransceiver", "failed to start COM server");
      throw_qstring (tr ("Failed to start OmniRig COM server"));
    }

  // COM/OLE exceptions get signaled
  connect (&*omni_rig_, SIGNAL (exception (int, QString, QString, QString)), this, SLOT (handle_COM_exception (int, QString, QString, QString)));

  // IOmniRigXEvent interface signals
  connect (&*omni_rig_, SIGNAL (VisibleChange ()), this, SLOT (handle_visible_change ()));
  connect (&*omni_rig_, SIGNAL (RigTypeChange (int)), this, SLOT (handle_rig_type_change (int)));
  connect (&*omni_rig_, SIGNAL (StatusChange (int)), this, SLOT (handle_status_change (int)));
  connect (&*omni_rig_, SIGNAL (ParamsChange (int, int)), this, SLOT (handle_params_change (int, int)));
  connect (&*omni_rig_
           , SIGNAL (CustomReply (int, QVariant const&, QVariant const&))
           , this, SLOT (handle_custom_reply (int, QVariant const&, QVariant const&)));

  TRACE_CAT ("OmniRigTransceiver", "OmniRig s/w version:" << QString::number (omni_rig_->SoftwareVersion ()).toLocal8Bit ()
             << "i/f version:" << QString::number (omni_rig_->InterfaceVersion ()).toLocal8Bit ());

  // fetch the interface of the RigX CoClass and instantiate a proxy object
  switch (rig_number_)
    {
    case One: rig_.reset (new OmniRig::RigX (omni_rig_->Rig1 ())); break;
    case Two: rig_.reset (new OmniRig::RigX (omni_rig_->Rig2 ())); break;
    }

  Q_ASSERT (rig_);
  Q_ASSERT (!rig_->isNull ());

  if (use_for_ptt_ && (TransceiverFactory::PTT_method_DTR == ptt_type_ || TransceiverFactory::PTT_method_RTS == ptt_type_))
    {
      // fetch the interface for the serial port if we need it for PTT
      port_.reset (new OmniRig::PortBits (rig_->PortBits ()));

      Q_ASSERT (port_);
      Q_ASSERT (!port_->isNull ());
      TRACE_CAT ("OmniRigTransceiver", "OmniRig RTS state:" << port_->Rts ());

      if (!port_->Lock ()) // try to take exclusive use of the OmniRig serial port for PTT
        {
          TRACE_CAT ("OmniRigTransceiver", "Failed to get exclusive use of serial port for PTT from OmniRig");
        }

      // start off so we don't accidentally key the radio
      if (TransceiverFactory::PTT_method_DTR == ptt_type_)
        {
          port_->SetDtr (false);
        }
      else      // RTS
        {
          port_->SetRts (false);
        }
    }

  rig_type_ = rig_->RigType ();
  readable_params_ = rig_->ReadableParams ();
  writable_params_ = rig_->WriteableParams ();

  TRACE_CAT ("OmniRigTransceiver", QString {"OmniRig initial rig type: %1 readable params = 0x%2 writable params = 0x%3 for rig %4"}
    .arg (rig_type_)
    .arg (readable_params_, 8, 16, QChar ('0'))
    .arg (writable_params_, 8, 16, QChar ('0'))
    .arg (rig_number_).toLocal8Bit ());

  offline_timer_.reset (new QTimer);
  offline_timer_->setSingleShot (true);
  offline_timer_->setInterval (5 * 1000);
  connect (&*offline_timer_, &QTimer::timeout, this, &OmniRigTransceiver::timeout_check);

  for (unsigned tries {0}; tries < 10; ++tries)
    {
      QThread::msleep (100);    // wait until OmniRig polls the rig
      auto f = rig_->GetRxFrequency ();
      int resolution {0};
      if (f)
        {
          if (OmniRig::PM_UNKNOWN == rig_->Vfo ()
              && (writable_params_ & (OmniRig::PM_VFOA | OmniRig::PM_VFOB))
              == (OmniRig::PM_VFOA | OmniRig::PM_VFOB))
            {
              // start with VFO A (probably MAIN) on rigs that we
              // can't query VFO but can set explicitly
              rig_->SetVfo (OmniRig::PM_VFOA);
            }
          if (f % 10) return resolution; // 1Hz resolution
          auto test_frequency = f - f % 100 + 55;
          if (OmniRig::PM_FREQ & writable_params_)
            {
              rig_->SetFreq (test_frequency);
            }
          else if (reversed_ && (OmniRig::PM_FREQB & writable_params_))
            {
              rig_->SetFreqB (test_frequency);
            }
          else if (!reversed_ && (OmniRig::PM_FREQA & writable_params_))
            {
              rig_->SetFreqA (test_frequency);
            }
          else
            {
              throw_qstring (tr ("OmniRig: don't know how to set rig frequency"));
            }
          switch (rig_->GetRxFrequency () - 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;
              if (OmniRig::PM_FREQ & writable_params_)
                {
                  rig_->SetFreq (test_frequency);
                }
              else if (reversed_ && (OmniRig::PM_FREQB & writable_params_))
                {
                  rig_->SetFreqB (test_frequency);
                }
              else if (!reversed_ && (OmniRig::PM_FREQA & writable_params_))
                {
                  rig_->SetFreqA (test_frequency);
                }
              if (9 == rig_->GetRxFrequency () - test_frequency)
                {
                  resolution = 2;   // 20Hz rounded
                }
            }
          if (OmniRig::PM_FREQ & writable_params_)
            {
              rig_->SetFreq (f);
            }
          else if (reversed_ && (OmniRig::PM_FREQB & writable_params_))
            {
              rig_->SetFreqB (f);
            }
          else if (!reversed_ && (OmniRig::PM_FREQA & writable_params_))
            {
              rig_->SetFreqA (f);
            }
          update_rx_frequency (f);
          return resolution;
        }
    }
  throw_qstring (tr ("OmniRig: Initialization timed out"));
  return 0;                     // keep compiler happy
}

void OmniRigTransceiver::do_stop ()
{
  if (offline_timer_)
    {
      offline_timer_->stop ();
      offline_timer_.reset ();
    }

  QThread::msleep (200);        // leave some time for pending
                                // commands at the server end
  if (port_)
    {
      port_->Unlock ();   // release serial port
      port_->clear ();
      port_.reset ();
    }
  if (omni_rig_)
    {
      if (rig_)
        {
          rig_->clear ();
          rig_.reset ();
        }
      omni_rig_->clear ();
      omni_rig_.reset ();
      CoUninitialize ();
    }
  if (wrapped_) wrapped_->stop ();
  TRACE_CAT ("OmniRigTransceiver", "stopped");
}

void OmniRigTransceiver::do_sync (bool force_signal, bool /*no_poll*/)
{
  // nothing much we can do here, we just have to let OmniRig do its
  // stuff and its first poll should send us and update that will
  // trigger a update signal from us. Any attempt to query OmniRig
  // leads to a whole mess of trouble since its internal state is
  // garbage until it has done its first rig poll.
  send_update_signal_ = force_signal;
  update_complete ();
}

void OmniRigTransceiver::handle_COM_exception (int code, QString source, QString desc, QString help)
{
  TRACE_CAT ("OmniRigTransceiver", QString::number (code) + " at " + source + ": " + desc + " (" + help + ')');
  throw_qstring (tr ("OmniRig COM/OLE error: %1 at %2: %3 (%4)").arg (QString::number (code)).arg (source). arg (desc). arg (help));
}

void OmniRigTransceiver::handle_visible_change ()
{
  TRACE_CAT ("OmniRigTransceiver", "visibility change: visibility =" << omni_rig_->DialogVisible ());
}

void OmniRigTransceiver::handle_rig_type_change (int rig_number)
{
  if (rig_number_ == rig_number)
    {
      readable_params_ = rig_->ReadableParams ();
      writable_params_ = rig_->WriteableParams ();
      TRACE_CAT ("OmniRigTransceiver", QString {"OmniRig rig type change to: %1 readable params = 0x%2 writable params = 0x%3 for rig %4"}
        .arg (rig_->RigType ())
        .arg (readable_params_, 8, 16, QChar ('0'))
        .arg (writable_params_, 8, 16, QChar ('0'))
        .arg (rig_number).toLocal8Bit ());
    }
}

void OmniRigTransceiver::handle_status_change (int rig_number)
{
  if (rig_number_ == rig_number)
    {
      auto const& status = rig_->StatusStr ().toLocal8Bit ();
      TRACE_CAT ("OmniRigTransceiver", QString {"OmniRig status change: new status for rig %1 = "}.arg (rig_number).toLocal8Bit () << status);
      if (OmniRig::ST_ONLINE != rig_->Status ())
        {
          if (!offline_timer_->isActive ())
            {
              offline_timer_->start (); // give OmniRig time to recover
            }
        }
      else
        {
          offline_timer_->stop ();
          update_rx_frequency (rig_->GetRxFrequency ());
          update_complete ();
          TRACE_CAT ("OmniRigTransceiver", "OmniRig frequency:" << state ().frequency ());
        }
    }
}

void OmniRigTransceiver::timeout_check ()
{
  offline ("Rig went offline");
}

void OmniRigTransceiver::handle_params_change (int rig_number, int params)
{
  if (rig_number_ == rig_number)
    {
      TRACE_CAT ("OmniRigTransceiver", QString {"OmniRig params change: params = 0x%1 for rig %2"}
        .arg (params, 8, 16, QChar ('0'))
        .arg (rig_number).toLocal8Bit ()
        << "state before:" << state ());
      //      starting_ = false;
      TransceiverState old_state {state ()};
      auto need_frequency = false;
      // state_.online = true;  // sometimes we don't get an initial
      //        // OmniRig::ST_ONLINE status change
      //        // event
      if (params & OmniRig::PM_VFOAA)
        {
          update_split (false);
          reversed_ = false;
          update_rx_frequency (rig_->FreqA ());
          update_other_frequency (rig_->FreqB ());
        }
      if (params & OmniRig::PM_VFOAB)
        {
          update_split (true);
          reversed_ = false;
          update_rx_frequency (rig_->FreqA ());
          update_other_frequency (rig_->FreqB ());
        }
      if (params & OmniRig::PM_VFOBA)
        {
          update_split (true);
          reversed_ = true;
          update_other_frequency (rig_->FreqA ());
          update_rx_frequency (rig_->FreqB ());
        }
      if (params & OmniRig::PM_VFOBB)
        {
          update_split (false);
          reversed_ = true;
          update_other_frequency (rig_->FreqA ());
          update_rx_frequency (rig_->FreqB ());
        }
      if (params & OmniRig::PM_VFOA)
        {
          reversed_ = false;
          need_frequency = true;
        }
      if (params & OmniRig::PM_VFOB)
        {
          reversed_ = true;
          need_frequency = true;
        }

      if (params & OmniRig::PM_FREQ)
        {
          need_frequency = true;
        }
      if (params & OmniRig::PM_FREQA)
        {
          if (reversed_)
            {
              update_other_frequency (rig_->FreqA ());
            }
          else
            {
              update_rx_frequency (rig_->FreqA ());
            }
        }
      if (params & OmniRig::PM_FREQB)
        {
          if (reversed_)
            {
              update_rx_frequency (rig_->FreqB ());
            }
          else
            {
              update_other_frequency (rig_->FreqB ());
            }
        }
      if (need_frequency)
        {
          if (readable_params_ & OmniRig::PM_FREQA)
            {
              if (reversed_)
                {
                  update_other_frequency (rig_->FreqA ());
                }
              else
                {
                  update_rx_frequency (rig_->FreqA ());
                }
              need_frequency = false;
            }
          if (readable_params_ & OmniRig::PM_FREQB)
            {
              if (reversed_)
                {
                  update_rx_frequency (rig_->FreqB ());
                }
              else
                {
                  update_other_frequency (rig_->FreqB ());
                }
              need_frequency = false;
            }
        }
      if (need_frequency && (readable_params_ & OmniRig::PM_FREQ)
          && !state ().ptt ())
        {
          update_rx_frequency (rig_->Freq ());
        }
      if (params & OmniRig::PM_PITCH)
        {
        }
      if (params & OmniRig::PM_RITOFFSET)
        {
        }
      if (params & OmniRig::PM_RIT0)
        {
        }
      if (params & OmniRig::PM_VFOEQUAL)
        {
          auto f = readable_params_ & OmniRig::PM_FREQA ? rig_->FreqA () : rig_->Freq ();
          update_rx_frequency (f);
          update_other_frequency (f);
          update_mode (map_mode (rig_->Mode ()));
        }
      if (params & OmniRig::PM_VFOSWAP)
        {
          auto temp = state ().tx_frequency ();
          update_other_frequency (state ().frequency ());
          update_rx_frequency (temp);
          update_mode (map_mode (rig_->Mode ()));
        }
      if (params & OmniRig::PM_SPLITON)
        {
          update_split (true);
        }
      if (params & OmniRig::PM_SPLITOFF)
        {
          update_split (false);
        }
      if (params & OmniRig::PM_RITON)
        {
        }
      if (params & OmniRig::PM_RITOFF)
        {
        }
      if (params & OmniRig::PM_XITON)
        {
        }
      if (params & OmniRig::PM_XITOFF)
        {
        }
      if (params & OmniRig::PM_RX)
        {
          update_PTT (false);
        }
      if (params & OmniRig::PM_TX)
        {
          update_PTT ();
        }
      if (params & OmniRig::PM_CW_U)
        {
          update_mode (CW_R);
        }
      if (params & OmniRig::PM_CW_L)
        {
          update_mode (CW);
        }
      if (params & OmniRig::PM_SSB_U)
        {
          update_mode (USB);
        }
      if (params & OmniRig::PM_SSB_L)
        {
          update_mode (LSB);
        }
      if (params & OmniRig::PM_DIG_U)
        {
          update_mode (DIG_U);
        }
      if (params & OmniRig::PM_DIG_L)
        {
          update_mode (DIG_L);
        }
      if (params & OmniRig::PM_AM)
        {
          update_mode (AM);
        }
      if (params & OmniRig::PM_FM)
        {
          update_mode (FM);
        }

      if (old_state != state () || send_update_signal_)
        {
          update_complete ();
          send_update_signal_ = false;
        }
      TRACE_CAT ("OmniRigTransceiver", "OmniRig params change: state after:" << state ());
    }
}

void OmniRigTransceiver::handle_custom_reply (int rig_number, QVariant const& command, QVariant const& reply)
{
  (void)command;
  (void)reply;

  if (rig_number_ == rig_number)
    {
      TRACE_CAT ("OmniRigTransceiver", "custom command" << command.toString ().toLocal8Bit ()
                 << "with reply" << reply.toString ().toLocal8Bit ()
                 << QString ("for rig %1").arg (rig_number).toLocal8Bit ());
      TRACE_CAT ("OmniRigTransceiver", "rig number:" << rig_number_ << ':' << state ());
    }
}

void OmniRigTransceiver::do_ptt (bool on)
{
  TRACE_CAT ("OmniRigTransceiver", on << state ());
  if (use_for_ptt_ && TransceiverFactory::PTT_method_CAT == ptt_type_)
    {
      TRACE_CAT ("OmniRigTransceiver", "set PTT");
      rig_->SetTx (on ? OmniRig::PM_TX : OmniRig::PM_RX);
    }
  else
    {
      if (port_)
        {
          if (TransceiverFactory::PTT_method_RTS == ptt_type_)
            {
              TRACE_CAT ("OmniRigTransceiver", "set RTS");
              port_->SetRts (on);
            }
          else      // "DTR"
            {
              TRACE_CAT ("OmniRigTransceiver", "set DTR");
              port_->SetDtr (on);
            }
        }
      else
        {
          TRACE_CAT ("OmniRigTransceiver", "set PTT using basic transceiver");
          Q_ASSERT (wrapped_);
          TransceiverState new_state {wrapped_->state ()};
          new_state.ptt (on);
          wrapped_->set (new_state, 0);
        }
    }
  update_PTT (on);
}

void OmniRigTransceiver::do_frequency (Frequency f, MODE m, bool /*no_ignore*/)
{
  TRACE_CAT ("OmniRigTransceiver", f << state ());
  if (UNK != m)
    {
      do_mode (m);
    }
  if (OmniRig::PM_FREQ & writable_params_)
    {
      rig_->SetFreq (f);
      update_rx_frequency (f);
    }
  else if (reversed_ && (OmniRig::PM_FREQB & writable_params_))
    {
      rig_->SetFreqB (f);
      update_rx_frequency (f);
    }
  else if (!reversed_ && (OmniRig::PM_FREQA & writable_params_))
    {
      rig_->SetFreqA (f);
      update_rx_frequency (f);
    }
  else
    {
      throw_qstring (tr ("OmniRig: don't know how to set rig frequency"));
    }
}

void OmniRigTransceiver::do_tx_frequency (Frequency tx, MODE m, bool /*no_ignore*/)
{
  TRACE_CAT ("OmniRigTransceiver", tx << state ());
  bool split {tx != 0};
  if (split)
    {
      if (UNK != m)
        {
          do_mode (m);
          if (OmniRig::PM_UNKNOWN == rig_->Vfo ())
            {
              if (writable_params_ & OmniRig::PM_VFOEQUAL)
                {
                  // nothing to do here because OmniRig will use VFO
                  // equalize to set the mode of the Tx VFO for us
                }
              else if ((writable_params_ & (OmniRig::PM_VFOA | OmniRig::PM_VFOB))
                   == (OmniRig::PM_VFOA | OmniRig::PM_VFOB))
                {
                  rig_->SetVfo (OmniRig::PM_VFOB);
                  do_mode (m);
                  rig_->SetVfo (OmniRig::PM_VFOA);
                }
              else if (writable_params_ & OmniRig::PM_VFOSWAP)
                {
                  rig_->SetVfo (OmniRig::PM_VFOSWAP);
                  do_mode (m);
                  rig_->SetVfo (OmniRig::PM_VFOSWAP);
                }
            }
        }
      TRACE_CAT ("OmniRigTransceiver", "set SPLIT mode on");
      rig_->SetSplitMode (state ().frequency (), tx);
      update_other_frequency (tx);
      update_split (true);
    }
  else
    {
      TRACE_CAT ("OmniRigTransceiver", "set SPLIT mode off");
      rig_->SetSimplexMode (state ().frequency ());
      update_split (false);
    }
  bool notify {false};
  if (readable_params_ & OmniRig::PM_FREQ || !(readable_params_ & (OmniRig::PM_FREQA | OmniRig::PM_FREQB)))
    {
      update_other_frequency (tx); // async updates won't return this
      // so just store it and hope
      // operator doesn't change the
      // "back" VFO on rig
      notify = true;
    }
  if (!((OmniRig::PM_VFOAB | OmniRig::PM_VFOBA | OmniRig::PM_SPLITON) & readable_params_))
    {
      TRACE_CAT ("OmniRigTransceiver", "setting SPLIT manually");
      update_split (split); // we can't read it so just set and
      // hope op doesn't change it
      notify = true;
    }
  if (notify)
    {
      update_complete ();
    }
}

void OmniRigTransceiver::do_mode (MODE mode)
{
  TRACE_CAT ("OmniRigTransceiver", mode << state ());
  // TODO: G4WJS OmniRig doesn't seem to have any capability of tracking/setting VFO B mode
  auto mapped = map_mode (mode);
  if (mapped & writable_params_)
    {
      rig_->SetMode (mapped);
      update_mode (mode);
    }
  else
    {
      offline ("OmniRig invalid mode");
    }
}


code:
#ifndef OMNI_RIG_TRANSCEIVER_HPP__
#define OMNI_RIG_TRANSCEIVER_HPP__

#include <memory>

#include <QScopedPointer>
#include <QString>

#include "TransceiverFactory.hpp"
#include "TransceiverBase.hpp"

#include "OmniRig.h"

//
// OmniRig Transceiver Interface
//
// Implemented as a Transceiver decorator  because we may want the PTT
// services of another Transceiver  type such as the HamlibTransceiver
// which can  be enabled by wrapping  a HamlibTransceiver instantiated
// as a "Hamlib Dummy" transceiver in the Transceiver factory method.
//
class OmniRigTransceiver final
  : public TransceiverBase
{
  Q_OBJECT;

public:
  static void register_transceivers (TransceiverFactory::Transceivers *, int id1, int id2);

  enum RigNumber {One = 1, Two};

  // takes ownership of wrapped Transceiver
  explicit OmniRigTransceiver (std::unique_ptr<TransceiverBase> wrapped, RigNumber, TransceiverFactory::PTTMethod ptt_type, QString const& ptt_port, QObject * parent = nullptr);

  int do_start () override;
  void do_stop () override;
  void do_frequency (Frequency, MODE, bool no_ignore) override;
  void do_tx_frequency (Frequency, MODE, bool no_ignore) override;
  void do_mode (MODE) override;
  void do_ptt (bool on) override;
  void do_sync (bool force_signal, bool no_poll) override;

private:
  Q_SLOT void timeout_check ();
  Q_SLOT void handle_COM_exception (int,  QString, QString, QString);
  Q_SLOT void handle_visible_change ();
  Q_SLOT void handle_rig_type_change (int rig_number);
  Q_SLOT void handle_status_change (int rig_number);
  Q_SLOT void handle_params_change (int rig_number, int params);
  Q_SLOT void handle_custom_reply (int, QVariant const& command, QVariant const& reply);

  static MODE map_mode (OmniRig::RigParamX param);
  static OmniRig::RigParamX map_mode (MODE mode);

  std::unique_ptr<TransceiverBase> wrapped_; // may be null
  bool use_for_ptt_;
  TransceiverFactory::PTTMethod ptt_type_;
  QScopedPointer<OmniRig::OmniRigX> omni_rig_;
  RigNumber rig_number_;
  QScopedPointer<OmniRig::RigX> rig_;
  QScopedPointer<OmniRig::PortBits> port_;
  QString rig_type_;
  int readable_params_;
  int writable_params_;
  QScopedPointer<QTimer> offline_timer_;
  bool send_update_signal_;
  bool reversed_;   // some rigs can reverse VFOs
};

#endif


code:
#include "PollingTransceiver.hpp"

#include <exception>

#include <QObject>
#include <QString>
#include <QTimer>

#include "moc_PollingTransceiver.cpp"

namespace
{
  unsigned const polls_to_stabilize {3};
}

PollingTransceiver::PollingTransceiver (int poll_interval, QObject * parent)
  : TransceiverBase {parent}
  , interval_ {poll_interval * 1000}
  , poll_timer_ {nullptr}
  , retries_ {0}
{
}

void PollingTransceiver::start_timer ()
{
  if (interval_)
    {
      if (!poll_timer_)
        {
          poll_timer_ = new QTimer {this}; // pass ownership to
                                           // QObject which handles
                                           // destruction for us

          connect (poll_timer_, &QTimer::timeout, this,
                   &PollingTransceiver::handle_timeout);
        }
      poll_timer_->start (interval_);
    }
  else
    {
      stop_timer ();
    }
}

void PollingTransceiver::stop_timer ()
{
  if (poll_timer_)
    {
      poll_timer_->stop ();
    }
}

void PollingTransceiver::do_post_start ()
{
  start_timer ();
  if (!next_state_.online ())
    {
      // remember that we are expecting to go online
      next_state_.online (true);
      retries_ = polls_to_stabilize;
    }
}

void PollingTransceiver::do_post_stop ()
{
  // not much point waiting for rig to go offline since we are ceasing
  // polls
  stop_timer ();
}

void PollingTransceiver::do_post_frequency (Frequency f, MODE m)
{
  // take care not to set the expected next mode to unknown since some
  // callers use mode == unknown to signify that they do not know the
  // mode and don't care
  if (next_state_.frequency () != f || (m != UNK && next_state_.mode () != m))
    {
      // update expected state with new frequency and set poll count
      next_state_.frequency (f);
      if (m != UNK)
        {
          next_state_.mode (m);
        }
      retries_ = polls_to_stabilize;
    }
}

void PollingTransceiver::do_post_tx_frequency (Frequency f, MODE)
{
  if (next_state_.tx_frequency () != f)
    {
      // update expected state with new TX frequency and set poll
      // count
      next_state_.tx_frequency (f);
      next_state_.split (f); // setting non-zero TX frequency means split
      retries_ = polls_to_stabilize;
    }
}

void PollingTransceiver::do_post_mode (MODE m)
{
  // we don't ever expect mode to goto to unknown
  if (m != UNK && next_state_.mode () != m)
    {
      // update expected state with new mode and set poll count
      next_state_.mode (m);
      retries_ = polls_to_stabilize;
    }
}

void PollingTransceiver::do_post_ptt (bool p)
{
  if (next_state_.ptt () != p)
    {
      // update expected state with new PTT and set poll count
      next_state_.ptt (p);
      retries_ = polls_to_stabilize;
      //retries_ = 0;             // fast feedback on PTT
    }
}

bool PollingTransceiver::do_pre_update ()
{
  // if we are holding off a change then withhold the signal
  if (retries_ && state () != next_state_)
    {
      return false;
    }
  return true;
}

void PollingTransceiver::do_sync (bool force_signal, bool no_poll)
{
  if (!no_poll) poll ();        // tell sub-classes to update our state

  // Signal new state if it is directly requested or, what we expected
  // or, hasn't become what we expected after polls_to_stabilize
  // polls. Unsolicited changes will be signalled immediately unless
  // they intervene in a expected sequence where they will be delayed.
  if (retries_)
    {
      --retries_;
      if (force_signal || state () == next_state_ || !retries_)
        {
          // our client wants a signal regardless
          // or the expected state has arrived
          // or there are no more retries
          force_signal = true;
        }
    }
  else if (force_signal || state () != last_signalled_state_)
    {
      // here is the normal passive polling path either our client has
      // requested a state update regardless of change or state has
      // changed asynchronously
      force_signal = true;
    }

  if (force_signal)
    {
      // reset everything, record and signal the current state
      retries_ = 0;
      next_state_ = state ();
      last_signalled_state_ = state ();
      update_complete (true);
    }
}

void PollingTransceiver::handle_timeout ()
{
  QString message;

  // we must catch all exceptions here since we are called by Qt and
  // inform our parent of the failure via the offline() message
  try
    {
      do_sync ();
    }
  catch (std::exception const& e)
    {
      message = e.what ();
    }
  catch (...)
    {
      message = tr ("Unexpected rig error");
    }
  if (!message.isEmpty ())
    {
      offline (message);
    }
}


code:
#ifndef POLLING_TRANSCEIVER_HPP__
#define POLLING_TRANSCEIVER_HPP__

#include <QObject>

#include "TransceiverBase.hpp"

class QTimer;

//
// Polling Transceiver
//
//  Helper base  class that  encapsulates the emulation  of continuous
//  update and caching of a transceiver state.
//
// Collaborations
//
//  Implements the TransceiverBase post  action interface and provides
//  the abstract  poll() operation  for sub-classes to  implement. The
//  poll operation is invoked every poll_interval seconds.
//
// Responsibilities
//
//  Because some rig interfaces don't immediately update after a state
//  change request; this  class allows a rig a few  polls to stabilise
//  to the  requested state before  signalling the change.  This means
//  that  clients don't  see  intermediate states  that are  sometimes
//  inaccurate,  e.g. changing  the split  TX frequency  on Icom  rigs
//  requires a  VFO switch  and polls while  switched will  return the
//  wrong current frequency.
//
class PollingTransceiver
  : public TransceiverBase
{
  Q_OBJECT;                     // for translation context

protected:
  explicit PollingTransceiver (int poll_interval, // in seconds
                               QObject * parent);

protected:
  void do_sync (bool force_signal = false, bool no_poll = false) override final;

  // Sub-classes implement this and fetch what they can from the rig
  // in a non-intrusive manner.
  virtual void poll () = 0;

  void do_post_start () override final;
  void do_post_stop () override final;
  void do_post_frequency (Frequency, MODE) override final;
  void do_post_tx_frequency (Frequency, MODE) override final;
  void do_post_mode (MODE) override final;
  void do_post_ptt (bool = true) override final;
  bool do_pre_update () override final;

private:
  void start_timer ();
  void stop_timer ();

  Q_SLOT void handle_timeout ();

  int interval_;    // polling interval in milliseconds
  QTimer * poll_timer_;

  // keep a record of the last state signalled so we can elide
  // duplicate updates
  Transceiver::TransceiverState last_signalled_state_;

  // keep a record of expected state so we can compare with actual
  // updates to determine when state changes have bubbled through
  Transceiver::TransceiverState next_state_;

  unsigned retries_;            // number of incorrect polls left
};

#endif


code:
# JS8Call

FT8 has taken over the airwaves as the digital communication mode for making QSOs over HF/VHF/UHF. The mode has been widely popular as the latest offering in K1JT’s WSJT-X application. FT8 is based on the same foundation as JT65, JT9, and WSPR modes for weak signal communication, but transmits faster with only slightly reduced sensitivity.

While FT8 is an incredibly robust weak signal mode, it is designed heavily to take advantage of short band openings on HF/VHF/UHF and only offers a minimal QSO framework. However, many operators are using these weak signal qualities to make successful QSOs on the HF bands where other modes fail.

JS8Call is an experiment to test the feasibility of a digital mode with the robustness of FT8, combined with a messaging and network protocol layer for weak signal communication on HF, using keyboard-to-keyboard style interface. JS8Call is heavily inspired by WSJT-X, Fldigi, and FSQCall and would not exist without the hard work and dedication of the many developers in the amateur radio community.

* Read more on the original design inspiration here: https://github.com/jsherer/js8call

* For release announcements and discussion, join the JS8Call mailing list here: https://groups.io/g/js8call

# Notice

JS8Call is a derivative of the WSJT-X application, restructured and redesigned for message passing using FT8 modulation. It is not supported by nor endorsed by the WSJT-X development group. While the WSJT-X group maintains copyright over the original work and code, JS8Call is a derivative work licensed under and in accordance with the terms of the GPLv3 license. The source code modifications are public and can be found in this repository: https://bitbucket.org/widefido/wsjtx/

# History

* July 6, 2017 - The initial idea of using a modification to the FT8 protocol to support long-form QSOs was developed by Jordan, KN4CRD, and submitted to the WSJT-X mailing list: https://sourceforge.net/p/wsjt/mailman/message/35931540/
* August 31, 2017 - Jordan, KN4CRD, did a little development and modified WSJT-X to support long-form QSOs using the existing FT8 protocol: https://sourceforge.net/p/wsjt/mailman/message/36020051/  He sent a video example to the WSJT-X group: https://widefido.wistia.com/medias/7bb1uq62ga
* January 8, 2018 - Jordan, KN4CRD, started working on the design of a long-form QSO application built on top of FT8 with a redesigned interface.
* February 9, 2018 - Jordan, KN4CRD, submitted question to the WSJT-X group to see if there was any interest in pursuing the idea: https://sourceforge.net/p/wsjt/mailman/message/36221549/
* February 10, 2018 - Jordan KN4CRD, Julian OH8STN, John N0JDS, and the Portable Digital QRP group did an experiment using FSQ. The idea of JS8Call, combining FT8, long-form QSOs, and FSQCall like features was born.
* February 11, 2018 - Jordan, KN4CRD, inquired about the idea of integrating long-form messages into WSJT-X: https://sourceforge.net/p/wsjt/mailman/message/36223372/
* February 12, 2018 - Joe Taylor, K1JT, wrote back: https://sourceforge.net/p/wsjt/mailman/message/36224507/ saying that “Please don't let my comment discourage you from proceeding as you wish, toward something new.”
* March 4, 2018 - Jordan, KN4CRD, published a design document for JS8Call: https://github.com/jsherer/js8call
* July 6, 2018 - Version 0.0.1 of JS8Call released to the development group
* July 15, 2018 - Version 0.1 released - a dozen testers
* July 21, 2018 - Version 0.2 released - 75 testers
* July 27, 2018 - Version 0.3 released - 150 testers
* August 12, 2018 - Version 0.4 released - (“leaked” on QRZ) - 500 testers
* September 2, 2018 - Version 0.5 released - 3000 testers
* September 14, 2018 - Version 0.6 released - 5000 testers

path: README


code:
#include "Radio.hpp"

#include <cmath>
#include <limits>

#include <QString>
#include <QChar>
#include <QRegularExpression>

namespace Radio
{
  namespace
  {
    double constexpr MHz_factor {1.e6};
    int constexpr frequency_precsion {6};

    // very loose validation - callsign must contain a letter next to
    // a number
    QRegularExpression valid_callsign_regexp {R"(\d[[:alpha:]]|[[:alpha:]]\d)"};

    // suffixes that are often used and should not be interpreted as a
    // DXCC Entity prefix used as a suffix
    QRegularExpression non_prefix_suffix {R"(\A([0-9AMPQR]|QRP|F[DF]|[AM]M|L[HT]|LGT)\z)"};
  }

  Frequency frequency (QVariant const& v, int scale, bool * ok, QLocale const& locale)
  {
    double value {0.};
    if (QVariant::String == v.type ())
      {
        value = locale.toDouble (v.value<QString> (), ok);
      }
    else
      {
        value = v.toDouble ();
        if (ok) *ok = true;
      }
    value *= std::pow (10., scale);
    if (ok)
      {
        if (value < 0. || value > std::numeric_limits<Frequency>::max ())
          {
            value = 0.;
            *ok = false;
          }
      }
    return std::llround (value);
  }

  FrequencyDelta frequency_delta (QVariant const& v, int scale, bool * ok, QLocale const& locale)
  {
    double value {0.};
    if (QVariant::String == v.type ())
      {
        value = locale.toDouble (v.value<QString> (), ok);
      }
    else
      {
        value = v.toDouble ();
        if (ok) *ok = true;
      }
    value *= std::pow (10., scale);
    if (ok)
      {
        if (value < -std::numeric_limits<Frequency>::max ()
            || value > std::numeric_limits<Frequency>::max ())
          {
            value = 0.;
            *ok = false;
          }
      }
    return std::llround (value);
  }

  QString frequency_MHz_string (Frequency f, QLocale const& locale)
  {
    return locale.toString (f / MHz_factor, 'f', frequency_precsion);
  }

  QString frequency_MHz_string (FrequencyDelta d, QLocale const& locale)
  {
    return locale.toString (d / MHz_factor, 'f', frequency_precsion);
  }

  QString pretty_frequency_MHz_string (Frequency f, QLocale const& locale)
  {
    auto f_string = locale.toString (f / MHz_factor, 'f', frequency_precsion);
    return f_string.insert (f_string.size () - 3, QChar::Nbsp);
  }

  QString pretty_frequency_MHz_string (double f, int scale, QLocale const& locale)
  {
    auto f_string = locale.toString (f / std::pow (10., scale - 6), 'f', frequency_precsion);
    return f_string.insert (f_string.size () - 3, QChar::Nbsp);
  }

  QString pretty_frequency_MHz_string (FrequencyDelta d, QLocale const& locale)
  {
    auto d_string = locale.toString (d / MHz_factor, 'f', frequency_precsion);
    return d_string.insert (d_string.size () - 3, QChar::Nbsp);
  }

  bool is_callsign (QString const& callsign)
  {
    return callsign.contains (valid_callsign_regexp);
  }

  bool is_compound_callsign (QString const& callsign)
  {
    return callsign.contains ('/');
  }

  // split on first '/' and return the larger portion or the whole if
  // there is no '/'
  QString base_callsign (QString callsign)
  {
    auto slash_pos = callsign.indexOf ('/');
    if (slash_pos >= 0)
      {
        auto right_size = callsign.size () - slash_pos - 1;
        if (right_size>= slash_pos)
          {
            callsign = callsign.mid (slash_pos + 1);
          }
        else
          {
            callsign = callsign.left (slash_pos);
          }
      }
    return callsign.toUpper ();
  }

  // analyze the callsign and determine the effective prefix, returns
  // the full call if no valid prefix (or prefix as a suffix) is specified
  QString effective_prefix (QString callsign)
  {
    auto prefix = callsign.toUpper ();
    auto slash_pos = callsign.indexOf ('/');
    if (slash_pos >= 0)
      {
        auto right_size = callsign.size () - slash_pos - 1;
        if (right_size >= slash_pos) // naive call is longer than
                                     // prefix/suffix algorithm
          {
            prefix = callsign.left (slash_pos);
          }
        else
          {
            prefix = callsign.mid (slash_pos + 1);
            if (prefix.contains (non_prefix_suffix))
              {
                prefix = callsign.left (slash_pos); // ignore
                                                    // non-prefix
                                                    // suffixes
              }
          }
      }
    return prefix;
  }
}


code:
#ifndef RADIO_HPP__
#define RADIO_HPP__

#include <QObject>
#include <QLocale>
#include <QList>

class QVariant;
class QString;

//
// Declarations common to radio software.
//

namespace Radio
{
  //
  // Frequency types
  //
  using Frequency = quint64;
  using Frequencies = QList<Frequency>;
  using FrequencyDelta = qint64;

  //
  // Qt type registration
  //
  void register_types ();

  //
  // Frequency type conversion.
  //
  //	QVariant argument is convertible to double and is assumed to
  //	be scaled by (10 ** -scale).
  //
  Frequency frequency (QVariant const&, int scale,
                                  bool * ok = nullptr, QLocale const& = QLocale ());
  FrequencyDelta frequency_delta (QVariant const&, int scale,
                                             bool * ok = nullptr, QLocale const& = QLocale ());

  //
  // Frequency type formatting
  //
  QString frequency_MHz_string (Frequency, QLocale const& = QLocale ());
  QString frequency_MHz_string (FrequencyDelta, QLocale const& = QLocale ());
  QString pretty_frequency_MHz_string (Frequency, QLocale const& = QLocale ());
  QString pretty_frequency_MHz_string (double, int scale, QLocale const& = QLocale ());
  QString pretty_frequency_MHz_string (FrequencyDelta, QLocale const& = QLocale ());

  //
  // Callsigns
  //
  bool is_callsign (QString const&);
  bool is_compound_callsign (QString const&);
  QString base_callsign (QString);
  QString effective_prefix (QString);
}

Q_DECLARE_METATYPE (Radio::Frequency);
Q_DECLARE_METATYPE (Radio::Frequencies);
Q_DECLARE_METATYPE (Radio::FrequencyDelta);

#endif


code:
#include "Radio.hpp"

#include <QMetaType>
#include <QDebug>
#include <QDataStream>

namespace Radio
{
  void register_types ()
  {
    qRegisterMetaType<Radio::Frequency> ("Frequency");
    qRegisterMetaType<Radio::FrequencyDelta> ("FrequencyDelta");
    qRegisterMetaType<Radio::Frequencies> ("Frequencies");

    // This is required to preserve v1.5 "frequencies" setting for
    // backwards compatibility, without it the setting gets trashed
    // by later versions.
    qRegisterMetaTypeStreamOperators<Radio::Frequencies> ("Frequencies");
  }
}


code:
#include "RestrictedSpinBox.hpp"

#include <algorithm>
#include <utility>

#include <QString>

QValidator::State RestrictedSpinBox::validate (QString& input, int& pos) const
{
  // start by doing the standard QSpinBox validation
  auto valid = HintedSpinBox::validate (input, pos);
  // extra validation
  if (QValidator::Acceptable
      && values ().end () == std::find (values ().begin (), values ().end (), valueFromText (input)))
    {
      valid = QValidator::Intermediate;
    }
  return valid;
}


code:
#ifndef RESTRICTED_SPIN_BOX_HPP_
#define RESTRICTED_SPIN_BOX_HPP_

#include "HintedSpinBox.hpp"

class QString;

//
// RestrictedSpinBox - select only from a sequence of values
//
class RestrictedSpinBox final
  : public HintedSpinBox
{
public:
  RestrictedSpinBox (QWidget * parent = nullptr)
    : HintedSpinBox {parent}
  {
  }

protected:
  // override the base class validation
  QValidator::State validate (QString& input, int& pos) const override;
};

#endif


code:
#include "SelfDestructMessageBox.h"

SelfDestructMessageBox::SelfDestructMessageBox(
        int timeout,
        const QString& title,
        const QString& text,
        QMessageBox::Icon icon,
        QMessageBox::StandardButtons buttons,
        QMessageBox::StandardButton defaultButton,
        QWidget* parent,
        Qt::WindowFlags flags)
  : QMessageBox(icon, title, text, buttons, parent, flags),
    m_timeout(timeout),
    m_text(text)
{
    connect(&m_timer, &QTimer::timeout, this, &SelfDestructMessageBox::tick);
    m_timer.setInterval(1000);

    setDefaultButton(defaultButton);
    connect(this->defaultButton(), &QPushButton::clicked, this, &SelfDestructMessageBox::accept);
}

void SelfDestructMessageBox::showEvent(QShowEvent* event)
{
    tick();
    m_timer.start();
    QMessageBox::showEvent(event);
}

void SelfDestructMessageBox::tick(){
    m_timeout--;

    if(m_timeout){
        setText(m_text.arg(m_timeout));
        return;
    }

    m_timer.stop();
    accept();
}


code:
#ifndef SELFDESTRUCTMESSAGEBOX_H
#define SELFDESTRUCTMESSAGEBOX_H

#include <QWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QString>
#include <QTimer>

class SelfDestructMessageBox : public QMessageBox
{
    Q_OBJECT

public:
    SelfDestructMessageBox(int timeout,
                           const QString& title,
                           const QString& text,
                           QMessageBox::Icon icon,
                           QMessageBox::StandardButtons buttons = QMessageBox::Ok | QMessageBox::Cancel,
                           QMessageBox::StandardButton defaultButton = QMessageBox::Ok,
                           QWidget* parent = nullptr,
                           Qt::WindowFlags flags = 0);

    void showEvent(QShowEvent* event) override;

private slots:
    void tick();

private:
    int m_timeout;
    QString m_text;
    QTimer m_timer;
};

#endif // SELFDESTRUCTMESSAGEBOX_H


code:
#ifndef SETTINGS_GROUP_HPP_
#define SETTINGS_GROUP_HPP_

#include <QSettings>
#include <QString>

//
// Class SettingsGroup
//
//	Simple RAII type class to apply a QSettings group witin a
//	scope.
//
class SettingsGroup
{
public:
  SettingsGroup (QSettings * settings, QString const& group)
    : settings_ {settings}
  {
    settings_->beginGroup (group);
  }

  SettingsGroup (SettingsGroup const&) = delete;
  SettingsGroup& operator = (SettingsGroup const&) = delete;

  ~SettingsGroup ()
  {
    settings_->endGroup ();
  }

private:
  QSettings * settings_;
};

#endif


code:
#include "SplashScreen.hpp"

#include <QPixmap>
#include <QVBoxLayout>
#include <QCheckBox>
#include <QCoreApplication>

#include "revision_utils.hpp"
#include "pimpl_impl.hpp"

#include "moc_SplashScreen.cpp"

class SplashScreen::impl
{
public:
  impl ()
    : checkbox_ {"Do not show this again"}
  {
    main_layout_.addStretch ();
    main_layout_.addWidget (&checkbox_, 0, Qt::AlignRight);
  }

  QVBoxLayout main_layout_;
  QCheckBox checkbox_;
};

SplashScreen::SplashScreen ()
  : QSplashScreen {QPixmap {":/splash.png"}, Qt::WindowStaysOnTopHint}
{
  setLayout (&m_->main_layout_);
  showMessage ("
" + QString {"WSJT-X v" +
        QCoreApplication::applicationVersion() + " " +
        revision ()}.simplified () + "
"
    "V1.8 has many new features.

"
    "The release notes have more details.

"
    "Send issue reports to wsjtgroup@yahoogroups.com, and be sure to save .wav
"
    "files where appropriate.

"
    "Open the Help menu and select Release Notes for more details.
"
    ""
    "", Qt::AlignCenter);
  connect (&m_->checkbox_, &QCheckBox::stateChanged, [this] (int s) {
      if (Qt::Checked == s) Q_EMIT disabled ();
    });
}

SplashScreen::~SplashScreen ()
{
}


code:
#ifndef SPLASH_SCREEN_HPP___
#define SPLASH_SCREEN_HPP___

#include <QSplashScreen>

#include "pimpl_h.hpp"

class SplashScreen final
  : public QSplashScreen
{
  Q_OBJECT

public:
  SplashScreen ();
  ~SplashScreen ();

  Q_SIGNAL void disabled ();

private:
  class impl;
  pimpl<impl> m_;
};

#endif


code:
#include "StationList.hpp"

#include <utility>
#include <algorithm>
#include <cmath>

#include <QMetaType>
#include <QAbstractTableModel>
#include <QObject>
#include <QString>
#include <QVector>
#include <QStringList>
#include <QMimeData>
#include <QDataStream>
#include <QByteArray>
#include <QDebug>
#include <QDebugStateSaver>

#include "DriftingDateTime.h"

#include "pimpl_impl.hpp"

#include "Radio.hpp"
#include "Bands.hpp"
#include "FrequencyList.hpp"

#if !defined (QT_NO_DEBUG_STREAM)
QDebug operator << (QDebug debug, StationList::Station const& station)
{
  QDebugStateSaver saver {debug};
  debug.nospace () << "Station("
                   << station.band_name_ << ", "
                   << station.frequency_ << ", "
                   << station.switch_at_ << ", "
                   << station.switch_until_ << ", "
                   << station.description_ << ')';
  return debug;
}
#endif

QDataStream& operator << (QDataStream& os, StationList::Station const& station)
{
  return os << station.band_name_
            << station.frequency_
            << station.switch_at_
            << station.switch_until_
            << station.description_;
}

QDataStream& operator >> (QDataStream& is, StationList::Station& station)
{
  return is >> station.band_name_
            >> station.frequency_
            >> station.switch_at_
            >> station.switch_until_
            >> station.description_;
}

class StationList::impl final
  : public QAbstractTableModel
{
public:
  impl (Bands const * bands, Stations stations, QObject * parent)
    : QAbstractTableModel {parent}
    , bands_ {bands}
    , stations_ {stations}
  {
  }

  Stations station_list (Stations);
  QModelIndex add (Station);
  FrequencyDelta offset (Frequency) const;

  // Implement the QAbstractTableModel interface.
  int rowCount (QModelIndex const& parent = QModelIndex {}) const override;
  int columnCount (QModelIndex const& parent = QModelIndex {}) const override;
  Qt::ItemFlags flags (QModelIndex const& = QModelIndex {}) const override;
  QVariant data (QModelIndex const&, int role) const override;
  QVariant headerData (int section, Qt::Orientation, int = Qt::DisplayRole) const override;
  bool setData (QModelIndex const&, QVariant const& value, int role = Qt::EditRole) override;
  bool removeRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override;
  bool insertRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override;
  Qt::DropActions supportedDropActions () const override;
  QStringList mimeTypes () const override;
  QMimeData * mimeData (QModelIndexList const&) const override;
  bool dropMimeData (QMimeData const *, Qt::DropAction, int row, int column, QModelIndex const& parent) override;

  // Helper method for band validation.
  QModelIndex first_matching_band (QString const& band_name) const
  {
    // find first exact match in bands
    auto matches = bands_->match (bands_->index (0, 0)
                                  , Qt::DisplayRole
                                  , band_name
                                  , 1
                                  , Qt::MatchExactly);
    return matches.isEmpty () ? QModelIndex {} : matches.first ();
  }

  static int constexpr num_columns {5};
  static auto constexpr mime_type = "application/wsjt.antenna-descriptions";

  Bands const * bands_;
  Stations stations_;
};

StationList::StationList (Bands const * bands, QObject * parent)
  : StationList {bands, {}, parent}
{
}

StationList::StationList (Bands const * bands, Stations stations, QObject * parent)
  : QSortFilterProxyModel {parent}
  , m_ {bands, stations, parent}
{
  setSourceModel (&*m_);
  setSortRole (SortRole);
}

StationList::~StationList ()
{
}

auto StationList::station_list (Stations stations) -> Stations
{
  return m_->station_list (stations);
}

auto StationList::station_list () const -> Stations const&
{
  return m_->stations_;
}

QModelIndex StationList::add (Station s)
{
  return mapFromSource (m_->add (s));
}

bool StationList::remove (Station s)
{
  auto row = m_->stations_.indexOf (s);
  if (0 > row)
    {
      return false;
    }
  return removeRow (row);
}

namespace
{
  bool row_is_higher (QModelIndex const& lhs, QModelIndex const& rhs)
  {
    return lhs.row () > rhs.row ();
  }
}

bool StationList::removeDisjointRows (QModelIndexList rows)
{
  bool result {true};

  // We must work with source model indexes because we don't want row
  // removes to invalidate model indexes we haven't yet processed. We
  // achieve that by processing them in decending row order.
  for (int r = 0; r < rows.size (); ++r)
    {
      rows[r] = mapToSource (rows[r]);
    }

  // reverse sort by row
  qSort (rows.begin (), rows.end (), row_is_higher);
  Q_FOREACH (auto index, rows)
    {
      if (result && !m_->removeRow (index.row ()))
        {
          result = false;
        }
    }

  return result;
}

auto StationList::impl::station_list (Stations stations) -> Stations
{
  beginResetModel ();
  std::swap (stations_, stations);
  endResetModel ();
  return stations;
}

QModelIndex StationList::impl::add (Station s)
{
  // Any band that isn't in the list may be added
  if (!stations_.contains (s))
    {
      auto row = stations_.size ();

      beginInsertRows (QModelIndex {}, row, row);
      stations_.append (s);
      endInsertRows ();

      return index (row, 0);
    }

  return QModelIndex {};
}

#if 0
auto StationList::impl::offset (Frequency f) const -> FrequencyDelta
{
  // Lookup band for frequency
  auto const& band = bands_->find (f);
  if (!band.isEmpty ())
    {
      // Lookup station for band
      for (int i = 0; i < stations_.size (); ++i)
        {
          if (stations_[i].band_name_ == band)
            {
              return stations_[i].frequency_;
            }
        }
    }
  return 0;                     // no offset
}
#endif

int StationList::impl::rowCount (QModelIndex const& parent) const
{
  return parent.isValid () ? 0 : stations_.size ();
}

int StationList::impl::columnCount (QModelIndex const& parent) const
{
  return parent.isValid () ? 0 : num_columns;
}

Qt::ItemFlags StationList::impl::flags (QModelIndex const& index) const
{
  auto result = QAbstractTableModel::flags (index);

  auto row = index.row ();
  auto column = index.column ();

  if (index.isValid ()
      && row < stations_.size ()
      && column < num_columns)
    {
      if (band_column == column){
          result |= Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;

      } else if (description_column == column)
        {
          result |= Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
        }
      else
        {
          result |= Qt::ItemIsEditable | Qt::ItemIsDropEnabled;
        }
    }
  else
    {
      result |= Qt::ItemIsDropEnabled;
    }

  return result;
}

QVariant StationList::impl::data (QModelIndex const& index, int role) const
{
  QVariant item;

  auto row = index.row ();
  auto column = index.column ();

  if (index.isValid ()
      && row < stations_.size ())
    {
      switch (column)
        {
        case band_column:
          switch (role)
            {
            case SortRole:
              {
                // Lookup band.
                auto band_index = first_matching_band (stations_.at (row).band_name_);
                // Use the sort role value of the band.
                item = band_index.data (Bands::SortRole);
              }
              break;

            case Qt::DisplayRole:
            case Qt::EditRole:
            case Qt::AccessibleTextRole:
              item = stations_.at (row).band_name_;
              break;

            case Qt::ToolTipRole:
            case Qt::AccessibleDescriptionRole:
              item = tr ("Band name");
              break;

            case Qt::TextAlignmentRole:
              item = (int)Qt::AlignHCenter | Qt::AlignVCenter;
              break;
            }
          break;

        case frequency_column:
          {
            auto frequency_offset = stations_.at (row).frequency_;
            switch (role)
              {
              case SortRole:
              case Qt::EditRole:
              case Qt::AccessibleTextRole:
                item = frequency_offset;
                break;

              case Qt::DisplayRole:
                item = Radio::pretty_frequency_MHz_string (frequency_offset) + " MHz";
                break;

              case Qt::ToolTipRole:
              case Qt::AccessibleDescriptionRole:
                item = tr ("Frequency");
                break;

              case Qt::TextAlignmentRole:
                item = (int)Qt::AlignRight | Qt::AlignVCenter;
                break;
              }
          }
          break;

      case switch_at_column:
        switch (role)
          {
          case SortRole:
          case Qt::EditRole:
          case Qt::DisplayRole:
          case Qt::AccessibleTextRole:
            item = stations_.at (row).switch_at_.time().toString("hh:mm");
            break;

          case Qt::ToolTipRole:
          case Qt::AccessibleDescriptionRole:
            item = tr ("Switch at this time");
            break;

          case Qt::TextAlignmentRole:
            item = (int)Qt::AlignHCenter | Qt::AlignVCenter;
            break;
          }
        break;

      case switch_until_column:
        switch (role)
          {
          case SortRole:
          case Qt::EditRole:
          case Qt::DisplayRole:
          case Qt::AccessibleTextRole:
            item = stations_.at (row).switch_until_.time().toString("hh:mm");
            break;

          case Qt::ToolTipRole:
          case Qt::AccessibleDescriptionRole:
            item = tr ("Switch until this time");
            break;

          case Qt::TextAlignmentRole:
            item = (int)Qt::AlignHCenter | Qt::AlignVCenter;
            break;
          }
        break;

      case description_column:
        switch (role)
          {
          case SortRole:
          case Qt::EditRole:
          case Qt::DisplayRole:
          case Qt::AccessibleTextRole:
            item = stations_.at (row).description_;
            break;

          case Qt::ToolTipRole:
          case Qt::AccessibleDescriptionRole:
            item = tr ("Antenna description");
            break;

          case Qt::TextAlignmentRole:
            item = (int)Qt::AlignLeft | Qt::AlignVCenter;
            break;
          }
        break;
      }
    }

  return item;
}

QVariant StationList::impl::headerData (int section, Qt::Orientation orientation, int role) const
{
  QVariant header;

  if (Qt::DisplayRole == role && Qt::Horizontal == orientation)
    {
      switch (section)
        {
        case band_column: header = tr ("Band"); break;
        case frequency_column: header = tr ("Freq. (MHz)"); break;
        case switch_at_column: header = tr ("Switch at (UTC)"); break;
        case switch_until_column: header = tr ("Until (UTC)"); break;
        case description_column: header = tr ("Description"); break;
        }
    }
  else
    {
      header = QAbstractTableModel::headerData (section, orientation, role);
    }

  return header;
}

bool StationList::impl::setData (QModelIndex const& model_index, QVariant const& value, int role)
{
  bool changed {false};

  auto row = model_index.row ();
  auto size = stations_.size ();
  if (model_index.isValid ()
      && Qt::EditRole == role
      && row < size)
    {
      QVector<int> roles;
      roles << role;

      switch (model_index.column ())
        {
        case band_column:
          {
            // Check if band name is valid.
            auto band_index = first_matching_band (value.toString ());
            if (band_index.isValid ())
              {
                stations_[row].band_name_ = band_index.data ().toString ();
                Q_EMIT dataChanged (model_index, model_index, roles);
                changed = true;
              }
          }
          break;

        case frequency_column:
          {
            if (value.canConvert<Frequency> ())
              {
                Frequency offset {qvariant_cast<Radio::Frequency> (value)};

                if(offset == 0){
                    Frequency l;
                    Frequency h;
                    if(bands_->findFreq(stations_[row].band_name_, &l, &h)){
                        offset = l;
                    }
                }

                if (offset != stations_[row].frequency_)
                  {
                    stations_[row].frequency_ = offset;
                    Q_EMIT dataChanged (model_index, model_index, roles);

                    auto band = bands_->find(offset);
                    if(band != stations_[row].band_name_){
                        stations_[row].band_name_ = band;
                        auto band_index = model_index.model()->index(row, band_column, model_index);
                        Q_EMIT dataChanged (band_index, band_index, roles);
                    }
                    changed = true;
                  }
              }
          }
          break;
        case switch_at_column:
        {
          QString s = value.toString();
          if(s.length() < 5){
              s = QString("0").repeated(5-s.length()) + s;
          }
          auto t = QTime::fromString(s);
          auto at = QDateTime(QDate(2000,1,1), t, Qt::UTC);
          auto until = stations_[row].switch_until_;
          stations_[row].switch_at_ = at;
          stations_[row].switch_until_ = until;

          Q_EMIT dataChanged (model_index, model_index, roles);

          auto switch_until_index = model_index.model()->index(row, switch_until_column, model_index);
          Q_EMIT dataChanged (switch_until_index, switch_until_index, roles);

          changed = true;
          break;
        }
        case switch_until_column:
        {
          QString s = value.toString();
          if(s.length() < 5){
              s = QString("0").repeated(5-s.length()) + s;
          }
          auto t = QTime::fromString(s);
          auto until = QDateTime(QDate(2000,1,1), t, Qt::UTC);
          auto at = stations_[row].switch_at_;
          stations_[row].switch_at_ = at;
          stations_[row].switch_until_ = until;

          Q_EMIT dataChanged (model_index, model_index, roles);

          auto switch_at_index = model_index.model()->index(row, switch_at_column, model_index);
          Q_EMIT dataChanged (switch_at_index, switch_at_index, roles);

          changed = true;
          break;
        }
        case description_column:
          stations_[row].description_ = value.toString ();
          Q_EMIT dataChanged (model_index, model_index, roles);
          changed = true;
          break;
        }
    }

  return changed;
}

bool StationList::impl::removeRows (int row, int count, QModelIndex const& parent)
{
  if (0 < count && (row + count) <= rowCount (parent))
    {
      beginRemoveRows (parent, row, row + count - 1);
      for (auto r = 0; r < count; ++r)
        {
          stations_.removeAt (row);
        }
      endRemoveRows ();
      return true;
    }

  return false;
}

bool StationList::impl::insertRows (int row, int count, QModelIndex const& parent)
{
  if (0 < count)
    {
      beginInsertRows (parent, row, row + count - 1);
      for (auto r = 0; r < count; ++r)
        {
          stations_.insert (row, Station ());
        }
      endInsertRows ();
      return true;
    }

  return false;
}

Qt::DropActions StationList::impl::supportedDropActions () const
{
  return Qt::CopyAction | Qt::MoveAction;
}

QStringList StationList::impl::mimeTypes () const
{
  QStringList types;
  types << mime_type;
  types << "application/wsjt.Frequencies";
  return types;
}

QMimeData * StationList::impl::mimeData (QModelIndexList const& items) const
{
  QMimeData * mime_data = new QMimeData {};
  QByteArray encoded_data;
  QDataStream stream {&encoded_data, QIODevice::WriteOnly};

  Q_FOREACH (auto const& item, items)
    {
      if (item.isValid ())
        {
          stream << QString {data (item, Qt::DisplayRole).toString ()};
        }
    }

  mime_data->setData (mime_type, encoded_data);
  return mime_data;
}

bool StationList::impl::dropMimeData (QMimeData const * data, Qt::DropAction action, int /* row */, int /* column */, QModelIndex const& parent)
{
  if (Qt::IgnoreAction == action)
    {
      return true;
    }
  if (parent.isValid ()
      && description_column == parent.column ()
      && data->hasFormat (mime_type))
    {
      QByteArray encoded_data {data->data (mime_type)};
      QDataStream stream {&encoded_data, QIODevice::ReadOnly};
      auto dest_index = parent;
      while (!stream.atEnd ())
        {
          QString text;
          stream >> text;
          setData (dest_index, text);
          dest_index = index (dest_index.row () + 1, dest_index.column (), QModelIndex {});
        }
      return true;
    }
  else if (data->hasFormat ("application/wsjt.Frequencies"))
    {
      QByteArray encoded_data {data->data ("application/wsjt.Frequencies")};
      QDataStream stream {&encoded_data, QIODevice::ReadOnly};
      while (!stream.atEnd ())
        {
          FrequencyList_v2::Item item;
          stream >> item;
          auto const& band = bands_->find (item.frequency_);
          if (stations_.cend () == std::find_if (stations_.cbegin ()
                                                 , stations_.cend ()
                                                 , [&band] (Station const& s) {return s.band_name_ == band;}))
            {
              // not found so add it
              add (Station {band, 0, DriftingDateTime::currentDateTimeUtc(), DriftingDateTime::currentDateTimeUtc(), QString {}});
            }
        }
      return true;
    }
  return false;
}


code:
#ifndef STATION_LIST_HPP__
#define STATION_LIST_HPP__

#include <QSortFilterProxyModel>
#include <QDateTime>
#include <QString>
#include <QList>

#include "pimpl_h.hpp"

#include "Radio.hpp"

class Bands;

//
// Class StationList
//
//  Encapsulates information  about a  collection of  unique operating
//  stations per  band. The implementation  is a table model  with the
//  first column  being the unique  (within the table rows)  band name
//  and, the  second the frequency  offset for transverter  usage and,
//  the third the antenna description. All are editable.
//
// Responsibilities
//
//  Stores internally an unordered table of bands.
//
//  If  an ordered  representaion is  required then  wrapping with  an
//  appropriate        proxy         model        is        sufficient
//  e.g. QSortFilterProxyModel. A custom SortRole role is provided for
//  the band  name column which  returns a numeric value  (Bands lower
//  frequency limit) which gives a strict frequency ordering by band.
//
// Collaborations
//
//  Implements the  QAbstractTableModel interface for a  grid of bands
//  with offset frequencies and antenna descriptions.
//
//  Uses the Bands model to lookup band information.
//
class StationList final
  : public QSortFilterProxyModel
{
public:
  using Frequency = Radio::Frequency;
  using FrequencyDelta = Radio::FrequencyDelta;

  //
  // Struct Station
  //
  //  Aggregation of fields that describe a radio station on a band.
  //
  struct Station
  {
    QString band_name_;
    Frequency frequency_;
    QDateTime switch_at_;
    QDateTime switch_until_;
    QString description_;
  };

  using Stations = QList<Station>;

  enum Column {band_column, frequency_column, switch_at_column, switch_until_column, description_column};

  explicit StationList (Bands const * bands, QObject * parent = nullptr);
  explicit StationList (Bands const * bands, Stations, QObject * parent = nullptr);
  ~StationList ();

  // Load and query contents.
  Stations station_list (Stations);
  Stations const& station_list () const;

  //
  // Model API
  //
  QModelIndex add (Station);                 // Add a new Station
  bool remove (Station);                     // Remove a Station
  bool removeDisjointRows (QModelIndexList); // Remove one or more stations

  // Custom sort role.
  static int constexpr SortRole = Qt::UserRole;

private:
  class impl;
  pimpl<impl> m_;
};

// Station equivalence
inline
bool operator == (StationList::Station const& lhs, StationList::Station const& rhs)
{
  return lhs.band_name_ == rhs.band_name_ &&
    lhs.description_ == rhs.description_ &&
    lhs.frequency_ == rhs.frequency_ &&
    lhs.switch_at_ == rhs.switch_at_ &&
    lhs.switch_until_ == rhs.switch_until_;
}

QDataStream& operator << (QDataStream&, StationList::Station const&);
QDataStream& operator >> (QDataStream&, StationList::Station&);

#if !defined (QT_NO_DEBUG_STREAM)
QDebug operator << (QDebug debug, StationList::Station const&);
#endif

Q_DECLARE_METATYPE (StationList::Station);
Q_DECLARE_METATYPE (StationList::Stations);

#endif


code:
#include "TraceFile.hpp"

#include <stdexcept>

#include <QDebug>
#include <QString>
#include <QFile>
#include <QTextStream>
#include <QMessageLogContext>
#include <QDateTime>
#include <QMutex>
#include <QMutexLocker>

#include "pimpl_impl.hpp"

namespace
{
  QMutex lock;
}

class TraceFile::impl
{
public:
  impl (QString const& trace_file_path);
  ~impl ();

  // no copying
  impl (impl const&) = delete;
  impl& operator = (impl const&) = delete;

private:
  // write Qt messages to the diagnostic log file
  static void message_handler (QtMsgType type, QMessageLogContext const& context, QString const& msg);

  QFile file_;
  QTextStream stream_;
  QTextStream * original_stream_;
  QtMessageHandler original_handler_;
  static QTextStream * current_stream_;
};

QTextStream * TraceFile::impl::current_stream_;

// delegate to implementation class
TraceFile::TraceFile (QString const& trace_file_path)
  : m_ {trace_file_path}
{
}

TraceFile::~TraceFile ()
{
}

TraceFile::impl::impl (QString const& trace_file_path)
  : file_ {trace_file_path}
  , original_stream_ {current_stream_}
  , original_handler_ {nullptr}
{
  // if the log file is writeable; initialise diagnostic logging to it
  // for append and hook up the Qt global message handler
  if (file_.open (QFile::WriteOnly | QFile::Append | QFile::Text))
    {
      stream_.setDevice (&file_);
      current_stream_ = &stream_;
      original_handler_ = qInstallMessageHandler (message_handler);
    }
}

TraceFile::impl::~impl ()
{
  // unhook our message handler before the stream and file are destroyed
  if (original_handler_)
    {
      qInstallMessageHandler (original_handler_);
    }
  current_stream_ = original_stream_; // revert to prior stream
}

// write Qt messages to the diagnostic log file
void TraceFile::impl::message_handler (QtMsgType type, QMessageLogContext const& context, QString const& msg)
{
  char const * severity;
  switch (type)
    {
    case QtDebugMsg:
      severity = "Debug";
      break;

    case QtWarningMsg:
      severity = "Warning";
      break;

    case QtFatalMsg:
      severity = "Fatal";
      break;

    default:
      severity = "Critical";
      break;
    }

  {
    // guard against multiple threads with overlapping messages
    QMutexLocker guard (&lock);
    Q_ASSERT_X (current_stream_, "TraceFile:message_handler", "no stream to write to");
    *current_stream_
      << QDateTime::currentDateTimeUtc ().toString ("yyyy-MM-ddTHH:mm:ss.zzzZ")
      << '(' << context.file << ':' << context.line /* << ", " << context.function */ << ')'
      << severity << ": " << msg.trimmed () << endl;
  }

  if (QtFatalMsg == type)
    {
      throw std::runtime_error {"Fatal Qt Error"};
    }
}


code:
#ifndef TRACE_FILE_HPP_
#define TRACE_FILE_HPP_

#include "pimpl_h.hpp"

class QString;

class TraceFile final
{
public:
  explicit TraceFile (QString const& TraceFile_file_path);
  ~TraceFile ();

  // copying not allowed
  TraceFile (TraceFile const&) = delete;
  TraceFile& operator = (TraceFile const&) = delete;

private:
  class impl;
  pimpl<impl> m_;
};

#endif


code:
#include "Transceiver.hpp"

#include "moc_Transceiver.cpp"

#if !defined (QT_NO_DEBUG_STREAM)

ENUM_QDEBUG_OPS_IMPL (Transceiver, MODE);

QDebug operator << (QDebug d, Transceiver::TransceiverState const& s)
{
  d.nospace ()
    << "Transceiver::TransceiverState(online: " << (s.online_ ? "yes" : "no")
    << " Frequency {" << s.rx_frequency_ << "Hz, " << s.tx_frequency_ << "Hz} " << s.mode_
    << "; SPLIT: " << (Transceiver::TransceiverState::Split::on == s.split_ ? "on" : Transceiver::TransceiverState::Split::off == s.split_ ? "off" : "unknown")
    << "; PTT: " << (s.ptt_ ? "on" : "off")
    << ')';
  return d.space ();
}

#endif

ENUM_QDATASTREAM_OPS_IMPL (Transceiver, MODE);

ENUM_CONVERSION_OPS_IMPL (Transceiver, MODE);

bool operator != (Transceiver::TransceiverState const& lhs, Transceiver::TransceiverState const& rhs)
{
  return lhs.online_ != rhs.online_
    || lhs.rx_frequency_ != rhs.rx_frequency_
    || lhs.tx_frequency_ != rhs.tx_frequency_
    || lhs.mode_ != rhs.mode_
    || lhs.split_ != rhs.split_
    || lhs.ptt_ != rhs.ptt_;
}

bool operator == (Transceiver::TransceiverState const& lhs, Transceiver::TransceiverState const& rhs)
{
  return !(lhs != rhs);
}