Program a Connected Device Using Raft, Part 3

Following on from the first and second parts of this post, we are now going to add some functionality to the basic app that we created. Let’s add support for the LED ring and an API to allow animated patterns to be displayed. The source code is available on Github – see the Releases for the code at each step if you want to follow along.

Adding a SysMod for the LED Ring

When adding functionality to a Raft app it is a good idea to leverage the modular system (SysMods) which Raft provides. We’ll add a SysMod called LEDRingSysMod. The first step is to create a new sub-folder in the components folder called LEDRingSysMod. In here we’re going to create files which are very similar in structure to the ones already inthe components/AppSysMod folder. There will be three files:

  • LEDRingSysMod.cpp
  • LEDRingSysMod.h
  • CMakeLists.txt

The initial contents of the .cpp file is as follows:

////////////////////////////////////////////////////////////////////////////////
//
// LEDRingSysMod.cpp
//
////////////////////////////////////////////////////////////////////////////////

#include "LEDRingSysMod.h"
#include "RaftUtils.h"

static const char *MODULE_PREFIX = "LEDRingSysMod";

LEDRingSysMod::LEDRingSysMod(const char *pModuleName, RaftJsonIF& sysConfig)
    : RaftSysMod(pModuleName, sysConfig)
{
}

LEDRingSysMod::~LEDRingSysMod()
{
}

void LEDRingSysMod::setup()
{
}

void LEDRingSysMod::loop()
{
}

The last two functions in here might look familiar if you have programmed using the Arduino framework before. They behave very much like the setup() and loop() functions in an Arduino application.

////////////////////////////////////////////////////////////////////////////////
//
// LEDRingSysMod.h
//
////////////////////////////////////////////////////////////////////////////////

#pragma once
#include "RaftArduino.h"
#include "RaftSysMod.h"

class LEDRingSysMod : public RaftSysMod
{
public:
    LEDRingSysMod(const char *pModuleName, RaftJsonIF& sysConfig);
    virtual ~LEDRingSysMod();

    // Create function (for use by SysManager factory)
    static RaftSysMod* create(const char* pModuleName, RaftJsonIF& sysConfig)
    {
        return new LEDRingSysMod(pModuleName, sysConfig);
    }
protected:
    // Setup
    virtual void setup() override final;
    // Loop (called frequently)
    virtual void loop() override final;
private:
};
idf_component_register(
    SRCS 
        "LEDRingSysMod.cpp"
    INCLUDE_DIRS
        "."
    REQUIRES
        RaftCore
)

The .h and CMakeLists.txt files simply need to follow the naming in the .cpp file – so if you make changes to the name of the file and class referred to in the .cpp file then reflect those changes in the other files.

Registering the new SysMod

To add the SysMod to the Raft app we need to register it in the main.cpp file. Three lines, all of which have LEDRing in them, have been added to the main.cpp file.

////////////////////////////////////////////////////////////////////////////
//
// Main entry point
//
////////////////////////////////////////////////////////////////////////////

#include "RaftCoreApp.h"
#include "RegisterSysMods.h"
#include "RegisterWebServer.h"
#include "LEDRingSysMod.h"
#include "AppSysMod.h"

// Entry point
extern "C" void app_main(void)
{
    RaftCoreApp raftCoreApp;
    
    // Register SysMods from RaftSysMods library
    RegisterSysMods::registerSysMods(raftCoreApp.getSysManager());

    // Register WebServer from RaftWebServer library
    RegisterSysMods::registerWebServer(raftCoreApp.getSysManager());

    // Register the LEDRing system module
    raftCoreApp.registerSysMod("LEDRingSysMod", LEDRingSysMod::create, true);

    // Register app
    raftCoreApp.registerSysMod("AppSysMod", AppSysMod::create, true);

    // Loop forever
    while (1)
    {
        // Yield for 1 tick
        vTaskDelay(1);

        // Loop the app
        raftCoreApp.loop();
    }
}

Note that the name in inverted commas (“LEDRingSysMod”) is the name that is used when configuring the SysMod. My advice is to make this the same as the name of the class that implements the SysMod. The only reason not to do this is when you want to make multiple instances of the same SysMod and then it is helpful to be able to register multiple SysMods of the same class but with different names.

At this point you can rebuild the app to check all is fine – the functionality has not changed yet.

Adding the LED ring functionality

Fortunately the Raft framework has support for LED pixels like the WS2812. This is provided by a class called LEDPixels. We will create an instance of this class as a member of the LEDRingSysMod and then initialize this object with settings from the SysTypes JSON file.

We’ll take a look at the .h file first:

////////////////////////////////////////////////////////////////////////////////
//
// LEDRingSysMod.h
//
////////////////////////////////////////////////////////////////////////////////

#pragma once

#include "RaftArduino.h"
#include "RaftSysMod.h"
#include "LEDPixels.h"

class APISourceInfo;

class LEDRingSysMod : public RaftSysMod
{
    // public section is unchanged ...

protected:

    // setup() and loop() are unchanged ...

    // Add endpoints
    virtual void addRestAPIEndpoints(RestAPIEndpointManager& pEndpoints) override final;

private:

    // LED pixels
    LEDPixels _ledPixels;
    
    // API
    RaftRetCode apiControl(const String &reqStr, String &respStr, const APISourceInfo& sourceInfo);
};

We’ve added:

  • a _ledPixels member of class LEDPixels
  • #include’d the LEDPixels header file
  • a virtual function which overrides the addRestAPIEndpoints() method from the RaftSysMod base class and this allows us to add our REST API function
  • the apiControl() function that implements the API functionality

Remember that REST API functions can be called through all interfaces – BLE, WebSocket & HTTP (over WiFi or Ethernet), Serial, etc – and not just through the normal web interfaces.

Now for the first section of the .cpp file (the final section is later on when we add the REST API functions):

////////////////////////////////////////////////////////////////////////////////
//
// LEDRingSysMod.cpp
//
////////////////////////////////////////////////////////////////////////////////

#include "LEDRingSysMod.h"
#include "RaftUtils.h"
#include "RaftJson.h"
#include "RestAPIEndpointManager.h"

// Constructor and destructor unchanged ...

void LEDRingSysMod::setup()
{
    // Setup LED Pixels
    bool rslt = _ledPixels.setup(config);

    // Log
#ifdef DEBUG_LED_GRID_SETUP
    LOG_I(MODULE_PREFIX, "setup %s numPixels %d", 
                rslt ? "OK" : "FAILED", _ledPixels.getNumPixels());
#endif
}

void LEDRingSysMod::loop()
{
    _ledPixels.loop();
}

A bit more to unpack here:

  • The setup() function now has a line which calls setup() on the _ledPixels object.
  • It passes in a variable called config which is a member of the RaftSysMod base class.
  • config contains the configuration for the SysMod (which comes originally from the SysTypes.json file) which was described in part 2
  • The LEDPixels module uses the ESP32 RMT unit to generate the signals that drive the LED Pixels. Further information about how this is implemented and how timing can be changed for different LED chips can be found in the ESP IDF led_strip example.
  • The LEDPixels::setup() function that we are calling has pre-defined names for the settings it expects to find in the config record:
    • colorOrder – “GRB”, “RGB” or “BGR” – defines the order for sending color information
    • brightnessPC – 0..100 brightness of LEDs
    • pattern – the name of a registered pattern to play initially
    • startupFirstPixel – the value for the first pixel to display on boot as a hex string in HTML format e.g. “#33AA55” – this can be useful as a power indicator
    • strips – a list of hardware information defining LED strips
      • pin – is the GPIO pin number used for the WS2812 (or similar chip) data input line
      • num – is the number of LED pixels in the chain
      • rmtResolutionHz – is the speed of the peripheral in the ESP32 RMT unit – if this value is not present in the JSON file then 10MHz will be used
      • bit0Duration0Us, bit0Duration1Us, bit1Duration0Us and bit1Duration1Us are the timings for the various transitions in the LED pixel protocol (see the ESP IDF led_strip example for more information). If these values aren’t present then defaults suitable for WS2812 are used.
      • resetDurationUs – reset time for LED pix protocol. If omitted defaults to WS2812 value.
  • The loop() function now has a line which calls loop() on the _ledPixels object.

Adding configuration settings to the SysTypes.json file

At this point we will add the configuration settings used by the _ledPixels object into the SysTypes.json file before we forget what we are doing.

I have chosen to put the LED ring data input on GPIO7 of the TinyS3 board, so we’ll add that into the SysTypes.json file:

{
    "SysTypeName": "SysTypeMain",

... this section of the file is elided for brevity

    "AppSysMod": {
        "exampleGroup": {
            "exampleKey": "Welcome to Raft!"
        }
    },
    "LEDRingSysMod": {
        "enable": 1,
        "brightnessPC": 10,
        "colorOrder": "GRB",
        "strips": [
            {
                "pin": 7,
                "num": 16
            }
        ]
    }
}

It is critically important that the name of the section in the JSON file “LEDRingSysMod” is EXACTLY the same (including upper/lower case) as the name used when the SysMod is registered. If it isn’t then the configuration will be unavailable to the SysMod:

  • enable – 1 or true for enable
  • brightnessPC – 10 ensures that the LEDs are not too bright
  • colorOrder – BGR is the normal order for WS2812 LED strips
  • we create a strip:
    • we set “pin” to the pin of the LED pixel chain data input
    • and “num” to the number of LEDs in the ring

Adding a REST API endpoint

////////////////////////////////////////////////////////////////////////////////
// Endpoints
////////////////////////////////////////////////////////////////////////////////

void LEDRingSysMod::addRestAPIEndpoints(RestAPIEndpointManager &endpointManager)
{
    // Control shade
    endpointManager.addEndpoint("ledring", RestAPIEndpoint::ENDPOINT_CALLBACK, RestAPIEndpoint::ENDPOINT_GET,
                            std::bind(&LEDRingSysMod::apiControl, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
                            "ledring?pattern=<pattern>&params=<params>");
    LOG_I(MODULE_PREFIX, "addRestAPIEndpoints ledring");
}

The addRestAPIEndpoints() function is used to add an endpoint. Each endpoint requires a number of parameters:

  • endpoint_url – this is the part of the final url which defines the endpoint – it cannot contain spaces and must conform to the rules on URL validity
  • endpoint_type – only ENDPOINT_CALLBACK is implemented
  • endpoint_method – can be one of ENDPOINT_GET, ENDPOINT_POST, ENDPOINT_PUT, ENDPOINT_DELETE and ENDPOINT_OPTIONS
  • main_callback – a callback function which will be called when the endpoint operation is triggered – in the case of POST and PUT operations it is only triggered after other data transfer (such as the payload) has been completed
  • description – a text description which will be displayed, for instance, when REST API endpoints are listed on the console
  • content_type – this is the media-type or content-type used in HTTP transfers – common types are text/html, text/json, etc – this may be a more complete list – defaults to text/json
  • content_encoding – e.g. gzip, deflate, compress, etc – explained further here
  • cache_control – either ENDPOINT_CACHE_NEVER or ENDPOINT_CACHE_ALWAYS are supported
  • extra_headers – a string containing extra headers to attach to the response – separated with CR+LF
  • body_callback – a callback function for the body of a request
  • chunk_callback – a callback function for each chunk in a chunked request
  • ready_callback – a callback used to determine if the channel is ready for more data – this is helpful for flow-control on slow links

Many of these parameters have default values so, in the case of a simple GET REST API definition, only the first 5 are provided.

REST API Callback Function

RaftRetCode LEDRingSysMod::apiControl(const String &reqStr, String &respStr, const APISourceInfo& sourceInfo)
{
    // Extract parameters
    std::vector<String> params;
    std::vector<RaftJson::NameValuePair> nameValues;
    RestAPIEndpointManager::getParamsAndNameValues(reqStr.c_str(), params, nameValues);
    RaftJson nameValueParamsJson = RaftJson::getJSONFromNVPairs(nameValues, true);

    // Debug
    LOG_I(MODULE_PREFIX, "apiControl %s JSON %s", reqStr.c_str(), nameValueParamsJson.c_str());

    // Return result
    bool rslt = true;
    return Raft::setJsonBoolResult(reqStr.c_str(), respStr, rslt);
}

Finally we get to the function that is actually called when the REST API is executed:

  • First we extract the parameters from the request
  • Then we just display the extracted information for now
  • Finally we return to say the operation completed successfully

Adding an LED Animation (Pattern)

The LEDPixels Raft module can handle animation of LEDs using a plug-in mechanism which enables very flexible operation. Let’s create an animation which will run a kind of “rainbow snake” along the LEDs.

All LEDPixels animations must be based on the LEDPatternBase class:

////////////////////////////////////////////////////////////////////////////////
// LED Pattern Base Class
// Rob Dobson 2023
////////////////////////////////////////////////////////////////////////////////

#pragma once
#include <stdint.h>
#include "RaftArduino.h"

class LEDPixels;
class NamedValueProvider;

// Base class for LED patterns
class LEDPatternBase
{
public:
    LEDPatternBase(NamedValueProvider* pNamedValueProvider, LEDPixels& pixels) :
        _pNamedValueProvider(pNamedValueProvider), _pixels(pixels)
    {
    }
    virtual ~LEDPatternBase()
    {
    }
    // Setup
    virtual void setup(const char* pParamsJson = nullptr) = 0;
    // Service
    virtual void loop() = 0;

protected:
    // Refresh rate
    uint32_t _refreshRateMs = 30;
    // Hardware state provider
    NamedValueProvider* _pNamedValueProvider = nullptr;
    // Pixels
    LEDPixels& _pixels;
};

All we have to worry about are overriding the setup() and loop() functions with our own implementation.

The setup() function takes an optional JSON document as a parameter and this can be used to parameterize the animation – make it faster or slower for instance.

The loop() function is called frequently – just as it would be in an Arduino application – and this allows the animation state to be updated.

The NamedValueProvider is a means by which the animation can respond to the “environment” – for instance if the LEDs are intended to pulse at the rate from a heart-rate monitor then the heart-rate value can be provided – as a named-value – through the NamedValueProvider interface.

Rainbow Snake Animation

The following code implements the Rainbow Snake plug-in animation. In order for the plug-in mechanism to work the class needs to define a create() function which is responsible for creating the LEDPatternRainbowSnake object(s) on demand.

////////////////////////////////////////////////////////////////////////////////
// LED Pattern Rainbow Snake
// Rob Dobson 2023
////////////////////////////////////////////////////////////////////////////////

#pragma once
#include "LEDPatternBase.h"

class LEDPatternRainbowSnake : public LEDPatternBase
{
public:
    LEDPatternRainbowSnake(NamedValueProvider* pNamedValueProvider, LEDPixels& pixels) :
        LEDPatternBase(pNamedValueProvider, pixels)
    {
    }
    virtual ~LEDPatternRainbowSnake()
    {
    }

    // Create function for factory
    static LEDPatternBase* create(NamedValueProvider* pNamedValueProvider, LEDPixels& pixels)
    {
        return new LEDPatternRainbowSnake(pNamedValueProvider, pixels);
    }

    // Setup
    virtual void setup(const char* pParamsJson = nullptr) override final
    {
        if (pParamsJson)
        {
            // Update refresh rate if specified
            RaftJson paramsJson(pParamsJson, false);
            _refreshRateMs = paramsJson.getLong("rateMs", _refreshRateMs);
        }
    }

    // Loop
    virtual void loop() override final
    {
        // Check update time
        if (!Raft::isTimeout(millis(), _lastLoopMs, _refreshRateMs))
            return;
        _lastLoopMs = millis();

        if (_curState)
        {
            uint32_t numPix = _pixels.getNumPixels();
            for (int pixIdx = _curIter; pixIdx < numPix; pixIdx += 3)
            {
                uint16_t hue = pixIdx * 360 / numPix + _curHue;
                _pixels.setHSV(pixIdx, hue, 100, 10);
            }
            // Show pixels
            _pixels.show();
        }
        else
        {
            _pixels.clear();
            _pixels.show();
            _curIter = (_curIter + 1) % 3;
            if (_curIter == 0)
            {
                _curHue += 60;
            }
        }
        _curState = !_curState;
    }
private:
    // State
    uint32_t _lastLoopMs = 0;
    bool _curState = false;
    uint32_t _curIter = 0;
    uint32_t _curHue = 0;
};

The key things to note here are:

  • The setup() function takes the JSON parameters passed in and updates the _refreshRateMs variable (which is defined in the base-class) with either the same value it already has (the default is set in the base-class) or a value provided in the paramsJson string. This allows, for instance, a REST API to control the speed of the animation.
  • The loop() function does all of the real work of animation:
    • It checks if a new “frame” is required and if not simply returns.
    • Alternately either create a “snake” of varying colours or clear the strip and move to the next animation stage

Registering the Animation & wiring up the REST API

Finally we need to complete the work on the SysMod to register the new animation and wire this up to the REST API so that we can run the animation whenever we want to (note that … indicates elided lines):

...

#include "LEDPatternRainbowSnake.h"

...

void LEDRingSysMod::setup()
{
    // Add patterns
    _ledPixels.addPattern("RainbowSnake", &LEDPatternRainbowSnake::create);

    // Setup LED Pixels
    bool rslt = _ledPixels.setup(config);

    // Log
#ifdef DEBUG_LED_GRID_SETUP
    LOG_I(MODULE_PREFIX, "setup %s numPixels %d", 
                rslt ? "OK" : "FAILED", _ledPixels.getNumPixels());
#endif
}

void LEDRingSysMod::loop()
{
    _ledPixels.loop();
}

...

RaftRetCode LEDRingSysMod::apiControl(const String &reqStr, String &respStr, const APISourceInfo& sourceInfo)
{
    // Extract parameters
    std::vector<String> params;
    std::vector<RaftJson::NameValuePair> nameValues;
    RestAPIEndpointManager::getParamsAndNameValues(reqStr.c_str(), params, nameValues);
    RaftJson nameValueParamsJson = RaftJson::getJSONFromNVPairs(nameValues, true);

    // Get pattern
    String pattern = nameValueParamsJson.getString("pattern", "");

    // Set pattern
    _ledPixels.setPattern(pattern, nameValueParamsJson.c_str());

    // Debug
    LOG_I(MODULE_PREFIX, "apiControl %s JSON %s", reqStr.c_str(), nameValueParamsJson.c_str());

    // Return result
    bool rslt = true;
    return Raft::setJsonBoolResult(reqStr.c_str(), respStr, rslt);
}

Registration of the animation is done using the addPattern() method on the _ledPixels. We pass in a name (which is the name that will be used to start and control the pattern via the API). Note that the pattern is added BEFORE the setup() method is called on _ledPixels. The main reason for doing this is that one of the options in the configuration for LEDPixels is to specify an initial pattern. This is convenient if you want to define a pattern to run on startup. By adding the patterns before calling setup() we ensure that the startup pattern is present when the configuration is set,

The API method apiControl() is updated to extract the name of the pattern from the “pattern” passed in the REST API parameters and call the setPattern() method with that name to start the pattern running. Note that other parameters are also passed as the second argument so the speed can be controlled (using the “rateMs” value) and other parameters could be used by other animations in this way.

Building and testing the app

We can now run (build, flash and monitor) the app using the Raft CLI:

raft run -p <serial-port>

And we should see the following when monitoring the app (where … indicates elided text for brevity)

...
I (396) RaftCoreApp: SysTypeMain 1.0.0 (built May 30 2024 03:27:44) Heap (int) 308932 (all) 308932
...
I (578) gpio: GPIO[7]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (580) RMTLedStrip: setup ok numPixels 16 rmtChannelHandle 0x3fcc27e4 encoderHandle 0x3fcc2c0c hw numPix=16 ledDataPin=7 rmtResolutionHz=10000000 bit0Duration0Us=0.30 bit0Duration1Us=0.90 bit1Duration0Us=0.90 bit1Duration1Us=0.30 resetDurationUs=50.00 msbFirst=1
I (583) LEDPixels: setup OK numStrips 1 totalPixels 16
I (584) LEDRingSysMod: setup OK numPixels 16
I (584) AppSysMod: Welcome to Raft!
...

The new code that we added seems to be ok – we have initialized an RMTLedStrip with 16 pixels on GPIO 7.

To test the LED ring functionality enter the following in the debug terminal:

ledring?pattern=RainbowSnake&rateMs=200

You will see something like: