= Tutorial on using the networking API =

The network stack in nano-RK follows a traditional 5 layered approach. The interaction between the different layers however is more tightly coupled than in a traditional network stack due to the severe resource constraints imposed by sensor nodes. The architecture of the stack is diagrammed below.

Image(NetworkStack.png, 120px)

There are two main data structures that are used extensively by the stack

 1/* This represents a standard UDP segment */
 2typedef struct
 3{
 4    uint8_t srcPort;            // source port of the application
 5    uint8_t destPort;            // destination port of the application
 6    int8_t length;                // actual length of the data payload
 7    uint8_t data[MAX_APP_PAYLOAD];    // application layer data 
 8
 9}Transport_Segment_UDP;
10
11/* This represents a network layer packet */
12typedef struct
13{
14    uint16_t src;    // source address
15    uint16_t dest;    // destination address
16    int8_t ttl;    // time to live
17    uint8_t type;     // bit mask for packet type and subtype
18
19    int8_t length;    // actual length of payload
20    int8_t prio;    // priority of this packet 
21    uint8_t data[MAX_NETWORK_PAYLOAD]; // payload
22}NW_Packet;
23

As can be seen above, the transport layer currently implements only UDP-like functionality. Each network layer packet encapuslates one transport layer segment which in turn encapsulates the application payload. The value of MAX_APP_PAYLOAD is configurable and will be explained later in this tutorial.

A brief description of each component in the architecture is given below.

1. Buffer Manager: This module is responsible for managing the various queues in the system.

a. Receive queue: Each application task in nano-RK can request a queue of receive buffers for it's use. The queue is priority based which means that higher priority messages will be delivered first to the application. This also implies that if the receive queue for a task is full, a new message of high priority will replace the lowest priority message in the queue.
These priority values can be set by the individual tasks while sending their messages. Priority values are integers ranging from 1 to MAX_PRIORITY (31). Higher values mean higher priority.

The buffer manager also provides an 'excess policy' setting for each priority level. Consider the situation in which there are two message priority levels HIGH and LOW. Currently the receive queue for a task is full with HIGH priority messages. A new HIGH priority message now arrives. There are two options available to the buffer manager. One is to simply drop the new message (DROP__ behavior). Another is to replace the most recently received message (of the same priority) with the new one (OVERWRITE__ behavior). Nano-RK allows the application designer to specify a setting for each of the 31 priority levels. How this can be done can be seen from the API for the buffer manager. The default setting is OVERWRITE__ for all priority levels.

b. Transmit queue: There is a single transmit queue for the entire system. This queue is also kept sorted as per priority of messages. When an application executes a 'send' operation (see API), the transport layer builds a network layer packet containing the application payload and inserts it into this queue based on its priority level. Eventually the message will get sent over the radio.

2. Transport Layer: This module implements a UDP-like transport layer for the application tasks. It provides a very familiar and easy-to-use sockets API for the tasks. Refer to the API for details on the available system calls.

The transport layer also maintains two signals per port. An application after executing send() can wait for a 'send_done' signal which will be sent by the network layer when its message is actually sent over the radio. Similarly an application doing a receive() will block till a 'data_arrived' signal is sent by the network layer indicating that a message has been buffered in the application's receive queue. Timeouts can also be specified using the set_timeout() call.

3. Network Layer: This module consists of two tasks. The tx_task is responsible for emptying out the transmit queue and sending the 'send_done' signals to the various applications. The rx_task is responsible for collecting packets from the link layer and inserting them into the corresponding receive queues for the applications. In addition, network control operations like routing, neighbor discovery etc are also performed by the network layer. Just like in traditional networks, each sensor node is identified with a unique address which is a 16 bit number ranging from 1 to 65535. Currently a flat addressing scheme has been employed. We plan to incorporate a hierarchical scheme soon.

4. Link layer: The link layer in nano-RK implements either the BMAC or the RT-Link protocol. No buffering is done at this layer and it is responsible for transmitting and receiving packets one at a time over the radio. The network layer collects these packets from the link layer.

5. Physical Layer: // to be filled

Network Stack configuration

All configurable parameters are placed together in the NWStackConfig.h file. This file can be edited by application designers depending on their needs. These parameters are explained below.

 1#define MAX_APP_PAYLOAD  24         // maximum size of the application payload in bytes 
 2#define MAX_SERIAL_PAYLOAD 24          // maximum size of serial data payload
 3#define MAX_RX_QUEUE_SIZE 16         // maximum number of receive buffers available in the 
 4
 5#define MAX_TX_QUEUE_SIZE 16          // maximum number of transmit buffers for the system
 6#define NUM_PORTS 4             // number of ports available on the node  
 7#define DEFAULT_EXCESS_POLICY OVERWRITE    // default setting for the excess policy 
 8#define CONNECTED_TO_GATEWAY TRUE    // indicates whether this node connects to the gateway     
 9

Note that the network stack supports only 127 ports (although the individual port numbers can be taken from 1 to 255). Hence setting NUM_PORTS to be greater than 127 will result in the node stopping its operation and flagging an error message.

Error handling

Almost all the networking APIs return NRK_OK on success and NRK_ERROR on failure. When NRK_ERROR is returned, a error number is set in a system variable. This number can be accessed by issuing a call to nrk_errno_get().

The print_nw_stack_errno(int8_t n) routine will print out a descriptive string explaining the error given an error number 'n'. This should be used by applications for debugging purposes.

Since the code is yet in its beta release, it is not entirely stable and fully tested. Several bug detection statements have been purposefully incorporated inside the various system calls. On detection, the node will hang, the red LED will light up and a bug detection message will get displayed on the terminal in an infinite loop. We would appreciate it if you could report such bugs to the nano-RK team alongwith the application code that discovered the bug.

Include files

The following files need to be included by all applications in order for them to use the networking APIs

1#include <TransportLayerUDP.h>
2#include <NetworkLayer.h>
3#include <Serial.h>
4#include <Pack.h>
5#include <Debug.h>
6#include <NWStackConfig.h>

Example usage: The basic_networking project under /projects/ directory illustrates how a simple client server program can be written using the networking APIs. We will briefly go through them again here.

The main() function of every application should start off by making a call to nrk_init_nw_stack();. This function initializes the network stack.

Server: To build a server, you first need to create a socket

1int8_t sock;
2sock = create_socket(SOCK_DGRAM);

The network stack at present supports 3 types of sockets.

SOCK_DGRAM: Socket that provides UDP-like functionality

SOCK_IPC: For interprocess communication within a node. Although, inter-task communication in Nano-RK can be done by signals, a socket interface can also be used. The API is identical to that for SOCK_DGRAM except now the communicating tasks will be running on the same node.

SOCK_RAW: This allows an application open a raw socket that bypasses the features of the network and transport layers and enables the application to communicate directly with the link layer. In this case, the socket APIs will simply be wrappers around
calls to the link layer functions. Programming using raw sockets is explained
here

create_socket() returns a socket descriptor which is an integer from 0 - (NUM_PORTS - 1). This descriptor should be used in all future calls to the socket APIs

The next step is to bind this socket to a port number (say 60). The port number space ranges from 1 to 255. Port 0 is reserved and cannot be used. Additionally, ports 1 to 20 are reserved for system level tasks (that handle routing, topology discovery etc) and hence ports 21 to 255 are available for applications' use.

1bind(sock, 60);

bind() by default sets aside a receive buffer space of size 1. If the server you are building is expected to cater to large traffic, you may optionally request the transport layer to set aside more receive buffers for this purpose.

1set_rx_queue_size(sock, 4);

The above call will try and set aside 4 receive buffers for the socket. The total number of receive buffers available on the system is defined by the macro MAX_RX_QUEUE_SIZE in NWStackConfig.h. Its possible that the requested number of buffers may not be available on the system at the time of the call. The function returns the actual number of buffers set aside for the socket. At any time, the get_rx_queue_size() function can also be used to read the number of buffers set aside for the socket

After this step, the server can read data from the socket with the receive() call

1int8_t len;
2uint16_t addr;
3uint8_t port;
4int8_t rssi;
5uint8_t *buf;
6
7buf = receive(sock, &len, &addr, &port, &rssi);
8

This function returns a pointer to the receive buffer containing the highest priority message for the task. The length of the message can be obtained from the value-result argument 'len'. The transport layer will also insert the source address and source port of the message into the 3rd and 4th arguments respectively. The rssi with which the underlying packet was received is placed in the 5th argument. If the task is not interested in these latter 3 values, NULLs can be passed as shown below

1buf = receive(sock, &len, NULL, NULL, NULL);

By default, the receive() is blocking, which means that the call will return only when a message is received for the socket. If you want to specify a maximum waiting period, use the set_timeout() function prior to making this call.

1set_timeout(sock, 1, 0);

This sets a timeout of 1 second on the socket and a subsequent receive() will wait for a maximum of this period before returning. If no message arrives within the timeout period, a NULL is returned.

A timeout value of (0,0) is not valid. If you just want to check whether the receive queue holds any new messages, use the check_receive_queue() function and if this returns a positive number, a subsequent call to receive() will return immediately with a pointer to the highest priority message.

Remember that receive() is returning a pointer to the buffer and not the actual buffer itself. Its a good idea to quickly copy the buffer contents to a local storage area and then release the buffer back to the buffer manager. This can be achieved using the release_buffer() call

1release_buffer(sock, buf); // pass the same pointer received earlier

In this way, the buffer manager will have enough space to hold newly arriving messages.

If the task wants to send out a message, the send() call can be used.

1uint8_t tx_bufr20;
2sprintf (tx_buf, "This is a test message\n");    // build an application payload
3
4send(sock, tx_buf, strlen(tx_buf), 2,  45, NORMAL_PRIORITY);

The second argument is a pointer to the application payload. The third argument is the length of the message. The above call sends the message encapsulated in tx_buf to the node with address 2 on port 45. The message is given a normal priority level of 2 (NORMAL_PRIORITY). For the programmer's convenience, three priority levels have been defined in NW_StackConfig.h

1// priority levels of messages 
2#define MAX_PRIORITY 31
3#define LOW_PRIORITY 1
4#define NORMAL_PRIORITY 2  
5#define HIGH_PRIORITY 3 

You can of course use any priority level from 1 to 31. Remember that higher values mean higher priority levels.

A call to send() doesnt actually transmit the message over the radio. It only appends the message to the transmit queue of the system and will be sent out subsequently. If you want to know exactly when the message leaves the node, you can use the wait_until_send_done() call

1wait_until_send_done(sock);

This call will block the task till the message is fully sent over the radio. If you want to specify a waiting period, use the set_timeout() function prior to the send() call.

Client: Writing the client side of a networking application is even simpler. The steps are as below

1. Create a socket

2. Optionally set a receive queue size

3. send / receive messages

A client can even explicitly bind to a port although this is not very common. The first call to send() / set_rx_queue_size() will associate the socket with a port number. This port number will be sent alongwith the message (which is encapsulated in a UDP datagram) to a receiver. In this way, the receiver will know how to send data back to the client.

Closing a socket can be done via the close_socket() function. This will free up all resources (port number and receive buffers) associated with the socket. However messages sent out earlier from this socket and currently enqueued in the transmit buffer will not be deleted and will be sent as usual. Note that closing a socket will delete any messages in its receive queue, regardless of whether they were read or not. You might therefore first want to use check_receive_queue() to see if there are any remaining unread messages before closing the socket.

Important note on byte orders: Different machines represent multi-byte data values differently. Different compilers pack structures also differently. This can cause problems when passing messages between dissimilar nodes.
Lets say you are building an environmental monitoring application in which node 1 periodically sends sensor readings to node 2. You define the following struct in your task running on node 1.

1typdef struct
2{
3   int8_t temperature; 
4   int16_t humidity;
5   uint8_t pressure;
6
7}SensorReadings;

The send() call expects a pointer to a buffer containing the application payload. It does not look inside the payload and knows nothing about its internal structure. You may think of performing the following sequence of operations on node 1

 1[[SensorReadings]] s;
 2s.temperature = -4;
 3s.humidity = 5;
 4s.pressure = 10;
 5
 6uint8_t tx_bufr40;
 7memcpy(tx_buf, (uint8_t*)(&s), sizeof(SensorReadings));
 8
 9send(sock1, tx_buf, sizeof(SensorReadings), 2, 45, NORMAL_PRIORITY);

On node 2, you execute a receive() to retrieve the sensor readings as follows

1uint8_t* local_buf; 
2int8_t len;
3local_buf = receive(sock2, &len, NULL, NULL, NULL);
4[[SensorReadings]] *t;
5
6t = (SensorReadings*)(local_buf);
7printf("Values received = %d, %d, %d\n", t -> temperature, t -> humidity, t -> pressure);

The above sequence will probably work if the data is being exchanged between two nodes using the same endianness and having the same compiler compile their respective programs. If however, the data is exchanged between dissimilar nodes, due to differences in padding and data representation, the values will not be decoded correctly with the typecast.

We suggest therefore that you manually pack your application payload into the transmit buffer in network byte order, which by convention is big endian. Two helper functions have been provided for this purpose

1int16_t hton(int16_t host);

This function takes a 16 bit integer value and and converts it into the network byte order. The converted integer is then returned.

1int16_t ntoh(int16_t nw);

Likewise this function converts its parameter value into the host byte order and returns it. Both these functions are available by including the header file Pack.h

The above application can then be coded as below

 1[[SensorReadings]] s;
 2s.temperature = -4;
 3s.humidity = 5;
 4s.pressure = 10;
 5
 6uint8_t tx_bufr40;
 7
 8tx_bufr0 = s.temperature;
 9memcpy(tx_buf + 1, &(hton(s.humidity)), 2);
10tx_bufr3 = s.pressure;
11
12send(sock1, tx_buf, 4, 2, 45, NORMAL_PRIORITY);

On the receiver's side, it can be retrieved as follows

 1uint8_t *buf;
 2int8_t len;
 3buf = receive(sock2, &len, NULL, NULL, NULL);
 4
 5[[SensorReadings]] t;
 6t.temperature = bufr0;
 7t.humidity = ntoh( *(int16_t*)(&bufr1) );
 8t.pressure = bufr3;
 9
10release_buffer(sock2, buf);

NetworkStack.png - Diagram of the network stack in Firefly (21.6 kB) Anthony Rowe, 07/01/2007 11:47 am