Tutorial: Add a radio feature

Overview

To add a basic radio feature, such as a parameter that is toggled on and off:

  • Add functions to rigcommander.cpp
    • Set function
    • Query (“get”) function
    • Intrepret / parse function for query replies
  • Connect the “set” functions to signals from wfmain.cpp
  • Connect the “query” functions to signals from wfmain.cpp
  • Connect the “parse” functions from rigcommander.cpp (SIGNAL) to wfmain.cpp (SLOT)
  • If the command is unique to certain radio models, add entries into the rigCaps structure and populate accordingly in rigcommander.cpp for the radios that have or do not have the control.
  • Add function shortcut names to the enumerated cmds list in wfmain.h (search for “cmdNone” in the file to find the enum)
  • Write doCmd switch entries in wfmain.cpp. You need two: “set” commands reside in doCmd(commandtype cmddata), and “get” commands reside in doCmd(cmds cmd).
  • Possibly, add an issueCmd(cmds cmd, argtype arg) function to correctly stuff the command queue with your command and associated data argument(s). Common datatypes (bool, float, char, etc) should already be covered and will work via polymorphism, so only add one if you are using a different datatype than what you already see.
  • Add desired UI element(s) or other code.
  • From the UI element action (click, for example), Issue set and query (“get”) commands using the command queue system:
    • issueCmd(cmdSetFreq, f); // puts cmdSetFreq into the queue with argument f
    • issueDelayedCommand(cmdGetFreq); // puts cmdGetFreq into the queue
    • Note that there are some different options available in terms of queue position and uniqueness. For commands that could be issued very rapidly, such as tuning, it’s usually sensible to request unique entry so as to not crowd the queue with outdated requests. For commands where immediacy is needed, such as PTT, use the priority function to jump ahead in line.

Detailed Example: IF Shift and Twin PBF

In this example, we will add a command to adjust the IF Shift. IF Shift seems to use a similar command between many radios, and accepts a single parameter of “shift” amount, which if omitted, will cause the radio to tell us the current IF Shift.

Some radios, such as the IC-7300, have a TWIN BPT adjustment instead of IF Shift. Some radios have both, such as the IC-756 Pro, and some just have IF Shift, such as the IC-718. So for this feature, we will implement IF-Shift, Inside TWIN BPT, and Outside TWIN BPT. This will be interesting as we will need to track which feature each radio has, and also deal with radios that seem to have both.

The commands (I will omit the CI-V addressing and ending 0xFD part, since rigCommander handles these aspects. ):

IF-Shift and Inside TWIN PBT:

0x14 0x07 0x01 0x28

Where the level is 0128 in my example above. To query the current level, we can omit the 0128:

0x14 0x07

For the Outside TWIN PBT, we use 0x14 0x08.

These two commands are in the 0x14 set of commands, so we can probably use the “set level” and “get level” commands and related functions. (For example, commands we use to set the RF Gain level.)

I’ll start with three new functions. Two will be the same, (IF Shift and Outside TWIN PBT), but I’ll make three anyway for completeness, and in case there are radios that use different commands.

First the header in rigcommander.h, in the “// Get Levels” area, which is under public slots:

void getIFShift();
void getTPBFInner();
void getTPBFOuter()

Within rigcommander.cpp, I’ll put this near the getAfGain() function:

void rigCommander::getIFShift()
{
    QByteArray payload("\x14\x07");
    prepDataAndSend(payload);
}

void rigCommander::getTPBFInner()
{
    QByteArray payload("\x14\x07");
    prepDataAndSend(payload);
}

void rigCommander::getTPBFOuter()
{
    QByteArray payload("\x14\x08");
    prepDataAndSend(payload);
}

Before we can add code to parse the return, we need to jump back to the fact that some radios use 0x07 for the IF Shift, and others for the TPBF Inner. The idea of rigCommander, is that the parent object doesn’t have to know much about the radio. If we say we have an IF Shift, then that’s what it is, and if we say it’s a TBPF Inner, then that’s what it is. Therefore, we need to write some code to determine, within rigCommander, which radios have which features. To make matters worse, some radios seem to have both features depending upon the mode selected! To keep it simple, I’m going to assume that radios which offer both will generally be used as TBPF. On the IC-756 Pro, for example, the knob labeled as Twin BPF becomes IF-Shift on AM mode. We can do the same thing in wfview, letting one knob do two things. But we do need to know about radios that simply do not have this at all, or only have IF Shift. Therefore, I will add to rigCaps “hasTBPF” and “hasIFShift” to the struct rigCapabilities as defined in rigidentities.h:

    bool hasIFShift;     
    bool hasTBPF;

Now I’ll go into rigCommander’s determineRigCaps() function, and add default values (false) and then add true for radios where it has these respective features. This involves going through each CI-V manual for each supported radio and verifying if the feature exists and has the same command.

 rigCaps.hasTBPF = true;

With the capabilities defined, now I’ll add the code for parsing radio returns. First, notice the sort of beginning of the parsing tree in rigCommander at parseCommand(). Within this function, the data from the radio are split up into categories, generally based on the first byte of the command. You can see that in the case of levels, which start with 0x14 or 0x15, they are sent to parseLevels(). So let’s go there.

The parseLevels() function begins by pulling out the level — whatever it may be for — and then decoding what the level was actually for. I’ll add some code within the 0x14 beginning branch, in the switch case, after the squelch level. This keeps the switch in numerical order, which, more importantly, matches the order of the radio documentation.

Within the parseLevels() function, we’ll need to be able to emit signals about the data we have found, thus, prior to adding the parsing code, we’ll need to add three signals to rigCommander.h under the signals area with the other levels:

void haveTBPFInner(unsigned char level);
void haveTBPFOuter(unsigned char level);
void haveIFShift(unsigned char level);

These signals look like functions, but the idea is that they have a name and they come with a type (unsigned char in this case), and they will bring that value to whatever they are connected to with a compatible slot (in our case, within wfmain later).

Now I can complete the parsLevels() modification:

            case '\x07':
                // Twin BPF Inner, or, IF-Shift level
                if(rigCaps.hasTBPF)
                    emit haveTBPFInner(level);
                else
                    emit haveIFShift(level);
                break;
            case '\x08':
                // Twin BPF Outer
                emit haveTBPFOuter(level);
                break;

At this point, rigCommander can send a query for the current IF Shift, and we can process the resulting answer. But we need to add something to set the value. This set function will be a slot, which can be connected to a signal from wfmain.

Within rigCommander.h, under public slots, I’ll add setIFShift, setTBPFInner, and setTBPFOuter functions, near the // Set Levels area:

public slots:
...
    // Set Levels: 
    ...
    void setIFShift(unsigned char level);
    void setTBPFInner(unsigned char level);
    void setTBPFOuter(unsigned char level);

And now to write the functions inside rigCommander.cpp (slots are regular functions in implementation):

void rigCommander::setIFShift(unsigned char level)
{
    QByteArray payload("\x14\x07");
    payload.append(bcdEncodeInt(level));
    prepDataAndSend(payload);
}

void rigCommander::setTBPFInner(unsigned char level)
{
    QByteArray payload("\x14\x07");
    payload.append(bcdEncodeInt(level));
    prepDataAndSend(payload);
}

void rigCommander::setTBPFOuter(unsigned char level)
{
    QByteArray payload("\x14\x08");
    payload.append(bcdEncodeInt(level));
    prepDataAndSend(payload);
}

I ended up placing this code just above the setTXLevel function. The bcdEncodeInt(unsigned char) function just converts an integer like “123” into data 0x01 0x23.
rigCommander is now finished. All the functionality needed has been added. It’s time now to think about what to connect it to. In the most general sense, we need a signal in wfmain for each slot that we added to rigCommander, and a slot in wfmain for each signal we added to rigCommander. Slots are inputs, signals are outputs.

And so, let us begin with the wfmain.h header file and add those items. For some reason, early on, I made a lot of the slots with names like “receiveMicGain”. The word “receive” was intended to help me remember that this was an input receiving data from an output (signal). But of course, the word “receive” makes it confusing when working with receivers and transceivers. Anyway, we’re sticking with it for now. Under private slots, in the Levels section, I’ll add slots to receive the three new signals:

private slots:
...
    // Levels: 
    ...
    void receiveIFShift(unsigned char level);
    void receiveTBPFInner(unsigned char level);
    void receiveTBPFOuter(unsigned char level);

For the sending of new levels and sending requests for current levels, we need to add signals that we can emit, which we’ll connect later over to rigCommander. Here’s where that goes, within the signals part of the header:

signals:
...
    // Level get:
    ...
    void getIfShift();
    void getTBPFInner();
    void getTBPFOuter();
    ...
    // Level set:
    ...
    void setIFShift(unsigned char level);
    void setTPBFInner(unsigned char level);
    void setTPBFOuter(unsigned char level);

Now that the signals and slots are defined, we can connect them together in wfmain. This is done, from wfmain to rigCommander, in a function inside wfmain called rigConnections(). The signal and slot connect syntax may seem tricky, but the slightest error will cause the signals to silently fail and will be quite difficult to find and fix. Thus, tread carefully. The sections are divided up, and here is where I added these functions:

    // Levels: Query:
    ...
    connect(this, SIGNAL(getIfShift()), rig, SLOT(getIFShift()));
    connect(this, SIGNAL(getTPBFInner()), rig, SLOT(getTPBFInner()));
    connect(this, SIGNAL(getTPBFOuter()), rig, SLOT(getTPBFOuter()));
    // Levels: Set:
    ...
    connect(this, SIGNAL(setIFShift(unsigned char)), rig, SLOT(setIFShift(unsigned char)));
    connect(this, SIGNAL(setTPBFInner(unsigned char)), rig, SLOT(setTPBFInner(unsigned char)));
    connect(this, SIGNAL(setTPBFOuter(unsigned char)), rig, SLOT(setTPBFOuter(unsigned char)));
    ...
    // Levels: handle return on query:
    ...
    connect(rig, SIGNAL(haveIFShift(unsigned char)), trxadj, SLOT(updateIFShift(unsigned char)));
    connect(rig, SIGNAL(haveTPBFInner(unsigned char)), trxadj, SLOT(updateTPBFInner(unsigned char)));
    connect(rig, SIGNAL(haveTPBFOuter(unsigned char)), trxadj, SLOT(updateTPBFOuter(unsigned char)));

At this point, the program is ready for adding UI elements for these functions. Once we add the UI elements, we can then add the massive amount of glue code to update their values from the radio on startup, send updates from the user to the radio, and also, to disable the elements for radios lacking these controls.

Let’s add three sliders to the transceiver adjustments window. The exact details are a bit beyond this document, and familiarity with QT Designer is probably worth gaining with some simpler test projects. It’s important to name the sliders useful and logical names within qt designer before editing the widget slots. I like to end the names of sliders with “Slider”. I’ll also set up the range of the slider to be 0 to 255 (just like the radio control). Once set up, right-click on the slider and select “Go to slot…” and then select the valueChanged(int) slot. What qt creator will do for you now is add a header entry for this slot and a prototype function for the slot. Mine look like this:

void transceiverAdjustments::on_IFShiftSlider_valueChanged(int value)
{
    
}

void transceiverAdjustments::on_TPBFInnerSlider_valueChanged(int value)
{
    
}

void transceiverAdjustments::on_TPBFOuterSlider_valueChanged(int value)
{
    
}

You’ll note that I am adding these widgets to a special pop-out window called “transceiver adjustments”, but these steps will be almost the same for adding to the main window.

Now we need to add commands to wfview’s queuing system. This queuing system was created out of necessity. The idea was to have a controlled queue of commands, optionally with parameters, which we can send to the radio at regular, controlled intervals. This keeps us from sending commands too quickly to the radio, and also lets us be careful and not send multiple slider update commands if a user scrolls rapidly on a slider — we just send the last value. The queue system uses a c++ standard double-ended queue, which meets our performance requirements (20ms polling is an eternity for a modern computer), and allows us a lot of flexibility. Each item in the queue has optional parameters, which need to be placed into a generic pointer using smart pointers managing ‘new’ items. The idea is that you can pretty much place anything you like into the queue as a parameter, and when the command is run, the parameter is pulled out, and the memory is released automatically. The parameter is simply set to NULL for parameterless use, such as “cmdGetTxPower”, where we’re just asking what the current power level is.

We’ll begin by adding the command names to the command enum list in wfmain.h. This list is part of the private definition for the wfmain class. Here’s what I ended up with:

    enum cmds {... cmdSetSql, cmdGetIFShift, cmdSetIFShift, cmdGetTPBFInner, cmdSetTPBFInner, 
              cmdGetTPBFOuter, cmdSetTPBFOuter, cmdGetATUStatus, 
              ...};

Now the functions that read this queue need to know what to do with these new commands.

For commands without parameters, modify void wfmain::doCmd(cmds cmd) :

   switch(cmd)
   {
    ...
        case cmdGetSql:
            emit getSql();
            break;
        case cmdGetIFShift:
            emit getIfShift();
            break;
        case cmdGetTPBFInner:
            emit getTPBFInner();
            break;
        case cmdGetTPBFOuter:
            emit getTPBFOuter();
            break;
        case cmdGetTxPower:
            emit getTxPower();
            break;

For the parameters, it gets a little more complicated. We’ll follow the examples in the code for setting the squelch, and modify void wfmain::doCmd(commandtype cmddata) :

        case cmdSetIFShift:
        {
            unsigned char IFShiftLevel = (*std::static_pointer_cast(data));
            emit setIFShift(IFShiftLevel);
            break;
        }
        case cmdSetTPBFInner:
        {
            unsigned char innterLevel = (*std::static_pointer_cast(data));
            emit setTPBFInner(innterLevel);
            break;
        }
        case cmdSetTPBFOuter:
        {
            unsigned char outerLevel = (*std::static_pointer_cast(data));
            emit setTPBFOuter(outerLevel);
            break;
        }

What’s going on there? The shared_ptr type within the commandtype struct holds a structure with a pointer to the memory we allocated when we placed the item into the queue. We are requesting from the shared pointer the data, in the form of an unsigned char. We then emit a command signal with the parameter. Once this function finishes, the memory gets released.

Since our parameter is just an unsigned char, we’ve already got functions set up to deal with adding commands with unsigned chars and we do not have to add another function for this datatype.

Now we can fill in those empty prototype functions for the sliders like this (mine are a little atypical because I am having to pass the signals and slots to another class that holds the transceiver adjustments window):

// These three are from the transceiver adjustment window: 
void wfmain::changeIFShift(unsigned char level)
{
    // from transceiverAdjustments::on_IFShiftSlider_valueChanged(int)
    issueCmd(cmdSetIFShift, level);
}
void wfmain::changeTPBFInner(unsigned char level)
{
    issueCmd(cmdSetTPBFInner, level);
}
void wfmain::changeTPBFOuter(unsigned char level)
{
    issueCmd(cmdSetTPBFOuter, level);
}

We’ve also got to do the opposite — provide a way for updated values to update the sliders. Note, make sure to update the sliders in a way that they won’t emit signals that they have been updated. All we need to do is to type out the slots that we defined earlier in wfmain.h into wfmain.cpp. wfmain.cpp also has a nice function to update sliders without the slider emitting a signal, so I’ll use that:

void wfmain::receiveIFShift(unsigned char level)
{
    changeSliderQuietly(ui->IFShiftSlider, level);
}

void wfmain::receiveTBPFInner(unsigned char level)
{
    changeSliderQuietly(ui->IFShiftSlider, level);    
}

void wfmain::receiveTBPFOuter(unsigned char level)
{
    changeSliderQuietly(ui->IFShiftSlider, level);    
}

If you’re following committed code in the repo, you’ll note that I did this slightly differently since I was working with a separate window for this adjustment.

Now we need to query the radio’s initial state for this control, and also to make sure we don’t query the radio unless it has this control. In wfview, the function void wfmain:: getInitialRigState() is used to grab initial state. The addition is simple:

void wfmain:: getInitialRigState()
{
    ...
    if(rigCaps.hasIFShift)
        issueDelayedCommand(cmdGetIFShift);
    if(rigCaps.hasTBPF)
    {
        issueDelayedCommand(cmdGetTPBFInner);
        issueDelayedCommand(cmdGetTPBFOuter);
    }

And that’s about it!