Lego MindStorms is a ``Robotics Invention System'' that allows you to build Lego toys with a programmable computer. This chapter looks at the issues in interfacing with a specialised hardware device, using MindStorms as example
Lego MindStorms (http://www.legomindstorms.com) is a ``Robotics Invention System'' which consists of a number of Lego parts and a microcomputer called the RCX, plus an infra-red transmitter (connected to the serial port of an ordinary computer) and various sensors and motors. Using this, one can build an almost indefinite variety of Lego robots that can be controlled by the RCX. This computer can be sent ``immediate'' commands, or can have a (small) program downloaded and then run.
MindStorms is a pretty cool system, that can be driven at a number of levels. A primary audience for programming this is children, and there is a visual programming environment to help in this. This visual environment only runs on Windows or Macintosh machines which are connected to the RCX by their serial port and the infrared transmitter. Behind this environment is a Visual Basic set of procedures captured in an OCX, and behind that is the machine code of the RCX which can be sent as byte codes on the serial port.
A MindStorms robot can be programmed and run from an infrared transmitter attached to the serial port of a computer. There is no security or real location for the RCX: it will accept commands from any transmitter in range. We will assume a ``home'' computer for it.
There must be a way of communicating with this device. For a MindStorms robot this is by the serial port, but other devices may have different mechanisms. Communication may be by Java code or by native code. Even if Java code is used, at some stage it must drop down to the native code level in order to communicate with the device - the only question is whether you write the native code or someone else does it for you and wraps it up in Java object methods.
For the serial port, Sun has an extension package - the commAPI
-
to talk to serial and parallel ports
(http://java.sun.com/products/javacomm/index.html)
.
This gives platform-independent Java code, and also platform
specific native code libraries supplied as DLL's for Windows and Solaris.
I am running Linux on
my laptop, so I need a Linux version of the DLL.
This has been made by Trent Jarvi (jarvi@ezlink.com), and can be found at
http://jarvi.ezlink.com/rxtx/.
The native code part of communicating to the device has been done for us,
and it is all wrapped up in a set of portable Java classes.
The RCX expects particular message formats, such as starting with standard headers. A Java package to make this easier is available by Dario Laverde at http://www.escape.com/~dario/java/rcx. There are other packages that will do the same thing: see the ``Lego Mindstorms Internals'' page by Russell Nelson at http://www.crynwr.com/lego-robotics/.
With this as background, we can look at how to make an RCX into a Jini service. It will involve being able to construct an RCX program on a client and send this back to the server where it can be sent on to the RCX via the serial port. This will then allow a client to control a Mindstorms robot remotely. Actually, the Jini part is pretty easy - the hard part was tracking down all the bits and pieces needed to drive the RCX from Java. With your own lumps of hardware, the hard part will be writing the JNI and Java code to drive it.
The package by Dario Laverde defines various classes, of which the
most important is RCXPort
:
package rcx;
public class RCXPort {
public RCXPort(String port);
public void addRCXListener(RCXListener rl);
public boolean open();
public void close();
public boolean isOpen();
public OutputStream getOutputStream();
public InputStream getInputStream();
public synchronized boolean write(byte[] bArray);
public void processRead();
public String getLastError();
public void showTable();
public static byte[] parseString(String str);
}
Not all of these should have been declared public, but never mind - we can just ignore the ones we don't want. The ones we do want are
RCXPort()
. This takes the name of
a port as parameter, and this should be something like
COM1
for Windows and /dev/ttyS0
for Linux.
write()
is used to send an array of opcodes
and their arguments to the RCX. This is machine code
and you can only read it by a dis-assembler or a Unix tool like
octal dump (od -t xC
).
parseString()
can be used to translate a string of insrtuctions in readable form
to an array of byte for sending to the RCX. It isn't as good as an
assembler, as you have to give strings such as "21 81"
to start the A motor. To use this for Jini, we will have to produce
a non-static method in our interface since static methods are not
allowed.
addRCXListener()
. The listener must implement the
interface
package rcx;
import java.util.*;
/*
* RCXListener copyleft (GPL) 1998 Dario Laverde
* - work in progress dario@escape.com
*/
public interface RCXListener extends EventListener {
public void receivedMessage(byte[] message);
public void receivedError(String error);
}
At the lowest level, the RCX is controlled by machine-code programs sent via the infrared link. It will respond to these by stopping and starting motors, changing speed, etc. As it completes commands or receives information from sensors, it can send replies back to the host computer. The RCX can handle instructions sent directly, or have a program downloaded into firmware and run from there.
Kekoa Proudfoot has produced a list of the opcodes understood by the
RCX which is available at
http://graphics.stanford.edu/~kekoa/rcx.
Using these and the rcx.RCXPort
from Dario Laverde
means we can control the RCX from the ``home'' computer by programs such as
/**
* TestRCX.java
*
*
* Created: Wed Jun 2 13:34:12 1999
*
* @author Jan Newmarch
* @version 1.0
*/
package standalone;
import rcx.*;
public class TestRCX implements RCXListener {
static final String PORT_NAME = "/dev/ttyS0"; // Linux
public TestRCX() {
RCXPort port = new RCXPort(PORT_NAME);
port.addRCXListener(this);
byte[] byteArray;
// send ping message, reply should be e7 or ef
byteArray = RCXPort.parseString("10"); // Alive
port.write(byteArray);
// beep twice
byteArray = RCXPort.parseString("51 01"); // Play sound
port.write(byteArray);
// turn motor A on (forwards)
byteArray = RCXPort.parseString("e1 81"); // Set motor direction
port.write(byteArray);
byteArray = RCXPort.parseString("21 81"); // Set motor on
port.write(byteArray);
try {
Thread.currentThread().sleep(2000);
} catch(Exception e) {
}
// turn motor A off
byteArray = RCXPort.parseString("21 41"); // Set motor off
port.write(byteArray);
// turn motor A on (backwards)
byteArray = RCXPort.parseString("e1 41"); // Set motor direction
port.write(byteArray);
byteArray = RCXPort.parseString("21 81"); // Set motor on
port.write(byteArray);
try {
Thread.currentThread().sleep(2000);
} catch(Exception e) {
}
// turn motor A off
byteArray = RCXPort.parseString("21 41"); // Set motor off
port.write(byteArray);
}
/**
* listener method for messages from the RCX
*/
public void receivedMessage(byte[] message) {
if (message == null) {
return;
}
StringBuffer sbuffer = new StringBuffer();
for(int n = 0; n < message.length; n++) {
int newbyte = (int) message[n];
if (newbyte < 0) {
newbyte += 256;
}
sbuffer.append(Integer.toHexString(newbyte) + " ");
}
System.out.println("response: " + sbuffer.toString());
}
/**
* listener method for error messages from the RCX
*/
public void receivedError(String error) {
System.err.println("Error: " + error);
}
public static void main(String[] args) {
new TestRCX();
}
} // TestRCX
We shall follow the pattern of ``option3'' in the chapter on ``Simple Examples''. This means constructing a hierarchy of classes
The RCXPortInterface
just defines the methods we shall
be making available from the Jini service. It doesn't have to follow
the RCXPort
methods completely, because these will be wrapped
up in implementation classes such as RCXPortImpl
.
The interface is defined as
/**
* RCXPortInterface.java
*
*
* Created: Wed Jun 2 23:08:12 1999
*
* @author Jan Newmarch
* @version 1.0
*/
package rcx.jini;
public interface RCXPortInterface extends java.io.Serializable {
/**
* Write an array of bytes that are RCX commands
* to the remote RCX.
*/
public boolean write(byte[] byteCommand) throws java.rmi.RemoteException;
/**
* Parse a string into a set of RCX command bytes
*/
public byte[] parseString(String command) throws java.rmi.RemoteException;
} // RCXPortInterface
We have chosen to make it a sub-package of the rcx
package
to make its role clearer. Note that it has no static methods, but makes
parseString()
into an ordinary instance method.
The interface RemoteRCXPort
adds the Remote
interface as before
/**
* RemoteRCXPort.java
*
*
* Created: Wed Jun 2 23:13:17 1999
*
* @author Jan Newmarch
* @version 1.0
*/
package rcx.jini;
import java.rmi.Remote;
public interface RemoteRCXPort extends RCXPortInterface, Remote {
} // RemoteRCXPort
The RCXPortImpl
constructs its own RCXPort
object and feeds methods through to it, such as write()
.
Since it extends UnicastRemoteObject
it also adds exceptions
to each method, which cannot be done to the original RCXPort
class. In addition, it picks up the value of the port name from the
port
property. (This follows the example of the
RCXLoader
in the rcx
package which gives a
GUI interface to driving the RCX.) It looks for this property in a file
parameters.txt
which should have lines such as
port=/dev/ttyS0
The implementation looks like
/**
* RCXPortImpl.java
*
*
* Created: Wed Jun 2 23:14:58 1999
*
* @author Jan Newmarch
* @version 1.0
*/
package rcx.jini;
import java.rmi.server.UnicastRemoteObject;
import rcx.*;
import java.io.*;
import java.util.*;
public class RCXPortImpl extends UnicastRemoteObject
implements RemoteRCXPort, RCXListener {
protected RCXPort port = null;
public RCXPortImpl()
throws java.rmi.RemoteException {
Properties parameters;
String portName = null;
File f = new File("parameters.txt");
if (!f.exists()) {
f = new File(System.getProperty("user.dir")
+ System.getProperty("path.separator")
+ "parameters.txt");
}
if (f.exists()) {
try {
FileInputStream fis = new FileInputStream(f);
parameters = new Properties();
parameters.load(fis);
fis.close();
portName = parameters.getProperty("port");
} catch (IOException e) { }
} else {
System.err.println("Can't find parameters.txt with \"port=...\" specified");
System.exit(1);
}
port = new RCXPort(portName);
port.addRCXListener(this);
}
public boolean write(byte[] byteCommands)
throws java.rmi.RemoteException {
return port.write(byteCommands);
}
public byte[] parseString(String command)
throws java.rmi.RemoteException {
return RCXPort.parseString(command);
}
public void receivedMessage(byte[] message) {
// later, we send these messages back to the client
// for now, just print them
if (message == null) {
return;
}
StringBuffer sbuffer = new StringBuffer();
for(int n = 0; n < message.length; n++) {
int newbyte = (int) message[n];
if (newbyte < 0) {
newbyte += 256;
}
sbuffer.append(Integer.toHexString(newbyte) + " ");
}
System.out.println("response: " + sbuffer.toString());
}
public void receivedError(String error) {
System.err.println(error);
}
} // RCXPortImpl
The RCXPortProxy
will run over on the client side
(well, at least an RMI stub for it will).
It's constructor takes a RCXPortImpl
object and passes
on all methods to it.
/**
* RCXPortProxy.java
*
*
* Created: Wed Jun 2 23:30:07 1999
*
* @author Jan Newmarch
* @version 1.0
*/
package rcx.jini;
import rcx.*;
public class RCXPortProxy implements RCXPortInterface {
protected RemoteRCXPort server = null;
public RCXPortProxy(RemoteRCXPort serv) {
server = serv;
}
public boolean write(byte[] commands)
throws java.rmi.RemoteException {
return server.write(commands);
}
public byte[] parseString(String str)
throws java.rmi.RemoteException {
return server.parseString(str);
}
} // RCXPortProxy
To make use of these classes, we need to provide a server to get the service put onto the network, and some clients to make use of the service. This section will just look at a simple way of doing this, and later sections will try to put in more structure.
A simple server follows exactly the
pattern of ``option 3'' in the chapter on ``Simple Examples'',
just substituting RCXPort
for FileClassifier
:
package rcx.jini;
import java.rmi.Naming;
import java.net.InetAddress;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;
import java.rmi.RMISecurityManager;
/**
* RCXServer.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version 1.1
* added LeaseRenewalManager
* moved sleep() from constructor to main()
*/
public class RCXServer implements DiscoveryListener, LeaseListener {
// this is just a name - can be anything
// impl object forces search for Stub
static final String serviceName = "RCX";
protected RCXPortImpl impl;
protected RCXPortProxy proxy;
protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
public static void main(String argv[]) {
new RCXServer();
// no need to keep server alive, RMI will do that
}
public RCXServer() {
try {
impl = new RCXPortImpl();
} catch(Exception e) {
System.err.println("New impl: " + e.toString());
System.exit(1);
}
// register this with RMI registry
System.setSecurityManager(new RMISecurityManager());
// make a proxy that knows our implementation
proxy = new RCXPortProxy(impl);
// now continue as before
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
// export the proxy service
ServiceItem item = new ServiceItem(null,
proxy,
null);
ServiceRegistration reg = null;
try {
reg = registrar.register(item, Lease.FOREVER);
} catch(java.rmi.RemoteException e) {
System.err.print("Register exception: ");
e.printStackTrace();
// System.exit(2);
continue;
}
try {
System.out.println("service registered at " +
registrar.getLocator().getHost());
} catch(Exception e) {
}
leaseManager.renewFor(reg.getLease(), Lease.FOREVER, this);
}
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // RCXServer
Why is it simplistic? Well, it doesn't contain any
information to allow a client to distinguish one Lego Mindstorms robot
from another, so that if there are many robots on the network then a
client could ask the wrong one to do things!
An equally simple client to make the RCX do a few actions is
package rcx.jini;
import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;
/**
* TestRCX.java
*
*
* Created: Wed Mar 17 14:29:15 1999
*
* @author Jan Newmarch
* @version 1.3
* moved sleep() from constructor to main()
* moved to package client
* simplified Class.forName to Class.class
*/
public class TestRCX implements DiscoveryListener {
public static void main(String argv[]) {
new TestRCX();
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(10000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public TestRCX() {
System.setSecurityManager(new RMISecurityManager());
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
Class [] classes = new Class[] {RCXPortInterface.class};
RCXPortInterface port = null;
ServiceTemplate template = new ServiceTemplate(null, classes,
null);
for (int n = 0; n < registrars.length; n++) {
System.out.println("Service found");
ServiceRegistrar registrar = registrars[n];
try {
port = (RCXPortInterface) registrar.lookup(template);
} catch(java.rmi.RemoteException e) {
e.printStackTrace();
System.exit(2);
}
if (port == null) {
System.out.println("port null");
continue;
}
// simple tests for now
byte[] command;
try {
// ping
command = port.parseString("10");
if (! port.write(command)) {
System.err.println("command failed");
}
// beep beep
command = port.parseString("51 01");
if (! port.write(command)) {
System.err.println("command failed");
}
// turn motor A on (forwards)
command = port.parseString("e1 81");
if (! port.write(command)) {
System.err.println("command failed");
}
command = port.parseString("21 81");
if (! port.write(command)) {
System.err.println("command failed");
}
try {
Thread.currentThread().sleep(2000);
} catch(Exception e) {
}
// turn motor A off
command = port.parseString("21 41");
if (! port.write(command)) {
System.err.println("command failed");
}
// turn motor A on (backwards)
command = port.parseString("e1 41");
if (! port.write(command)) {
System.err.println("command failed");
}
command = port.parseString("21 81");
if (! port.write(command)) {
System.err.println("command failed");
}
try {
Thread.currentThread().sleep(2000);
} catch(Exception e) {
}
// turn motor A off
command = port.parseString("21 41");
if (! port.write(command)) {
System.err.println("command failed");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
public void discarded(DiscoveryEvent evt) {
// empty
}
} // TestRCX
Why is this one simplistic? It tries to find all robots on the local
network, and sends the same set of commands to it. Worse, if a robot
has registered with, say, half-a-dozen service locators, and the client
finds all of these, then it will send the same set of commands six times
to the same robot! Some smarts are need here...
See next installment :-)
See next installment :-)
See next installment :-)
See next installment :-)
This file is Copyright ©Jan Newmarch (http://jan.newmarch.name) jan@newmarch.name