User-Defined Tasks
Sample Task
Most situations can be handled using the rule engine that's provided with the Vesta. However, there could be situations where more complex logic is required. As an Open Source platform, the Vesta comes with complete source code and the ability to add custom code. This section describes the process of compiling a task that allows the Vesta to perform operations that can't easily be done using the standard rule engine.
Tasks are written in C++ and built on a standard template using library functions to access shared memory and other services. The source and Makefile for all Vesta tasks are in the /usr/local/vesta/bbb/src directory. This page documents the process of creating user-defined Vesta tasks using the ctlUsr1.cpp program that’s distributed with the system. We’ll go through the sample program section by section.
Well-Behaved Tasks
Vesta tasks are completely independent of each other - they only communicate through shared memory. All Vesta data - physical inputs and outputs as well as variables - are managed in shared memory as 'data elements', and most task activity is concerned with reading and writing data element values. In the Vesta system, there are a number of things a task must do to be 'well-behaved' - that is, to run smoothly and in harmony with other tasks.
- A task needs to have a unique ID. The task ID is used with almost all shared memory API functions such as claiming write access to shared memory data elements. The ID values are defined in shm.h, and there are four reserved for user defined tasks - usr1_id, usr2_id, usr3_id, and usr4_id.
- Using it's ID, a task must link to shared memory.
- In most cases, tasks should claim the data elements that they will write values to.
- Tasks must check for and respond appropriately to 'reload' requests. These generally indicate that there has been a configuration change that requires re-initializing.
- Tasks run at a defined frequency, and must sleep to free up resources for other tasks. The ‘Settings’ tab provides a mechanism for setting task frequency.
The shared memory API provides functions to facilitate these task behaviors.
This example will be written to run as USR1, so its task ID will be usr1_id and it will run at an interval determined by the usr1_period value in shared memory.
Task Outline
All well-behaved tasks have the same basic structure:
Perform startup initialization (get shared memory link at a minimum)
Eternal Loop: { Perform reload (re-initialization) if instructed Read data from shared memory Do some computation Write results to shared memory Sleep until next cycle }
Task Support API
The system provides a set of functions that simplify writing a well behaved task. These functions handle interaction with shared memory as well as sleep management.
This sample task uses most of the task support API:
- get_shm(taskid) - get link to shared memory
- getNow() - get current timestamp
- reloadRequired(taskid) - true if this task needs to reload data due to changes
- clearReloadFlag(taskid) - clear reload flag for this task
- claimElement(elementid,taskid) - claim write access to a shared memory data element
- elementValue(elementid) - get value of data element from shared memory
- setElement(elementid,value,diskflag) - set data element value in shared memory
- timedSleep(processname,period,starttime) - sleep until next cycle is due
Sample Task Function
This task will perform the following functions:
- Read two temperature values
- Determine the difference
- Find the higher of the two
- Set one variable to the value of the difference
- Set another variable equal to the higher of the two
Screenshot of control panel with sampleTask running:
Sample Task Code
This section contains the complete code for sampleTask, with each section described in detail.
Includes and Defines
Like all C++ programs, you’ll need to include header files appropriate to the functions that you use. Additionally, you’ll need to include ‘shm.h’ and 'vestautils.h'.
#include <stdio.h> #include <iostream> #include "shm.h" #include "vestautils.h" // If you have struct or class definitions, put them in usr1.h and include here //#include "usr1.h" using namespace vesta; using namespace std;
Variables
Like all C++ programs, a function named ‘main’ is required. In this case all of the code is in this single function. We’ll need a variable to keep track of timing - start_usec in this example. We also have variables to hold the temperatures from shared memory and the values that we’ll write to shared memory. The ID values for Vesta elements (Physical I/O and variables) can be obtained using the 'View' link under 'Elements Table' on the 'Settings' tab.
int main(){ // Variable for calculating sleep interval unsigned long long start_usec; // We need element ID values for elements that we'll use. // Normally will get list from disk file. In this case we'll hard code. int temp1_id = 3; int temp2_id = 4; int diff_id = 109; int max_id = 113; // Variables to store shared memory values. Not necessary but can improve readability. float temp1, temp2, diff, max; // For error messages char logbuffer[80];
Initialization
There are four initializations that could be necessary:
- Establish shared memory link
- Initialize cycle timer
- Claim shared memory variables that this task will write to
- Read task-specific data from disk file(s)
Initializing shared memory and cycle timer:
// ************ One - time initialization ************ // Get shared memory link. This will create shared memory if necessary. Must pass valid ID: // usr1_id, usr2_ID, usr3_ID, or usr4_ID SHM shm(usr1_id); // Get start time for sleep calculations start_usec = getNow();
Controller tasks typically run forever with a timed sleep after each pass through the main loop.
// Do forever while (1){
At the start of each cycle, check to see if there has been a change that requires us to refresh our shared memory or disk file data. For instance, the user could have changed element numbers. There’s a shared memory flag that we check to see if we need to re-initialize. This is always invoked the very first pass through. At the very least, we should claim the shared memory elements that we are going to write to. Claiming elements will generate warnings if other tasks have already claimed them, although it is legal for more than one process to write to the same element.
// ************ As-needed reinitialization ************ // Each cycle, check to see if we should respond to config changes in shared memory. // If we read a disk file, we should do that here too. Must use valid ID here as well if (shm.reloadRequired(usr1_id)){ // Claim our elements (the ones we'll write to) shm.claimElement(diff_id,usr1_id); shm.claimElement(max_id,usr1_id); // Clear our bit in the reload flag shm.clearReloadFlag(usr1_id); }
Do the Work
Now that we’re through with any initialization, we need to do the actual task. Typically this involves reading some data from shared memory, performing some computation, and writing values back into shared memory. In this example, we’ll read two temperature values as ‘temp1’ and ‘temp2’. We’ll calculate the difference and determine the larger of the two temperatures.
// ************ Read Shared Memory Values ************ // Read values from shared memory using elementValue() function temp1 = shm.elementValue(temp1_id); temp2 = shm.elementValue(temp2_id); // ************ Perform Calculations ************ // Calculate diff diff = temp1 - temp2; // Determine value for max if(temp1 > temp2){ max = temp1; }else{ max = temp2; }
Post the Results
Once the calculations are complete, the resulting values will be written back into two shared memory variables using the setElement() function. This function requires three arguments:
- The ID of the element to be written
- The value to be written
- A ‘disk write’ flag
The disk write flag tells the controller whether to update the elements.csv file on disk with the new value. Writing to disk means that the value will persist across system restarts. This would be appropriate for a user-defined variable such as ‘Top Floor Setpoint’, but would be pointless for this example. We’ll use a value of ‘0’ to indicate that the disk file does not need to be written.
// ************ Write results to shared memory ************ // Update shared memory with new values using setElement. // setElement needs element ID, value, and disk write flag. We don't need to save these values to disk. shm.setElement(diff_id, diff, 0); shm.setElement(max_id, max, 0);
Sleep
Once the work is done, we need to sleep until it’s time to run again. Our sleep interval is determined by a system configuration value - in this case, usr1_period which can be set on the controller’s ‘System’ tab.
// ************ Sleep ************ // Sleep until next cycle. shm.timedSleep(usr1_id, &start_usec);