ESP32 inja: Custom functions

Introduction

In this tutorial we are going to learn how to define custom functions to be used in inja templates. We will run the code on the ESP32, using the Arduino core.

In the previous tutorial we saw how to use some of inja’s built-in functions, which already offer basic functionality. Nonetheless, it is very likely that some other data manipulation functions could be useful when defining templates. Fortunately, inja provides a way of defining our own custom functions.

Note that although I’ll be referring to these as “Custom functions”, inja documentation calls them callbacks, as can be seen in the corresponding section.

If you haven’t yet installed inja, please check the tutorial that covers the procedure in detail. Also, please take in consideration that inja depends on the Nlohmann/json library. The installation of this lib is detailed here.

On this tutorial, the inja library version used was 3.3.0 and the Nlohmann/json library version was 3.10.2. The Arduino core version used was 2.0.0 and the Arduino IDE version was 1.8.15, operating on Windows 8.1.

The tests shown below were performed on a ESP32-E FireBeetle board from DFRobot.

A simple example

For illustration purposes, on this section we will create a custom function that receives two strings, concats them and returns the result. We will call it “concat“.

We will start our code by the library includes. We will need the json.hpp library, so we have access to the json class that serves as data source to the templates, and the inja.hpp, which exposes to us the template rendering features.

#include <json.hpp>
#include <inja.hpp>

Moving on to the Arduino setup, where we will write the rest of our code, we will start by opening a serial connection. This will later be used to print the results of rendering the template.

Serial.begin(115200);

To be able to configure custom functions we will need to use an Environment. Basically, this is a class that allows us to set some configurations for the template engine, being its usage recommend for advanced cases [1].

Note that, in the previous tutorials, we have already used an Environment without noticing it. If we look into the implementation of the render function, we can see the usage of an Environment, which will use the default rendering engine settings.

As such, we will first create an Environment object.

inja::Environment env;

Now that we have our env, we can add a new function with a call to the add_callback method. This method receives the following arguments:

  • Name of the function, as a string. This is the name we will later need to use in our template. Like mentioned before, the function will concatenate two strings, so we will call it “concat“.
  • Number of arguments that the function will receive, as an integer. We will pass the value 2.
  • The function. We will use the C++ lambda syntax to define this function.

Note that this function will receive as input a reference to an Arguments object, which is basically a vector of json pointers [1]. We will check its usage later when we analyze the function implementation. For now we will check the call to the add_callback method.

env.add_callback("concat", 2, [](inja::Arguments& args) {
  
      // Custom function implementation
});

In the implementation of our function, we will first extract the arguments from the vector, as std::string. Since we have two arguments, our vector has two positions. We want the elements at position 0 and 1 and, for that, we will use the at method.

Then, since the elements of the array are pointers to json objects, we can use the get method to obtain the values as strings. As template parameter, we use the std::string data type.

When defining the variables that will hold the string, I’ll be using the auto keyword for the compiler to infer the data type without having to write again std::string. If you don’t like this automatic inference, you can simply use std::string instead of auto.

auto str1 = args.at(0)->get<std::string>(); 
auto str2 = args.at(1)->get<std::string>(); 

Finally, we will return the concatenation of both strings. Note that since we are working with the std::string type, we can use the + operator for concatenation.

return str1 + str2;

The full function registration is summarized below.

env.add_callback("concat", 2, [](inja::Arguments& args) {
    auto str1 = args.at(0)->get<std::string>(); 
    auto str2 = args.at(1)->get<std::string>(); 
    
    return str1 + str2;
});

Now that we have our function, we will define the json object that will hold the two strings to be concatenated. We will call str1 and str2 to the two properties.

nlohmann::json data;
data["str1"] = "some";
data["str2"] = "thing";

Then we will define our template. For simplicity, it will be just a placeholder where we will call our concat function, passing as first input the property called str1 and as second the property called str2. After rendering the template, we expect that the concatenation of the two strings will replace this placeholder.

std::string tmplt = R"({{concat(str1, str2)}})";

To finalize, we are going to render our template. Note that since we added the function to our env object, we will also need to call the render method on it. The parameters and the returning value is the same like we have been covering in the previous tutorials: it receives as first input the template string and as second the json object, and it returns a string with the rendered template.

std::string result = env.render(tmplt, data);

Finally, we will print the rendered template to the serial port. Since the println method doesn’t support receiving as input a std:string, we need to call the c_str method first.

Serial.println(result.c_str());

The complete code is shown below. The main loop was left empty as we don’t do anything there.

#include <json.hpp>
#include <inja.hpp>

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

  inja::Environment env;

  env.add_callback("concat", 2, [](inja::Arguments& args) {
    auto str1 = args.at(0)->get<std::string>(); 
    auto str2 = args.at(1)->get<std::string>(); 
    
    return str1 + str2;
  });

  nlohmann::json data;
  data["str1"] = "some";
  data["str2"] = "thing";

  std::string tmplt = R"({{concat(str1, str2)}})";
 
  std::string result = env.render(tmplt, data);
  Serial.println(result.c_str());
}

void loop() {}

Now that we have our code complete, we simply need to compile it and upload it to the ESP32. Once the procedure finishes, open the Arduino IDE serial monitor.

As output, you should get a result like the one illustrated in figure 1 below. As can be seen, we have obtained the concatenation of both original strings, meaning our custom function worked as expected.

Result of using a custom function in a inja template.
Figure 1 – Result of using a custom function in a inja template.

Parameterless function

In this section we are going to check how to add a parameterless function to our env, so we can use it in our templates. In our case, we are going to create a function that returns a random number between 0 and 9. For a tutorial on how to generate random numbers using the ESP32, please check here. We will call our function “random“.

For this section we are going to keep our template very simple, only printing the random number. As such, we won’t have any data source, meaning we don’t need the json.hpp include. As such, we will only include the inja.hpp lib.

#include <inja.hpp>

To check that the template outputs different numbers every time we render it, we will do it periodically in the Arduino main loop. Nonetheless, we will keep the initialization of the Environment on the Arduino setup. As such, we will define a global Environment object, so it is accessible to both the setup and the loop.

inja::Environment env;

We are also going to define our template as a global string. Note that, when using inja parameterless functions, we don’t need to use the empty parentheses. Instead, we can call that function like it was a variable.

std::string tmplt = R"(Random Nr: {{random}})";

Moving on to the Arduino setup, we will start by opening a serial connection.

Serial.begin(115200);

Then we will define our “random” function with a call to the add_callback method on our env object. Like before, we pass as first input the name of the function. As second we pass the number of arguments which, in this case, is zero.

Finally we can define the function as a lambda. The implementation will be quite simple as we will simply call the random function, passing as input the value 10 (the upper bound is exclusive, meaning it will generate a random number between 0 and 9).

env.add_callback("random", 0, [](inja::Arguments& args) {    
    return random(10);
}); 

The complete setup can be seen below.

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

  env.add_callback("random", 0, [](inja::Arguments& args) {    
    return random(10);
  }); 
}

Moving o to the Arduino main loop, we will start by rendering our template. Since we don’t have source data, as second parameter of the render method we can simply pass NULL.

std::string result = env.render(tmplt, NULL);

Finally, we will print the result to the console and add a 5 seconds delay between each iteration of the loop. The complete loop, already containing this part of the code, can be seen below.

void loop() {
  
  std::string result = env.render(tmplt, NULL);
  Serial.println(result.c_str());

  delay(5000);
}

The whole code is shown below.

#include <inja.hpp>

inja::Environment env;
std::string tmplt = R"(Random Nr: {{random}})";

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

  env.add_callback("random", 0, [](inja::Arguments& args) {    
    return random(10);
  }); 
}

void loop() {
  
  std::string result = env.render(tmplt, NULL);
  Serial.println(result.c_str());

  delay(5000);
}

Upon running the code, you should get a result similar to figure 2.

Testing a parameterless function in a inja template.
Figure 2 – Testing a parameterless function in a inja template.

Suggested ESP32 readings

References

[1] https://github.com/pantor/inja

Leave a Reply

Discover more from techtutorialsx

Subscribe now to keep reading and get access to the full archive.

Continue reading