1. Introduction

MANTIS (Multimodal Networks of In-situ Sensors) is a wireless sensor network Operating System that provides an integrated hardware and software platform suitable for a wide variety of sensor network applications. MANTIS is designed to be easy to learn and use, yet sophisticated enough to allow the kind of flexibility that's required for advanced deployment scenarios.

The two central components of the MANTIS platform are the MANTIS Operating System (MOS) (a small, UNIX-like runtime environment) and a wireless sensor node that is specifically designed to take advantage of the advanced capabilities of MOS.


Why "multimodal"?
We say that MANTIS is a multimodal platform because of the wide variety of applications and deployment scenarios in which it can be used. We envision that MANTIS will be used in a broad range of applications, including such diverse areas as weather surveys, biomedical research, embedded interfaces, wireless networking research, and artistic works. Thanks to the fact that both MOS itself and all end user applications are written in ANSI C and compiled using gcc, MANTIS applications can be deployed on multiple hardware platforms for use in heterogeneous environments; for example, a simulation running multiple instances of MOS for x86 (xMOS) can interact with actual sensor nodes, allowing for staged deployment scenarios. We have tried to keep the MANTIS platform very flexible, and have tried to make few assumptions about the nature of the applications that will be built using MOS on a variety of sensor nodes.


Who can use MANTIS?
Anyone who is familiar with the C programming language and has a basic understanding of operating system fundamentals can use MANTIS. There is no specialized programming language to learn, nor is the user required to build or purchase separate circuit boards in order to perform basic sensing tasks.

Why should I chose MANTIS? How does the MANTIS platform compare to other solutions?
Many different wireless sensor platforms are available today, and each has its advantages and disadvantages. No platform can be all things to all people, and there are certainly applications for which MANTIS may not be the best solution. However, MOS offers a unique combination of features that are not currently found on any other single platform.

The MANTIS Operating System (MOS) provides:



The Xbow sensor node provides:

||

...page...
2. MANTIS OS Overview

The MANTIS Operating System (MOS) is a simple, UNIX-like operating system that is intended to provide a familiar and comfortable development environment for programmers who are familiar with C and UNIX. It provides true preemptive multi-threading through an interface similar to that of the POSIX threads API, and insulates the programmer from the low level details of the hardware on which it is running.

MOS is designed with a multi-layered approach that allows the operating system to be easily ported to new hardware platforms, and allows many applications to be migrated across platforms with a simple recompile (again, following the C/UNIX philosophy). A set of low level drivers are written for each new hardware architecture, which allows for a common user-level API to be shared over multiple platforms.

Basic MOS Application Outline

In this section we will present the outline of a typical MOS application, sense_and_forward, which demonstrates some of the most common operations: reading sensor values, transmitting and recieving data via the radio, writing data to the serial port, and turning LEDs on and off.

You will notice the start() function at the beginning of each MOS application. This plays a role that is similar to the main() function in a standard UNIX C program. start() is similar to main() in that your application begins execution from that point. We recommend that you perform no heavy computations in the start() function, rather spawn your own threads to do the heavy-lifting.

A Word About Stack Space

In a heavily resource constrained system such as MOS, stack space is limited and valuable. You will notice that mos_thread_new() takes a stack size parameter, the number of bytes to allocate for your thread's stack. 128 bytes is a good starting point, but remember: all local variables, function calls, and interrupt handlers use the stack. Recursion and deeply-nested functions are generally a bad idea in MOS. If you are experiencing strange problems, you may be experiencing stack overflow. Try increasing your thread's stack size.

A Word About Priorities

MOS' scheduler is round-robin, and any thread with a higher priority will always run before any thread with a lower priority. If you have a high-priority thread, keep in mind that you will be starving the system if you set its priority to high and never block.

Remote sensor

The following code runs on the remote sensor node. It continuously sends packets over the radio interface to the relay node. While the node is sending, the yellow LED flashes.

sense_and_forward.c


 #include "mos.h"
 #include "led.h"
 #include "dev.h"
 #include "com.h"
 #include "msched.h"

static comBuf send_pkt; //comBuf goes in heap

void send_thread();

void start(void)
{
   mos_thread_new(send_thread, 128, PRIORITY_NORMAL);
}

void send_thread()
{
   send_pkt.size=2; //2 bytes

   while(1)
   {
      mos_led_toggle(0);
      ~np~(uint16_t)send_pkt.data[0] = dev_get(DEV_MICA2_TEMP);~/np~
      com_send(IFACE_RADIO, &send_pkt);
      mos_thread_sleep(1000);
   }
}



Receiver (relay)

This is the code for the receiving node, which is attached to a PC via the serial port and listens for remote sensor readings over the radio. When it receives a packet, it sends it byte by byte to the PC over the serial port. Also, for each packet received the yellow LED flashes.

Notice the use of com_free_buf () after we are finished using the packet. Since we are using a buffer that is managed by the com layer, we must explicitly tell the system we are done with the buffer, and it is safe to be returned to the global buffer pool. When sending, the comBuf is declared in static memory, so we do not need to free anything.

receiver.c

 #include "led.h"
 #include "com.h"        //give us the communication layer
 #include "msched.h"

void receiver();

void start(void)
{
   comBuf *recv_pkt;                    //give us a packet pointer

   com_mode(IFACE_RADIO, IF_LISTEN);
   
   while(1)
   {
      recv_pkt = com_recv(IFACE_RADIO); //blocking recv a packet
      com_send (IFACE_SERIAL, recv_pkt);   //send packet out over serial
      com_free_buf(recv_pkt);           //free the recv'd packet to the pool

      mos_led_toggle(0);
   }
}



...page...
3. The Command Daemon

The command daemon provides an easy way to interface with a sensor node. It provides a pseudo-shell for the embedded system. Specifically it has a user defined list of commands, which it listens for over the serial port. These commands are text strings which can be entered in using the shell. For starters, you must #include "command_daemon.h" in your file.

Next, in the start() function, you must start the command daemon thread with mos_thread_new(mos_command_daemon,MOS_COMMANDER_STACK_SIZE,PRIORITY_NORMAL); (see Starting Threads for more information on this function).

You can register the functions you wish to be called, and the strings the user can type in to call them with:


However, please note that your function prototype must look like void function_name(void).

A simple program which will respond with "hi there" when you type in "hello" would go as follows:

hello_world.c

#include "msched.h"
 #include "command_daemon.h"
 #include "printf.h"


/* this function will be called when user types hello */
void hello_response(void)
{
   printf("hi there\\n");
}

void start(void)
{   
   mos_thread_new(mos_command_daemon, MOS_COMMANDER_STACK_SIZE,
		  PRIORITY_NORMAL);
   mos_register_function("hello", hello_response);
}



The above function call would ouput:



Mantis Commander v1.3. Node:2

MOS Commander 2$hello
hi there

MOS Commander 2$


Starting Threads

The Mantis Operating System is multi-threaded, thus the user must know information about starting threads.

The system starts in the main function found in mantis/src/mos/sys/main.c. After initialization, it calls a void start(void) function that your app must include. If the app is short, doesn't require a large stack and doesn't need to be multi-threaded, the code can go in this function. To spawn a new thread, one must use mos_thread_new ().

The function must follow the prototype of void function().

The stack_size must be smaller than the available RAM (after static variables).

The priority can be one of PRIORITY_NORMAL, PRIORITY_HIGH where high priority always preempts normal.

...page...
4. The Device Layer

Using the device layer

The dev layer is a common interface for communicating with devices. On the system level, it is a list of device-id's and function pointers.

Device drivers can be found in mantis/src/mos/dev. The device list can be found in mantis/src/mos/dev/include/dev.h, devices are denoted by DEV_NAME.

To use the device layer, first #include "dev.h". Next all you have to do is call either:


additionally some devices have ioctl, which can be called with


Devices can be locked using open and close:


You need to call open and close when the device may be shared across other threads (especially to lock the effects of an ioctl before a read/write). Also, several devices may be used by the OS at any point including EEPROM, flash memory, and RSSI so any calls to those devices always need to be locked. In general, it is always a good idea to lock devices before using them.

For example, say we wanted to write our name in the eeprom, at address 123, we use the following program:

dev_funcs.c


 #include "msched.h"
 #include "dev.h"

void start(void)
 {
   uint8_t *data = "charles";
   dev_open(DEV_AVR_EEPROM);
   dev_ioctl(DEV_AVR_EEPROM, MOS_DEV_SEEK, 123);
   dev_write(DEV_AVR_EEPROM, data, sizeof(data));
   dev_close(DEV_AVR_EEPROM);
 }



NOTE: Not all devices support the read and write operations. For example, attempting to write to a light sensor will return an error code (listed in dev.h)

To retrieve your data, use the read function:


dev_read is the standard command, where the user specifies which buffer to use and how many bytes the data contains. The other 2 functions are simplified versions of dev_read, eliminating both the choice of buffer and data size field. dev_get reads exactly 1 byte and dev_get16 reads exactly 2 bytes.

Creating a device:

To Implement a new device....

1. Start by putting the device driver's .c file in mantis/src/mos/dev and it's header in the include subdirectory. Make sure to add it to the build system such that it will be compiled into the kernel.

You must consider if this will be a device one can write to or read from or both, and if it needs ioctl.

2. Create each of the devices functions necessary, they should look like


3. Next the device needs a mutex for the dev_open() and dev_close() macros. Declare it in your dev file, and declare it extern in dev.h following the format of the other devices in the file.

4. Lastly the init call must be placed in main.c. The init call should initialize the device mutex and perform any necessary device-specific initialization.

...page...
5. The Communications Layer

Structure:

The Com Layer is a communication layer that abstracts away the details creating buffers and interfacing with the different communication devices. Specifically it has a pool of buffers shared amongst all devices for incoming packets, these packets can be retrieved with a com_recv call, additionally packets may be sent out with the com_send call.

A com buffer looks similar to the following struct:


 typedef struct {
   uint8_t size;
   ~np~uint8_t data[COM_DATA_SIZE];~/np~
 } comBuf;



Receiving a packet:

To receive a packet using com_bufs...

1. Include the source file


2. Create a com_buf pointer, which will be assigned by the com layer in a com_recv()


3. Put the device in listen mode. This is necessary to enable the receive interrupt and begin receiving data.


4. Finally do a blocking receive


The size can be retrieved with recv_pkt->size, and the data members with recv_pkt->data[index].

Sending works essentially the same way, except you statically allocate the comBuf* (or use on allocated for you from a com_recv() before freeing it), and pass the com_send() function the pointer such as:


NOTE: comBufs have COM_DATA_SIZE user data bytes available. Keep this in mind when allocating them (64 bytes might be 1/2 of your thread's available stack space)

If you are simply passing a packet through, you can call com_send() on a received packet immediately.


...page...
6. Sleeping and Timing

Sleeping:

Putting your thread to sleep with MOS is relatively easy with the timer abstraction. All one has to do in order to put a thread to sleep is call the 'mos_thread_sleep' function. The function takes in a 32-bit parameter corresponding to the number of milliseconds the thread should sleep for...


This call will put the thread into sleep mode and schedule other processes for the duration of the sleep. If no threads are in the ready queue, then the kernel will put the processor into a low power mode to save energy. Once the alloted time has elapsed, the sleeping thread will be put back onto the ready queue. Note that there is not a strict guarantee that the thread will be resumed immediately after it's time is up, it may have to wait a few time-slices depending on what else is going on in the system.

Timing:

It is possible to determine the ammount of time something takes by using 'realtimer'. This is basically an abstraction to the underlying hardware timers.

First you must include the header file that gives us this abstraction...

Next we must initialize the hardware timers to be in CTC mode and fire every millisecond...


Lastly all we need to do is grab the elapsed time... Since the call to 'real_timer_get_ticks' returns a pointer, we must dereference it to get the actual time... note the value is a 32 bit number representing the number of milliseconds past since the init...


If you need to restart the timer (set the running value back to zero)... simply call 'real_timer_clear()'.

Also note that the oscillaor onboard is not perfect and therefore some skew (around 5 minutes a day or so) should be expected.

Alarms:
The alarms have been developed to replicate the set_itimer functionality on the nodes.

First we need to include the header file which provides the alarm abstraction...


Next we need an actual alarm, which is a structure that holds the function to call back and a "void *" pointer that can be user defined data...


Next we set the callback function which will be executed when the alarm fires. This function must fit this prototype: 'void myfunc(void *data);'. It is also important to note that this function will be executing in an interrupt handler, so it is important not to do any printf's or a lot of processing in this function. Generally, the best thing to do is to simply post a semaphore that a thread is waiting on, or perform a very simple computation such as incrementing a value.


The structure also contains two other important members. The first is the number of milliseconds after which the alarm should fire.


This will make the alarm fire in 1 second.
The second member of the structure determines whether or not the alarm should be repeatable.


This will cause the alarm to fire every 1 second. If you only wish the alarm to fire once, the reset_to member should be set to 0. The reset_to value can be modified from the alarm callback, meaning you can terminate an alarm by setting it to 0 there, or change it to a different value.

After these three members of the structure are filled in, you can add the alarm to the list by calling mos_alarm like so::


If you'd like to remove an alarm, use the 'mos_remove_alarm' function like this::


...page...
7. Using printf

printf:

The printf function found in MOS has a few differences from the regular system printf. Basically the printf call formats the packet into separate com_buf's and sends it over the serial line. The shell must be listening on the other side in order to see these packets. One should also note that the parameters are slightly different as follows:

%s - String
%c - character
%C - 8-bit decimal
%d - 16-bit decimal
%l - 32-bit decimal
%x - 16-bit hexacecimal
%o - 16-bit octal
%b - 16-bit binary
%% - the '%' character

...page...
7. NodeMD, the MOS debugger

Overview

Integrated into the unstable branch of the MOS source code is a beta implementation of the NodeMD debugger. NodeMD adds fault detection algorithms and diagnostic logging to any MOS application. Currently, only the Mica2 and MicaZ platforms are supported. The debugging mode can be enabled by adding the debug=1 command line option to the SCons compile command.

With this enabled, several detection algorithms for stack overflow, deadlock and livelock are now periodically called in the debug-enabled application. Code in the OS will also log system events (defined in /src/mos/kernel/micro/include/mos_debugging.h).

Note that all of this code is within the OS itself. The SCons build system also calls a C parser to add several lines of debugging code to each C file before compilation. If you believe an error has been caused by this parser, the preprocessed files can be found with a filename.debug extension in the target build directory.

Using NodeMD

While the "behind the scenes" code for NodeMD can be found in the OS itself and in the NodeMD tech report, this section will describe how to use the debugger once it is enabled. Several things are handled by the OS, but some code must also be added to the application manually. We'll divide the actual function calls added to MOS first by their system component, whether they need to be inserted by the programmer or not, and then by their purpose.

Finally, this guide references several examples in the nodemd_example MOS application, found in /src/apps/nodemd_example/.

Event Logging Functions:


void mos_debug_set_trace(uint8_t code);


Precondition: code is a number no greater than 2 to the DBCODE_SIZE power.
Postcondition: code is written to the least recent segment of the event trace

Most of the event logging is handled by calls to this function within the Mantis OS itself, i.e. when a thread blocks, etc. Although traces normally handled by the OS can be added to an application, it is not recommended. The only time a programmer will want to call this function is to set the DBCODE_BREAKPOINT (1111) code, which is reserved as a custom user-defined trace, or other custom codes available when DBCODE_SIZE is increased. See nodemd_example/debug_app.c, line 78.


void mos_debug_clear_trace();


Precondition: None
Postcondition: The event trace is zeroed, and the trace cursor is set to the front of the trace array.

NOTE: This function has a bug and does not work correctly.


Deadlock and Livelock Detection: Data Structure and Functions

NodeMD implements a set of software watchdog timers for MOS, denoted as "checkpoints". By "registering" a checkpoint, the defining thread is expected to "set" that checkpoint within the declared timeout value. All of these operations use the debug_checkpoint_t data structure. See lines 29, 50, and 59 in nodemd_example/debug_app.c for the calling context of the following code.

typedef struct debug_checkpoint_ {
uint32_t timeout;
uint32_t timestamp;
struct debug_checkpoint_* next;
} debug_checkpoint_t;



void mos_debug_register_checkpoint(debug_checkpoint_t *cp, uint32_t ms);


Precondition: The debug_checkpoint_t pointer cp has been defined; ms > 0.
Postcondition: The debug_checkpoint_t structure pointed to be cp has been added to a linked list of checkpoints.


void mos_debug_set_checkpoint(debug_checkpoint_t *cp);


Precondition: cp != NULL
Postcondition: If cp is found in the linked list of checkpoints (cp has been registered), cp->timestamp = current system time.

App-specific Faults - The ASSERT Macro

#define ASSERT(condition) { \\
if(condition) \\
mos_debug_error_report(SIGNAL_ASSERTION_FAILED); \\
}


Precondition: None
Postcondition: Node enters debug mode if condition is true, nothing if condition is false.

See nodemd_example/debug_app.c, line 41.

Stack Overflow Detection

void mos_debug_check_stack();


Precondition: None
Postcondition: The current thread stack will not overflow in this function.

This function is automatically inserted at the entry to every MOS function by a parser called in the SCons build system. There is no need to manually add this anywhere.


Other System Function Calls

void mos_debug_status_check();


Precondition: 1 or more checkpoints have been registered. At least 1 checkpoint has been set.
Postcondition: None of the registered checkpoints have timed out.

This function is called every 1000 ms from the MOS scheduler. A checkpoint is not verified until it has been set at least once because the first timestamp must initialize the data structure value.


void mos_debug_error_report(uint8_t signal);


This function creates and context switches to a high priority "debug" thread. In this thread the faulty system state is halted and the event trace is broadcasted over the radio. It is recommended that programmers use the ASSERT(0) call to start this mode manually, rather than calling it directly.