Mike Ash mail@mikeash.comwrote and maintains this section. Mad, mad props to him for that :)
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.
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.) |
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.
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".
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"); |
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 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); |
lnp_addressing_set_handler (MY_PORT_1, addr_handler_1); |
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); |
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.
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); } |
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 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; } |
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.
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; } } |
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.