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);
}
}