Artekit PropBoard: PropConfig Library

Read and write INI files with the PropConfig library.


Introduction

PropConfig is a library for the PropBoard that allows you to read/write INI files from/to the SD card.

INI files are well known in the Windows environment and for a while they were used for configuring everything. Due to their simplistic structure, INI files are easy to use, read and understand. Learn more about INI files in this [Wikipedia article] about them.

The PropConfig library is intensively used in our PBSaber project, and in this guide, we are going to learn how to use it to read and write INI files.

INI file structure

We’ll start by taking a look into the INI files the PropConfig library can read/write. An INI file is composed of four things: sections, keys, values and comments.

# This is a comment

[Section]
key = value

Comments are those lines starting with # or with // and are ignored.

Sections are defined between squared brackets, for example [section]. All the keys that follow a section definition belong to that section. A section ends where another section begins.

Keys are defined within sections and a value is associated to a key by the means of the equal (=) sign. A key must belong to a section, otherwise it will be ignored by the PropConfig library.

[Addresses]
Apple = 1 Infinite Loop
Google = 1600 Amphitheatre Parkway
Microsoft = One Microsoft Way

Keys must be unique for a given section, but can be reused in another section. For example:

[Addresses]
Apple = 1 Infinite Loop
Microsoft = One Microsoft Way

[Founders]
Apple = Steve Jobs
Microsoft = Bill Gates

Usage example

We are going to read some values from a ‘config.ini’ file. The file is as follows:

[settings]
inputs = 8
outputs = 2

Here is an example about opening ‘config.ini’ and reading the ‘inputs’ value. The example will also print to the serial console the value that has been read.

// Create a PropConfig object
PropConfig config;

void setup()
{
    // Initialization. Open the config.ini file.
    config.begin("config.ini");
    
    // Read the value for the 'inputs' key in the 'settings' section and
    // store it into the 'value' variable.
    uint32_t value;
    config.readValue("settings", "inputs", &value);
    
    // Print
    Serial.begin();
    Serial.print("Inputs = ");
    Serial.println(value);
}

Reading numbers

You can read all kind of values from INI files, from floating point numbers to strings. This is done with the readValue function as seen in the previous example.

The readValue function will read and convert the value of a key based on the type of variable passed to the function. For example, to read a floating point value like:

[section]
number = 1.5

You can do:

// Note the 'float' type
float value;
config.readValue("section", "number", &value);

// Now the 'value' variable will be 1.5

If you instead use an int to read a floating point value you can still read it, but the decimal part will be missing.

int32_t value

config.readValue("section", "number", &value);

// Now the 'value' variable will be 1

Reading booleans

Regarding booleans, you can use special, human-readable values in your configuration file and still read them as booleans. These values are:

  • true or false
  • enabled and disabled
  • yes or no
  • and finally 1 or 0

The concept will be much more clear by analyzing the following INI file:

[section]
feature1 = enabled
always_on = yes
exit_on_error = false

You can read those keys and store the values in a boolean variable in your program. The PropConfig library will automatically convert those values into true and false boolean values.

// Note that these three variables are of type 'bool'
bool feature1;
bool always_on;
bool exit_on_error;

config.readValue("section", "feature1", &feature1);
config.readValue("section", "always_on", &always_on);
config.readValue("section", "exit_on_error", &exit_on_error);

// The resulting value of the variables will be:
// 'feature1' variable is true.
// 'always_on' variable is true.
// 'exit_on_error' variable is false.

Reading strings

The readValue function can also read strings. It requires a fourth parameter that holds the size of the input buffer and, after reading, the quantity of bytes read from the file.

For an INI file like this:

[section]
my string = Hello World!

You can read the ‘my string’ key value using the following code:

// Declare a buffer
char myString[32];

// Set the buffer size
uint32_t myStringSize = sizeof(myString);

// Read
config.readValue("section", "my string", &myString, &myStringSize);

Reading arrays

You can read arrays of values using the readArray function. Arrays are values separated by commas, like this:

[section]
rgb_color = 255,127,33

The only condition is that the values of the array should be of the same type (i.e. you can’t mix integers with floating point numbers, booleans, etc. in the same array), and it’s not a string (arrays of strings are not supported).

To read the above array, the code could be:

// Declare an array
uint8_t colors[3];

// Set the array size
uint8_t colors_size = 3;

config.readArray("section", "rgb_color", colors, &color_size);

// Now the 'colors' array will contain the 'rgb_color' value from the file,
// that is:
// colors[0] = 255
// colors[1] = 127
// colors[2] = 33
// and the 'color_size' variable will contain the quantity of values read from
// the file.

Writing values

Values, and so sections, can be created and/or updated using the writeValue and writeArray functions. Like this:

// Write a string to the 'demo_string' key in the 'my_section' section.
config.writeValue("my_section", "demo_string", "Hello World!");

// Write the 155.25 float number
config.writeValue("my_section", "some_float", 155.25f);

// Write some numbers (note that you will need to put the values into variables
// or 'cast' them to the wanted variable type).
uint32_t my_number = 123456;
config.writeValue("my_section", "number1", my_number);

// Here we are casting the value -16
config.writeValue("my_section", "number2", (int8_t) -16);

// Write an array with 3 values
uint8_t my_color[3] = { 255, 127, 0 };
config.writeArray("my_section", "color", my_color, 3);

The above code will result in the following INI file:

[my_section]
demo_string = Hello World!
some_float = 155.25
number1 = 123456
number2 = -16
color = 255,127,0

Notes about writing

  • If the section you are writing to does not exist, then a new section is created along with the new key/value and it will be appended at the end of the file.

  • If the key you are writing does not exits but the section does, then the new key is added at the begining of the section.

  • Every time you write a value, a copy of the INI file is created (a backup) and the copy is updated with the new or modified section/key/value. Then the original INI file is deleted and it is replaced by the backup file.

Rules

Here follows a series of rules when dealing with the INI files:

  • A line can be maximum 255 bytes length.
  • Section names (the string between the brackets) must not be empty and should be less than or equal to 32 characters long.
  • Every key has to be inside a section, otherwise it is discarded.
  • A key/value pair must be contained within a single line (no multi-line values).
  • Every line has to be terminated with either or (i.e. make sure that the last line of the file is an empty line).
  • Section lines and key/value lines cannot contain comments. For example:

    # This is a correct comment
    [section]           # This is an incorrect comment
    key = value         # This is also an incorrect comment
    
  • Whitespaces: you can add or omit whitespaces before, between and after sections, keys and values. Whitespaces will be trimmed. For example all the following lines are valid:

    [     section      ]
         key1 = value
    key2=value
    key3 =        the white spaces that precede this string will be trimmed.
    
  • No case sensitive: sections and keys are not case sensitive.

  • The INI file must exist. The library doesn’t create new files.

The begin function

Call this function to open an INI file. Is the only function you have to call before any other operation.

Syntax

bool begin(const char* path, bool writable = true);

Parameters

  • path is the full path of the INI file. Note that the file extension (.ini) can be actually anything (for example .txt).
  • writable is an optional parameter. When true then the write functionality of the library is enabled. Otherwise the file will be opened as read-only. By default this parameter is true and can be ommited.

Return value

The function returns true if it was able to open the file. Otherwise it returns false.

The readValue function

Use this function to read the value for a given key contained in a given section.

Syntax

bool readValue(const char* section, const char* key, int8_t* value);
bool readValue(const char* section, const char* key, uint8_t* value);
bool readValue(const char* section, const char* key, int16_t* value);
bool readValue(const char* section, const char* key, uint16_t* value);
bool readValue(const char* section, const char* key, int32_t* value);
bool readValue(const char* section, const char* key, uint32_t* value);
bool readValue(const char* section, const char* key, char* value, uint32_t* len);
bool readValue(const char* section, const char* key, float* value);
bool readValue(const char* section, const char* key, bool* value);

Parameters

  • section is the section name to read from.
  • key is the key to read.
  • value is a pointer to a variable that will receive the read value.
  • len is used only when reading strings. When calling the function it must contain the length in bytes of the value parameter. On function exit, it will contain the quantity of bytes read from the file.

Return value

The function returns true if the value was succesfully read. It returns false if the section or key do not exist or if there was an error while reading.

The readArray function

Use this function to read an array of values for a given key contained in a given section.

Syntax

bool readArray(const char* section, const char* key, int8_t* values, uint8_t* count);
bool readArray(const char* section, const char* key, uint8_t* values, uint8_t* count);
bool readArray(const char* section, const char* key, int16_t* values, uint8_t* count);
bool readArray(const char* section, const char* key, uint16_t* values, uint8_t* count);
bool readArray(const char* section, const char* key, int32_t* values, uint8_t* count);
bool readArray(const char* section, const char* key, uint32_t* values, uint8_t* count);
bool readArray(const char* section, const char* key, float* values, uint8_t* count);

Parameters

  • section is the section name to read from.
  • key is the key to read.
  • value is a pointer to an array that will receive the read value.
  • count is the quantity of items in the value array. On function exit, it will contain the quantity of item that were read from the file.

Return value

The function returns true if the values were succesfully read. It returns false if the section or key do not exist or if there was an error while reading.

The writeValue function

Use this function to write a value into a given key contained in a given section.

Syntax

bool writeValue(const char* section, const char* key, int8_t value);
bool writeValue(const char* section, const char* key, uint8_t value);
bool writeValue(const char* section, const char* key, int16_t value);
bool writeValue(const char* section, const char* key, uint16_t value);
bool writeValue(const char* section, const char* key, int value);
bool writeValue(const char* section, const char* key, int32_t value);
bool writeValue(const char* section, const char* key, uint32_t value);
bool writeValue(const char* section, const char* key, const char* value);
bool writeValue(const char* section, const char* key, float value);
bool writeValue(const char* section, const char* key, bool value);

Parameters

  • section is the section name to write to.
  • key is the key to write.
  • value is the value to write.

Return value

The function returns true if the values were succesfully written. It returns false if there was an error while writing.

Note

Compilers may complain about not being able to find the right function to call, specially when writing numbers. You can overcome this by:

  • putting the value into a variable:

    uint32_t my_num = 5;
    config.writeValue("some section", "some key", my_num);
    
  • explicit type convertion (casting):

    config.writeValue("some section", "some key", (uint32_t) 5);
    

The writeArray function

Use this function to write an array of values into a given key contained in a given section.

Syntax

bool writeArray(const char* section, const char* key, int8_t* values, uint8_t count);
bool writeArray(const char* section, const char* key, uint8_t* values, uint8_t count);
bool writeArray(const char* section, const char* key, int16_t* values, uint8_t count);
bool writeArray(const char* section, const char* key, uint16_t* values, uint8_t count);
bool writeArray(const char* section, const char* key, int32_t* values, uint8_t count);
bool writeArray(const char* section, const char* key, uint32_t* values, uint8_t count);
bool writeArray(const char* section, const char* key, float* values, uint8_t count);

Parameters

  • section is the section name to write into.
  • key is the key to write.
  • value is a pointer to an array of values to write.
  • count is the quantity of items in the value array.

Return value

The function returns true if the values were succesfully written. It returns false if there was an error while writing.

Advanced topics

Section mapping

You can use the mapSections() function to scan the entire file while calling a callback function for every section found. This is useful to retrieve the position in the file of sections you are interested in and want to randomly read without re-scanning the entire file as the readValue function has to do while searching for a section that doesn’t know where is at.

PropConfig config;

uint32_t section_location;

void setup()
{
    // Open the file
    config.begin("my_config.ini");
    
    // Start mapping
    // The first parameter is the callback function (myCallback), the second
    // parameter is a pointer to a user-provided parameter (NULL in this case).
    config.mapSections(myCallback, NULL);
}

// This function is called for every section found
bool myCallback(uint32_t section_starts, uint32_t keys_starts, char* section, void* param)
{
    // 'section_starts' contains the position in the INI file where the section starts
    // 'keys_starts' contains the position in the INI file where the keys start
    // 'section' contains a string with the name of the section
    // 'param' points to the user-provided parameter
    
    // If the section we've just scanned is "section123", store its position
    if (strncasecmp("section123", key_name, key_len) == 0)
    {
        section_location = section_starts;
    }
    
    // Return true to continue with the mapping, or false to stop.
    return true;
}

The above code retrieves the location of “section123” and stores it into the section_starts variable. Now to quickly jump to that location and start reading keys you can do:

// Jump to specific position and read from there.
config.setFileRWPointer(section_starts);
config.readValue("X", "my_key", &value);

Special note: make sure you re-map the file after writing values to it. A previously retrieved position may be invalid if characters are inserted or removed from the file.

Tokenized read

You can optimize the reading of large sections by using tokenized reading. That is, you instruct the library to jump and lock into a given section and to start giving you tokens for every key found within the section. You can pass these tokens into special readValue and readArray functions to quickly retrieve the key value. Useful when you have to read ALL the keys in a large section in no specific order.

PropConfig config;

void setup()
{
    char key[255];
    uint32_t key_len;
    uint32_t token;
    uint32_t number_read;
    
    // Open the file
    config.begin("my_config.ini");
    
    // Find and lock into a section
    config.startSectionScan("that_section");
    
    // If you know the section position in the file, you can also use that, like:
    // config.startSectionScan(123456);
    
    // Get key tokens until section ends
    while (config.getNextKey(&token, &key, &key_len))
    {
        // 'token' contains a value you can pass to readValue and quickly retrieve the key value.
        // 'key' contains the found key name.
        // 'key_len' contains the length of 'key'
        
        // Now use the token we have to read the values for every key
        config_file.readValue(token, &number_read))
    }
    
    // This call is mandatory to close the tokenized read loop.
    config.endSectionScan();
}