LegOS Network Protocol

About this section

Mike Ash (mail@mikeash.com) wrote and maintains this section. Mad, mad props to him for that :)

What is LNP?

LNP stands for LegOS Network Protocol. It allows for communication between legOS-powered robots and host computers.

Although LNP can be used for communication between many devices, with any mixture of computers and RCXs, my experience only covers communication between my computer and a single RCX, and only that situation will be discussed. LNP is still very useful in this case, since it takes care of a lot of details and is generally easier to work with than raw IR communication.

Getting Started

LNP is included in the legOS kernel, but to use it in a program on the PC, you need to download the LNP package here. Currently it appears the page for the Windows version is down. I run Linux, so these instructions may have OS-specific issues I am not aware of, but once LNP is set up, the code to use it will be the same. Compile the package according to the directions in the README. This will create lnpd (the LNP daemon), liblnp (the library that your programs use on the PC), and example programs for both the RCX and the PC. It will also create a version of dll that uses lnpd, which is extremely useful (I'll get to that in a moment). Note that the documents in the package talk about a patch to legOS that's necessary when using LNP from multiple threads; however, that patch has since been rolled into legOS itself, and it's not necessary to apply it.

In order to use any LNP programs on the PC side, the lnpd program must be running. It does not have to run as root, but there are performance problems with a particular serial chipset, and running as root lets lnpd work around those problems. However, beware! I apparently do not have this chipset, and running lnpd as root crashes my entire machine when it tries to reconfigure the serial port. I have no problems running lnpd as a normal user.

The command-line options for running lnpd are explained in the README, but there are a couple that are particularly useful. One is the --nolock option. Normally, lnpd tries to lock the serial port on startup, but permissions to do that are usually denied for users. Lnpd will exit if it fails to create a lock. The --nolock option will keep lnpd from even trying, and so it will run successfully. Second is the --log[=filename] option. This option makes lnpd log messages to the specified file, or to the system log if no file is specified. The log file can be extremely useful for tracking down problems. If your IR tower isn't on the port lnpd expects, you may specify another port with --tty=<device>. So, putting all this together, when I start up lnpd my command line looks like this:

	  ./lnpd --nolock --log=foo (My tower is on the default port.)
	
The daemon starts up, and the command prompt returns. Assuming all goes well, the green LED on your IR tower should be lit (it will go off in a few seconds), and you should be able to use LNP programs. However, any other program that uses the serial port (firmdl3, normal dll) will not work. This is why the version of dll that comes with the LNP package is so valuable, as it allows you to dowload new versions of your program without killing lnpd every time.

Programming

Intro

Programming for LNP is fairly simple. Anyone with knowledge in network programming will probably pick it up easily, but it shouldn't be necessary at all. For the most part, code on the RCX is identical to the equivalent code on the PC. Any places where they are different will be noted.

LNP has two messaging layers, the integrity layer and the logical layer. The integrity layer makes sure that packets get through uncorrupted, but they aren't directed anywhere in particular. The logical layer adds addressing on top of the integrity layer, so that packets can be directed to a specific port on a specific device. The integrity layer is marginally easier to use than the logical layer, but the minor additional detail is worth the ability to address specific ports and devices, so using the integrity layer will not be covered.

The logical layer provides similar functionality to the internet's UDP. Packets are guaranteed to arrive free of corruption, if they arrive at all. No guarantees are made as to whether your packets actually get delivered, and if they drop off into the bit bucket your program may not be notified, nor will another transmission be attempted. The rate of packet loss can be very low if the environment is clear and no network collisions happen, but it's still important to keep the possibility of lost packets in mind.

Headers

As always, you have to include headers in order to use LNP. On the RCX, the #include line is #include <lnp.h> and on the PC it is #include "liblnp.h".

Initialization

On the PC, LNP must be initialized before use. LNP is always running on the RCX, but it is still a good idea to do a little setup before using it.

On the PC, the function to call is lnp_init(tcp_hostname, tcp_port, lnp_address, lnp_mask, flags). For all parameters, 0 means default. PC programs connect to lnpd over TCP, so tcp_hostname and tcp_port are the hosntame and port of the lnpd daemon to connect to. (Yes, you can actually have an LNP-using program running on one computer and have the daemon and IR tower on another.) lnp_address is the network address for your program on the LNP network. lnp_mask determines which bits of the eight-bit LNP address determine the network device, and which bits determine the port number. flags can change the behavior of lnpd in certain situations. There is rarely a reason to change the defaults for this call. lnp_init() will return 0 if init was successful, non-zero if it was not successful.

if(lnp_init(0,0,0,0,0))
{
  perror("lnp_init");
  exit(1);
}
else
  printf(stderr,"init OK\n");
	  
No initialization is necessary on the RCX, but it can be a good idea to set the IR transmitter's range at the beginning of your program. The function call is lnp_logical_range(range). Pass it 0 for near range, and 1 for far range. The advantage of far is that it works over much greater distances. However it uses much more battery power. This function call is like setting the switch on the front of the IR tower. Set the range to 0 unless the RCX needs to communicate from great distances.

There is no requirement for the computer and the RCX to have the same range. If you want the computer to command the RCX but you don't need the RCX to talk back, the RCX can be set to near while the switch on your IR tower is set to far. Of course, the computer won't be able to tell if it loses contact with the RCX this way, but maybe that's not important.

It's good to have a set of #defines with your ports, the remote host, and its ports. In my RCX program, these look like this:

#define MY_PORT 2
#define DEST_HOST 0x8
#define DEST_PORT 0x7
#define DEST_PORT_2 0x8
#define DEST_ADDR ( DEST_HOST << 4 | DEST_PORT )
#define DEST_ADDR_2 ( DEST_HOST << 4 | DEST_PORT_2 )
	  
In other words, my program's port is port 2. The remote host is at address 8, and I'll be sending messages to ports 7 and 8 on the remote host. With the default mask, the upper four bits of the address determine the host, and the lower four bits determine the port on that host. When transmitting data, the address must be passed to LNP with the host and port already stuffed into one byte, which is why I have those last two #defines.

Packet Handlers

In order to recieve data with your program, you must set up packet handlers. A handler will be called whenever a packet arrives on that handler's port. You must set up a handler for each port you expect to recieve data on. Addressing handlers are defined as follows:

void addr_handler_1(const unsigned char *data, unsigned char length, unsigned char src);
	  
data is a pointer to the data in the packet. Length is how much data is in the packet. Src is the address of the program that sent the packet. Notice that since length is a char, it cannot hold more than 255 .With networking overhead, the limitation is actually 253 bytes per packet. Now the handler must be registered with LNP.
lnp_addressing_set_handler (MY_PORT_1, addr_handler_1);
	  
This will install the addr_handler_1() function on port MY_PORT_1. Now, if any data is recieved on that port, your function will be called. Note that if your code is running under legOS (as opposed to on your PC), this function is called from the legOS interrupt routines, so avoid things like memory functions or thread control functions. Things like memcpy, or anything that doesn't cause memory allocation or task switching, are ok. It's also a bad idea to take too much time inside a handler. A good basic strategy would be to copy the data into a buffer and setting a flag, with a thread standing by to process the data. But that will be covered later.

Sending Data

Sending data is pretty simple as well. You need to have some data to send, of course. You need to know how big the data is. Finally, you need an address to send it to.

result = lnp_addressing_write(data, length, DEST_ADDR, MY_PORT);
	  
Where data is a pointer to what you actually want to send, length is how many bytes to send, DEST_ADDR is the address you're sending to, and MY_PORT is the port you're sending from. This function returns 0 if successful, and usually nonzero if it failed. "Usually" because you are not guaranteed to know if the transmit failed. Never rely on your packets arriving 100% of the time. The function call will block, not returning until the transmission is complete.

What do I do with it? (and how?)

By now you probably have some great ideas as to what to do with LNP. In case you don't, or you want some more, or you just want some examples about getting things done with LNP, read on! That's what this section will cover.

Debugging

One of the things that can be frustrating with legOS is the difficulty in debugging programs. Debugging using the LCD and sound is just not very easy. However, debugging can be made simple using LNP and a basic program on the host PC. The idea is to use the old method of "debug by printf". That is, print status messages and variable values to figure out what's going on in the program.

I want the ability to send strings and numbers. Since I can send multiple types of data, I make it so that the first byte of any packet is a code that identifies the type of data, and the remaining bytes will be the data itself. Strings will include the terminating \0 for convenience, though it's not strictly necessary (the length parameter in the handler could be used instead). This method will also allow the easy addition of other data types. Note that I am not taking a printf route, with a format string and optional arguments. Not only would it be harder to write, but I feel the overhead would not be justified. You, of course, are free to work differently.

void printString(char *s)
{
  int len, result;
  char *buffer;

  len = strlen(s);
  buffer = malloc(len + 2);
  buffer[0] = 's';
  memcpy(buffer + 1, s, len + 1);
  result = lnp_addressing_write(buffer,len + 2, DEST_ADDR, 	MY_PORT);
  free(buffer);
}
	  
This function takes a string as input, and sends it out with a type identifier of 's' (for string) in front of it. First we need to know how long the string is, then a temporary buffer is allocated. All of the data to be sent has to be in one buffer, so some scratch space is needed to build the packet in. That scratch space is allocated, the identifier is placed into the first byte, and the string (including terminating null) is copied in using memcpy. Finally, the whole thing is sent out with lnp_addressing_write(), and the buffer is deallocated.

Note that an array for a buffer would be faster than allocating and deallocating a buffer for each function call. There are disadvantages, however. If the array is a normal stack variable, then it will take up a large amount of stack space, which could cause a problem with overflows. If the array is declared static, then you must ensure that only one thread at a time calls the function, either by design or with semaphores.

void printInt(int i)
{
  char buf[3];
  int result;

  num = (char *)(&i);
  buf[0] = 'i';
  memcpy(buf + 1, &i, 2)
  result = lnp_addressing_write(buf,3, DEST_ADDR, MY_PORT); 
}
	

This function takes a single int and sends it over the network. Here the buffer is just a local array, since it only needs to be three bytes long. The first byte of the buffer is set to the type identifier ('i', for int), and the following two are set to the two bytes composing the int. Finally, again, the buffer is sent over the network with lnp_addressing_write.

Now we have the LNP handler on the PC side:

void addr_handler(const unsigned char* data,unsigned char length, unsigned char src)
{
  short temp;
  char *ptr;

  switch(data[0])
  {
  case 's':
    puts(data + 1);
    break;
  case 'i':
    ptr = (char *)∧temp;
    #ifdef BIG_ENDIAN
      ptr[0] = data[1];
      ptr[1] = data[2];
    #else
      ptr[0] = data[2];
      ptr[1] = data[1];
    #endif
    printf("%d", temp);
    break;
  }
}
	  
The basic idea here is to examine the type identifier in the packet and then either print the string that follows or extract the int and print it. Some trouble comes when considering so-called endian issues. (If you know what endian is and how to deal with it, skip to the paragraph after next.) The short version is, there are two main ways to represent numbers that take up multiple bytes in memory. "Big-endian" processors store the most-significant byte at the lowest address, and the least-significant byte at the highest address. If the number 0x1234 is stored at location 100, then the byte at 100 contains 0x12 and the byte at 101 contains 0x34. Little-endian processors store things in the opposite direction, so that byte 100 would contain 0x34 and byte 101 contains 0x12.

The good news is, you almost never have to deal with this. As long as your processor is consistent (and if it wasn't, it wouldn't work) and you don't do anything evil like examining the individual bytes of a number, you'll never notice it. However, there is a problem when it comes to transferring data from one computer to another. If the two computers have different byte orders, any numbers transferred will appear scrambled. To fix this, the bytes must be swapped, and that's why the indices into data are backwards in the #else clause in the function. The RCX's processor is big-endian, so if the host PC is also big-endian then the two processors can use each others data without modification. Of course, the BIG_ENDIAN identifier is fictional: you have to figure out what your computer is for yourself. It's not hard. If you're running an Intel-based PC, your processor is little-endian. If you're running a PowerPC, it's big-endian. If you're running something else, chances are it's big-endian, but you can also use the program below to check:

int main(void)
{
  int x = 1;
  char *foo = (char *)&x;
  if(foo[0] == 1)
    printf("little-endian\n");
  else
    printf("big-endian\n");
  return 0;
}
	  
Now you have a much better debugging system! These function calls can be called pretty much anywhere, anytime, with the exception of LNP data handlers. There are a couple of things to watch out for, though. One problem is that these functions take some time to run, since they don't return until the full packet has been transmitted. Keep that in mind when debugging time-critical routines. Also, these strings are actually stored on the RCX, and sent over the network every time they are printed. This takes up limited memory and time. In my project, turning off debugging strings reduces my program size by over 1k, a significant portion of the program. If memory is a problem, it may be worth the trouble to send string ID numbers from the RCX which the PC would then look up in a table to get the actual string.

Once you understand the principles above, it should be pretty easy to move past simple debugging and into real data exchange. Keep in mind endian issues when transmitting any numbers bigger than one byte if your machine is little-endian.

Remote Control

Sending data from the RCX to the PC is pretty simple, since anything goes in an LNP packet handler on the PC. On the RCX side, however, an LNP packet handler is limited in what it can do. Things like memory allocation and thread controls are off-limits. Heavy data processing is highly discouraged. In general, it's best to simply copy the data into a buffer, set a flag, and let a thread take care of the processing. An example setup of this follows:

int gNewData = 0;
int gMessagingData[256];
int gDataLength;

void packet_handler(const unsigned char* data,unsigned char length, unsigned char src)
{
  if(gNewData == 0)
  {
    memcpy(gMessagingData, data, length);
    gDataLength = length;
    gNewData = 1;
  }
}

int PacketWatcher(int argc, char **argv)
{
  wakeup_t WaitForData(wakeup_t data)
  {
    return gNewData;
  }

  while(1)
  {
    wait_event(WaitForData, 0);
    switch(gMessagingData[0])
    {
      ...
    }
    gNewData = 0;
  }
}
	  
All the packet handler does is copy the data to gMessagingData and then set gNewData, but only if gNewData is clear. That flag is the signal for the packet watcher thread to look at the new data. If any packets comes in before the packet watcher can take care of the data, those packets will be discarded. If that is a problem, it may be possible to use the pool more efficiently, or use some other scheme. Since the legOS scheduler does not support explicitly waking threads, the packet watcher has to wait for its turn to be touched before it can go into action. Again, the first byte of the packet is used as an identifier to figure out what to do with the rest of the data. Fill in the blank in the switch statement to do whatever you need. Finally the flag is reset, signalling that the packet watcher is ready to process more data.

On the PC side, send data just like in the RCX programs. Keep in mind byte-order issues if your machine is little-endian. Again, since your PC has the processing power and the memory, I recommend doing the byte swapping in the PC program, rather than on the RCX.