package edu.hawaii.ics.yucheng; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import lejos.nxt.Button; import lejos.nxt.LCD; import lejos.nxt.SensorPort; import lejos.nxt.Sound; import lejos.nxt.SoundSensor; import lejos.nxt.comm.Bluetooth; import lejos.nxt.comm.NXTConnection; /** * This is the main class that implements the client and server state machines. * * @author Cheng Jade * @project NXT Acoustic Tape Measure (Distance) * @date Jul 18, 2010 * @bugs When Bluetooth communication fails the state machine break and the * NXTs do not re-synchronize deterministically. */ public final class Acoustic { /** * The pulse interval (nanosecond), which determines the time between sound * pulses. */ private static final long PULSE_INTERVAL = 2000 * 1000 * 1000; /** * The amount of time (nanosecond) before an expected sound pulse during * which sounds are monitored. */ private static final long SYNC_ERROR_MARGIN = 50 * 1000 * 1000; /** * The speed of sound (inches per nanosecond), which corresponds to 347 * meters per second at 80�F. */ private static final float SPEED_OF_SOUND = 1.3665228e-5f; /** The duration (millisecond) of a sound pulse. */ private static final int TONE_DURATION = 500; /** The frequency (Hz) of the tone of a sound pulse. */ private static final int TONE_FREQUENCY = 1100; /** A value transmitted to indicate failures. */ private static final float FAILED_MEASUREMENT = Float.MIN_VALUE; /** The states of the client and server state machines. */ private static enum State { /** The NXT starts to emit a sound pulse. */ PLAY, /** The NXT detects a sound pulse S1. */ RECORD_S1, /** * The NXT detects a sound pulse S2. As the client state machine leaves * this state, the NXT sends the recorded M1 and waits for the computed * distance, which is then displayed on its LCD. As the server state * machine leaves this state, the NXT waits to receive M1, computes the * distance, sends the distance result, and display the result on its * LCD */ RECORD_S2, /** The NXT waits for the end of a sound pulse S1. */ STOP_S1, /** The NXT waits for the end of a sound pulse S2. */ STOP_S2, /** * The NXT recovers from a failed detection of a sound pulse S1, and it * synchronizes with the other participant. */ RECORD_S1_FAILURE } /** An object that detects sound pulses. */ private static final PulseDetector detector = new PulseDetector(); /** * A main method entry point of the application. * * @param args * The command line arguments (not used). */ public static void main(final String[] args) { AcousticConnection connection = null; try { // Print to the LCD a menu of choices, client or server. LCD.drawString("Acoustic Distance", 0, 2); LCD.drawString("<L> Client", 0, 3); LCD.drawString("<R> Server", 0, 4); // Wait for the user to choose an action. while (!Button.ESCAPE.isPressed()) { // If the left button is pressed, initiate a connection to the // neighbor and then execute the client state machine. if (Button.LEFT.isPressed()) { println("Connecting..."); connection = AcousticConnection.newClient(); println("Connected."); runClient(connection); break; } // If the right button is pressed, waits for a connection from // the neighbor and then execute the server state machine. if (Button.RIGHT.isPressed()) { println("Listening..."); connection = AcousticConnection.newServer(); println("Connected."); runServer(connection); break; } } // Display a "Goodbye" briefly when the application terminates. println("Goodbye."); } catch (final AcousticException e) { println(e.getMessage()); } finally { // Always close the connection. if (null != connection) connection.close(); } } /** * The client state machine. * * @param connection * The connection with the neighboring NXT. */ private static void runClient(final AcousticConnection connection) throws AcousticException { // Set the initial pulse time. long pulseTime = now() + PULSE_INTERVAL; // A variable to record S1. long recordS1 = 0; // Set the initial state, PLAY. State state = State.PLAY; // Execute the state machine until the escape button is pressed. while (!Button.ESCAPE.isPressed()) { switch (state) { // Start emitting a sound pulse, and enter the RECORD_S1 state. case PLAY: Sound.playTone(TONE_FREQUENCY, TONE_DURATION); state = State.RECORD_S1; break; // Detect a sound pulse S1. case RECORD_S1: switch (detector.poll(pulseTime)) { // If a sound is successfully detected, record the time and // enter the STOP_S1 state. case SUCCESS: recordS1 = detector.getNanoTime(); state = State.STOP_S1; break; // If a timeout occurs while detecting a sound, print a message // and enter the RECORD_S1_FAILURE state. case TIMEOUT: println("Failed Record S1"); state = State.RECORD_S1_FAILURE; break; // If the escape button is pressed while detecting a sound, // terminate the state machine. case ABORT: return; } break; // Recover from a failed detection of sound pulse S1. case RECORD_S1_FAILURE: if (now() < pulseTime - SYNC_ERROR_MARGIN) break; pulseTime += PULSE_INTERVAL; connection.writeFloat(FAILED_MEASUREMENT); connection.readFloat(); state = State.STOP_S2; break; // Wait for the end of the sound pulse S1. case STOP_S1: if (now() < pulseTime - SYNC_ERROR_MARGIN) break; pulseTime += PULSE_INTERVAL; state = State.RECORD_S2; break; // Detect a sound pulse S2. case RECORD_S2: switch (detector.poll(pulseTime)) { // If a sound is successfully detected, record the time and // enter the STOP_S2 state. case SUCCESS: { final long m1 = detector.getNanoTime() - recordS1; // Send M1 to the neighboring NXT. connection.writeFloat(m1); // Receive the distance from the neighboring NXT, and check // for errors. final float distance = connection.readFloat(); if (distance == FAILED_MEASUREMENT) { println("Server Failed"); state = State.STOP_S2; break; } println("" + distance); state = State.STOP_S2; break; } // If a timeout occurs while detecting a sound, print a // message, synchronize, and enter the STOP_S2 state. case TIMEOUT: println("Failed Record S2"); connection.writeFloat(FAILED_MEASUREMENT); connection.readFloat(); state = State.STOP_S2; break; // If the escape button is pressed while detecting a sound, // terminate the state machine. case ABORT: return; } break; // Wait for the end of a sound pulse S2. case STOP_S2: if (now() < pulseTime) break; pulseTime += PULSE_INTERVAL; state = State.PLAY; break; } } } /** * The server state machine. * * @param connection * The connection with the neighboring NXT. */ private static void runServer(final AcousticConnection connection) throws AcousticException { // Set the initial pulse time. long pulseTime = now() + PULSE_INTERVAL; // A variable to record S1. long recordS1 = 0; // Set the initial state, RECORD_S1. State state = State.RECORD_S1; // Execute the state machine until the escape button is pressed. while (!Button.ESCAPE.isPressed()) { switch (state) { // Detect a sound pulse S1. case RECORD_S1: switch (detector.poll(pulseTime)) { // If a sound is successfully detected, record the time and // enter the STOP_S1 state. case SUCCESS: recordS1 = detector.getNanoTime(); state = State.STOP_S1; break; // If a timeout occurs while detecting a sound, print a message // and enter the RECORD_S1_FAILURE state. case TIMEOUT: println("Failed Record S1"); state = State.RECORD_S1_FAILURE; break; // If the escape button is pressed while detecting a sound, // terminate the state machine. case ABORT: return; } break; // Recover from a failed detection of sound pulse S1. case RECORD_S1_FAILURE: if (now() < pulseTime) break; pulseTime += PULSE_INTERVAL; connection.readFloat(); connection.writeFloat(FAILED_MEASUREMENT); state = State.STOP_S2; break; // Wait for the end of the sound pulse S1. case STOP_S1: if (now() < pulseTime) break; pulseTime += PULSE_INTERVAL; Sound.playTone(TONE_FREQUENCY, TONE_DURATION); state = State.RECORD_S2; break; // Detect a sound pulse S2. case RECORD_S2: switch (detector.poll(pulseTime)) { // If a sound is successfully detected, record the time and // enter the STOP_S2 state. case SUCCESS: { final long m2 = detector.getNanoTime() - recordS1; // Receive M1 from the neighboring NXT and check for errors. final float m1 = connection.readFloat(); if (m1 == FAILED_MEASUREMENT) { println("Client Failed"); connection.writeFloat(FAILED_MEASUREMENT); state = State.STOP_S2; break; } // Calculate the distance and send it to the neighboring // NXT. final float distance = (((m1 - m2) * SPEED_OF_SOUND) / 2.0f); println("" + distance); connection.writeFloat(distance); state = State.STOP_S2; break; } // If a timeout occurs while detecting a sound, print a // message, synchronize, and enter the STOP_S2 state. case TIMEOUT: println("Failed Record S2"); connection.readFloat(); connection.writeFloat(FAILED_MEASUREMENT); state = State.STOP_S2; break; // If the escape button is pressed while detecting a sound, // terminate the state machine. case ABORT: return; } break; // Wait for the end of a sound pulse S2. case STOP_S2: if (now() < pulseTime - SYNC_ERROR_MARGIN) break; pulseTime += PULSE_INTERVAL; state = State.RECORD_S1; break; } } } /** * A method that returns the current time in nanoseconds. * * @return The current time in nanoseconds. */ private static long now() { return System.nanoTime(); } /** * A method that scrolls the LCD and displays a new message. */ private static void println(final String msg) { LCD.scroll(); if (null != msg && msg.length() > 0) LCD.drawString(msg, 0, LCD.DISPLAY_CHAR_DEPTH - 1); } } /** * Possible results from the {@link PulseDetector#poll} method. */ enum PulseStatus { /** Indicates a sound pulse has been detected. */ SUCCESS, /** Indicates a timeout has occurred. */ TIMEOUT, /** Indicates an abort has been issued. */ ABORT; } /** * An object that detects sound pulses. */ final class PulseDetector { /** The sound sensor object, which must be plugged into sensor port #4. */ private final SoundSensor sensor = new SoundSensor(SensorPort.S4); /** The time, in nanoseconds, when a sound pulse was detected. */ private long nanoTime; /** * A method that attempts to detect a sound pulse within a given a timeout. * * @param nanoTimeout * The timeout. * * @return A {@link PulseStatus} that describes the outcome. */ public PulseStatus poll(final long nanoTimeout) { // The DB threshold value. final int minSensorValue = 30; // The minimum consecutive recordings above the DB threshold for a valid // pulse. final int minConsecutiveSamples = 5; // A variable counting consecutive recordings above the DB threshold. int consecutiveSamples = 0; // Loop until the escape button is pressed, a timeout occurs, or a sound // pulse is successfully detected. do { // If the escape button is pressed, abort. if (Button.ESCAPE.isPressed()) return PulseStatus.ABORT; this.nanoTime = System.nanoTime(); // Check for a timeout. if (this.nanoTime > nanoTimeout) return PulseStatus.TIMEOUT; // Ignore recordings that are below the DB threshold. if (this.sensor.readValue() < minSensorValue) { consecutiveSamples = 0; continue; } // Loop until enough consecutive samples are above the DB threshold. } while (++consecutiveSamples < minConsecutiveSamples); // Return successfully. return PulseStatus.SUCCESS; } /** * Get the time in nanosecond when a sound pulse was last detected. * * @return The time, in nanosecond, when a sound pulse was last detected. */ public long getNanoTime() { return this.nanoTime; } } /** * A class that encapsulates the Bluetooth connection between the two NXTs. */ final class AcousticConnection { /** A Bluetooth connection object. */ private final NXTConnection connection; /** An input stream to read from the connection. */ private final DataInputStream inputStream; /** An output stream to write to the connection. */ private final DataOutputStream outputStream; /** The pin for the Bluetooth connection. Both NXTs have the same pins. */ private static final byte[] pin = { '1', '2', '3', '4' }; /** * A method that returns the neighboring NXT's physical address. * * @return The neighboring NXT's physical address. * * @throws AcousticException * Thrown if the local address is unknown. */ private static String getRemoteAddress() throws AcousticException { // The physical address for NXT #1. final String address1 = "0016530025b1"; // The physical address for NXT #2. final String address2 = "00165304c000"; // Obtain the local address. final String local = Bluetooth.getLocalAddress(); // If the local NXT is NXT #1, return NXT #2's physical address. if (local.equalsIgnoreCase(address1)) return address2; // If the local NXT is NXT #2, return NXT #1's physical address. if (local.equalsIgnoreCase(address2)) return address1; // Throw an exception if the local NXT is unknown. throw new AcousticException("Unsupported operation."); } /** * Initiates a new instance of the class * * @param conncetion * A NXTConnection used to initiate the class fields. */ private AcousticConnection(final NXTConnection connection) { this.connection = connection; this.inputStream = connection.openDataInputStream(); this.outputStream = connection.openDataOutputStream(); } /** * A method that initiates a connection to the neighboring NXT. * * @return A connection with the neighboring NXT. * * @throws AcousticException * Thrown if the connection fails. */ public static AcousticConnection newClient() throws AcousticException { // Obtain the neighbor NXT's physical address. final String address = getRemoteAddress(); // Try to connect, and abort and if the escape button is pressed. while (true) { if (Button.ESCAPE.isPressed()) throw new AcousticException("Operation aborted."); final NXTConnection connection = Bluetooth.connect(address, NXTConnection.PACKET, pin); if (null != connection) return new AcousticConnection(connection); } } /** * A method that listens for a connection from the neighboring NXT. * * @return A connection with the neighboring NXT. * * @throws AcousticException * Thrown if the connection fails. */ public static AcousticConnection newServer() throws AcousticException { // Wait for a connection, and abort if the escape button is pressed. while (true) { if (Button.ESCAPE.isPressed()) throw new AcousticException("Operation aborted."); final int timeout = 5000; final NXTConnection connection = Bluetooth.waitForConnection( timeout, NXTConnection.PACKET, pin); if (null != connection) return new AcousticConnection(connection); } } /** * A method closes the connection with the neighboring NXT. */ public void close() { try { this.inputStream.close(); } catch (final IOException e) { } try { this.outputStream.close(); } catch (final IOException e) { } this.connection.close(); } /** * A method that sends a float to the neighboring NXT. * * @param value * A float value to send. * * @throws AcousticException * Thrown if an error occurs while sending the value. */ public void writeFloat(final float value) throws AcousticException { try { this.outputStream.writeFloat(value); this.outputStream.flush(); } catch (final IOException e) { throw new AcousticException("Connection failed."); } } /** * A method that receives a float from the neighboring NXT. * * @return The float value received from the neighboring NXT. * * @throws AcousticException * Thrown if an error occurs while receiving the value. */ public float readFloat() throws AcousticException { try { return this.inputStream.readFloat(); } catch (final IOException e) { throw new AcousticException("Connection failed."); } } } /** * An exception thrown from within the Acoustic package. */ final class AcousticException extends Exception { /** * Initializes a new instance of the {@link AcousticException} class. * * @param message * The detail message (which is saved for later retrieval by the * Throwable.getMessage() method). */ public AcousticException(final String message) { super(message); } }