CCallbacks ( What are C Callbacks? ) enable you to use the lightweight Storyboard Engine Public API from within your C/C++ code to directly interact with the Storyboard Engine and offer an additional option alongside scripting using our Lua or Javascript plugins when you need to to add dynamic application behaviour and or low-level hardware access.
On resource constrained MCU systems it can be necessary to achieve a lower (RAM) memory resource footprint in order to deploy Storyboard applications successfully to the target - C Callbacks can help by removing remove the higher resource overheads (grabage collection and heap usage) associated with Lua and or Javascript at runtime. It can also be useful to be able to interact directly with low-level IO code to handle GPIO, read sensors or other low-level operations.
On more powerful MPU systems running on Windows, Linux or QNX it can still be useful to be able to extend your Storyboard application with an external dynamic library ( .s or .DLL ) which wraps C Callback functions to handle functionality not easily achieved through the scripted Lua or Javascript code paths.
TLDR; Here are a few considerations and constraints to help you choose the right approach, as well as when or not to use C Callbacks:
Tip
The Storyboard engine in the desktop simulator will automatically fallback to call a Lua script function implementation of exactly the same name - if available. This allows you to create and test your UI as normal by using Lua function substitutes for any back-end C Callback functions and target hardware dependencies
Notice
The Storyboard Engine Public API features a little less capability than that available with the full Storyboard Lua API from within a Lua or the Storyboard Javascript API with a Javascript callback - such as dynamic object creation such as runtime timers and clone functions, scripted animations - so you may need to think carefully what functionality you can replace with a C Callback
Notice
You can freely mix Lua or Javascript and native CCallback functions within in your application as long as you have their associated plugins enabled within the sbengine_plugins.h engine initialisation table
Caution
It is quite feasible to make asynchronous calls from parallel OS tasks to read and write to application data variables and control properties ( using their FQN ). Doing so triggers the Storyboard Engine Data Manager to re-evaluate the UI state and may trigger a render update immediately if any controls bound to the variables which have been modified ON EACH API CALL
Put simply - making multiple asynchronous calls using the Storyboard Engine Public API, potentially at a high frequency to get and importantly set application data and or variables can have a significant impact on CPU performance through driving an unnecessarily high framerate
The best approach when needing to update front-end data in the UI asynchronously from a back-end task is to ‘send’ a custom event to the UI which triggers a C Callback action to a back-end callback function and apply an atomic set of changes in one pass = a single Data Manager sequence and result in at most 1 subsequent render update to the screen
Caution
Any calls to C Callback functions are made from the Storyboard Engine main thread - this is your running UI task - and are as such these are BLOCKING calls! it is important not to cause long delays, use polling loops or other code which can hang whilst in the C Callback as the UI is effectively halted and non-responsive until the function called returns. Events such as touch input, keyboard actions or custom Storyboard IO events will however still be received and queued however so they will be processed when control is passed back to the engine
With standard Storyboard development it is common to add dynamic behaviour to the UI model using Lua scripts behind Lua callback actions. The screen captures below show one of the Storyboard demo project examples for an eBike Cluster UI.
Initially Lua function callbacks were used for implementation of certain dynamic behaviours within the UI using interpreted Lua script at runtime via the Lua plugins(s):
The application was then modified for a lightweight implementation using ‘native’ CCallbacks to call out into the back-end C code domain :
Using this approach you can extend the example to add more CCallback ‘C Function’ calls which make use of other Storyboard Engine Public API calls to set and get data variables, send events or perhaps interact directly with your system hardware peripherals.
Caution
Calls to CCallbacks occur from the main Storyboard engine thread and as such will block the application UI for the duration of the call until it returns, freezing display updates. It is therefore important not to have delay loops or other code dependencies within which may block and prevent the application UI from continuing execution
Link : CPA-114-CCallbacks-Bike-Cluster.zip
We will focus on the CoffeeApp demo application which was modified to migrate Lua calls to use C Functions callbacks for handling dynamic UI updates through the Storyboard Engine Public API.
CCallback actions are added to an Application model which is then exported using the Storyboard Application Export Configuration with package type Storyboard Embedded Resource Header (C/C++). The export wizard will automatically append a lookup table which includes the function prototype(s) for the CCallback functions that you defined in the Storyboard model. For the CoffeeApp example you should see the code below towards the end of the model header file:
: #ifndef GR_APPLICATION_CCALLBACKS_H #define GR_APPLICATION_CCALLBACKS_H void cbResetBrew(gr_action_context_t *action_context); void cbResetBrewing(gr_action_context_t *action_context); void cbResetSizes(gr_action_context_t *action_context); void cbResetStart(gr_action_context_t *action_context); DLLExport const sb_ccallback_t sb_ccallbacks[] = { { "cbResetBrew", &cbResetBrew }, { "cbResetBrewing", &cbResetBrewing }, { "cbResetSizes", &cbResetSizes }, { "cbResetStart", &cbResetStart }, {NULL, NULL} }; #endif
You can export the implementation template code for completion using File->Export->Storyboard C Callback Action Code (C/C++) then specify the files to export. In the case of the CoffeeApp example this may be used to generate two template files for implementation : CoffeeApp_ccallbacks.c and CoffeeApp_ccallbacks.h.
Inspecting the code we first see that the application handle app
is obtained from the action_context
passed in from the Storyboard engine on callback, via the gr_context_get_application()
API call:
app = gr_context_get_application(action_context);
Once the application content has be received we can use the gr_application_set_data()
API call to update the runtime application data manager directly:
gr_application_set_data(app, keyName, GR_DATA_FORMAT_4s1, &data);
The code implementation for the callback cbResetSizes
could look something like the following snippet from ccallbacks.c which wraps the data management update process in a setDynamicValue()
utility function:
Note
This example uses a utility function to set application data parameters of type GR_DATA_FORMAT_4s1
which are 32 bit signed integer (data.i32) datatypes. Similar utility functions can be created for alternative datatypes (see gre_types.h
for additional formats)
#include <stdlib.h> #include <string.h> #include "ccallbacks.h" #if defined(GRE_FEATURE_VFS_RESOURCES) #include <gre/sdk/sbresource_vfs.h> #endif void setDynamicValue(gr_application_t *app, char *keyName, int value){ gr_data_union_t data; memset(&data, 0, sizeof(data)); data.i32 = value; gr_application_set_data(app, keyName, GR_DATA_FORMAT_4s1, &data); } void cbResetSizes(gr_action_context_t *action_context) { int value; gr_application_t *app; app = gr_context_get_application(action_context); if(app == NULL) { return; } value = 0; setDynamicValue(app, "size_layer.size_blocker.grd_hidden", value); setDynamicValue(app, "size_layer.SELECT_SIZE.alpha", value); setDynamicValue(app, "size_layer.cup_sm_control.alpha", value); setDynamicValue(app, "size_layer.cup_sm_control.alpha1", value); setDynamicValue(app, "size_layer.cup_sm_control.alpha2", value); setDynamicValue(app, "size_layer.cup_sm_control.grd_width", value); setDynamicValue(app, "size_layer.cup_sm_control.grd_height", value); setDynamicValue(app, "size_layer.cup_md_control.alpha", value); setDynamicValue(app, "size_layer.cup_md_control.alpha1", value); setDynamicValue(app, "size_layer.cup_md_control.alpha2", value); setDynamicValue(app, "size_layer.cup_md_control.grd_width", value); setDynamicValue(app, "size_layer.cup_md_control.grd_height", value); setDynamicValue(app, "size_layer.cup_lg_control.alpha", value); setDynamicValue(app, "size_layer.cup_lg_control.alpha1", value); setDynamicValue(app, "size_layer.cup_lg_control.alpha2", value); setDynamicValue(app, "size_layer.cup_lg_control.grd_width", value); setDynamicValue(app, "size_layer.cup_lg_control.grd_height", value); value = 255; setDynamicValue(app, "size_layer.cup_sm_control.alpha3", value); setDynamicValue(app, "size_layer.cup_md_control.alpha3", value); setDynamicValue(app, "size_layer.cup_lg_control.alpha3", value); value = 178; setDynamicValue(app, "size_layer.cup_sm_control.alpha4", value); setDynamicValue(app, "size_layer.cup_md_control.alpha4", value); setDynamicValue(app, "size_layer.cup_lg_control.alpha4", value); value = 90; setDynamicValue(app, "size_layer.cup_sm_control.grd_x", value); value = 153; setDynamicValue(app, "size_layer.cup_sm_control.grd_y", value); setDynamicValue(app, "size_layer.cup_md_control.grd_y", value); setDynamicValue(app, "size_layer.cup_lg_control.grd_y", value); value = 240; setDynamicValue(app, "size_layer.cup_md_control.grd_x", value); value = 390; setDynamicValue(app, "size_layer.cup_lg_control.grd_x", value); value = 230; setDynamicValue(app, "logo_overlay.logo.grd_y", value); } :
Caution
Calls to CCallbacks occur from the main Storyboard engine thread and as such will block the application UI for the duration of the call until it returns, freezing display updates. It is therefore important not to have delay loops or other code dependencies within which may block and prevent the application UI from continuing execution
Update libraries to include CCallback plugin
Plugins and libraries need to be added to the project to enable the CCallback support.
Note
If you have trace output from Storyboard configured on the target then you are likely getting amissing plugin?
warning if you do not have this in place already.
Important
We issue a warning and don't error if the plugin has not been initialised because the CCallback plugin is one of the optional plugins. If you use it in the model but don't enable it nothing happens and the model action is ignored and skipped.
Add the /plugins/libgre-plugin-ccallback.a to the linker library input list:
Storyboard Engine Callback Hooks - sbvfs_get_ccallback_list()
A CCallback table lookup function callback is used by the engine to dereference the CCallback function by name from the exported table in the header file.
Place this this sbvfs_get_ccallback_list()
lookup function in sbengine_task.c or similar.
extern const sb_ccallback_t sb_ccallbacks[]; /* * Storyboard engine will callback into this function to retrieve the CCallback lookup table * exported in the model header */ sb_ccallback_t * sbvfs_get_ccallback_list() { return (sb_ccallback_t*)&sb_ccallbacks; }
Enable Storyboard Engine CCallback Plugin
Add the optional CCallback plugin (gre_plugin_c_callback) entry to the sbengine_plugins.h file and add to the sb_plugins[] list to initialise and enable it.
sbengine_plugins.h: /* * Copyright 2013, Crank Software Inc. All Rights Reserved. * * For more information email info@cranksoftware.com. */ #include <gre/sdk/plugin.h> /* * This is a list of the available plugins. Add the functions to the plugin list * below to make the features available to your application */ // gre plugins extern int gre_plugin_animate(gr_plugin_state_t *); extern int gre_plugin_timer(gr_plugin_state_t *); extern int gre_plugin_greio(gr_plugin_state_t *); extern int gre_plugin_logger(gr_plugin_state_t *); : extern int gre_plugin_script_lua(gr_plugin_state_t *); // optional : enable Lua script support extern int gre_plugin_c_callback(gr_plugin_state_t *); // optional : add native CCallbacks function support extern int gre_plugin_fio_vfs(gr_plugin_state_t *); extern int gre_plugin_sbimage_soil(gr_plugin_state_t *); extern int gre_plugin_sbimage_png(gr_plugin_state_t *); : const gr_plugin_create_func_tsb_plugins[] = { gre_plugin_fio_vfs, gre_plugin_animate, gre_plugin_circle, : gre_plugin_c_callback, // optional : enable native CCallbacks function support gre_plugin_script_lua, // optional : enable Lua script support : gre_plugin_greio, gre_plugin_sbimage_soil, gre_plugin_timer, NULL, };
Switching out the Lua script plugin will disable Lua support if you don't need it and can significantly reduce the system heap memory usage, no Lua module parsing or garbage collection support needed.
In addition to what we have describes so far relating to MCU applications, CCallbacks can also be used with desktop or MPU targets running Windows, Linux or QNX high-level OSes. We will use as an example a custom logging service added to a modified Thermostat app in this example but the same process can be used for any application..
Add a CCallback with a C Function action on gre.init
to call a function CBRegisterLogger
which we will implement to configure the custom logging service.
You can export the implementation code using File->Export->Storyboard C Callback Action Code (C/C++) then specify the files to export:
The result in this example will be two files created in the Storyboard project folder:
Thermostat_ccallbacks.h containing the lookup table for the functions by name sb_ccallbacks[]
:
#include<gre/gre.h> #ifndef GR_THERMOSTAT_CCALLBACKS_H #define GR_THERMOSTAT_CCALLBACKS_H voidCBRegisterLogger(gr_action_context_t *action_context); DLLExportconst sb_ccallback_t sb_ccallbacks[] = { { "CBRegisterLogger", &CBRegisterLogger }, {NULL, NULL} }; #endif
Thermostat_ccallbacks.c which holds the template function that will be called and thus where you to insert your code:
#include <Thermostat_ccallbacks.h> void CBRegisterLogger(gr_action_context_t *action_context) { //TODO: Complete this function callback }
Looking now at the actual implementation code for the callback function that we will build as a DLL on Windows or a shared library (.so) on Linux. This code will be implemented using just two of the Storyboard Engine Public API calls.
First the application handle is obtained from the action_context
passed in from the Storyboard engine on callback:
app = gr_context_get_application(action_context);
Next we can register a custom logger callback handler function which simply writes the Storyboard logger output strings to a specified file:
gr_application_register_log_cb(app, log_writer, udata, &udata->old_cb, &udata->old_udata);
The full code implementation could look something like the following:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <inttypes.h> #ifdef _WINDOWS #include <windows.h> #else #include <unistd.h> // for usleep #endif #if defined(GRE_FEATURE_VFS_RESOURCES) #include #endif #include #include "ccallbacks.h" typedef struct _log_cb_data { gr_log_writer_cb_t old_cb; void* old_udata; FILE* file; } log_cb_data_t; void log_writer(gr_application_t* app, int level, const char* logmsg, void* udata) { log_cb_data_t* data = udata; fprintf(data->file, "[level:%d] %s\n", level, logmsg); //We can also choose to log to the original method provided by the logger: data->old_cb(app, level, logmsg, data->old_udata); } void CBRegisterLogger(gr_action_context_t* action_context) { gr_application_t* app; char* mode = "w"; log_cb_data_t* udata; app = gr_context_get_application(action_context); if (app == NULL) { printf("Exiting..."); return; } gre_app_log(app, GR_LOG_INFO, "In CBRegisterLogger() registering custom callback"); udata = calloc(1, sizeof(*udata)); if (!udata) { gre_app_log(app, GR_LOG_ERROR, "Failed to allocate memory : OOM"); return; } #ifdef _WINDOWS errno_t err; // Call your custom API here. In this sample we simply write to a file err = fopen_s(&udata->file, "c:\temp\custom_log.txt", mode); if (err) { gre_app_log(app, GR_LOG_ERROR, "Failed to open log file"); return; } #else // Call your custom API here. In this sample we simply write to a file using fopen udata->file = fopen("~/custom_log.txt", mode); if (NULL == udata->file) { gre_app_log(app, GR_LOG_ERROR, "Failed to open log file"); return; } #endif gr_application_register_log_cb(app, log_writer, udata, &udata->old_cb, &udata->old_udata); return; }
The CCallback DLL / .so project and Visual Studio C/C++ has a dependency on several Storyboard runtime files such as library libgre.lib which will need to be included as a linker library as well as several header files.
These resources are found in the Storyboard engine runtime package path eg:
<runtime engine path>\windows-x86_64-opengles_2.0-obj\lib <runtime engine path>\windows-x86_64-opengles_2.0-obj\include
A sample Makefile to build this example code for a Linux target would be :
SB_ROOT=<runtime engine path> SB_INC=-I$(SB_ROOT)/include SB_LIB=-L$(SB_ROOT)/lib # Linux users add this to build a shared library CFLAGS+=-shared -fPIC -g -Wall -DGRE_TARGET_OS_linux SB_LIB+=-lgre TARGET = ccallbacks.so all: $(TARGET) clean: rm -f $(TARGET) $(TARGET): ccallbacks.c $(CC) $(CFLAGS) $(SB_INC) ccallbacks.c -o $(TARGET) $(LDFLAGS) $(SB_LIB)
Once built your new CCallback extension library must be added to the Storyboard plugin launch options –occallback,
to specify the path and name of the component to load eg:
-occallback,path= C:\Project\example_ccallback_logger\Thermostat\ccallbacks.dll
Now when you simulate the project you should see the trace output both in the standard console and also emitted into the specified file eg: c:\Projects\test\custom_log.txt
[level:3] EVENT [0.447598]:IO: Queue [2] gre.screenshow.pre [level:3] EVENT [0.447878]:IO: Queue [4] gre.screenshow.post [level:3] EVENT [0.448033]:IO: Dispatch [gre.screenshow.pre] [level:3] EVENT [0.484086]:IO: Dispatch [gre.screenshow.post] [level:4] ACTION [0.484283]:ACTION: Invoke [gre.screenshow.post]->[gra.lua] on screen [thermostat] [level:4] ACTION [0.484524]:Lua: [cb_init_thermostat] [level:4] ACTION [0.484705]:ACTION: Invoke [gre.screenshow.post]->[gra.animate] on screen [thermostat] [level:4] ACTION [0.484986]:Animation Start: [buttons_show_thermostat] [level:4] ACTION [0.992986]:Animation Completed: [buttons_show_thermostat]->[gre.animate.complete.buttons_show_thermostat] [level:3] EVENT [0.993477]:IO: Queue [3] gre.animate.complete.buttons_show_thermostat [level:3] EVENT [0.997915]:IO: Dispatch [gre.animate.complete.buttons_show_thermostat] [level:4] ACTION [5.485688]:Animation Start: [fan_spin_thermostat]