Skip to content

Format for Commands from the Tympan Remote App

hgeithner-creare edited this page Sep 17, 2021 · 21 revisions

The Tympan Remote App has buttons. You define those buttons. As a result, you also get to define what "command" each button sends back to the Tympan. A "command" is simply one or more characters. You have total control over what that command looks like.

Since this much freedom can be paralyzing, this page discusses how we have chosen to define our commands. It will help you understand the example programs that we wrote. It is a good place for you to start, if you need to add your own commands.

Single-Character Commands

The simplest choice is to have a "command" be just a single letter. See the example, 06-AppTutorial/BasicGain_wApp. In this example, one button sends a lower-case 'k' while another button sends an upper-case 'K'. When the Tympan receives a 'k', you can see that the Tympan code changes the overall gain by 3.0 dB. When the Tympan receives a 'K', you can see that the TYmpan code changes the overall gain by -3.0 dB. Easy!

A more detailed breakdown of this example is described in the "Following the Commands Through the System" section down below.

Four-Character Commands

For complex programs, it can be very cumbersome to write your own set of buttons to control all of the different functions. A single WDRC compressor, for example, has about 10 settings that you might wish to adjust. And, you might want an 8-band system such that you have 8 of these WDRC compressors. Will you really want to write an App GUI that has 10x8 = 80 different sets of buttons?!? Instead, you might choose to invoke the pre-written App GUI that is included with the WDRC audio class. An example that uses the pre-written App GUI for a single WDRC compressor is 06-AppTutorial/TrebleBoost_wComp_wApp.

Knowing that a more complex program could have many compressors, we know that there are not enough single letters and single numbers to fit within the number of single-character commands that are available. So, the Tympan Library has a second command convention. In this convention, each App button is told that its "command" is actually four characters. An example of a four-character command that might be intended for a WDRC compressor is '_2xa'.

Let's interpret this four-character command:

  • First Character: The first character is always an underscore '_'. The underscore is our convention is a four-character command and not simply a bunch of single-character commands strung together. If you are intending this to be a four-character command, start with the underscore!

  • Second Character: In this case, the second character is a '2'. In our convention, the second character is an ID character associated with the specific audio processing class. In this case, the Tympan doesn't know that the overall message is intended for a WRDC compressor. But, because of this character, it knows that the message is intended for an instance of an audio class whose ID is "2". Because of our behind-the-scenes code, the ID characters are automatically created for each instance of each audio class. So, each instance gets its own unique ID character. This instance-specific character is the key to ensuring that the command goes to the right place to be interpreted (such as the WDRC compressor code) and not to some other place (such as by the filterbank) that might misinterpret an otherwise-similar command string.

  • Third Character: This can be anything you want. In fact, this character is often ignored. In this case, the character is 'x', which is a generic placeholder generally indicating that this character is to be ignored. In other cases, such as for the filterbank or compressor bank, this character is used to specify the channel that we'd like to change. So, if this command were being sent to a filterbank, and the third character had been a '0', the command would be applied to the first filter. If the character had been a '7', the command would be applied to the 8th filter.

  • Fourth Character: This is typically the main character representing the command that you want to be executed. In the case of the single-channel WDRC Compressor, scroll down to processCharacterTriple() and we see that receiving an 'a' will increase the compressor's attack time. Alternatively, if this character had been an 'r', it would increase the release time.

If you are not using any of the built-in, pre-written App GUIs, you are unlikely to ever run across the four-character commands.

Following the Commands Through the System

Want more detail of how the system actually generates, receives, and interprets the commands? Want to see the actual lines of code that matter? This section is for you!

As described earlier, you get to decide what characters are used as commands. This happens when you define the buttons that you want to appear in the App. When you create the button, you specify what character(s) the button will send to back to the Tympan when the button is pressed. As an example, open 06-AppTutorial/BasicGain_wApp and scroll down to function createTympanRemoteLayout(). The buttons for the App are defined in this section of code:

//Add a "-" digital gain button with the Label("-"); Command("K"); Internal ID ("minusButton"); and width (4)
card_h->addButton("-","K","minusButton",4);  //displayed string, command, button ID, button width (out of 12)

//Add an indicator that's a button with no command:  Label (value of the digital gain); Command (""); Internal ID ("gain indicator"); width (4).
card_h->addButton("","","gainIndicator",4);  //displayed string (blank for now), command (blank), button ID, button width (out of 12)

//Add a "+" digital gain button with the Label("+"); Command("K"); Internal ID ("minusButton"); and width (4)
card_h->addButton("+","k","plusButton",4);   //displayed string, command, button ID, button width (out of 12)

Note that it creates three buttons. One button is created with the command simply being a lower-case 'k'. Another button is created with command simply being an upper-case 'K'.

When the human user taps on that first button, the phone sends the letter 'k' to the Tympan over the Bluetooth connection. The Tympan's Bluetooth module receives the message and tells the Tympan's central processor that it received a message. In the Tympan code, look in the loop() function for the line that has ble.available().

if (ble.available() > 0) {
  String msgFromBle; int msgLen = ble.recvBLE(&msgFromBle);
  for (int i=0; i < msgLen; i++) respondToByte(msgFromBle[i]);
}

This is the part of your code that receives the messages from the Bluetooth module (ble). The message might have more than one character, so the code above loops over each character in the received message. In this simple example, there will be just one character: 'k'. To see what the system will do with the 'k', scroll down to look at the respondToByte() function.

//respond to serial commands
void respondToByte(char c) {
  Serial.print("Received character "); Serial.println(c);
  
  switch (c) {
    case 'J': case 'j':           //The TympanRemote app sends a 'J' to the Tympan when it connects
      printTympanRemoteLayout();  //in resonse, the Tympan sends the definition of the GUI that we'd like
      break;
    case 'k':
      changeGain(3.0);
      printGainLevels();
      setButtonText("gainIndicator", String(digital_gain_dB));
      break;
    case 'K':
      changeGain(-3.0);
      printGainLevels();
      setButtonText("gainIndicator", String(digital_gain_dB));
      break;
  }
}

Looking at this code, you see a switch yard. This is where we define the Tympan's response to whatever character might get received. In this case, you can see that a lower-case 'k' results in the Tympan changing its gain by 3.0 dB. Similarly, you see that receiving an upper-case 'K' results in changing the gain by -3.0 dB.

For a more complex example, look at the example: 06-AppTutorial/TrebleBoost_wApp. It adds more buttons and more single-character commands. A lower-case 't' will raise the corner frequency of the treble boost. An upper-case 'T' will lower the corner frequency of the treble boost. Similarly, 'c' will enable reporting of the CPU usage and a 'C' will stop the reporting.

Finally, you can imagine that a complex program might have many, many commands that you need to manage. You might be creating many buttons and then you might have a large switch yard to handle all of those different commands coming back from the App. In this case, all of that code that you write will take up a lot of space. To better encapsulate all that code and to make it easier to read, see the next App Tutorial example: 06-AppTutorial/TrebleBoost_wApp_Alt. This example has identical functionality to the previous example, TrebleBoost_wApp, but moves the buttons and command handling over to its own file and its own class called "SerialManager". This is an abstraction and an encapsulation that is very common in the Tympan Library's examples.

Following Four-Character Commands

If you are using four-character commands, it means that your program has gotten more complex...probably too complex to continue to use only single-character commands. Being complex, you will likely want to keep all of the code for handling of your commands in place, separate from the rest of your Tympan code. In other words, you will want to use an approach that is like the SerialManager that is used in the 06-AppTutorial/TrebleBoost_wApp_Alt example introduced in the paragraph above. This example still uses single-character commands, but it introduces SerialManager as a critical middle-man.

As a four-character example, see the AppTutorial example TrebleBoost_wComp_wApp. It is like the TrebleBoost example but adds a WDRC compressor. The WDRCS compressor has so many parameters to adjust that writing and handling enough buttons to adjust and display all of those parameters is cumbersome and prone to error. So, this example invokes the pre-written buttons and command-handling that is built into the WDRC class.

First, in the main *.ino file, near the top, you can see where we instantiate the audio-process classes. Note where we create the WDRC compressor to be used for the left ear and for the right.

AudioEffectCompWDRC_F32_UI  comp1(audio_settings);      //Compresses the dynamic range of the audio.  Left.  UI enabled!!!
AudioEffectCompWDRC_F32_UI  comp1(audio_settings);      //Compresses the dynamic range of the audio.  Right.  UI enabled!!!

Note that the name for the WDRC audio class has "_UI" at the end. This is a clue that this class has some sort of pre-write user interface (ie, App buttons and command handling) built-in. This is the first clue that four-character commands will be involved.

Next, scrolling down, find the setupSerialManager() function. This is where we register the two WDRC instances with the SerialManager. This is a bit of pre-work that tells the SerialManager that, if commands are received, the SerialManager should check with these registered classes to see if they know how to interpret the command. We could write our own code in the SerialManager to interpret the commands, but because the compressors already contain this logic, let's use the built-in logic!

void setupSerialManager(void) {
  //register all the UI elements here
  serialManager.add_UI_element(&myState); //myState itself has some UI stuff we can use (like the CPU reporting!)
  serialManager.add_UI_element(&comp1);   //The left compressor
  serialManager.add_UI_element(&comp2);   //The right compressor
}

We're still laying the ground work for handling the four-character commands. The next step is to create buttons in the App that tell the button to send four-character commands. Because we're using the built-in UI that has been pre-written for the WDRC, this all happens behind-the-scenes. With any of the audio classes with a pre-written UI, this will happen behind-the-scenes. That's the whole point! But how does it start?

Still in this example program, but jumping over to the file SerialManage.h, you can scroll down to where the App's pages and buttons are created, which is in creareTympanRemoteLayout(). In that method, you see were it continues to manually create a few buttons that use single-character commands (note the 't' and 'T' used to control the treble boost corner frequency). Yes, single-character commands and four-character commands can be intermingled!

card_h->addButton("-","T","",        4);  //displayed string, command, button ID, button width (out of 12)
card_h->addButton("", "", "cutoffHz",4);  //displayed string (blank for now), command (blank), button ID, button width (out of 12)
card_h->addButton("+","t","",        4);  //displayed string, command, button ID, button width (out of 12)

Scrolling down just a few more lines, you see where we invoke the pre-written GUI elements for the two WDRC compressors.

//Add a page for the left compressor's built-in GUI
page_h = comp1.addPage_default(&myGUI);
page_h->setName("Left Compressor");  //I'm overwriting the default name so that we know that this is the left WDRC compressor

//Add a page for the right compressor's built-in GUI
page_h = comp2.addPage_default(&myGUI);
page_h->setName("Right Compressor");  //I'm overwriting the default name so that we know that this is the right WDRC compressor

That's it. That's all that we had to write to create two pages in the App, each fully populated with all the buttons needed to adjust all 10 parameters of the WDRC compressor. Nice!

If you were to actually dig into the WDRC compressor code in the Tympan Library (AudioEffectCompWDRC_F32.h and AudioEffectCompWDRC_F32.cpp you could trace addPage_default() to addPage_allParams() to the addition of individual button groups, such as addCard_attack(), to individual buttons via addCardPreset_UpDown() (which is a general-purpose function in SerialManager_UI.h or SerialManager_UI.cpp) and finally to addButtons_presetUpDown(). This is where the three buttons are made to control the attack time:

void addButtons_presetUpDown(TR_Card *card_h, const String field_name, const String down_cmd, const String up_cmd) {
  if (card_h == NULL) return;

  String prefix = getPrefix();    //3 character code.  getPrefix() is here in SerialManager_UI.h, unless it is over-ridden in the child class somewhere
  String field_name1 = ID_char + field_name;  //prepend the field name with the unique ID_char to that it is only associated with a particular instance of the class

  card_h->addButton("-",   prefix+down_cmd, "",          4); //label, command, id, width...this is the minus button
  card_h->addButton("",    "",              field_name1, 4); //label, command, id, width...this text to display the value 
  card_h->addButton("+",   prefix+up_cmd,   "",          4); //label, command, id, width...this is the plus button
}

Note that the actual command for the down button is a prefix plus the down command character. The prefix is defined by getPrefix, which is in SerialManager_UI.h or SerialManager_UI.cpp and it creates the first three characters of the four-character command via the convention defined earlier: and underscore, plus an instance ID character, plus a channel character (which is a placeholder 'x' in this case, because there is no channel to worry about). The last charcter, the down command or the up command, is a single letter. For the attack time, an upper-case 'A' is used as the down command and a lower-case 'a' is used as up command. So, the full, four-character command is defined. Phew!

Once the button exists in the App, and once the user presses the button, the App sends out the four-character command back to the Tympan. The command first arrives in the main sketch file. Scroll down to the loop() function and note these lines, which are the same as in the single-character examples described earlier.

//respond to BLE
if (ble.available() > 0) {
  String msgFromBle; int msgLen = ble.recvBLE(&msgFromBle);
  for (int i=0; i < msgLen; i++) serialManager.respondToByte(msgFromBle[i]);
}

As before, this receives the message from the Bluetooth module. While the command does have four characters, the message is sent as individual characters to be interpreted by the serialManagervia its respondToByte() method.

Here's the tricky bit: respondToByte() is no longer in the SerialManager.h that lives there with your program. Instead, a careful eye would have noted that SerialManager inherits from the class SerialManagerBase. As a result, every SerialManager gets to inherit some really common and really helpful functionality. respondToByte() is built into SerialManagerBase because everyone needs to use it, so why recreate the wheel every time!

//now, define the Serial Manager class
class SerialManager : public SerialManagerBase  {  // see Tympan_Library for SerialManagerBase for more functions!

In SerialManagerBase, the respondToByte() method, figures out whether it is a single-character command, part of a four-character command ("QUAD"). If a series of characters starts with the underscore, the respondToByte() method waits for three more characters and then fires off the message to interpretQuadChar(), which is still in SerialManagerBase.

interpretQuadChar(mode_char, chan_char, data_char);

In interpretQuadChar(), it loops over all the classes that registered with the SerialManager (we did that earlier!) and sends them the three characters to see if the message is intended for them and to see if they can interpret it. It does this by invoking each class's processCharacterTriple().

// Loop over each registered UI element and try its processCharacterTriple() method.
// Their processCharacterTriple() will return TRUE if the character was recognized.
// If the TRUE is recevied, processing stops.  If a FALSE is received, this routine
// continues on to the next registered UI element.
for (int i=0; i < next_UI_element_ind; i++) {
  if (UI_element_ptr[i]) { //make sure it isn't NULL
    ret_val = UI_element_ptr[i]->processCharacterTriple(mode_char, chan_char, data_char);
    if (ret_val) return ret_val;
 }
}

Now, we go to the WDRC class, because that is what we registered with the SerialManager way up at the beginning. In AudioEffectCompWDRC_F32.cpp, we find the processCharacterTriple(). The first thing we see is that it checks the ID character to see if it matches the unique ID character attached to this particular instance of the WDRC class.

//check the mode_char to see if it corresponds with this instance of this class.  If not, return with no action.
if (mode_char != ID_char) return false;  //ID_char is from SerialManager_UI.

Assuming that the ID character does match, the rest of processCharacterTriple() ignores the channel character and then interprets the final character in the four-character command. it does this through big switch yard, just like in the single-character command examples! Here are the two switch cases for the attack time:

switch (data_char) {    
  case 'a':
    incrementAttack(time_incr_fac);
    Serial.println(F("AudioEffectCompWDRC_F32_UI: changed attack to ") + String(getAttack_msec(),0) + " msec"); 
    updateCard_attack();  //send updated value to the GUI
    break;
  case 'A':
    incrementAttack(1.0f/time_incr_fac);
    Serial.println(F("AudioEffectCompWDRC_F32_UI: changed attack to ") + String(getAttack_msec(),0) + " msec"); 
    updateCard_attack();  //send updated value to the GUI
    break;

As you can see, it increments the attack time up or down, as expected. The interpretation of the four-character command is complete. Good work.