package lejos.pc.comm; import lejos.nxt.remote.*; import java.io.*; import java.util.Iterator; import java.util.Vector; /** * Base Implementation of NXTComm for USB * * This module implements two types of I/O over USB. * 1. The standard Lego LCP format used for LCP command processing. * 2. A Simple packet based protocol that can be used to transport a simple * byte stream. * Protocol 2 is required (rather then using raw USB operations), to allow the * signaling of things like open, and close over the connection. * * Notes * This module assumes that the device read and write functions have a built in * timeout period of approx 20 seconds. This module assumes that this timeout * exists and uses it to timeout some requests. * * Should not be used directly - use NXTCommFactory to create * an appropriate NXTComm object for your system and the protocol * you are using. * */ public abstract class NXTCommUSB implements NXTComm { private NXTInfo nxtInfo; private boolean packetMode = false; private boolean EOF = false; static final int USB_BUFSZ = 64; static final String VENDOR_ATMEL = "0x03EB"; static final String PRODUCT_SAMBA = "0x6124"; private byte[] inBuf = new byte[USB_BUFSZ]; private byte[] outBuf = new byte[USB_BUFSZ]; /** * Return a vector of available nxt devices. Each NXTInfo item should * have the address field populated and the other fields must contain * sufficient information such that a call to devIsValid will return * true and that devOpen will connect to the device. The name field may * be left empty, in which case it will be populated by code in this class. * @return vector of available nxt devices. */ abstract Vector<NXTInfo> devFind(); /** * Connect to the specified nxt device. * @param nxt The device to connect to * @return A handle to the device */ abstract long devOpen(NXTInfo nxt); /** * Close the device. The device will no longer be available for use. * @param nxt The device to be closed. */ abstract void devClose(long nxt); /** * Write bytes to the device. The call must timeout after approx 20 seconds * if it is not possible to write to the device. * @param nxt Device to write to. * @param message Bytes to be written. * @param offset Offset to start writing from. * @param len Number of bytes to write. * @return Number of bytes written, 0 if timed out < 0 if an error. */ abstract int devWrite(long nxt, byte [] message, int offset, int len); /** * Read bytes from the device. The call must timeout after approx 20 seconds * if it is not possible to read from the device. * @param nxt Device to read from. * @param data Location to place the read bytes. * @param offset Offset of where to place the bytes. * @param len Number of bytes to read. * @return The number of bytes read, 0 if timeout < 0 if an error. */ abstract int devRead(long nxt, byte[] data, int offset, int len); /** * Test to see if the contents of the NXTInfo structure are sufficient * to allow connection to the device. * @param nxt The device to check. * @return True if ok, False otherwise. */ abstract boolean devIsValid(NXTInfo nxt); /** * Helper function to return the nth string that is part of a standard * double colon separated USB address. Note that first entry in a string is * entry 1 (not 0), -ve values may be used to access the address in reverse * so that the last entry is entry -1. * @param addr The address containing the string * @param loc The location of the entry. * @return The string at location loc or null if not found. */ String getAddressString(String addr, int loc) { if (addr == null || addr.length() == 0) return null; int start, end; if (loc < 0) { end = addr.length(); start = end; for(;;) { start = addr.lastIndexOf("::", end - 2) + 2; if (start < 2) start = 0; if (++loc >= 0) break; if (start <= 0) return null; end = start - 2; } } else { start = 0; end = 0; for(;;) { end = addr.indexOf("::", start); if (end < 0) end = addr.length(); if (start > end) return null; if (--loc <= 0) break; if (end >= addr.length()) return null; start = end+2; } } if (start > end) return null; return addr.substring(start, end); } /** * Helper function. Open the specified nxt, get its name and close it. * @param nxt the device to obtain the name for * @return the nxt name. */ private String getName(NXTInfo dev) { String name = null; long nxt = devOpen(dev); if (nxt == 0) return name; byte[] request = { NXTProtocol.SYSTEM_COMMAND_REPLY, NXTProtocol.GET_DEVICE_INFO }; if (devWrite(nxt, request, 0, request.length) > 0) { int ret = devRead(nxt, inBuf, 0, 33); if (ret >= 33) { char nameChars[] = new char[16]; int len = 0; for (int i = 0; i < 15 && inBuf[i + 3] != 0; i++) { nameChars[i] = (char) inBuf[i + 3]; len++; } name = new String(nameChars, 0, len); } } devClose(nxt); return name; } /** * Helper function, reads a single packet from the USB device. Handles * packet headers and timeouts. Blocks until data is available. * @param block true if request should block rather than timeout * @return date or null if at EOF */ private byte[] readPacket(boolean block) throws IOException { int len; while((len=devRead(nxtInfo.nxtPtr, inBuf, 0, inBuf.length)) == 0 && block) {} if (len < 0) throw new IOException("Error in read"); int offset = 0; if (packetMode) { if (((int)inBuf[0] & 0xff) != len - 1) throw new IOException("Bad packet format"); if (inBuf[0] == 0) return null; offset = 1; } if (len == 0) return new byte[0]; byte [] ret = new byte[len - offset]; System.arraycopy(inBuf, offset, ret, 0, len - offset); return ret; } /** * Helper function. Write a single packet. Handles packet format and * request timeouts. * @param data * @param offset * @param len * @param block true if requests should block rather than timeout * @return number of bytes actually written * @throws java.io.IOException */ private int writePacket(byte[] data, int offset, int len, boolean block) throws IOException { byte [] out = null; if (packetMode) { if (len > USB_BUFSZ-1) len = USB_BUFSZ - 1; outBuf[0] = (byte) len; System.arraycopy(data, offset, outBuf, 1, len); out = outBuf; offset = 0; len += 1; } else out = data; int ret; while ((ret = devWrite(nxtInfo.nxtPtr, out, offset, len)) == 0 && block) {} if (ret < 0) throw new IOException("Error in write"); return ret; } private boolean writeEOF() { outBuf[0] = 0; return (devWrite(nxtInfo.nxtPtr, outBuf, 0, 1) == 1); } private void waitEOF() { while(devRead(nxtInfo.nxtPtr, inBuf, 0, inBuf.length) > 1) {} } /** * Helper function, convert an array of names into an NXTInfo vector. This * function takes an array of standard Lego USB string adresses and converts * them into an nxtVector. It handles the both NXT and Samba type devices. * @param nxtNames an array of device address strings. * @return */ Vector<NXTInfo> find(String[] nxtNames) { if (nxtNames == null) return new Vector<NXTInfo>(); Vector<NXTInfo> nxtInfos = new Vector<NXTInfo>(); for(int idx = 0; idx < nxtNames.length; idx++) { String addr = nxtNames[idx]; NXTInfo info = new NXTInfo(); // Use the default way to obtain the name info.name = null; info.btResourceString = addr; info.protocol = NXTCommFactory.USB; // Look to see if this is a Samba device if (getAddressString(addr, 2).equals(VENDOR_ATMEL) && getAddressString(addr, 3).equals(PRODUCT_SAMBA)) info.name = "%%NXT-SAMBA%%"; info.deviceAddress = getAddressString(addr, -2); // if the device address is "000000000000" then it is not // supplying a serial number. This is either a very old version // of leJOS, or leJOS is not responding. Either way we ignore // this device. if (info.deviceAddress != null && !info.deviceAddress.equals("000000000000")) nxtInfos.addElement(info); else System.out.println("Ignoring device " + addr); } return nxtInfos; } /** * Locate availabe nxt devices and return them. Optionally filter the list * to those that match name. * @param name The name to search for. If null return all devices. * @param protocol The protocol to search for, must be USB * @return The list of devices. */ public NXTInfo[] search(String name, int protocol) { Vector<NXTInfo> nxtInfos = devFind(); if (nxtInfos.size() == 0) return new NXTInfo[0]; Iterator<NXTInfo> devs = nxtInfos.iterator(); // Keep track of how many of the devices have names... We put these // first in the returned list int nameCnt = 0; // Filter the list against name while (devs.hasNext()) { NXTInfo nxt = devs.next(); if (nxt.deviceAddress == null) nxt.deviceAddress = "000000000000"; if (nxt.name == null) { nxt.name = getName(nxt); } if (name != null && (nxt.name == null || !name.equals(nxt.name))) devs.remove(); else if (nxt.name != null) nameCnt++; } NXTInfo[] nxts = new NXTInfo[nxtInfos.size()]; int named = 0; int unnamed = nameCnt; // Copy the elements over placing the ones with names first. for (int i = 0; i < nxts.length; i++) { NXTInfo nxt = nxtInfos.elementAt(i); if (nxt.name == null) { nxt.name = "Unknown"; nxts[unnamed++] = nxt; } else nxts[named++] = nxt; } // Print out the list for (int i = 0; i < nxts.length; i++) System.out.println("Found NXT: " + nxts[i].name + " " + nxts[i].deviceAddress); return nxts; } /** * Open a connection to the specified device, and make it available for use. * @param nxtInfo The device to connect to. * @param mode the I/O mode to be used on this connection. * @return true if the device is now open, false otherwise. */ public boolean open(NXTInfo nxtInfo, int mode) { nxtInfo.connectionState = NXTConnectionState.DISCONNECTED; // Is the info valid enough to connect directly? if (!devIsValid(nxtInfo)) { // not valid so search for it. String addr = nxtInfo.deviceAddress; if (addr == null || addr.length() == 0) return false; Vector<NXTInfo> nxtInfos = devFind(); Iterator<NXTInfo> devs = nxtInfos.iterator(); while (devs.hasNext()) { NXTInfo nxt = devs.next(); if (addr.equalsIgnoreCase(nxt.deviceAddress)) { nxtInfo = nxt; break; } } } if (nxtInfo == null) return false; this.nxtInfo = nxtInfo; this.nxtInfo.nxtPtr = devOpen(nxtInfo); if (this.nxtInfo.nxtPtr == 0) return false; // now the connection is open nxtInfo.connectionState = (mode == LCP ? NXTConnectionState.LCP_CONNECTED : NXTConnectionState.PACKET_STREAM_CONNECTED); if (mode == RAW || mode == LCP) return true; // Now try and switch to packet mode for normal read/writes byte[] request = { NXTProtocol.SYSTEM_COMMAND_REPLY, NXTProtocol.NXJ_PACKET_MODE }; byte [] ret = null; try { ret = sendRequest(request, USB_BUFSZ); } catch(IOException e) { ret = null; } // Check the response. We are looking for a non standard response of // 0x02, 0xfe, 0xef if (ret != null && ret.length >= 3 && ret[0] == 0x02 && ret[1] == (byte)0xfe && ret[2] == (byte)0xef) packetMode = true; EOF = false; return true; } public boolean open(NXTInfo nxt) throws NXTCommException { return open(nxt, PACKET); } /** * Close the current device. */ public void close() { if (nxtInfo == null || nxtInfo.nxtPtr == 0) return; if (packetMode) { writeEOF(); if (!EOF) waitEOF(); } devClose(nxtInfo.nxtPtr); nxtInfo.nxtPtr = 0; } /** * Send a Lego Command Protocol (LCP) request to the device. * @param data The command to send. * @param replyLen How many bytes in the optional reply. * @return The optional reply, or null * @throws java.io.IOException Thrown on errors. */ public byte[] sendRequest(byte [] data, int replyLen) throws IOException { int written = devWrite(nxtInfo.nxtPtr, data, 0, data.length); if (written <= 0) throw new IOException("Failed to send data"); if (replyLen == 0) return new byte [0]; byte[] ret = new byte[replyLen]; int len = devRead(nxtInfo.nxtPtr, ret, 0, replyLen); if (len <= 0) throw new IOException("Failed to read reply"); return ret; } /** * Read bytes from the device * @param block true if requests should block rather than timeout * @return An array of bytes read from the device. null if at EOF * @throws java.io.IOException */ byte [] read(boolean timeout) throws IOException { if (EOF) return null; byte [] ret = readPacket(timeout); if (packetMode && ret == null) EOF = true; return ret; } /** * Read bytes from the device * @return An array of bytes read from the device. null if at EOF * @throws java.io.IOException */ public byte [] read() throws IOException { return read(true); } /** * The number of bytes that can be read without blocking. * @return Bytes available to be read. * @throws java.io.IOException */ public int available() throws IOException { return 0; } /** * Write bytes to the device. * @param data Data to be written. * @param block true if request should block rather than timeout * @throws java.io.IOException */ int write(byte [] data, boolean block) throws IOException { int total = data.length; int written = 0; while( written < total) { int len = writePacket(data, written, total-written, block); if (len <= 0) return written; written += len; } return written; } /** * Write bytes to the device. * @param data Data to be written. * @throws java.io.IOException */ public void write(byte [] data) throws IOException { write(data, true); } public OutputStream getOutputStream() { return new NXTCommOutputStream(this); } public InputStream getInputStream() { return new NXTCommInputStream(this); } }