-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial
#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!
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 function gets executed on a periodic basis
* each time this task is scheduled 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.
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.
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
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
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
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.
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 function gets executed on a periodic basis
* each time this task is scheduled 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.
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 provided 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 function gets executed on a periodic basis
* each time this task is scheduled 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 function gets executed on a periodic basis
* each time this task is scheduled 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 periodic 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 different .lua scripts or modify scripts to remove tasks 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 periodic 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 immediately avaialble to the lower priority tasks and deterministic 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.
One useful pattern for using libSPRITE is to create services. A service is an interface to some capability. Often that capability is to talk to hardware. The service is implemented as a Singleton containing function pointers. A SRTX task is created that implements the interface. This allows for seperation of interface and implementation. This can be useful when different platforms may provide different implementations, or we might use this to replace a real implementation with a fake one to provide data for testing. Below is an example of a UDP port implemented as a service.
First we define the service interface is service/UDP.hpp
#ifndef service_UDP_hpp
#define service_UDP_hpp
namespace service {
class UDP {
public:
static UDP &get_instance()
{
static UDP instance;
return instance;
}
typedef int (*read_t)(unsigned int channel, void *buffer,
unsigned int nbytes);
typedef int (*write_t)(unsigned int channel, const void *buffer,
unsigned int nbytes);
read_t read;
write_t write;
private:
static int default_read(unsigned int channel, void *buffer,
unsigned int nbytes)
{
(void)channel; // Suppress unused variable warnings.
(void)buffer;
(void)nbytes;
return -1;
}
static int default_write(unsigned int channel, const void *buffer,
unsigned int nbytes)
{
(void)channel; // Suppress unused variable warnings.
(void)buffer;
(void)nbytes;
return -1;
}
/**
* Constructor.
* The constructor is made private as part of the singleton
* pattern.
*/
UDP() : read(default_read), write(default_write)
{
}
/**
* Copy constructor.
* The copy constructor is made private as part of the singleton
* pattern.
*/
UDP(const UDP &);
/**
* Assignment operator.
* The assignment operator is made private as part of the singleton
* pattern.
*/
UDP &operator=(const UDP &);
};
} // namespace
#endif // service_UDP_hpp
Notice that we've provided some default implentation of the service functions so if no implementation is provided by the developer, the function pointers are not NULL.
Now we will provide an implementation of this service.
hwio/UDP.hpp
#ifndef hwio_UDP_hpp
#define hwio_UDP_hpp
#include <net/UDP_connection.h>
#include <SRTX/Task.h>
namespace hwio {
/**
* This class manages a UDP port.
*/
class UDP : public SRTX::Task {
public:
/**
* Constructor.
* @param name Task name.
* @param port Port number.
* @param hostname Hostname (client-only)
*/
UDP(const char *const name, unsigned int port,
const char *const hostname = NULL);
/**
* UDP initialization routine.
* @return true on success or false on failure.
*/
bool init();
/**
* UDP terminate routine.
*/
void terminate();
private:
/* Hide copy constructor and assignment operator.
*/
UDP(const UDP&);
UDP& operator=(const UDP&);
/**
* Read input from the UDP socket.
* @param channel UDP channel to read from.
* @param buffer Input data buffer.
* @param nbytes Maximum number of bytes to read.
* @return Number of bytes read, or -1 on failure.
*/
static int read(unsigned int channel, void *buffer,
unsigned int nbytes);
/**
* Write output through the UDP socket.
* @param channel UDP channel to write to.
* @param buffer Output data buffer.
* @param nbytes Number of bytes to write.
* @return Number of bytes written, or -1 on failure.
*/
static int write(unsigned int channel, const void *buffer,
unsigned int nbytes);
/**
* Define the number of ports that we can handle.
*/
static const unsigned int NUM_UDP = 20;
/**
* Connection to the UDP socket.
*/
static net::UDP_connection *m_conn[NUM_UDP];
/**
* Port number.
*/
const unsigned int m_port;
/**
* Hostname
*/
const char* const m_hostname;
};
} // namespace
#endif // hwio_UDP_hpp
hwio/UDP.cpp
#include <net/UDP_client.h>
#include <net/UDP_server.h>
#include "hwio/UDP.hpp"
#include "service/UDP.hpp"
namespace hwio {
net::UDP_connection *UDP::m_conn[UDP::NUM_UDP];
UDP::UDP(const char *const name, unsigned int port,
const char *const hostname)
: SRTX::Task(name), m_port(port), m_hostname(hostname)
{
for(unsigned int i = 0; i < NUM_UDP; ++i) {
m_conn[i] = NULL;
}
}
bool UDP::init()
{
DPRINTF("channel = %d, hostname = %s, port = %d\n", 0, m_hostname,
m_port);
if(m_hostname) {
DPRINTF("Create new UDP client\n");
m_conn[0] = new net::UDP_client(m_hostname, m_port);
if((NULL == m_conn[0]) || (false == m_conn[0]->is_valid())) {
EPRINTF("Error creating UDP client\n");
delete(m_conn[0]);
m_conn[0] = NULL;
return false;
}
/* When acting as a client, we have to send an initial message
* before we receive messages.
*/
char buffer[] = "Hi\n";
m_conn[0]->write(buffer, sizeof(buffer));
}
else {
DPRINTF("Create new UDP server\n");
m_conn[0] = new net::UDP_server(m_port);
if((NULL == m_conn[0]) || (false == m_conn[0]->is_valid())) {
EPRINTF("Error creating UDP server\n");
delete(m_conn[0]);
m_conn[0] = NULL;
return false;
}
}
/* Set the function pointers for the UDP service.
*/
service::UDP &udp = service::UDP::get_instance();
udp.read = read;
udp.write = write;
/* We return false here because there is no need to schedule this task.
* It's execute function does nothing.
*/
return false;
}
void UDP::terminate()
{
if(m_conn[0] != NULL) {
delete m_conn[0];
m_conn[0] = NULL;
}
}
int UDP::read(unsigned int channel, void *buffer, unsigned int nbytes)
{
if((NULL == m_conn[channel]) ||
(false == m_conn[channel]->is_valid())) {
return -1;
}
/* Read blocks program execution until data is received.
* If the number of characters to be recieved is larger than
* the number of chars available remaining data may be dropped.
* res will be set to the actual number of characters
* actually read
*/
DPRINTF("Attempting to read channel %d\n", channel);
int res = m_conn[channel]->read(buffer, nbytes);
if(-1 == res) {
PERROR("read");
return res;
}
/* Set end of string, so we can print.
*/
((char *)buffer)[res] = '\0';
DPRINTF("UDP read channel %d:%s:%d\n", channel, (char *)buffer, res);
return res;
}
int UDP::write(unsigned int channel, const void *buffer,
unsigned int nbytes)
{
if((NULL == m_conn[channel]) ||
(false == m_conn[channel]->is_valid())) {
return -1;
}
return m_conn[channel]->write(const_cast<void *>(buffer), nbytes);
}
}
Notice that the implementation above is a bit lazy. It created an array of pointers to UDP connections, but then only used element 0
of the array. This code was stripped from an implementation that read the UDP host and port names from a configuration file. In its original form, this one task provided many UDP connections, both client and server. For illustration purposes, I've stripped out the configuration file and modified the constructor to take the port and option hostname as a parameter and only handle one connection.
hwio/UDP_lua.hpp
#ifndef hwio_UDP_lua_hpp
#define hwio_UDP_lua_hpp
#include <SCALE/LuaWrapper.h>
#include "hwio/UDP.hpp"
namespace hwio {
class UDP_lua {
public:
/**
* The name regsitered 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[];
/**
* Because we are passing arguments to the constuctor, we cannot use the
* default constructor method.
* @param L Pointer to the Lua state.
* @return A pointer to the Task.
*/
static UDP *allocator(lua_State *L)
{
/* How many arguments were passed?
*/
int nargs = lua_gettop(L);
/* 2 arguments are passed for UDP clients, 3 for servers.
*/
return new UDP(luaL_checkstring(L, 1), luaL_checknumber(L, 2),
(3 == nargs) ? luaL_checkstring(L, 3) : NULL);
}
/**
* 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<UDP>(L, "UDP", NULL, methods, allocator);
luaW_extend<UDP, SRTX::Task>(L);
return 0;
}
};
const char UDP_lua::class_name[] = "UDP";
luaL_Reg UDP_lua::methods[] = { { NULL, NULL } };
} // namespace
#endif // hwio_UDP_lua_hpp
Now we'll add a task that reads data from a UDP port and outputs what it receives to the console.
task/Echo.hpp
#ifndef task_Echo_hpp
#define task_Echo_hpp
#include <SRTX/Task.h>
namespace task {
class Echo : public SRTX::Task {
public:
/**
* Constructor.
* @param name Task name.
*/
Echo(const char *const name);
/**
* Initialization routine.
* @return true on success or false on failure.
*/
bool init();
/**
* This function gets executed each time this task is scheduled to run.
* @return Return true to continue execution or false to terminate
* the task.
*/
bool execute();
/**
* Terminate routine.
*/
void terminate();
};
} // namespace
#endif // task_Echo_hpp
task/Echo.cpp
#include "task/Echo.hpp"
#include "service/UDP.hpp"
#include <base/XPRINTF.h>
namespace task {
Echo::Echo(const char *const name) : SRTX::Task(name)
{
}
bool Echo::init()
{
return true;
}
bool Echo::execute()
{
static service::UDP &udp = service::UDP::get_instance();
const unsigned int NBYTES = 256;
static char buffer[NBYTES];
if(-1 == udp.read(0, buffer, NBYTES)) {
EPRINTF("ERROR in UDP read\n");
}
else {
IPRINTF("%s\n", buffer);
}
return true;
}
void Echo::terminate()
{
}
} // namespace
task/Echo_lua.hpp
#ifndef __task_Echo_lua_hpp__
#define __task_Echo_lua_hpp__
#include <SCALE/LuaWrapper.h>
#include "task/Echo.hpp"
namespace task {
class Echo_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 Echo task.
* @param L Pointer to the Lua state.
* @return A pointer to the Task.
*/
static Echo *allocator(lua_State *L)
{
return new Echo(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<Echo>(L, "Echo", NULL, methods, allocator);
luaW_extend<Echo, SRTX::Task>(L);
return 0;
}
};
const char Echo_lua::class_name[] = "Echo";
luaL_Reg Echo_lua::methods[] = { { NULL, NULL } };
} // namespace
#endif // __task_Echo_lua_hpp__
Add the following lines to sprite_main.cpp
#include "hwio/UDP_lua.hpp"
#include "task/Echo_lua.hpp"
/* Register my tasks with with the Lua executive.
*/
hwio::UDP_lua::register_class(scale.state());
task::Echo_lua::register_class(scale.state());
Create a Lua script to create and run the new tasks
echo.lua
package.path = '/usr/local/lib/SPRITE/?.lua;' .. package.path
local s = require 'scheduler'
--------------------------------------------------------------------------------
-- Set some script constants
--------------------------------------------------------------------------------
HELLO_NTIMES = 10
PORT = 9876
HOST = "localhost"
--------------------------------------------------------------------------------
-- 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", HELLO_NTIMES)
s.set_task_properties(hello, tp, SCHEDULER_PERIOD, priority)
priority = priority - 1
-- Create the udp service.
udp = UDP.new("UDP", PORT)
s.set_task_properties(udp, tp, 0, priority)
priority = priority - 1
-- Create the aperiodic echo task.
echo = Echo.new("Echo")
s.set_task_properties(echo, tp, 0, priority)
priority = priority - 1
--------------------------------------------------------------------------------
-- Start up the tasks.
--------------------------------------------------------------------------------
-- Start everything up.
print "Starting tasks..."
scheduler:start()
hello:start()
udp:start()
echo: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"
echo:start()
hello:stop()
scheduler:stop()
Notice we started with the hello.lua
file and added to it. I decided to leave the hello
task in place so you can see the task doing something even without input over the network.
And make the following changes to CMakeLists.txt
# Add the executable and it's source.
add_executable(sprite_main sprite_main.cpp
hwio/UDP.cpp
task/Echo.cpp
task/Hello.cpp
task/Ping.cpp
task/Pong.cpp
)
# Copy Lua scripts to the build directory.
file(COPY hello.lua ping_pong.lua echo.lua DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
Now build and run the program.
cd build
cmake ..
make ..
sudo ./sprite_main echo.lua
We have now started a UDP server listening on port 9876
. In another window, we can use netcat
to send data to that port.
nc -u localhost 9876
When you enter data into the netcat
window, you should see it output in the window running the libSPRITE Echo
task.