Acoustic.java

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);
    }
}
Valid HTML 4.01 Valid CSS