Lesson 4: Composing Components to Send and Receive Messages

This lesson introduces two concepts: hierarchical descriptions of component graphs and how to form distributed applications that send and receive messages.  The applications that we will consider are apps/cnt_to_leds_and_rfm.desc and apps/rfm_to_led/rfm_to_leds.desc.  cnt_to_leds_and_rfm is essentially the counting variant of blink except that it is structured to use a generic output device.  Using the desc file, we can fan out the output to two devices - the LEDS and the Radio.  Down inside of the radio device, we see our messages are sent.  rfm_to_leds illustrates how messages are received; it takes a value that comes in from a packet and displays it on the LEDS, so the two function like a distributed display.  You could change the count application to something like sense and you would have a wireless sensor!
 

CNT_TO_LEDS_AND_RFM

The cnt_to_leds_and_rfm application is essentially an extension of BLINK, although it is structured to provide a more flexible composition.    The entire cnt_to_leds_and_rfm application description includes five components:

include modules{
  MAIN;
  COUNTER;
  INT_TO_LEDS;
  INT_TO_RFM;
  CLOCK;
};

As you've seen before, MAIN is a generic prelude system component used in most applications. CLOCK is a system component that accepts an INIT command to set its event frequency and signals a periodic clock event once initialized.   You may notice that none of these components are in the application directory, they are all shared among several application.  The logic is convey by the way they are wired together.  You may also notice the LEDS does not even appear.  This illustrates how description files may be used to introduce multicomponent abstractions - they are hierarchical.

COUNTER

The tos/shared/COUNTER.c component is the "guts" of the application.

COUNTER uses a TOS_FRAME to hold a counter state variable.
#define TOS_FRAME_TYPE COUNTER_frame
TOS_FRAME_BEGIN(COUNTER_frame) {
    char state;
}
TOS_FRAME_END(COUNTER_frame);

It accepts INIT and START commands from main to initialize its operation.

char TOS_COMMAND(COUNTER_INIT)(){
  VAR(state) = 0; /* initialize output component */
  return TOS_CALL_COMMAND(COUNTER_SUB_OUTPUT_INIT)();
}

char TOS_COMMAND(COUNTER_START)(){
  /* initialize clock component and start event processing */
  return TOS_CALL_COMMAND(COUNTER_SUB_CLOCK_INIT)(tick4ps);
}

COUNTER's Output

Rather than render the output directly to the LEDS, as in BLINK, COUNTER calls a command in a generic output device.

void TOS_EVENT(COUNTER_CLOCK_EVENT)(){
  VAR(state) ++;
  TOS_CALL_COMMAND(COUNTER_OUTPUT)(VAR(state));
}

The first novel aspect of COUNTER is that COUNTER_OUTPUT is wired into two output components: tos/shared/INT_TO_LEDS.c and tos/shared/INT_TO_RFM.c.

INT_TO_LEDS is a simple output device, which places the lower three bits of the integer value onto the LEDS.  This gives us the behavior of the countig variant of BLINK.

COUNTER:COUNTER_OUTPUT INT_TO_LEDS:INT_TO_LEDS_OUTPUT INT_TO_RFM:INT_TO_RFM_OUTPUT

A description file may fanout commands or events to multiple components.  The program generation tools handle the actual fanout.

If  you examine  tos/shared/INT_TO_LEDS.comp  you will see a new construct:

TOS_MODULE INT_TO_LEDS;
JOINTLY IMPLEMENTED_BY INT_TO_LEDS;

A component may be the root of a subgraph, described by the associated description file.  This is declared in the comp file using
JOINTLY IMPLEMENTED BY name;  The named file provides the subgraph rooted at the component.

Here   tos/shared/INT_TO_LEDS.desc  brings in one subcomponent, LED, and provides all the wiring to the primitive LED functions.

COUNTER also handles the event generated by asynchronous output devices upon completion.

COUNTER:COUNTER_OUTPUT_COMPLETE INT_TO_RFM:INT_TO_RFM_COMPLETE

/* Output Completion Event Handler
   Indicate that notification was successful */
char TOS_EVENT(COUNTER_OUTPUT_COMPLETE)(char success)
{
   return 1;
}
 

INT_TO_RFM: sending a message


INT_TO_RFM  ( tos/shared/INT_TO_RFM.c ) is the real point of this lesson - now nicely abstracted from the higher level application.  It's job is to send a packet  containing the output value to message handlers on all neighboring nodes.

Messages in TinyOS  follow the Active Message (AM) model, so when a packet is injected into the network it names a handler that will be invoked on the recipient nodes.  This fits especially nicely in the TinyOS event-driven environment, because message arrival is just another kind of event.

In any messaging layer, there are 5 aspects involved in successful transmission and reception (1) specifying what message is to be sent, (2) specifying who is to receive it, (3) determining when the storage associated with the source message can be reused, (4) providing buffering for the incoming message, and (5) processing the message.  Tiny Active Messages are no exception, however, the storage management is more constrained than is typical and the events are more natural.

TOS message buffers are contained in frames and must be declared using the declaration type TOS_Msg.  This provides a clean way to perform encapsulation as the packet moves through the stack without copying.

INT_TO_RFM declares a TOS_Msg type variable called, data, inside the application frame.

#define TOS_FRAME_TYPE INT_TO_RFM_frame
TOS_FRAME_BEGIN(INT_TO_RFM_frame)
{
  char pending;
  TOS_Msg data;
} TOS_FRAME_END(INT_TO_RFM_frame);

The TOS_Msg structure, defined in tos/include/MSG.h , has a field "data", which is a character pointer pointing to the beginning of the message's payload.  The payload is currently standardized to be 30 bytes long, although this is a configuration parameter.

You can bang away at the character string and produce unreadable message processing code, but a much better way is to let the compiler deal with all the packaging.   Use a C structure such as int_to_led_msg to describe the format of the packet payload.

typdef struct{
 char val;
 int src;
}int_to_led_msg;

INT_TO_RFM constructs and sends a message when an upper component such as COUNTER, issues a command.

char TOS_COMMAND(INT_TO_RFM_OUTPUT)(int val)
{
  ...
  return 0;
}

To place the data into the payload, we first declare a int_to_led_msg pointer variable, assign it to the address of the start of the payload, and fill in the value of each field.

int_to_led_msg* message = (int_to_led_msg*)VAR(data).data;
...
message->val = val;
message->src = TOS_LOCAL_ADDRESS;
...

To send out the packet, we call a SEND_MSG command specifying the destination node address, the handler id on the destination and the source message buffer (the buffer, not the payload).

if (TOS_COMMAND(INT_TO_RFM_SUB_SEND_MSG)(TOS_BCAST_ADDR, AM_MSG(INT_READING), &VAR(data)))

This command is a split-phase request, like acquiring sensor data; it starts the process of sending a message and runs concurrently with the caller.  Notice that the command may fail (like any other); this means that the messaging component didn't even accept the request.  If it succeeds, it is working on the send.

TOS_BCAST_ADDR is a special address signifying that any node that hears the message will handle it.  This is the native mode of radio operation.  Alternatively, you can specify a particular node and all other that hear the message will discard it.

The handler that will process the message on the specfied recipient nodes is specified using AM_MSG(handler_name). The handler will be declared as TOS_MSG_EVENT(handler_name).

In this case AM_MSG(INT_READING) specifies the handler.  Most of the time yu will find that messages are sent to similar conponents on other nodes, so it make sense to specify the handler on the sending side.  This example is a little unusual as we will build a recipient application that is different.  It will be register in a compatible fashion.

TinyOS message buffers follow a strict alternating ownership protocol to avoid expensive memory management while allowing concurrent operation.  If the message layer accepts the send command, it owns the send buffer and the requesting component should not touch it until the send is complete, as indicated by a send_done event.  The requesting component may use any method to track the status of the buffer.
The only time pointers are carried across component boundaries is pointers to TOS_MSGs.

Here we use a pending flag to keep track of the status of the buffer.  If the previous message is still being sent, we cannot touch the buffer, so we drop this output operation.  If the send buffer is available, we can modify it and request it to be sent.  If the send request fails, we clear the flag, so we can try another one later.  (Notice there is no race condition in the use of the flag.)
 

char TOS_COMMAND(INT_TO_RFM_OUTPUT)(int val){
  ...
  if (!VAR(pending)) {
    VAR(pending) = 1;
    message->val = val;
    message->src = TOS_LOCAL_ADDRESS;
    if (TOS_COMMAND(INT_TO_RFM_SUB_SEND_MSG)(TOS_BCAST_ADDR,
        AM_MSG(INT_READING), &VAR(data))) {
        return 1;
    } else {
      VAR(pending) = 0; /* request failed, free buffer */
    }
  }
  return 0;
}
 

GENERIC_COMM network stack


In tos/shared/INT_TO_RFM.desc, INT_TO_RFM is wired to tos/shared/GENERIC_COMM.comp, which is the component providing the generic network stack.  This is a much more substantial use of hierarchical description files.  If we look inside the joint description,    tos/shared/GENERIC_COMM.desc, we see that it contains six additional components, from the high level messaging component all the way down to modulation of individual bits on the radio link.  Many of these components exist in numerous versions for different application contexts, but this is nicely abstracted.
 

The TOS_CALL_COMMAND(INT_TO_RFM_SUB_SEND_MSG)(TOS_BCAST_ADDR,
                                                    AM_MSG(INT_READING), &VAR(data))

directly links to

char COMM_SEND_MSG(short addr, char type, TOS_Msg* data) in GENERIC_COMM.

The COMM_SEND_MSG command returns a value to indicate whether it accepts the request or not. Return value of 1 indicates request is accepted while a return value of 0 indicates rejection.

The destination handler is wired to a specific Active Message handler.  The AM component will dispatch incoming message to many different components that send messages within an application.

Here the INT_TO_RFM:INT_READING message handler is wired to GENERIC_COMM:GENERIC_COMM_MSG_HANDLER_4.  GENERIC_COMM provides the mapping from handler name to handler identifier (here 4).  An application specific register of handler types is maintained in the release.

When transmission of a message is completed, GENERIC_COMM will signal a COMM_SEND_DONE event to all components that are registered with the Active Message component.  A pointer to the buffer sent is provided as an argument to the event.

Here this is wired to INT_TO_RFM_SUB_MSG_SEND_DONE event handler.  It checks if the buffer is its own and, if so, clears the pending flag and signals COUNTER:COUNTER_OUTPUT_COMPLETE.

char TOS_EVENT(INT_TO_RFM_SUB_MSG_SEND_DONE)(TOS_MsgPtr sentBuffer){
  if (VAR(pending) && sentBuffer == &VAR(data)) {
    VAR(pending) = 0;
    TOS_SIGNAL_EVENT(INT_TO_RFM_COMPLETE)(1);
    return 1;
  }
  return 0;
}

This event is broadcast to all potential senders because other components may have failed to send a previous message and be waiting for the message layer to become available.  We don't want them to spin consuming precious resources, so the notification is provided.

On the receiving side, the INT_READING msg event will be signaled.  In this case, we will use a different application to handle that.
 

Receiving Messages with RFM_TO_LEDS

apps/rfm_to_leds/rfm_to_leds.desc is the application that receives the message from  CNT_TO_LEDS_AND_RFM, and displays the least significant 3 bits of the integer from the packet on the LEDs.  Similar to CNT_TO_LEDS_AND_RFM,  this application is also a composition of different components.

include modules{
  MAIN;
  RFM_TO_LEDS;
};

The RFM_TO_LEDS compoent in tos/shared/RFM_TO_LEDS.desc uses GENERIC_COMM to receive messages,and INT_TO_LEDS as its output device.

include modules{
  GENERIC_COMM;
  INT_TO_LEDS;
};

In tos/shared/RFM_TO_LEDS.desc,  wiring to the GENERIC_COMM for receiving messages, and INT_TO_LEDS for outputing the value on the LEDs are shown below

RFM_TO_LEDS:INT_READING GENERIC_COMM:GENERIC_COMM_MSG_HANDLER_4
RFM_TO_LEDS:RFM_TO_LEDS_LED_OUTPUT INT_TO_LEDS:INT_TO_LEDS_OUTPUT

Recall that INT_TO_RFM:INT_READING has an Active Message handler type that equals 4. RFM_TO_LEDS should also wire its INT_READING handler to have the same Active Message type equals 4.

Storage management of the receiving side is inherently dynamic.  A message arrives and fills a buffer.  The Active Message layer decodes the handler type and dispatches it.  It gives the buffer to the application component when it does so, but the application must return a pointer to a free buffer upon completion.

All GENERIC_COMM handlers have the following interface that application handler for incoming messages:.

TOS_MsgPtr GENERIC_COMM_HANDLER_X(TOS_MsgPtr data);

where data is the TOS_Msg pointer pointing to the incoming message.

In RFM_TO_LEDS, this is the INT_READING message handler:
/* Active Message handler
  Pull out the data and send it to the output. */

TOS_MsgPtr TOS_MSG_EVENT(INT_READING)(TOS_MsgPtr msg){
  int_to_led_msg* message = (int_to_led_msg*)msg->data;

  TOS_CALL_COMMAND(RFM_TO_LEDS_LED_OUTPUT)(message->val);
  return msg;
}

Observe that the event process the data within the packet and returns the buffer it was provided.

A clean way to extract the data from the payload is to use the same int_to_led_msg structure that we used in INT_TO_RFM.   The integer value can  then be retrieved by the expression, message->val.  The value is then passed  as an argument to the command RFM_TO_LEDS_LED_OUTPUT, which displays the least significant 3 bits of the integer on the LEDs.
 

Underlying details

Each message contains a header containing system specific information.  In order to allow multiple, logically distinct networks to share the channel, collections of communicating nodes carry a group id.

In this lab setting, you will want a unique group id.  Edit apps/Makeinclude and change the line DEFAULT_LOCAL_GROUP = 0x7D to be the number on your programming board.

In addition, the header carries the destination node number (which may be BCAST_ADDR or the special UART address) and the handler id.

We have standardized our message size to be 36 bytes, where 4 bytes are allocated for the header (2 bytes for destination, 1 byte for type, and 1 byte for GROUP_ID), 30 bytes are allocated for application data payload, and 2 bytes are allocated for 16-bit CRC checksum.   It is important to note that there are two addresses that have special purposes. TOS_BCAST_ADDR (0xffff) is used as a broadcast address while TOS_UART_ADDR (0x7e) is for messages destined to the serial port directly. Finally, TOS_LOCAL_ADDRESS is the address of the current node.

Exercise:

Question, what would happen if you built multiple cnt_to_leds_and rfm nodes and turned them on?

Compile and run both apps/cnt_to_leds_and_rfm.desc and  apps/rfm_to_leds/rfm_to_leds.desc. When you turn on cnt_to_leds_and_rfm you should see the count displayed on the rfm_to_leds device.  Congratulations  you are doing wireless networking.   Can you change this to provide a wireless sensor?  (hint: SENS_TO_RFM).


< Previous Lesson | Next Lesson >