Storing configuration options on the RFduino.

One of the common requirements for an IoT device is to be able to make configuration changes to the device without having to modify its source code.   Some of the changes we might make could be changing the device name, advertising data or interval, or perhaps some application specific startup options.  Any kind of change like this will require some kind of non-volatile storage to preserve the changes between power cycles.  On the RFduino we have the ability to store changes in an unused flash page (assuming our code doesn’t fill up the 128k we are allocated).

Lets get started. Here is the code. See the included comments for more information.

/*
Based on RFduino example FlashStructue.ino
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
*/
#include <RFduinoBLE.h>


//For flash access 
//Notice that the page is 251? User space is 124 to 251, inclusive. 
//This gives us our 128k for sketches. If you look at the RFduino 
//flash examples, you can see how to use the first unused page. 
//I like using the last page in user space, however, as I know it will not change.
//Loading the sketch will not overwrite our storage. 
#define FLASH_STORAGE 251 //Use the last page 
// double level of indirection required to get gcc
// to apply the stringizing operator correctly
#define  str(x)   xstr(x)
#define  xstr(x)  #x
//This is the data structure we will use. 
//Since the max transfer is <20 bytes over the radio, 
//the lazy way is to make all the arrays 20 bytes long. 
struct data_t
{
 char name[20];
 char advertisement[20];
 int interval;
 int update_delay;
 char config[20];
 char valid[6];

};

//A method to display the data structure. Force is used to 
//have the in_memory config display its contents. 
void dump_data(struct data_t *p, bool force = false)
{

 if (String(p->valid) == "valid" || force)
 {
        Serial.print("Name = ");
        Serial.println(p->name);
        Serial.print("Advertisement  = ");
        Serial.println(p->advertisement);
        Serial.print("Interval  = ");
        Serial.println(p->interval);
        Serial.print("Update Delay  = ");
        Serial.println(p->update_delay);
        Serial.print("Config  = ");
        Serial.println(p->config);
        if (force){
            Serial.print("Valid = "); 
            Serial.println(p->valid); 
        }
    }
    else {
        Serial.println("No valid data in flash");
    }

}




//This is a pointer to the data stored in flash. This is read only.
data_t *in_flash = (data_t*)ADDRESS_OF_PAGE(FLASH_STORAGE);
//This is our config struct. We will use this to store our config options in memory for normal operations. 
//When writing, the contents of this struct will overwrite the in_flash data. 
data_t config = { { "Probe" }, { "Probe_adv" }, 500, 1, { "Sample config data" }, { "" } }; //default data

void setup()   {
    Serial.begin(9600);

    //If we have config data in memory, use it instead of default.
    //This will be false initially, or when the flash page is erased. 
    if (String(in_flash->valid) == "valid")
    {
        //Since the struct in flash contains char arrays we need
        //to do a quick array copy to move the in_flash config to 
        //our config struct. 
        //This could be probably done better. 
        for (int i = 0; i < 20; i++)         {             
              config.name[i] = in_flash->name[i];
        }

        for (int i = 0; i < 20; i++)         {             
              config.advertisement[i] = in_flash->advertisement[i];
        }

        for (int i = 0; i < 20; i++)         {             
              config.config[i] = in_flash->config[i];
        }

        for (int i = 0; i < 6; i++)         {             
              config.valid[i] = in_flash->valid[i];
        }


        config.update_delay = in_flash->update_delay;
        config.interval = in_flash->interval;

    }


    //RFDuino Specific. 
    // this is the data we want to appear in the advertisement
    // (if the deviceName and advertisementData are too long to fix into the 31 byte
    // ble advertisement packet, then the advertisementData is truncated first down to
    // a single byte, then it will truncate the deviceName)
    RFduinoBLE.advertisementData = config.advertisement;
    RFduinoBLE.advertisementInterval = config.interval;
    RFduinoBLE.deviceName = config.name;

    // start the BLE stack
    RFduinoBLE.begin();

}

void loop() {
//Nothing going on here. 
}


//We will use this built in event to handle our received data.
//This is a way to accept commands, but not the only way. 
//This will let anyone that connects change options. 
void RFduinoBLE_onReceive(char *data, int len)
{
    //The first byte is cast to int for use in our switch
    int command = data[0];

    String string = "";
    //Get the string from the rest of the array, if there is one. 
    if (len > 1){
        for (int i = 1; i < len; i++){
            string += String(data[i]);
        }
        string.trim(); //Remove excess spaces. 
    }

    Serial.println("Received data over BLE");
    //We will print our command and string to the console
    Serial.println(command); 
    Serial.println(string);


    switch (command)
    {
        char buf[20]; // this is used when setting string variables.
    case 0:
        //Don't do any thing. 
        break;
    case 1:
        read();
        break;
    case 2:
        erase();
        break;
    case 3:
        write();
        break;
    case 10:
        erase(); 
        break;
    case 11: 
        //rename device -- save to flash 
        string.toCharArray(buf, string.length() + 1, 0);
        RFduinoBLE.deviceName = buf;
        for (int i = 0; i < string.length(); i++){
            config.name[i] = buf[i];
        }
        //Set the rest of the array to null 
        for (int j = string.length(); j < 20; j++){
             config.name[j] = 0;
        }
        write();
        break;
    case 9:
        //change config -- save to flash 
        string.toCharArray(buf, string.length() + 1, 0);
        for (int i = 0; i < string.length(); i++){
            config.config[i] = buf[i];
        }
        //Set the rest of the array to null 
        for (int j = string.length(); j < 20; j++){
            config.name[j] = 0;
        }
        write();
        //TODO -- apply config settings to running sketch
        break;
    case 32:
        //Used to clear the receive buffer. The buffer 
        //will contain the contents of the last received message
        //unless the current message overwrites it. This can add 
        //characters from the previous message to the current message 
        //and pretty much jack up what you are trying to accomplish. To 
        //prevent this I send 20 spaces after a message to clear the 
        //receive buffer and then trim them from the current message. 
        break;
    case 255:
        //Allows reset of the system.
        RFduino_systemReset(); 
        break; 

    default:
        Serial.println("Nothing to do"); 
    }

}


//This is where the work is done. 
void erase(){
    //erase  
    int rc;
    Serial.print("Attempting to erase flash page " str(FLASH_STORAGE) ": ");
    while (!RFduinoBLE.radioActive);
    while (RFduinoBLE.radioActive);
    RFduinoBLE.end();
    rc = flashPageErase(PAGE_FROM_ADDRESS(in_flash));
    if (rc == 0)
        Serial.println("Success");
    else if (rc == 1)
        Serial.println("Error - the flash page is reserved");
    else if (rc == 2)
        Serial.println("Error - the flash page is used by the sketch");
    RFduinoBLE.begin();
    in_flash = (data_t*)ADDRESS_OF_PAGE(FLASH_STORAGE);
    Serial.println("The data stored in flash page " str(FLASH_STORAGE) " contains: ");
    dump_data(in_flash);

}

void write(){
    //If we don't have valid in the config object - add it here. 
    //This lets us determine if the in_memory struct has valid 
    //data in it or not. 
    if (String(config.valid) != "valid"){
        char valid[6] = { "valid" };
        for (int i = 0; i < 6; i++){
            config.valid[i] = valid[i];
        }
    }

    int rc;
    Serial.print("Attempting to write data to flash page " str(FLASH_STORAGE) ": ");
    while (!RFduinoBLE.radioActive);
    while (RFduinoBLE.radioActive);
    RFduinoBLE.end();
    flashPageErase(PAGE_FROM_ADDRESS(in_flash));
    rc = flashWriteBlock(in_flash, &config, sizeof(config));
    if (rc == 0)
        Serial.println("Success");
    else if (rc == 1)
        Serial.println("Error - the flash page is reserved");
    else if (rc == 2)
        Serial.println("Error - the flash page is used by the sketch");
    RFduinoBLE.begin();
    in_flash = (data_t*)ADDRESS_OF_PAGE(FLASH_STORAGE);
    Serial.println("The data stored in flash page " str(FLASH_STORAGE) " contains: ");
    dump_data(in_flash);

}

void read(){   
    Serial.println("Data in flash");
    dump_data(in_flash);
    Serial.println("Data in memory");
    dump_data(&config, true);
}

As you can see, most of the work is done in the last three functions — read(), write() and erase(). The write function erases before it writes, but an erase function is needed in case we mess up our saved data and need to clean house.

To test this we can use a program similar to Punch Through’s Light Blue (for IOS and Mac ) or any BLE app of your choosing. We simply flash the code to our RFduino the normal way (using the Arduino IDE) and open up the serial monitor.

LightBlue
Here is Punch Through LightBlue connected to our RFduino.

Probe, in this case, is the RFduino running our sketch. The write characteristic is 2220 for our device. Be sure to send hex, it makes it easier to get the correct command. As noted in the code above, messages received are character codes that are cast to ints. So of we use ASCII 1 when trying to get command 1 – read flash – we end up sending character code 49. Sending 0x01 in hex will get us 1.

Sending 0x01 hex will trigger the read command.
Sending 0x01 hex will trigger the read command.

My experience here is that when we try to erase or write the flash with the radio on the RFDuino will reboot — likely due to interrupts from the radio not being handled while erase and write operations are under way.  So what we need to do is turn off the radio before the write or erase and turn it back on after. This causes our application to lose the connection to the device causing us to have to reconnect. This can be handled silently in any app we create, but using LightBlue we will need to reconnect. Write (command 3) will write the contents of the config array to flash.

The result of a write command. The contents of the config array is written to flash.
The result of a write command. The contents of the config array is written to flash.

If we wanted to change the device name, for example, we would use command 11 (0x0B) and provide the name  in a character codes. If we wanted to change the name to “new” it would look like this  — 0x0B4E6577.  This also causes us to reconnect.

We can change the device name by send the new name in character codes
We can change the device name by send the new name in character codes

Other options can be changed in a similar fashion.

The only gotcha to look out for is ensuring strings sent to the device are long enough to clear the receive buffer. For example: If we change the name of the device by sending the string “NewDevice” (in character codes) and then send a string to change the advertising name of “Hello”, the resulting string will be “Hellovice” .  This is because the receive buffer does not flush between messages. We can get around this by sending a string of 20 0x32 (spaces) after a string to flush text out of the buffer. Or we can add an end of line character and check for it upon receipt.

So there you have it. This is a fairly simple way to allow over the air configuration changes to persist on your RFduino device. The code supplied here can certainly be refactored and enhanced to allow more functionality. Some things I plan on adding are password access to config changes and end of line checking.

Please feel free to contact me if you have questions.

One comment

  1. m2ag says:

    Hi,

    I can’t really comment on the thinking of the author of that snippet. Looks like the author is doing the same thing as I do using the !active and then active while loops. I imagine this works just as well, but I think using RFduinoBLE.radioActive will be more accurate in determining the max amount of time the radio will be inactive. In our case we shut the radio down, but other applications might use this time to try to sample some sensor data while the radio is inactive.

Leave a Reply