Skip to content
Daniel Heater edited this page Nov 27, 2015 · 17 revisions

#Motivation This tutorial will walk you through some of the basics of building an application using libSPRITE.

libSPRITE provides for scheduling and deterministic data routing for multi-tasking applications on POSIX compliant operating systems. It also allows you to configure and control applications using the Lua scripting language.

libSPRITE hides some of the complexity of building multi-threaded applications and attempts to reduce the errors and indeterminism associated with concurrent programming. Especially when running on multi-core machines.

#Prerequisites

  • A C++ compiler
  • CMake
  • Lua development tools
  • Download and install libSPRITE.

#Hello World!

Step 1

A good first step is to build a simple, straight forward application that prints "Hello World!"

The source code for this step is available here

Create a file called sprite_main.cpp. For this first step, we are not going to use the SCALE Lua interface. We will stick with straight C/C++.

Start by creating a directory called task. This is where we will put source code for SRTX tasks. The write the following two source files.

Hello.hpp

#ifndef task_Hello_hpp
#define task_Hello_hpp

#include "SRTX/Task.h"

namespace task {
    class Hello : public SRTX::Task {

      public:
        /**
         * Constructor.
         * @param name Task name.
         */
        Hello(const char *const name);

        /**
         * Initialization routine.
         * @return true on success or false on failure.
         */
        bool init();

        /**
         * This the the function that gets executed on a periodic basis
         * each time this task is scheduler to run.
         * @return Return true to continue execution or false to terminate
         * the task.
         */
        bool execute();

        /**
         * Terminate routine.
         */
        void terminate();
    };

} // namespace

#endif // task_Hello_hpp

and Hello.cpp

#include "task/Hello.hpp"
#include "base/XPRINTF.h"

namespace task {

    Hello::Hello(const char *const name)
        : SRTX::Task(name)
    {
    }

    bool Hello::init()
    {
        return true;
    }

    bool Hello::execute()
    {
        IPRINTF("Hello World\n");
        return true;
    }

    void Hello::terminate()
    {
    }

} // namespace

In the top level directory, create the file sprite_main.cpp

#include <SRTX/Scheduler.h>
#include <base/XPRINTF.h>
#include <signal.h>

#include "task/Hello.hpp"

static volatile bool done(false);

static void kill_me_now(int)
{
    done = true;
}

static units::Nanoseconds HZ_to_period(unsigned int hz)
{
    return units::Nanoseconds(1*units::SEC / hz);
}

int main(void)
{
    /* Set up the signal handler for control-C.
     */
    signal(SIGINT, kill_me_now);

    /* Declare the task properties.
     */
    SRTX::Task_properties tp;
    SRTX::priority_t priority = SRTX::MAX_PRIO;

    /* Create the scheduler
     */
    tp.prio = priority;
    tp.period = HZ_to_period(1);
    SRTX::Scheduler &s = SRTX::Scheduler::get_instance();
    s.set_properties(tp);

    /* Create the "Hello World!" task
     */
    --tp.prio;
    task::Hello hello("Hello");
    hello.set_properties(tp);

    s.start();
    hello.start();

    while(!done)
    {
        ;
    }

    hello.stop();
    s.stop();

    return 0;
}

Then add a CMakeLists.txt file at the top level to build the project.

cmake_minimum_required(VERSION 2.8)

# I like to have the warning level set high.
add_definitions("-Wall -Wextra -Wparentheses -Wuninitialized -Wcomment -Wformat -Weffc++")

# Add the source tree directory to the search path for include files.
# Add the path to the libSPRITE header files.
include_directories(${CMAKE_CURRENT_SOURCE_DIR} "/usr/local/include/SPRITE")

# Set libSPRITE in the libraries path.
link_directories(/usr/local/lib/SPRITE)

# Add the executable and it's source.
add_executable(sprite_main sprite_main.cpp
               task/Hello.cpp)

# Specify libraries to link.
target_link_libraries(sprite_main SPRITE_SRTX rt pthread)

To build the executable, create a directory called build and cd into that directory. Execute cmake .. to generate the Makefiles. Then enter make to create an executable called sprite_main.

You must have root privledges to run the executable. This is required because tasks are set to real-time priorities. If you are not root, you will get a warning to run using sudo.

So what happens here. Well, we've created an executable that has one task called Hello. There is also a scheduler task running in the system and the Hello task is registered with the scheduler. Both the scheduler and the Hello task are set to run at 1 Hz with the scheduler running at the highest priority in the system and the Hello task running a the highest priority minus 1 --tp.prio.

The scheduler and Hello tasks are both started and continue to run until control-C is entered, at which point the Hello task and the scheduler are stopped and the program exits.

Bonus tip

So what's up with the IPRINTF("Hello World!\n")? The definition comes from the base/XPRINTF.h file in libSPRITE/base. It is a macro that can be turned on and off. IPRINTF stands for "information print". The base/XPRINTF.h header also contains macros for DPRINTF (debug), WPRINTF (warning), and EPRINTF (error).

By adding the line set_property(SOURCE task/Hello.cpp APPEND_STRING PROPERTY COMPILE_FLAGS " -DNO_PRINT_INFO") to CMakeLists.txt, you can turn off the print statement. This can be quite useful in controlling the level of verbosity on a file by file or system wide basis. Just use -DPRINT_DEBUG, -DNO_PRINT_WARNING, and -DNO_PRINT_ERROR to change the default behavior of DPRINTF, WPRINTF, and EPRINTF macros respectively. Note that the default behavior of DPRINTF is to not print.

Step 2

Source code for step 2 is available here

Notice that the execute() function in the Hello task returns true. When a task returns true it signals the scheduler that the task should be scheduled again. By returning false from the execute() function we can cause the scheduler to remove the task from the schedule. This slightly modified execute() function runs 5 times then exits.

bool Hello::execute()
{
    static unsigned int i = 0;
    IPRINTF("Hello World\n");
    return (++i < 5); 
}

Using SCALE

Step 3.

Next we will convert this simple world application to be executed using the SCALE bindings to the Lua scripting language.

Source code for step 3 is available here

First, we create a class that binds the Hello World task to Lua using the SCALE language extension.

task/Hello_lua.hpp

#ifndef __task_Hello_lua_hpp__
#define __task_Hello_lua_hpp__

#include "SCALE/LuaWrapper.h"
#include "task/Hello.hpp"

namespace task {

    class Hello_lua {
      public:
        /**
         * The name registered with Lua to describe the class.
         */
        static const char class_name[];

        /**
         * The set of methods being exposed to Lua through the adapter
         * class.
         */
        static luaL_Reg methods[];

        /**
         * Allocate a new instance of the Hello task.
         * @param L Pointer to the Lua state.
         * @return A pointer to the Task.
         */
        static Hello *allocator(lua_State *L)
        {
            return new Hello(luaL_checkstring(L, 1));
        }

        /**
         * Register the contents of this class as an adapter between Lua
         * and C++ representations of SRTX::Task.
         * @param L Pointer to the Lua state.
         * @return Number of elements being passed back through the Lua
         * stack.
         */
        static int register_class(lua_State *L)
        {
            luaW_register<Hello>(L, "Hello", NULL, methods, allocator);
            luaW_extend<Hello, SRTX::Task>(L);
            return 0;
        }
    };

    const char Hello_lua::class_name[] = "Hello";

    luaL_Reg Hello_lua::methods[] = { { NULL, NULL } };

} // namespace

#endif // __task_Hello_lua_hpp__

The above piece of code looks nasty (and it kinda is). It's doing some magic looking stuff right now. We'll get to explaining some of the pieces later, for now, know that as you create more tasks, you can usually just copy this code and change the name when you add new tasks.

Now we rewrite the main() function to use the Lua script in place of hard-coded C++ tasks.

sprite_main.cpp

#include <SCALE/Scale_if.h>
#include <base/XPRINTF.h>

#include "task/Hello_lua.hpp"

int main(int argc, char* argv[])
{
    (void)argc; // Supress unused variable warning.

    SCALE::Scale_if& scale = SCALE::Scale_if::get_instance();

    /* Register my tasks with with the Lua executive.
     */
    task::Hello_lua::register_class(scale.state());

    /* Execute the main script that drives the simulation.
     */
    if(false == scale.run_script(argv[1]))
    {
        EPRINTF("Failed executing script: %s\n", argv[1]);
        return -1;
    }

    return 0;
}

So main just got a lot simpler. Now we create a Lua script that defines the tasks, thier parameters, and starts and stops them.

hello.lua

package.path = '/usr/local/lib/SPRITE/?.lua;' .. package.path
local s = require 'scheduler'

--------------------------------------------------------------------------------
-- Initialize the tasks.
--------------------------------------------------------------------------------

-- Create task properties and set an initial priority.
tp = Task_properties.new()
priority = tp:MAX_USER_TASK_PRIO()

-- Create the scheduler.
SCHEDULER_PERIOD = s.HZ_to_period(1)
scheduler = s.create(tp, SCHEDULER_PERIOD, priority)
priority = priority - 1

-- Create the hello world task.
hello = Hello.new("Hello")
s.set_task_properties(hello, tp, SCHEDULER_PERIOD, priority)
priority = priority - 1

--------------------------------------------------------------------------------
-- Start up the tasks.
--------------------------------------------------------------------------------

-- Start everything up.
print "Starting tasks..."
scheduler:start()
hello:start()

-- Use debug to pause the script and let the tasks run.
print "Use control-D to cleanly terminate execution."
debug:debug()

--------------------------------------------------------------------------------
-- Terminate the tasks.
--------------------------------------------------------------------------------

print "...Exiting"

hello:stop()
scheduler:stop()

Notice that the Lua script is now doing much of the work that had been in the main() function. An advantage here is that tasks, task rates, priorities, etc, can all be modified in the Lua script without the need for recompiling the application. We'll see some additional benefits later.

To finish up, we need to modify the CMakeLists.txt file.

At the time of writing, CMake does not handle varying versions of Lua, so we'll supply our own module for finding the correct paths for Lua. At the top level source directory, create the directory path cmake/Modules and download this FindLua.cmake file.

Finally, we need to modify CMakelists.txt.

cmake_minimum_required(VERSION 2.8)

# I like to have the warning level set high.
add_definitions("-Wall -Wextra -Wparentheses -Wuninitialized -Wcomment -Wformat -Weffc++")

# Set target specific rules.
#set_property(SOURCE task/Hello.cpp APPEND_STRING PROPERTY COMPILE_FLAGS " -DNO_PRINT_INFO")

# Find libSPRITE and set flags.
find_package(libSPRITE REQUIRED)
include_directories(${libSPRITE_INCLUDE_DIRS})
link_directories(${libSPRITE_LINK_DIRS})
set(LIBS ${LIBS} ${libSPRITE_LIBRARIES})

# Find Lua and set paths to Lua.
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
find_package(Lua REQUIRED)
include_directories(${LUA_INCLUDE_DIR})
set(LIBS ${LIBS} ${LUA_LIBRARIES})

# Add the source tree directory to the search path for include files.
include_directories(${CMAKE_CURRENT_SOURCE_DIR})

# Add the executable and it's source.
add_executable(sprite_main sprite_main.cpp
               task/Hello.cpp)

# Specify libraries to link.
target_link_libraries(sprite_main ${LIBS} pthread rt)

# Copy Lua scripts to the build directory.
file(COPY hello.lua DESTINATION ${CMAKE_CURRENT_BINARY_DIR})

Now to run the executable, we must supply the Lua script to be used with the executable. sudo ./sprite_main hello.lua

Modifying task properties from Lua

One advantage of specifying task properties in the Lua script is that they are easily modifiable. The current hello.lua sets the task to run periodically at 1 Hz. Let's change the task to run aperiodically by setting the rate to 0. This means the task will run as fast as possible. Normally, we would use aperiodic tasks in cases where the task blocks on something, like an input. Since this task just runs a fixed number of times then exits it will be OK.

s.set_task_properties(hello, tp, 0, priority) -- 0 means aperiodic

Step 4

Note that we use the Lua debugger to get a prompt that pauses execution of the script (the tasks run independent of the debug shell). From this debug shell we can execute any Lua command valid in the current environment. For example we can stop and start tasks.

Type:

hello:stop()
hello:start()

Note that "Hello world!" only prints once when we run from the command line. That is because the exit condition is already satisfied. Let's expose some functions to Lua that allow us to manipulate the number of times the task executes.

Step 4

Source code for step 4 is available here

We'll change task allocator function to take a second parameter indicating the number of times to run the execute function.

static Hello *allocator(lua_State *L) 
{
     return new Hello(luaL_checkstring(L, 1), luaL_checknumber(L, 2));
}

We'll also add a function binding to Lua to allow use to modify the number of times the execute function run.

/**
 * Set the number of times the task should run.
 * @param L Pointer to the Lua state.
 * @return Number of elements being passed back through the Lua stack.
 */
static int set_ntimes(lua_State *L)
{
    Hello *p = luaW_check<Hello>(L, 1);
    p->set_ntimes(luaL_checknumber(L, 2));
    return 0;
}

And add the new set_ntimes() function to the list of methods to register with Lua.

luaL_Reg Hello_lua::methods[] = { { "set_ntimes", Hello_lua::set_ntimes },
                                  { NULL, NULL } };

Now we modify the Hello task to implement the logic for setting the number of times to execute.

#ifndef task_Hello_hpp
#define task_Hello_hpp

#include "SRTX/Task.h"

namespace task {
    class Hello : public SRTX::Task {

      public:
        /**
         * Constructor.
         * @param name Task name.
         * @param ntimes Number of times to run before exiting the task.
         */
        Hello(const char *const name, unsigned int ntimes);

        /**
         * Initialization routine.
         * @return true on success or false on failure.
         */
        bool init();

        /**
         * This the the function that gets executed on a periodic basis
         * each time this task is scheduler to run.
         * @return Return true to continue execution or false to terminate
         * the task.
         */
        bool execute();

        /**
         * Terminate routine.
         */
        void terminate();

        /**
         * Set the number of times the program executes.
         * @param ntimes Number of times to execute.
         */
        void set_ntimes(unsigned int ntimes);

      private:
        /**
         * Number of times to run.
         */
        unsigned int m_ntimes;

        /**
         * Number of times we have run.
         */
        unsigned int m_count;
    };

} // namespace

#endif // task_Hello_hpp


#include "task/Hello.hpp"
#include "base/XPRINTF.h"

namespace task {

    Hello::Hello(const char *const name, unsigned int ntimes)
        : SRTX::Task(name)
        , m_ntimes(ntimes)
        , m_count(0)
    {
    }

    bool Hello::init()
    {
        m_count = 0;
        return true;
    }

    bool Hello::execute()
    {
        IPRINTF("Hello World\n");
        return (++m_count < m_ntimes);
    }

    void Hello::terminate()
    {
    }

    void Hello::set_ntimes(unsigned int ntimes)
    {
        m_count = 0;
        m_ntimes = ntimes;
    }

} // namespace

Add the definition of a global variable to the hello.lua file. We could hard code this, but as scripts become large it can be nice to have a single place to list modifiable parameters.

--------------------------------------------------------------------------------
-- Set some script constants
--------------------------------------------------------------------------------

HELLO_NTIMES = 10

And modify the new call to pass the initial number of times to run.

hello = Hello.new("Hello", HELLO_NTIMES)

Now when we can change the number of times the execute() method gets called by changing the HELLO_NTIMES parameter in the Lua script. We can also call enter a new value for the number of times to execte using a call like hello:set_ntimes(7) at the Lua debug prompt. If the task has already completed and stopped, you can still change the number of times to run then enter hello:start() to run again with the new parameter. If the execute() function is still being called, calling hello:set_ntimes(x) will reset the count and cause it to run x more times. Play around!

NOTE: CMake copies the hello.lua file to the CMake build directory. Changes should be made to the original hello.lua file. After making changes to hello.lua be sure to go back until the build directory and rerun cmake .. to copy the changes to the build directory.

Publish/Subscribe

Step 5

Source code for step 5 is available here

A key feature of libSPRITE is the deterministic publish/subscribe mechanism provided by the SRTX library.

There are many implementations of publish/subscribe. The one provide here is intentionally simple. It only supports communication with tasks on that are part of the same application running on a single computer. That decision was made to adhere to the "simple" nature of SRTX (Simple RunTime eXecutive).

By deterministic publish/subscribe, we refer to the property that messages are always delivered in the same order, adhering to the same set of rules, from run to run of the application, regardless of the number of CPUs/cores in the system. This property is important when developing applications on one set of hardware and deploying the final system on different hardware as is the case when we develop spacecraft software on multicore desktop computers, and later deploy to typically, single core flight hardware. This property is also useful for running repeatable simulations without regard to the hardware the simulation is run upon.

The first step in a publish/subscribe system is to create the messages to be passed in the system. Create a directory called topic at the same level as task, and insert the following files Ping.hpp and Pong.hpp within the topic directory.

topic/Ping.hpp

#ifndef topic_Ping_hpp
#define topic_Ping_hpp

namespace topic {
    /**
     * This is the Ping message type.
     */
    typedef struct {
        unsigned int count;
    } Ping_msg_t;

    /**
     * This is the Ping message name.
     */
    const char ping_topic[] = "Ping";

} // namespace

#endif // topic_Ping_hpp

topic/Pong.hpp

#ifndef topic_Pong_hpp
#define topic_Pong_hpp

namespace topic {
    /**
     * This is the Pong message type.
     */
    typedef struct {
        unsigned int count;
    } Pong_msg_t;

    /**
     * This is the Pong message name.
     */
    const char pong_topic[] = "Pong";

} // namespace

#endif // topic_Pong_hpp

You can probably guess that the next step is to create a Ping task and a Pong task, and they are going to send each other messages using the publish/subscribe system.

task/Ping.hpp

#ifndef task_Ping_hpp
#define task_Ping_hpp

#include <SRTX/Task.h>
#include <SRTX/Publication.h>
#include <SRTX/Subscription.h>
#include "topic/Ping.hpp"
#include "topic/Pong.hpp"

namespace task {
    class Ping : public SRTX::Task {
      public:
        /**
         * Constructor.
         * @param name Task name.
         */
        Ping(const char *const name);

        /**
         * Initialization routine.
         * @return true on success or false on failure.
         */
        bool init();

        /**
         * This the the function that gets executed on a periodic basis
         * each time this task is scheduler to run.
         * @return Return true to continue execution or false to terminate
         * the task.
         */
        bool execute();

        /**
         * Terminate routine.
         */
        void terminate();

      private:
        /* Hide copy constructor and assignment operator.
         */
        Ping(const Ping&);
        Ping& operator=(const Ping&);

        /**
         * Publication for ping data.
         */
        SRTX::Publication<topic::Ping_msg_t> *m_ping_publication;

        /**
         * Subscription to pong data.
         */
        SRTX::Subscription<topic::Pong_msg_t> *m_pong_subscription;
    };

} // namespace

#endif // task_Ping_hpp

task/Ping.cpp

#include "task/Ping.hpp"
#include <base/XPRINTF.h>

namespace task {

    Ping::Ping(const char *const name)
        : SRTX::Task(name), m_ping_publication(NULL), m_pong_subscription(NULL)
    {
    }

    bool Ping::init()
    {
        m_pong_subscription = new SRTX::Subscription<topic::Pong_msg_t>(
            topic::pong_topic, get_period());
        if((NULL == m_pong_subscription) ||
           (false == m_pong_subscription->is_valid())) {
            return false;
        }

        m_ping_publication = new SRTX::Publication<topic::Ping_msg_t>(
            topic::ping_topic, get_period());
        if((NULL == m_ping_publication) ||
           (false == m_ping_publication->is_valid())) {
            return false;
        }

        /* Set an initial value in the published content.
         */
        m_ping_publication->content.count = 0;
        m_ping_publication->put();

        return true;
    }

    bool Ping::execute()
    {
        /* Read the pong message and output the count.
         */
        if(false == m_pong_subscription->get()) {
            EPRINTF("Error retreiving pong data\n");
        }
        if(m_pong_subscription->was_updated()) {
            IPRINTF("Ping gets %u from pong\n",
                    m_pong_subscription->content.count);
        }

        /* Increment and publish the ping count.
         */
        ++m_ping_publication->content.count;
        IPRINTF("Ping puts %u\n", m_ping_publication->content.count);
        m_ping_publication->put();

        return true;
    }

    void Ping::terminate()
    {
        delete(m_pong_subscription);
        delete(m_ping_publication);
    }

} // namespace

task/Ping_lua.hpp

#ifndef __task_Ping_lua_hpp__
#define __task_Ping_lua_hpp__

#include <SCALE/LuaWrapper.h>
#include "task/Ping.hpp"

namespace task {

    class Ping_lua {
      public:
        /**
         * The name registered with Lua to describe the class.
         */
        static const char class_name[];

        /**
         * The set of methods being exposed to Lua through the adapter
         * class.
         */
        static luaL_Reg methods[];

        /**
         * Allocate a new instance of the Ping task.
         * @param L Pointer to the Lua state.
         * @return A pointer to the Task.
         */
        static Ping *allocator(lua_State *L)
        {
            return new Ping(luaL_checkstring(L, 1));
        }

        /**
         * Register the contents of this class as an adapter between Lua
         * and C++ representations of SRTX::Task.
         * @param L Pointer to the Lua state.
         * @return Number of elements being passed back through the Lua
         * stack.
         */
        static int register_class(lua_State *L)
        {
            luaW_register<Ping>(L, "Ping", NULL, methods, allocator);
            luaW_extend<Ping, SRTX::Task>(L);
            return 0;
        }
    };

    const char Ping_lua::class_name[] = "Ping";

    luaL_Reg Ping_lua::methods[] = { { NULL, NULL } };

} // namespace

#endif // __task_Ping_lua_hpp__

task/Pong.hpp

#ifndef task_Pong_hpp
#define task_Pong_hpp

#include <SRTX/Task.h>
#include <SRTX/Publication.h>
#include <SRTX/Subscription.h>
#include "topic/Ping.hpp"
#include "topic/Pong.hpp"

namespace task {
    class Pong : public SRTX::Task {
      public:
        /**
         * Constructor.
         * @param name Task name.
         */
        Pong(const char *const name);

        /**
         * Initialization routine.
         * @return true on success or false on failure.
         */
        bool init();

        /**
         * This the the function that gets executed on a periodic basis
         * each time this task is scheduler to run.
         * @return Return true to continue execution or false to terminate
         * the task.
         */
        bool execute();

        /**
         * Terminate routine.
         */
        void terminate();

      private:
        /* Hide copy constructor and assignment operator.
         */
        Pong(const Pong&);
        Pong& operator=(const Pong&);

        /**
         * Publication for pong data.
         */
        SRTX::Publication<topic::Pong_msg_t> *m_pong_publication;

        /**
         * Subscription to ping data.
         */
        SRTX::Subscription<topic::Ping_msg_t> *m_ping_subscription;
    };

} // namespace

#endif // task_Pong_hpp

task/Pong.cpp

#include "task/Pong.hpp"
#include <base/XPRINTF.h>

namespace task {

    Pong::Pong(const char *const name)
        : SRTX::Task(name), m_pong_publication(NULL), m_ping_subscription(NULL)
    {
    }

    bool Pong::init()
    {
        m_ping_subscription = new SRTX::Subscription<topic::Ping_msg_t>(
            topic::ping_topic, get_period());
        if((NULL == m_ping_subscription) ||
           (false == m_ping_subscription->is_valid())) {
            return false;
        }

        m_pong_publication = new SRTX::Publication<topic::Pong_msg_t>(
            topic::pong_topic, get_period());
        if((NULL == m_pong_publication) ||
           (false == m_pong_publication->is_valid())) {
            return false;
        }

        /* Set an initial value in the published content.
         */
        m_pong_publication->content.count = 0;
        m_pong_publication->put();

        return true;
    }

    bool Pong::execute()
    {
        /* Read the pong message and output the count.
         */
        if(false == m_ping_subscription->get()) {
            EPRINTF("Error retreiving ping data\n");
        }
        if(m_ping_subscription->was_updated()) {
            IPRINTF("Pong gets %u from ping\n",
                    m_ping_subscription->content.count);
        }

        /* Increment and publish the pong count.
         */
        ++m_pong_publication->content.count;
        IPRINTF("Pong puts %u\n", m_pong_publication->content.count);
        m_pong_publication->put();

        return true;
    }

    void Pong::terminate()
    {
        delete(m_ping_subscription);
        delete(m_pong_publication);
    }

} // namespace

task/Pong_lua.hpp

#ifndef __task_Pong_lua_hpp__
#define __task_Pong_lua_hpp__

#include <SCALE/LuaWrapper.h>
#include "task/Pong.hpp"

namespace task {

    class Pong_lua {
      public:
        /**
         * The name registered with Lua to describe the class.
         */
        static const char class_name[];

        /**
         * The set of methods being exposed to Lua through the adapter
         * class.
         */
        static luaL_Reg methods[];

        /**
         * Allocate a new instance of the Pong task.
         * @param L Pointer to the Lua state.
         * @return A pointer to the Task.
         */
        static Pong *allocator(lua_State *L)
        {
            return new Pong(luaL_checkstring(L, 1));
        }

        /**
         * Register the contents of this class as an adapter between Lua
         * and C++ representations of SRTX::Task.
         * @param L Pointer to the Lua state.
         * @return Number of elements being passed back through the Lua
         * stack.
         */
        static int register_class(lua_State *L)
        {
            luaW_register<Pong>(L, "Pong", NULL, methods, allocator);
            luaW_extend<Pong, SRTX::Task>(L);
            return 0;
        }
    };

    const char Pong_lua::class_name[] = "Pong";

    luaL_Reg Pong_lua::methods[] = { { NULL, NULL } };

} // namespace

#endif // __task_Pong_lua_hpp__

Notice that each publication and subscription is created in the init() function of the task, and takes the argument get_period(). This passes the periodic rate of the task which is used to enforce deterministic delivery. For periodic tasks, messages are latched at the beginning of the frame for that task's rategroup. I.e., for a 1hz task, before the schedule runs any 1hz task, it makes a copy of all messages subscribed to by 1hz tasks, and the copy is used, even if a task running at a different peridoic rate publishes new data.

Aperiodic tasks can also use the publish/subscribe mechanism, but deterministic delivery is not guaranteed. It is often useful for aperiodic tasks to use the get_blocking() method in place of the get() method on data that it subscribes to. In this way, the aperiodic task only runs when new data arrives.

Now, lets add the new tasks to the main program and the build system.

In sprite_main.cpp, add lines for the ping and pong tasks.

#include "task/Hello_lua.hpp"
#include "task/Ping_lua.hpp"
#include "task/Pong_lua.hpp"

....

/* Register my tasks with with the Lua executive.
 */
task::Hello_lua::register_class(scale.state());
task::Ping_lua::register_class(scale.state());
task::Pong_lua::register_class(scale.state());

We need a Lua script to drive the program:

ping_pong.lua

package.path = '/usr/local/lib/SPRITE/?.lua;' .. package.path
local s = require 'scheduler'

--------------------------------------------------------------------------------
-- Set some script constants
--------------------------------------------------------------------------------

--------------------------------------------------------------------------------
-- Initialize the tasks.
--------------------------------------------------------------------------------

-- Create task properties and set an initial priority.
tp = Task_properties.new()
priority = tp:MAX_USER_TASK_PRIO()

-- Create the scheduler.
SCHEDULER_PERIOD = s.HZ_to_period(1)
scheduler = s.create(tp, SCHEDULER_PERIOD, priority)
priority = priority - 1

-- Create the ping task.
ping = Ping.new("Ping")
s.set_task_properties(ping, tp, SCHEDULER_PERIOD, priority)
priority = priority - 1

-- Create the pong task.
pong = Pong.new("Pong")
s.set_task_properties(pong, tp, SCHEDULER_PERIOD * 3, priority)
priority = priority - 1

--------------------------------------------------------------------------------
-- Start up the tasks.
--------------------------------------------------------------------------------

-- Start everything up.
print "Starting tasks..."
scheduler:start()
pong:start()
ping:start()

-- Use debug to pause the script and let the tasks run.
print "Use control-D to cleanly terminate execution."
debug:debug()

--------------------------------------------------------------------------------
-- Terminate the tasks.
--------------------------------------------------------------------------------

print "...Exiting"

ping:stop()
scheduler:stop()

In CMakeLists.txt, and the new task *.cpp files to the add_executable() call:

# Add the executable and it's source.
add_executable(sprite_main sprite_main.cpp
               task/Hello.cpp
               task/Ping.cpp
               task/Pong.cpp
               )

And add an argument to copy the ping_pong.lua file to the build directory for convenience:

Copy Lua scripts to the build directory.

file(COPY hello.lua ping_pong.lua DESTINATION ${CMAKE_CURRENT_BINARY_DIR})

Notice that the main program still includes, the Hello task, but since we don't create the hello task in the ping_pong.lua file, that task doesn't exist in the running system. It can be useful to have difference .lua scripts or modify scripts to remove task or create alternate tasks when testing and debugging systems.

Now to test our program:

cd build
cmake ..
make
sudo ./sprite_main ping_pong.lua

You should get an output like this:

Starting tasks...
Use control-D to cleanly terminate execution.
lua_debug> Error retreiving pong data
Ping puts 1
Error retreiving pong data
Ping puts 2
Ping gets 0 from pong
Ping puts 3
Pong gets 2 from ping
Pong puts 1
Ping puts 4
Ping puts 5
Ping gets 1 from pong
Ping puts 6
Pong gets 5 from ping
Pong puts 2
...Exiting

In the ping_pong.lua script we set the ping tasks to run at 1hz, and the pong task to run three times slower, and that's exactly what we get.

Notice that the Ping task gets an error the first time it tries to get Pong's data. That's because the input to ping was latched before Pong had a chance to write it's first output. You can also see that when Pong updates it's value, Ping doesn't see the new value of Pong until the 3 second period of the Pong task completes. That is the other half of ensuring deterministic data delivery. Slower tasks don't see new data from faster tasks until thier next frame, and faster tasks don't see slower tasks data until the slower task has completed its frame.

An exception is made when tasks run at the same rate. Same rate data is not latched. Tasks running at the same rate run in priority order. The published data from a higher priority task will be immediatly avaialble to the lower priority tasks and dterministic delivery is still guaranteed regardless of the number of CPU-cores available.

Play with the order and rates of the ping and pong tasks to see how the behavior varies.

Clone this wiki locally