User Interfaces for Jini Services

Contents

  1. User Interfaces as Entries
  2. User Interfaces from Factory Objects
  3. Objects Returned from getUI()
  4. Support Classes
  5. Images
  6. MindStorms
    1. RCXLoaderFrame
    2. RCXLoaderFrameFactory
    3. Exporting the RCXLoaderFrameFactory
    4. Customised User Interfaces
    5. CarJFrame
    6. CarJFrameFactory
    7. Exporting the CarJFrameFactory
    8. The RCX Client

1. User Interfaces as Entries

Interaction with a service is specified by its interface. This will be the same across all implementations of the interface. This doesn't allow any flexibility, and of course it shouldn't. It is the bottom level for using this type of service. But service implementations do differ, and there is a need to allow for this. The mechanism used in Jini is to put these differences in Entry objects. Typical objects supplied by vendors may include Name and ServiceInfo.

Clients can make use of the interface and these additional entry items, primarily in the selection of a service. But once they have the service, are they just constrained to use it via the type interface? Many services will probably benefit from some sort of user interface. For example, a printer may supply a method to print a file, but it may have the capability to print multiple copies of the same file. Rather than relying on the client to be smart enough to figure this out, the printer vendor may want to call attention to this by supplying a user-interface object with a special component for ``number of copies''.

The user interface for a service cannot expect to have all details supplied by the client - at best a client could only manage a fairly generic user interface. It should come from the vendor, or maybe even third parties. When your video player becomes Jini enabled, it would be a godsend for someone to supply a decent user interface for it, since the video player vendors seem generally incapable of doing so! The Entry objects need not just provide static data - as Java objects they are perfectly capable of running as user-interface objects.

User interfaces are not part of the Jini standard. But the Jini community (with a semi-formal organisation as the ``Jini Community'') is coming towards a standard way of specifying many things, including user-interface standards and guidelines. Guideline number one from the ``serviceUI'' group is: user interfaces for a service should be given in Entry objects.

2. User Interfaces from Factory Objects

In the chapter on ``More Complex Examples'' some discussion was given to the location of code, using user-interface components as examples. The chapter suggested that user interfaces should not be created on the server side but on the client side. So the user-interface should be exported as a factory object that can create the user-interface on the client side.

More arguments can be given to support this:

  1. A service exported from a low resource computer, such as an embedded Java engine may not have the classes on the service side needed to create the user-interface (it may not have the Swing or even the AWT libraries)
  2. There may be many potential user-interfaces for any particular service: the Palm Pilot (with small grey-scale screen) requires a different interface to a high-end workstation with huge screen and enormous numbers of colours. It is not reasonable to expect the service to create every one of these, but it could export a factory capable of doing so
  3. Localisation of internationalised services cannot be done on the service side, only on the client side

The service should export a user-interface factory, with a method to create the interface, such as getUI(). The service and its user-interface factory entry will both be retrieved by the client. The client will then create the user-interface. Note that the factory will not know the service object - if it was given one during its construction (on the service side) it would end with another copy of the service. So when it is asked for a user-interface (on the client side), it should be passed the service as well. The actual creation may fail in lots of ways: no suitable libraries, out of memory, screen too small, etc. So this method may need to throw an exception.

The factory will probably know many attributes of the user-interface: preferred and minimum screen sizes, required class libraries etc. These can all be stored as a set of features of the factory. Two have been singled out as being of general importance: the name and the role (such as main and admin). The current definition of a user-interface factory is


public class UIFactory extends AbstractEntry {
    public String name;
    public String role;
    public Map properties;

    public UIFactory(String name, String role);
    public UIFactory(String name, String role, Map properties);

    public Object getUI(Object service) throws UICreationException;
}
A service may have lots of these factories, each capable of creating a different user interface object.

3. Objects Returned from getUI()

There are many kinds of user-interface objects that can be returned. They could be individual components such as buttons or lists, containers such as panels, or components with their own toplevel windows such as frames and dialogs. Dialogs may be modal or non-modal. These will all need to be handled by a client in different ways: a component needs to be placed within a container, a frame is shown and processing independently continues, a modal dialog blocks execution of the calling process until it is dismissed. The client will need to know these broad categories.

A user interface may just use AWT components. It may use Swing components. But it may also use the 2-D or 3-D classes, or even things like the MPEG class from the Media API. Perhaps it doesn't have a ``traditional'' screen interface, but uses a speech interface instead. There may even be possibilities of some other description apart from Java objects: JACL scripts, perhaps (JACL is a tcl interpreter written in Java).

The client needs to have a way of figuring out which types of objects a factory will produce, without actually going all the way through the actual creation. There are two parts to this:

  1. What type of object is returned from getUI()?
  2. What classes are needed by the client before it can succeed it creating the complete set of objects required by the user-interface?

The first problem (what type of object is returned from getUI()) can be answered by using suitable subclasses of UIFactory:

  1. AWTFrameFactory returns an AWT Frame
  2. SwingFrameFactory returns a Swing JFrame
  3. AWTPanelFactory returns an AWT Panel
  4. SwingPanelFactory returns a Swing JPanel
and so on. The list of these classes, and any subclassing relations between them is not fixed yet, and may change in later versions of the ``serviceui'' project. Nevertheless, it is likely that this type of scheme will be eventually decided upon.

The second problem (what classes are needed) is even less settled. A set of ``required libraries'' is currently favoured. This would be one of the properties stored in the map of additional properties in each factory. Each element of this could contain the full package name of the library (such as "javax.swing") plus other information such as version number. There are still some tricky elements in this: for example, must the client supply these libraries, or could they be downloaded from an HTTP server? Some libraries such as the Java Media Framework contain objects written in pure Java, while others in this same package require native code underneath (such as the MPEG player). Must a native library be available even if the widget requiring the native library isn't used?

Processes to define standards acceptable to the Jini Community are being set up. Standards within this are also being defined. Right now things are unsettled, but this will change.

4. Support Classes

The ``serviceUI'' will eventually decide on a standard set of support classes. In the meantime you will have to ``wing it'' and be aware that you are writing/using unstable code. For the purposes of the rest of this chapter this section will define classes that will approximate the final classes of the ``serviceUI'' specification.

A possible implementation of the UIFactory class is



/**
 * UIFactory.java
 *
 *
 * Created: Sun Nov 14 18:07:12 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

package ui;

import net.jini.entry.AbstractEntry;
import java.util.Map;
import java.util.HashMap;

public class UIFactory extends AbstractEntry {
    public String name;
    public String role;
    public Map properties;

    public UIFactory() {
	this(null, null, null);
    }

    public UIFactory(String name, String role) {
	this(name, role, null);
    }

    public UIFactory(String name, String role, Map properties) {
	this.name = name;
	this.role = role;
	if (properties == null) {
	    this.properties = null;
	} else {
	    this.properties = new HashMap(properties);
	}
    }

    public Object getUI(Object service) throws UICreationException {
	throw new UICreationException();
    }
} // UIFactory

(The package name is convenient for this book: it will not be the final package name.)

A factory that can be used to return AWT Frame's is the AWTFrameFactory. It does not enforce this return type, and users of this class are expected to examine the class type in order to determine what to do with it.



/**
 * AWTFrameFactory.java
 *
 *
 * Created: Sun Nov 14 18:18:32 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

package ui;

import java.util.Map;

public class AWTFrameFactory extends UIFactory {

    public AWTFrameFactory() {
	super();
    }
        
    public AWTFrameFactory(String name, String role) {
	super(name, role);
    }

    public AWTFrameFactory(String name, String role, Map properties) {
	super(name, role, properties);
    }

} // AWTFrameFactory

5. Images

User interfaces often contain images. They may be used as icons in toolbars, for general images on the screen, or for the icon image when the application is iconified. When a user interface is created on the client, these images will also need to be created and installed in the relevant part of the application. Images are not serializable, so they cannot be created on the server and exported as live objects in some manner. They need to be created from scratch on the client.

The Swing package contains a convenience class ImageIcon. This can be instantiated from a byte array, a filename, or most interestingly here, from a URL. So if an image is stored where an HTTP server can find it, then the ImageIcon constructor can use this directly. There may be failures in this: the URL may be incorrect or malformed, or the image may fail to exist on the HTTP server. Suitable code is


        ImageIcon icon = null;
	try {
	    icon = new ImageIcon(new URL("http://localhost/images/mindstorms.jpg"));
	    switch (icon.getImageLoadStatus()) {
	    case MediaTracker.ABORTED:
	    case MediaTracker.ERRORED:
		System.out.println("Error");
		icon = null;
		break;
	    case MediaTracker.COMPLETE:
		System.out.println("Complete");
		break;
	    case MediaTracker.LOADING:
		System.out.println("Loading");
		break;
	    }
	} catch(java.net.MalformedURLException e) {
	    e.printStackTrace();
	}
	// icon is null or is a valid image

6. MindStorms

6.1 RCXLoaderFrame

A MindStorms robot is primarily defined by the RCXPort interface. The Jini version is defined by the RCXPortImplementation interface:



/**
 * RCXPortInterface.java
 *
 *
 * Created: Wed Jun  2 23:08:12 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

package rcx.jini;

import net.jini.core.event.RemoteEventListener;

public interface RCXPortInterface extends java.io.Serializable {

    /** 
     * constants to distinguish message types
     */
    public final long ERROR_EVENT = 1;
    public final long MESSAGE_EVENT = 2;

    /**
     * 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;

    /**
     * Add a RemoteEvent listener to the RCX for messages and errors
     */
    public void addListener(RemoteEventListener listener) 
	throws java.rmi.RemoteException;

    /**
     * The last message from the RCX
     */
    public byte[] getMessage(long seqNo)
	throws java.rmi.RemoteException;

    /**
     * The error message from the RCX
     */
    public String getError(long seqNo)
	throws java.rmi.RemoteException;

} // RCXPortInterface

This allows programs to be downloaded and run, and instructions to be sent for direct execution. As it stands, the client needs to call these interface methods directly. To make it more useable for the human trying to drive a robot, some sort of user interface would be useful.

There can be several general purpose user interfaces for the RCX robot, including:

  1. Enter machine code (somehow) and download that
  2. Enter RCX assembler code in the form of strings, assemble and download them
  3. Enter NQC (Not Quite C) code, compile and download it
The set of RCX classes by Laverde includes a standalone application called RCXLoader which does the second of these. We can steal code from this and some of his other classes, to define a class RCXLoaderFrame

package rcx.jini;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*; 
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.RemoteEvent;
import net.jini.core.event.UnknownEventException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
import rcx.*;

/*
 * RCXLoaderFrame
 * @author Dario Laverde
 * @author Jan Newmarch
 * @version 1.1
 * Copyright 1999 Dario Laverde, under terms of GNU LGPL
 */

public class RCXLoaderFrame extends Frame 
    implements ActionListener, WindowListener, RemoteEventListener
{
    private String      portName;
    private RCXPortInterface     rcxPort;
    private Panel       textPanel;
    private Panel       topPanel;
    private TextArea    textArea;
    private TextField   textField;
    private Button      tableButton;
    private Properties  parameters;
    private int inByte;
    private int charPerLine = 48;
    private int lenCount;
    private StringBuffer sbuffer;
    private byte[] byteArray;
    private Frame opcodeFrame;
    private TextArea opcodeTextArea;

    public static Hashtable Opcodes=new Hashtable(55);

    static {
        Opcodes.put(new Byte((byte)0x10),"PING            ,void, void,P");
        Opcodes.put(new Byte((byte)0x12),"GETVAL          ,byte src byte arg, short val,P");
        Opcodes.put(new Byte((byte)0x13),"SETMOTORPOWER   ,byte motors byte src byte arg, void,CP");
        Opcodes.put(new Byte((byte)0x14),"SETVAL          ,byte index byte src byte arg, void,CP");
        Opcodes.put(new Byte((byte)0x15),"GETVERSIONS     ,byte key[5], short rom[2] short firmware[2],P");
        Opcodes.put(new Byte((byte)0x17),"CALLSUB         ,byte subroutine, void,C");
        Opcodes.put(new Byte((byte)0x20),"GETMEMMAP       ,void, short map[94],P");
        Opcodes.put(new Byte((byte)0x21),"SETMOTOR        ,byte code, void,CP");
        Opcodes.put(new Byte((byte)0x22),"SETTIME         ,byte hours byte minutes, void,CP");
        Opcodes.put(new Byte((byte)0x23),"PLAYTONE        ,short frequency byte duration, void,CP");
        Opcodes.put(new Byte((byte)0x24),"ADDVAR          ,byte index byte src short arg, void,CP");
        Opcodes.put(new Byte((byte)0x25),"DOWNLOADTASK    ,byte unknown short task short length, byte error,P");
        Opcodes.put(new Byte((byte)0x27),"BRANCHALWAYSNEAR,byte offset, void,C");
        Opcodes.put(new Byte((byte)0x30),"GETBATTERYPOWER ,void, short millivolts,P");
        Opcodes.put(new Byte((byte)0x31),"SETTRANSMITRANGE,byte range, void,CP");
        Opcodes.put(new Byte((byte)0x32),"SETSENSORTYPE   ,byte sensor byte type, void,CP");
        Opcodes.put(new Byte((byte)0x33),"SETDISPLAY      ,byte src short arg, void,CP");
        Opcodes.put(new Byte((byte)0x34),"SUBFROMVAR      ,byte index byte src short arg, void,CP");
        Opcodes.put(new Byte((byte)0x35),"DOWNLOADSUB     ,byte unknown short subroutine short length, byte error,P");
        Opcodes.put(new Byte((byte)0x40),"DELALLTASKS     ,void, void,CP");
        Opcodes.put(new Byte((byte)0x42),"SETSENSORMODE   ,byte sensor byte code, void,CP");
        Opcodes.put(new Byte((byte)0x43),"WAIT            ,byte src short arg, void,C");
        Opcodes.put(new Byte((byte)0x44),"DIVIDEVAR       ,byte index byte src short arg, void,CP");
        Opcodes.put(new Byte((byte)0x45),"TRANSFERDATA    ,short index short length byte data[length] byte checksum, byte error,P");
        Opcodes.put(new Byte((byte)0x50),"STOPALLTASKS    ,void, void,CP");
        Opcodes.put(new Byte((byte)0x51),"PLAYSOUND       ,byte sound, void,CP");
        Opcodes.put(new Byte((byte)0x52),"SETDATALOGSIZE  ,short size, byte error,CP");
        Opcodes.put(new Byte((byte)0x54),"MULTIPLYVAR     ,byte index byte src short arg, void,CP");
        Opcodes.put(new Byte((byte)0x60),"POWEROFF        ,void, void,CP");
        Opcodes.put(new Byte((byte)0x61),"DELTASK         ,byte task, void,CP");
        Opcodes.put(new Byte((byte)0x62),"DATALOGNEXT     ,byte src byte arg, byte error,CP");
        Opcodes.put(new Byte((byte)0x64),"SIGNVAR         ,byte index byte src short arg, void,CP");
        Opcodes.put(new Byte((byte)0x65),"DELFIRMWARE     ,byte key[5], void,CP");
        Opcodes.put(new Byte((byte)0x70),"DELALLSUBS      ,void, void,CP");
        Opcodes.put(new Byte((byte)0x71),"STARTTASK       ,byte task, void,CP");
        Opcodes.put(new Byte((byte)0x72),"BRANCHALWAYSFAR ,byte offset byte extension, void,C");
        Opcodes.put(new Byte((byte)0x74),"ABSVAL          ,byte index byte src short arg, void,CP");
        Opcodes.put(new Byte((byte)0x75),"DOWNLOADFIRMWARE,short address short checksum byte unknown, void,P");
        Opcodes.put(new Byte((byte)0x81),"STOPTASK        ,byte task, void,CP");
        Opcodes.put(new Byte((byte)0x82),"SETLOOPCOUNTER  ,byte src byte arg, void,C");
        Opcodes.put(new Byte((byte)0x84),"ANDVAR          ,byte index byte src byte arg, void,CP");
        Opcodes.put(new Byte((byte)0x90),"CLEARMESSAGE    ,void, void,C");
        Opcodes.put(new Byte((byte)0x91),"SETPROGRAMNUM   ,byte program, void,CP");
        Opcodes.put(new Byte((byte)0x92),"DECCNTRANDBRANCH,short offset, void,C");
        Opcodes.put(new Byte((byte)0x94),"ORVAR           ,byte index byte src byte arg, void,CP");
        Opcodes.put(new Byte((byte)0x95),"TESTANDBRANCH   ,byte opsrc1 byte src2 short arg1 byte arg2 short offset,void,C");
        Opcodes.put(new Byte((byte)0xa1),"CLEARTIME       ,byte timer, void,CP");
        Opcodes.put(new Byte((byte)0xa4),"UPLOADDATALOG   ,short first short count, dlrec data[length],P");
        Opcodes.put(new Byte((byte)0xa5),"UNLOCKFIRMWARE  ,byte key[5], byte data[25],P");
        Opcodes.put(new Byte((byte)0xb1),"SETPOWERDOWN    ,byte minutes, void,CP");
        Opcodes.put(new Byte((byte)0xb2),"SENDMESSAGE     ,byte src byte arg, void,C");
        Opcodes.put(new Byte((byte)0xc1),"DELSUB          ,byte subroutine, void,CP");
        Opcodes.put(new Byte((byte)0xd1),"CLEARSENSOR     ,byte sensor, void,CP");
        Opcodes.put(new Byte((byte)0xe1),"SETMOTORDIR     ,byte code, void,CP");
        Opcodes.put(new Byte((byte)0xf7),"SETMESSAGE      ,byte message, void,PC");
    }

    // added port interface parameter to Dario's code
    public RCXLoaderFrame(RCXPortInterface port) {
    	super("RCX Loader");

	// changed from Dario's code    	
	rcxPort = port;

        addWindowListener(this);
        
        topPanel  = new Panel();
        topPanel.setLayout(new BorderLayout());

        tableButton = new Button("table");
        tableButton.addActionListener(this);
        
        textField = new TextField();
        // textField.setEditable(false);
        // textField.setEnabled(false);
        // tableButton.setEnabled(false);
        textField.addActionListener(this);

        textPanel = new Panel();
        textPanel.setLayout(new BorderLayout(5,5));        

        topPanel.add(textField,"Center");
        topPanel.add(tableButton,"East");
        textPanel.add(topPanel,"North");

        textArea = new TextArea();
        // textArea.setEditable(false);
        textArea.setFont(new Font("Courier",Font.PLAIN,12));
        textPanel.add(textArea,"Center");

        add(textPanel, "Center");

        textArea.setText("initializing...\n");
        
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        setBounds(screen.width/2-370/2,screen.height/2-370/2,370,370);
        // setVisible(true);

	// changed listener type from Dario's code
        try {
	    // We are remote to the object we are listening to 
	    // (the RCXPort), so the RCXPort must get a stub object
	    // for us. We have subclassed from Frame, not from
	    // UnicastRemoteObject. So we must export ourselves
	    // for the remote references to work
	    UnicastRemoteObject.exportObject(this);
	    rcxPort.addListener(this);
	} catch(Exception e) {
	    textArea.append(e.toString());
	}
        tableButton.setEnabled(true);        
        
	/*
        if(rcxPort.isOpen()) {
            textArea.append("RCXPort initialized.\n");
            String lasterror = rcxPort.getLastError();
            if(lasterror!=null)
                textArea.append(lasterror+"\n");
            textField.setEditable(true);
            textField.setEnabled(true);
            textField.requestFocus();            
        }
        else {
            if(portName!=null) {
                textArea.append("Failed to create RCXPort with "+portName+"\n");            
                textArea.append("Port "+portName+" is invalid or may be ");
                textArea.append("currently used.\nTry another port.\n");
                textArea.append("Edit or create a file named parameters.txt ");
                textArea.append("that has:\nport=COM1\n(replace COM1 with ");
                textArea.append("the correct port name)\n");
            }
            else
                textArea.append("Please specify a port in parameters.txt\n");
                textArea.append("Create a file that has:\nport=COM1\n");
                textArea.append("(replace COM1 with the correct port name)\n");
        }
	*/
    }

    /*
    public void receivedMessage(byte[] responseArray) {
        if(responseArray==null)
            return;
        for(int loop=0;loop<responseArray.length;loop++) {       
            int newbyte = (int)responseArray[loop];
            if(newbyte<0) newbyte=256+newbyte;
            sbuffer = new StringBuffer(Integer.toHexString(newbyte));
            
            if(sbuffer.length()<2)
                sbuffer.insert(0,'0');
            textArea.append(sbuffer+" ");
            lenCount+=3;
            if(lenCount==charPerLine) {
                lenCount=0;
                textArea.append("\n");
            }
        }
        if(lenCount!=charPerLine)
            textArea.append("\n");        
    }
    */

    /*
    public void receivedError(String error) {
        textArea.append(error+"\n");
    }
    */
        
    public void actionPerformed(ActionEvent evt) {
        Object obj = evt.getSource();
        if(obj==textField) {
            String strInput = textField.getText();
            textField.setText("");
            textArea.append("> "+strInput+"\n");
            try {
		byteArray = rcxPort.parseString(strInput);
	    } catch(RemoteException e) {
		textArea.append(e.toString());
	    }
            // byteArray = RCXOpcode.parseString(strInput);
            if(byteArray==null) {
                textArea.append("Error: illegal hex character or length\n");
                return;
            }
            if(rcxPort!=null) {
		try {
		    if(!rcxPort.write(byteArray)) {
			textArea.append("Error: writing data to port "+portName+"\n");
		    }
		} catch(Exception e) {
		    textArea.append(e.toString());
		}
            }
        }
        else if(obj==tableButton) {
	    // make this all in the ui side
            showTable();
            setLocation(0,getLocation().y);
        }
    }

    public void windowActivated(WindowEvent e) { }
    public void windowClosed(WindowEvent e) { }
    public void windowDeactivated(WindowEvent e) { }
    public void windowDeiconified(WindowEvent e) { }
    public void windowIconified(WindowEvent e) { }
    public void windowOpened(WindowEvent e) { }
    public void windowClosing(WindowEvent e) {
	/*
        if(rcxPort!=null)
        	rcxPort.close();
	*/
        System.exit(0);
    }

    public void notify(RemoteEvent evt) throws UnknownEventException, 
                                                 java.rmi.RemoteException {
    
	long id = evt.getID();
	long seqNo = evt.getSequenceNumber();
	if (id == RCXPortInterface.MESSAGE_EVENT) {
	    byte[] message = rcxPort.getMessage(seqNo);
	    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) + " ");
	    }
	    textArea.append(sbuffer.toString());
	    System.out.println("MESSAGE: " + sbuffer.toString());
	} else if (id == RCXPortInterface.ERROR_EVENT) {
	    textArea.append(rcxPort.getError(seqNo));
	} else {
	    throw new UnknownEventException("Unknown message " + evt.getID());
	}
    }

    public void showTable()
    {
        if(opcodeFrame!=null)
        {
            opcodeFrame.dispose();
            opcodeFrame=null;
            return;
        }
        opcodeFrame = new Frame("RCX Opcodes Table");
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        opcodeFrame.setBounds(screen.width/2-70,0,screen.width/2+70,screen.height-25);
        opcodeTextArea = new TextArea("   Opcode          ,parameters, response, C=program command P=remote command\n",60,100);
        opcodeTextArea.setFont(new Font("Courier",Font.PLAIN,10));
        opcodeFrame.add(opcodeTextArea);
        Enumeration k = Opcodes.keys();
        for (Enumeration e = Opcodes.elements(); e.hasMoreElements();) {
            String tmp = Integer.toHexString(((Byte)k.nextElement()).intValue());
            tmp = tmp.substring(tmp.length()-2)+" "+(String)e.nextElement()+"\n";
            opcodeTextArea.append(tmp);
        }
        opcodeTextArea.setEditable(false);
        opcodeFrame.setVisible(true);
    }
}

6.2 RCXLoaderFrameFactory

The factory object for the RCX is now easy to define, it just returns a RCXLoaderFrame in the getUI() method:



/**
 * RCXLoaderFrameFactory.java
 *
 *
 * Created: Sun Nov 14 18:22:46 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

package rcx.jini;

import ui.AWTFrameFactory;
import ui.UICreationException;
import java.util.Map;

public class RCXLoaderFrameFactory extends AWTFrameFactory {
    
    public RCXLoaderFrameFactory() {
	super();
    }

    public RCXLoaderFrameFactory(String name, String role) {
	super(name, role);
    }

    public RCXLoaderFrameFactory(String name, String role, Map properties) {
	super(name, role, properties);
    }

    public Object getUI(Object obj) throws UICreationException {
	if (obj instanceof RCXPortInterface) {
	    return new RCXLoaderFrame((RCXPortInterface) obj);
	} else {
	    throw new UICreationException();
	}
    }
    
} // RCXLoaderFrameFactory


6.3 Exporting the FrameFactory

The factory object is exported by making it one of the Entry objects with a name and role that is agreed on by all services and clients:


Entry[] entries = {new RCXLoaderFrameFactory("generic", "user")};
JoinManager joinMgr = new JoinManager(impl,
				      entries,
				      this,
				      new LeaseRenewalManager());

6.4 Customised User Interfaces

The RCXLoaderFrame is a general interface to any RCX robot. Of course, there could be many other such interfaces, differing in the classes used, the amount of internationalisation support, the appearance, etc. All the variations, however, will just use the standard RCXPortInterface, as that is all they know about.

The Lego pieces can be combined in a huge variety of ways, and the RCX itself is programmable. So you can build an RCX car, an RCX crane, an RCX maze-runner, and so on. Each different robot can be driven by the general interface, but most could benefit from a custom-built interface for that type of robot. This is typical: for example, every blender could be driven from a general blender user interface (using the - possibly - forthcoming standard blender interface :-). But the blenders from individual vendors would have their own customised user interface for their brand of blender.

I have been using an RCX car. While it can do lots of things, for demonstrations it has been convenient to use five commands: forward, stop, back, left and right, with a user interface looking like

In the MindStorms chapter this appearance was hard-coded into the client - which really shouldn't know about this sort of detail.

6.5 CarJFrame

The CarJFrame class produces the user interface as a Swing JFrame, with the buttons generating specific RCX code for this model.



package rcx.jini;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.RemoteEvent;
import net.jini.core.event.UnknownEventException;
import java.net.URL;

class CarJFrame extends JFrame
    implements RemoteEventListener, ActionListener {
    
    protected RCXPortInterface port = null;
    
    JFrame frame;
    JTextArea text;
    
    public CarJFrame(RCXPortInterface port) {
	super() ;
	this.port = port;
	
	frame = new JFrame("Lego MindStorms");
	Container content = frame.getContentPane();
	JLabel label = null;
	ImageIcon icon = null;
	try {
	    icon = new ImageIcon(new 
					  URL("http://localhost/images/mindstorms.jpg"));
	    switch (icon.getImageLoadStatus()) {
	    case MediaTracker.ABORTED:
	    case MediaTracker.ERRORED:
		System.out.println("Error");
		icon = null;
		break;
	    case MediaTracker.COMPLETE:
		System.out.println("Complete");
		break;
	    case MediaTracker.LOADING:
		System.out.println("Loading");
		break;
	    }
	} catch(java.net.MalformedURLException e) {
	    e.printStackTrace();
	}
	if (icon != null) {
	    label = new JLabel(icon);
	} else {
	    label = new JLabel("MINDSTORMS");
	}

	JPanel pane = new JPanel();	    
	pane.setLayout(new GridLayout(2, 3));
	
	content.add(label, "North");
	content.add(pane, "Center");
	
	JButton btn = new JButton("Forward");
	pane.add(btn);
	btn.addActionListener(this);
	
	btn = new JButton("Stop");
	pane.add(btn);
	btn.addActionListener(this);
	
	btn = new JButton("Back");
	pane.add(btn);
	btn.addActionListener(this);
	
	btn = new JButton("Left");
	pane.add(btn);
	btn.addActionListener(this);
	
	label = new JLabel("");
	pane.add(label);
	
	btn = new JButton("Right");
	pane.add(btn);
	btn.addActionListener(this);
	
	frame.pack();
	frame.setVisible(true);
    }
    
    public void actionPerformed(ActionEvent evt) {
	String name = evt.getActionCommand();
	byte[] command;
	
	try {
	    if (name.equals("Forward")) {
		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");
		}
	    } else  if (name.equals("Stop")) {
		command = port.parseString("21 41");
		if (! port.write(command)) {
		    System.err.println("command failed");
		}
	    } else  if (name.equals("Back")) {
		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");
		}
		
	    } else  if (name.equals("Left")) {
		command = port.parseString("e1 04");
		if (! port.write(command)) {
		    System.err.println("command failed");
		}
		command = port.parseString("21 84");
		if (! port.write(command)) {
		    System.err.println("command failed");
		}
		Thread.sleep(100);
		command = port.parseString("21 44");
		if (! port.write(command)) {
		    System.err.println("command failed");
		}
		
	    } else  if (name.equals("Straight")) {
		
	    } else  if (name.equals("Right")) {
		command = port.parseString("e1 84");
		if (! port.write(command)) {
		    System.err.println("command failed");
		}
		command = port.parseString("21 84");
		if (! port.write(command)) {
		    System.err.println("command failed");
		}
		Thread.sleep(100);
		command = port.parseString("21 44");
		if (! port.write(command)) {
		    System.err.println("command failed");
		}
	    }
	} catch(Exception e) {
	    e.printStackTrace();
	}
    }
    
    public void notify(RemoteEvent evt) throws UnknownEventException, 
	java.rmi.RemoteException {
	// System.out.println(evt.toString());
	
	long id = evt.getID();
	long seqNo = evt.getSequenceNumber();
	if (id == RCXPortInterface.MESSAGE_EVENT) {
	    byte[] message = port.getMessage(seqNo);
	    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("MESSAGE: " + sbuffer.toString());
	} else if (id == RCXPortInterface.ERROR_EVENT) {
	    System.out.println("ERROR: " + port.getError(seqNo));
	} else {
	    throw new UnknownEventException("Unknown message " + evt.getID());
	}
    }
}





6.6 CarJFrameFactory

The factory generates a CarJFrame object



/**
 * CarJFrameFactory.java
 *
 *
 * Created: Fri Oct 29 23:27:16 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

package rcx.jini;

import ui.SwingFrameFactory;
import ui.UICreationException;
import java.util.Map;
import rcx.*;

public class CarJFrameFactory extends SwingFrameFactory {
    
    public CarJFrameFactory() {
	super();
    }

    public CarJFrameFactory(String name, String role) {
	super(name, role);
    }

    public CarJFrameFactory(String name, String role, Map properties) {
	super(name, role, properties);
    }

    public Object getUI(Object service)
	throws UICreationException {

	if (service instanceof RCXPortInterface) {
	    return new CarJFrame((RCXPortInterface) service);
	} else {
	    throw new UICreationException();
	}
    }
    
} // CarJFrameFactory




6.7 Exporting the FrameFactory

Both of the user interfaces discussed can be exported by expanding the set of Entry objects.


Entry[] entries = {new RCXLoaderFrameFactory("generic", "user"),
                   new CarJFrameFactory("car", "user")};
JoinManager joinMgr = new JoinManager(impl,
				      entries,
				      this,
				      new LeaseRenewalManager());

6.8 The RCX Client

The following client will start up all user interfaces that it understands



package client;

import rcx.jini.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

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;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.RemoteEvent;
import net.jini.core.event.UnknownEventException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
import net.jini.core.entry.Entry;
import net.jini.core.lookup.ServiceMatches;
import net.jini.core.lookup.ServiceItem;

import ui.UIFactory;
import ui.AWTFrameFactory;
import ui.SwingFrameFactory;
import ui.UICreationException;

/**
 * TestRCX2.java
 *
 *
 * Created: Wed Mar 17 14:29:15 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class TestRCX2 implements DiscoveryListener {

    public static void main(String argv[]) {
	new TestRCX2();

        // stay around long enough to receive replies
        try {
            Thread.currentThread().sleep(1000000L);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
    }

    public TestRCX2() {
	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;
	Entry[] entries = {new UIFactory(null, null)};
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       entries);
 
        for (int n = 0; n < registrars.length; n++) {
	    System.out.println("Service found");
            ServiceRegistrar registrar = registrars[n];
	    ServiceMatches matches = null;
	    try {
		matches = registrar.lookup(template, 10);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
		System.exit(2);
	    }
System.out.println("match count; " + matches.items.length);
	    for (int nn = 0; nn < matches.items.length; nn++) {
		ServiceItem item = matches.items[nn];
		port = (RCXPortInterface) item.service;
		if (port == null) {
		    System.out.println("port null");
		    continue;
		}	

		Entry[] attributes = item.attributeSets;
System.out.println("Attribute count: " + attributes.length);
		for (int m = 0; m < attributes.length; m++) {
System.out.println(attributes[m].toString());
		    Entry attr = attributes[m];
		    if (attr instanceof UIFactory) {
			showUI(port, (UIFactory) attr);
		    }
		}
	    }
	


	}
    }

    public void discarded(DiscoveryEvent evt) {
	// empty
    }

    private void showUI(RCXPortInterface port, UIFactory attr) {
System.out.println("showing ui");
	if (attr instanceof AWTFrameFactory) {
	    Frame frame = null;
	    try {
		frame = (Frame) attr.getUI(port);
	    } catch(UICreationException e) {
		e.printStackTrace();
		return;
	    }
	    frame.setVisible(true);
	} else if (attr instanceof SwingFrameFactory) {
	    JFrame frame = null;
	    try {
		frame = (JFrame) attr.getUI(port);
	    } catch(UICreationException e) {
		e.printStackTrace();
		return;
	    }
	    frame.setVisible(true);
	} else {
	    System.out.println("non-gui entry");
	}	
    }
} // TestRCX


This file is Copyright (©) 1999 by Jan Newmarch (http://jan.newmarch.name) jan@newmarch.name.

This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v0.4 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.