Tutorial for new customizations

This tutorial explain how to modify the VMCustom module of Terra System to add new functionalities. Having some familiarity with nesC will help you understand this tutorial. Also understanding the event-driven model and the importance of not creating blocking operations is critical to avoiding side effects in the Terra execution. Finally, it is strongly recommended to already know the Terra programming environment, especially the Céu-T features.

Customization Process

The next figure presents the customization and usage flows. The developer activities related to customization are indicated in blue and the user activities related to application script are indicated in red. The user activities are presented in more details in the Terra tutorial page and at the end of each Terra Platform page like this one. Next section presents the developer activities to customize a basic Terra implementation.

The main developer activities are:


Figure: Customization flow (Dev) vs Usage flow (User)
Customization Process Diagram

The developer activities

Note: This procedure assumes that you already have Terra ported to the desired platform with a very basic customization. In general this very basic customization is based on TerraNet customization.

At least three files must be edited to add new functionalities to a specific customization.

The definitions between these three files must be compatibles. In the case of events, the numeric IDs must be equal to equivalent operations. In the case of data structures, the type and position of the each fields must be equivalent. Same events and functions may have different names between the files, considering that the numeric IDs are the same.

The syntax of each type of operations are described in the sequence. The last section presents some real code examples.

In general, small functionalities are written inside VMCustomP.nc file. If a new functionality requires additional files, a nesC component and/or a library of external functions may be created. In this case the developer may need to modify the VMCustomC.nc file and must modify the Make system to be able to compile the new functionalities. These modifications are out of scope of this tutorial.

TerraXXX.defs file

This file is used during terrac compilation to validate the event and function names. Any customized data structure used by events or functions are also defined in this file. Additionally, the developer may define constant values to be used in the new functionalities.

Constant definition

A constant definition uses the #define command as used in the C language. Some examples are:

#define HIGH 1
#define LOW 0
#define ON 1
#define OFF 0
Data structures definition
Note: you need to be familiar with Terra data structures. The page Céu-T Language presents details about the Types and data structures of Terra scripting language.

Registers - Some new functionalities may need exchange values that a single basic type cannot support. In these cases we need to create a specific data structure that includes all needed values. In Céu-T language, a regtype declaration creates a new register type. A register can only have fields that are values of basic Terra types or arrays of basic types. In Céu-T, all variables must start with a lowercase character.

// regtype example
regtype dhtData_t with
	var ubyte stat;
	var ubyte hum;
	var ubyte temp;
end

Packets - In special cases the new functionality may leave some fields to be defined by the user. This is the case of Radio messages where one part of the message (header) is predefined by the Terra message protocol and second part (payload) is application dependent. The pktype declaration creates a new abstract type with predefined fields and a payload section. The predefined fields depend on the new functionality needs and the payload section is left to the user application needs.

// pktype example
packet radioMsg with
	var ubyte type;
	var ushort source;
	var ushort target;
	var payload[20] data;
end

When using registers in events or functions, Terra system always pass the data address instead of data values. Then, you must use the TerraVM internal support to copy memory data when building your customized function. See below the "VMCustomP.nc file" section to find some examples.

Output event definition

Output events are defined by:

output void EventName ArgumentType NumericID;

Where:

An example of output event definition is:

output void REQ_DHT ubyte 10;
Note: all output events must return void.
Input event definition

Input events are defined by:

input ReturnType EventName ArgumentType NumericID;

Where:

An example of input event definition is:

input ubyte SEND_DONE void 41;
Function definition

Functions are defined by:

function ReturnType FunctionName(ListOfArgumentsType) NumericID;

Where:

Examples of function definition are:

function ushort getNodeId() 0;
function ubyte  pinWrite(ubyte,ubyte,ubyte) 21;

VMCustom.h file

This file defines the events and function numeric identifiers to be used during the building of TerraVM firmware. Each numeric ID must be equal to the equivalent one in the .defs file. These IDs are defined as a list of enumeration values as:

enum{
  OutEvent1 =NumericId1,
  OutEvent2 =NumericId2,
  InEvent1  =NumericId1,
  InEvent2  =NumericId2,
  Function1 =NumericId1,
  Function2 =NumericId2,
}
Note: the Numeric ID must be different only for each group of OutEvent, InEvent, and Function.

VMCustomP.nc file

The entry point of custom functionalities are written in this file. In the case of functions or output events, you may write all functionality code inside this file or you may call external library functions or nesC component commands. In the case of input events you need to call a internal TerraVM function passing the event ID and event data as argument. In general an input event is triggered by a nesC event, a callback operation, or some kind of system interruption. Data structures must be created when required.

Note: customized operations must comply to Terra asynchronous execution model. Blocking operations are forbidden!
Custom data structures

If you have created a custom data structures in the .defs file, then you must define an equivalent structure in nesC code to be used by the custom operations. The definition of this structure can be placed in any file of your project at your convenience. For example, may be in the VMCustom.h, VMCustomP.nc, or another additional file.

The rules to convert the structures are very simple, (i) each basic data type of Ceú-T has its equivalent in nesC, (ii) you must use nesC typedef nx_struct construct to define the structure, and (iii) you need to follow the same sequence of the fields definition.

The next code shows an example of an equivalent data structure definitions.

// Céu-T Struct                // nesC nx struct
regtype dhtData_t with         typedef nx_struct dhtData {
  var ubyte stat;                nx_uint8_t stat;
  var ubyte hum;                 nx_uint8_t hum;
  var ubyte temp;                nx_uint8_t temp;
end                            } dhtData_t;

Each Céu-T type must be associated to respective network type nx as defined in the following tables

Céu-T type C type
ubyte nx_uint8_t
ushort nx_uint16_t
ulong nx_uint32_t
float nx_float
Céu-T type C type
byte nx_int8_t
short nx_int16_t
long nx_int32_t
Output event - code structure

During the script execution, a Céu-T command like emit EVTx(Arg1); will call the command VM.procOutEvt(uint8_t id,uint32_t value), where id is the EVTx numeric identifier and value is a integer value representing either the value of argument or the address of data structure.

Then, the VM.procOutEvt() function implements a switch/case block for all expected output events. Each case tests the id to call a specific function like proc_EvtX(id,value);. The new custom operation must be implemented in this function paying attention to get corretly the argument value. If the value is an integer argument, a cast operation is enough to get the correct value like: uint16_t val = (uint16_t)value;. If the value is an address, you must execute the function VM.getRealAddr() casting the result to the type of expected data structure. For example: usrMsg_t* usrMsg = (usrMsg_t*)signal VM.getRealAddr(value);

In the case of a new output event, you must create a new case inside VM.procOutEvt() and must implement the respective proc_xxxx() function. The following code shows a example of customized output event.

// Custom operation
void proc_req_dht_read(uint16_t id, uint32_t value){
	call dht.read((uint8_t)value);
}

// Entry point
command void VM.procOutEvt(uint8_t id,uint32_t value){
  switch (id){
	...
    case O_DHT  : proc_req_dht_read(id,value); break;
	...
  }
}
Function - code structure

Function entry points are very similar to those for exit events. The function VM.callFunction(uint8_t id) is called when a Céu-T function is executed. The id argument identify the function and, similar to the output event, a block of switch/case calls a specific func_xxx(id);. The main differences are that a function allows more than one argument and always returns a basic type value. Both the arguments and return values are passed using the stack, this allows the use of functions in expressions.

Similar to the output events, the new custom operation must be implemented in func_xxx(id); function also paying attention to get corretly the values of each argument. The arguments must be popped out in reverse sequence, from the last to the first, using the function VM.pop(); and casting the result to corresponding type. For example, use the comand uint8_t val = (uint8_t)signal VM.pop(); to pop a ubyte argument. If the argument is a data structure address, you must first pop it as ushort type and use the function VM.getRealAddr() to get the data pointer value.

After getting the arguments and executing the custom function operation, you must push the result value to the stack. Use the function VM.push(val); to push a basic type value to the stack. Remenber that you can push only the value type that was defined in the .defs file. The following code shows a example of customized function.

// Custom function
void func_pinWrite(uint16_t id){
  uint8_t stat=0;
  uint8_t port, pin, val;
  val  = (uint8_t)signal VM.pop(); // pop 3⁰ arg from stack
  pin  = (uint8_t)signal VM.pop(); // pop 2⁰ arg from stack
  port = (uint8_t)signal VM.pop(); // pop 1⁰ arg from stack
  stat = pinWrite(port,pin,val);   // execute local function
  signal VM.push(stat);            // Returns the stat value
}

// Entry point
command void VM.callFunction(uint8_t id){
  switch (id){
  	...
    case F_PIN_WRITE : func_pinWrite(id); break;
	...
  }
}
Input event - code structure

In general, a input event is generated outside the core of the Virtual Machine and must be notified to the VM engine. The command VM.queueEvt() is used to enqueue a Terra event and its respective data allowing the engine to trigger, as necesserary, the script await commands.

Note: In the absence of an active await, Terra engine discards the respective event.

Depending on the custom operation, the event source may be a nesC event, a callback operation, or some kind of system interruption. All options will end up running a local customized function that must call VM.queueEvt() passing the event ID, the sub-event value, and a pointer to the data buffer.

The sub-event value is an uint8_t value used as base to filter a specific await that must be triggered for same input event. For example, in the Radio receive event, the sub-event indicates the user message ID value. In this case is possible to have a Céu-T trail waiting for a RECEIVE(id) event with a specific message ID. The sub-event 0 is used as general sub-event ID that triggers events without arguments. For example, the command msg = await RECEIVE(); will be triggered for any received message, no matter what the message ID is. The RECEIVE event is an example where the VM.queueEvt() is called twice, one passing the message ID as sub-event and another passing 0 as sub-event.

As the event receiving is not synchronous with VM engine execution, an event must stores its data in a global buffer. Later, when handling internally the event, the VM engine copies, if necessary, the buffer to the target script variable or data structure. Each event must have its own data buffer. This data buffer is a global variable or global data structure that stores, when necessary, the last event data. If a next specific event may happen before the previous one be handled by the VM engine, the custom implementation may allocate more than one buffer avoiding data overlay. For example, a circular buffer may be used to avoid data overlay.

Note: 1 - Be cautious when using large buffers, mainly when running TerraVM in low memory platforms.
2 - In general, a single buffer by event type is enough for devices where the own hardware limmits the data rate.

Next code excerpt shows an example of custom input event implementation for the SEND_DONE event. In this event the VM.queueEvt() is called twice for different sub-event IDs.


nx_uint8_t ExtDataSendDoneError;
event void BSRadio.sendDone(uint8_t am_id,message_t* msg,void* dataMsg, error_t error){
  if (am_id == AM_USRMSG){
    ExtDataSendDoneError = (uint8_t)error;
    signal VM.queueEvt(I_SEND_DONE_ID, ((usrMsg_t*)dataMsg)->type, &ExtDataSendDoneError);
    signal VM.queueEvt(I_SEND_DONE   ,                          0, &ExtDataSendDoneError);
  }
}

Implementation examples

Example file for TerraXXX.defs

// Interruption used for DHT sensor
#define INT0 0  // PD0
#define INT1 1  // PD1 
#define INT2 2  // PD2 
#define INT3 3  // PD3 

// DHT11 data structure
regtype dhtData_t with
	var ubyte stat;
	var ubyte hum;
	var ubyte temp;
end

// radioMsg - Radio/Queue Data Message Register
packet radioMsg with
	var ubyte type;
	var ushort source;
	var ushort target;
	var payload[20] data;
end

output void   REQ_DHT   ubyte    10;
output void   SEND      radioMsg 40;

input  dhtData_t DHT    void      6;
input  ubyte     SEND_DONE ubyte 40;
input  ubyte     SEND_DONE void  41;

function ushort getNodeId()                      0;
function ubyte  pinWrite(ubyte,ubyte,ubyte) 	21;

Example file for VMCustom.h

enum{
  O_DHT           =10,
  O_SEND          =40,

  I_DHT           = 6,
  I_SEND_DONE_ID  =40,
  I_SEND_DONE     =41,
  
  F_GETNODEID     =  0,
  F_PIN_WRITE     = 21,
}

Example file for VMCustomP.nc

void proc_req_dht_read(uint16_t id, uint32_t value){
	call dht.read((uint8_t)value);
}

void  proc_send(uint16_t id, uint32_t addr){
  usrMsg_t* usrMsg;
  usrMsg = (usrMsg_t*)signal VM.getRealAddr(addr);
  // .....
}

void  func_getNodeId(uint16_t id){
  uint16_t stat;
  stat = TOS_NODE_ID;
  signal VM.push(stat);// Returns a value
}	

void func_pinWrite(uint16_t id){
  uint8_t stat=0;
  uint8_t port, pin, val;
  val  = (uint8_t)signal VM.pop(); // pop 3⁰ arg from stack
  pin  = (uint8_t)signal VM.pop(); // pop 2⁰ arg from stack
  port = (uint8_t)signal VM.pop(); // pop 1⁰ arg from stack
  stat = pinWrite(port,pin,val);   // execute local function
  signal VM.push(stat);            // Returns a value
}

dhtData_t ExtDHTData;
event void dht.readDone(dhtData_t* data){
  memcpy(&ExtDHTData,data,sizeof(dhtData_t));  // copy data to event buffer
  signal VM.queueEvt(I_DHT, 0, &ExtDHTData); // Generates a Terra event
}

nx_uint8_t ExtDataSendDoneError;
event void BSRadio.sendDone(uint8_t am_id,message_t* msg,void* dataMsg, error_t error){
  if (am_id == AM_USRMSG){
    ExtDataSendDoneError = (uint8_t)error;
    signal VM.queueEvt(I_SEND_DONE_ID, ((usrMsg_t*)dataMsg)->type, &ExtDataSendDoneError);
    signal VM.queueEvt(I_SEND_DONE   ,                          0, &ExtDataSendDoneError);
  }
}

command void VM.procOutEvt(uint8_t id,uint32_t value){
  switch (id){
    case O_DHT  : proc_req_dht_read(id,value); break;
    case O_SEND : proc_send(id,value); break;
  }
}

command void VM.callFunction(uint8_t id){
  switch (id){
    case F_GETNODEID : func_getNodeId(id); break;
    case F_PIN_WRITE : func_pinWrite(id); break;
  }
}