All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gboxsw.acpmod.gep.GEPMessenger Maven / Gradle / Ivy

The newest version!
package com.gboxsw.acpmod.gep;

import java.io.*;
import java.util.logging.*;

/**
 * Thread-safe implementation of messenger implementing the GEP.
 */
public class GEPMessenger {

	/**
	 * Interface representing a full-duplex stream.
	 */
	public interface FullDuplexStream extends Closeable {
		/**
		 * Returns the input stream of the full-duplex stream.
		 * 
		 * @return the input stream.
		 */
		public InputStream getInputStream();

		/**
		 * Returns the output stream of the full-duplex stream.
		 * 
		 * @return the output stream.
		 */
		public OutputStream getOutputStream();

		/**
		 * Closes the full-duplex stream.
		 */
		public void close();
	}

	/**
	 * Interface to access network or remote side over a thread-safe stream
	 * session.
	 */
	public interface FullDuplexStreamSocket {
		/**
		 * Creates a new full duplex stream.
		 * 
		 * @return the open full-duplex stream.
		 * 
		 * @throws IOException
		 *             when construction and opening of full-duplex stream
		 *             failed.
		 */
		public FullDuplexStream createStream() throws IOException;
	}

	/**
	 * Listener for receiving messages sent over the channel.
	 */
	public interface MessageListener {
		/**
		 * Invoked when a message is received.
		 * 
		 * @param tag
		 *            the tag of message or a negative number, if the received
		 *            message does not contain a tag
		 * @param message
		 *            the message
		 */
		void onMessageReceived(int tag, byte[] message);
	}

	/**
	 * Name of the communication thread.
	 */
	private static final String THREAD_NAME = "GEPMessenger - Communication thread";

	/**
	 * Logger.
	 */
	private static final Logger logger = Logger.getLogger(GEPMessenger.class.getName());

	/**
	 * Delay (in milliseconds) between two attempts to connect.
	 */
	private final long RECONNECT_DELAY = 5000;

	/**
	 * Byte indicating start of a new message
	 */
	private final int MESSAGE_START_BYTE = 0x0C;

	/**
	 * Byte indicating end of the message without tag
	 */
	private final int MESSAGE_END_BYTE = 0x03;

	/**
	 * Byte indicating end of the message with tag
	 */
	private final int MESSAGE_END_WITH_TAG_BYTE = 0x06;

	/**
	 * States of implementation during receiving bytes according to the
	 * protocol.
	 */
	private enum ProtocolState {
		/**
		 * Waits for the beginning of a new message
		 */
		WAIT_START,
		/**
		 * Waits for a byte encoding the destination id of the message
		 */
		WAIT_DESTINATION_ID,
		/**
		 * Waits for a high nibble of the next message byte
		 */
		WAIT_HIGH_NIBBLE,
		/**
		 * Waits for a low nibble of the next message byte
		 */
		WAIT_LOW_NIBBLE,
		/**
		 * Waits for a CRC byte after receiving marker indicating end of a
		 * message without a tag
		 */
		WAIT_CRC,
		/**
		 * Waits for a CRC byte after receiving marker indicating end of a
		 * message with a tag
		 */
		WAIT_CRC_WITH_TAG
	}

	/**
	 * Listener for receiving messages.
	 */
	private final MessageListener messageListener;

	/**
	 * The socket for accessing network or remote side.
	 */
	private final FullDuplexStreamSocket socket;

	/**
	 * Maximal length of a message.
	 */
	private final int maxMessageLength;

	/**
	 * Identifier of the messenger applied to filter incoming messages. If id is
	 * set to 0, all messages are received.
	 */
	private final byte messengerId;

	/**
	 * Precomputed table for CRC checksums.
	 */
	private final short[] crcTable = new short[256];

	/**
	 * Thread for receiving and sending messages. Null, if the messenger is not
	 * running.
	 */
	private Thread communicationThread;

	/**
	 * Sets whether connection should be reconnected if failed.
	 */
	private boolean automaticReconnect = true;

	/**
	 * Indicates that the communication thread is a daemon thread.
	 */
	private boolean daemon;

	/**
	 * Indicates that the messenger is in connected state, i.e., it is ready to
	 * send and receive messages.
	 */
	private boolean connected;

	/**
	 * Full-duplex stream for sending and receiving bytes from remote side or a
	 * network.
	 */
	private FullDuplexStream ioStream;

	/**
	 * Indicates that messenger thread should be terminated as soon as possible.
	 */
	private volatile boolean stopFlag = false;

	/**
	 * Indicates that the messenger is connecting (for the first time, i.e., not
	 * after connection has been broken).
	 */
	private boolean firstConnecting = false;

	/**
	 * Internal synchronization lock.
	 */
	private final Object lock = new Object();

	/**
	 * Lock ensuring serialization of messages transmissions.
	 */
	private final Object sendSerializationLock = new Object();

	/**
	 * Constructs a messenger.
	 * 
	 * @param socket
	 *            the stream socket for accessing network or remote side.
	 * @param messengerId
	 *            the identifier of the messenger applied to filter incoming
	 *            messages. Allowed values are between 0 and 15, if the value is
	 *            0, all messages are received.
	 * @param maxMessageLength
	 *            the maximal accepted length of a message.
	 * @param messageListener
	 *            the message listener.
	 */
	public GEPMessenger(FullDuplexStreamSocket socket, int messengerId, int maxMessageLength,
			MessageListener messageListener) {
		if ((messengerId < 0) || (messengerId > 15)) {
			throw new RuntimeException("Invalid identifier of messenger (allowed: 0-15).");
		}

		this.socket = socket;
		this.maxMessageLength = Math.abs(maxMessageLength);
		this.messengerId = (byte) messengerId;
		this.messageListener = messageListener;
		this.daemon = true;

		initializeCRCTable();
	}

	/**
	 * Returns whether communication thread of the messenger is a daemon thread.
	 * 
	 * @return true, if the communication thread is marked as a daemon thread.
	 */
	public boolean isDaemon() {
		synchronized (lock) {
			return daemon;
		}
	}

	/**
	 * Marks the communication thread of the messenger as either a daemon thread
	 * or a user thread.
	 * 
	 * @param daemon
	 *            if true, marks the communication thread as a daemon thread.
	 */
	public void setDaemon(boolean daemon) {
		synchronized (lock) {
			if (isRunning()) {
				throw new IllegalStateException("The messenger is running. The method cannot be invoked.");
			}
			this.daemon = daemon;
		}
	}

	/**
	 * Returns whether the client will automatically attempt to reconnect, if
	 * the connection is lost.
	 * 
	 * @return true, if automatic reconnect is enabled, false otherwise.
	 */
	public boolean isAutomaticReconnect() {
		synchronized (lock) {
			return automaticReconnect;
		}
	}

	/**
	 * Sets whether the client will automatically attempt to reconnect, if the
	 * connection is lost.
	 * 
	 * @param automaticReconnect
	 *            if set to true, automatic reconnect will be enabled.
	 */
	public void setAutomaticReconnect(boolean automaticReconnect) {
		synchronized (lock) {
			if (isRunning()) {
				throw new IllegalStateException("The messenger is running. The method cannot be invoked.");
			}
			this.automaticReconnect = automaticReconnect;
		}
	}

	/**
	 * Returns whether the messenger is running.
	 * 
	 * @return true, if the messenger is running, false otherwise.
	 */
	public boolean isRunning() {
		synchronized (lock) {
			return (communicationThread != null);
		}
	}

	/**
	 * Returns whether the messenger is connected, i.e., it is ready to send and
	 * receive messages.
	 * 
	 * @return true, if the messenger is connected, false otherwise.
	 */
	public boolean isConnected() {
		synchronized (lock) {
			return connected;
		}
	}

	/**
	 * Starts the messenger, if the messenger is not running.
	 * 
	 * @param waitForConnection
	 *            true, if the method is blocked until connection is
	 *            established, false otherwise.
	 */
	public void start(boolean waitForConnection) {
		synchronized (lock) {
			if (isRunning()) {
				throw new RuntimeException("Messenger is already running.");
			}

			// create communication thread
			communicationThread = new Thread(new Runnable() {
				@Override
				public void run() {
					communicate();
				}
			}, "GEPMessenger");

			// start the communication thread
			stopFlag = false;
			firstConnecting = true;
			communicationThread.setDaemon(daemon);
			communicationThread.setName(THREAD_NAME);
			communicationThread.start();

			// if required, wait for establishing a connection
			if (waitForConnection) {
				while (firstConnecting) {
					try {
						lock.wait();
					} catch (InterruptedException ignore) {

					}
				}
			}
		}
	}

	/**
	 * Executes code of the communication thread.
	 */
	private void communicate() {
		try {
			while (!stopFlag) {
				connectAndReadMessages();
				if (automaticReconnect) {
					try {
						Thread.sleep(RECONNECT_DELAY);
					} catch (InterruptedException ignore) {
						// interrupted exceptions are ignored
					}
				} else {
					break;
				}
			}
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Unexpected state or error on GEP connection.", e);
		} finally {
			synchronized (lock) {
				// store that there is no running communication
				// thread
				communicationThread = null;
			}
		}
	}

	/**
	 * Stops the messenger.
	 * 
	 * @param blocked
	 *            true, if the current thread should be blocked until the
	 *            communication terminates, false otherwise.
	 * @throws InterruptedException
	 *             if any thread has interrupted the current thread during
	 *             waiting for stopping of the messenger.
	 */
	public void stop(boolean blocked) throws InterruptedException {
		Thread waitThread = null;

		synchronized (lock) {
			if (communicationThread == null) {
				return;
			}

			waitThread = communicationThread;
			stopFlag = true;
			if (ioStream != null) {
				ioStream.close();
			}
			communicationThread.interrupt();
			lock.notifyAll();
		}

		if (waitThread != null) {
			if ((blocked) && (waitThread != Thread.currentThread())) {
				waitThread.join();
			}
		}
	}

	/**
	 * Sends a message and returns a send request objects that provides status
	 * information. The method is blocked until the message is sent.
	 * 
	 * @param destinationId
	 *            the identifier of the receiver. Allowed values are between 0
	 *            and 15, 0 stands for broadcasted message.
	 * @param message
	 *            the binary message.
	 * @param tag
	 *            the tag to be associated with the message.
	 * @return true, if the message has been sent, false otherwise.
	 * @throws RuntimeException
	 *             when message cannot be sent due to reasons different from the
	 *             state of communication channel.
	 */
	public boolean sendMessage(int destinationId, byte[] message, int tag) {
		// check tag
		if (tag >= 256 * 256) {
			throw new IllegalArgumentException("Message tag cannot be greater than 65535.");
		}

		// check destination id.
		if ((destinationId < 0) || (destinationId >= 16)) {
			throw new IllegalArgumentException("Invalid destination id. Only values between 0 and 15 are allowed.");
		}

		// get output stream
		final OutputStream outputStream;
		synchronized (lock) {
			if (communicationThread == null) {
				throw new RuntimeException("Messenger is not running.");
			}

			if (ioStream != null) {
				outputStream = ioStream.getOutputStream();
			} else {
				outputStream = null;
			}
		}

		// message cannot be sent, if connection is not open.
		if (ioStream == null) {
			return false;
		}

		// ensure that only one message is sending
		synchronized (sendSerializationLock) {
			return encodeAndSendMessage(outputStream, (short) destinationId, message, tag);
		}
	}

	/**
	 * Sends a message and returns a send request objects that provides status
	 * information. The method is blocked until the message is sent.
	 *
	 * @param destinationId
	 *            the identifier of the receiver. Allowed values are between 0
	 *            and 15, 0 stands for broadcasted message.
	 * @param message
	 *            the binary message.
	 * @return true, if the message has been sent, false otherwise.
	 * @throws RuntimeException
	 *             when message cannot be sent due to reasons different from the
	 *             state of communication channel.
	 */
	public boolean sendMessage(int destinationId, byte[] message) {
		return sendMessage(destinationId, message, -1);
	}

	/**
	 * Encodes and sends a message.
	 * 
	 * @param outputStream
	 *            the output stream where the message content (request) will be
	 *            written.
	 * @param destinationId
	 *            the identifier of the receiver.
	 * @param message
	 *            the binary message.
	 * @param tag
	 *            the tag to be associated with the message.
	 * @return true, if the message has been sent, false otherwise.
	 */
	private boolean encodeAndSendMessage(OutputStream outputStream, short destinationId, byte[] message, int tag) {
		try {
			short crc = 0;
			// write byte starting a message
			outputStream.write(MESSAGE_START_BYTE);

			// write destination id
			int encodedDestId = destinationId * 16 + ((destinationId ^ 0x0F) & 0x0F);
			outputStream.write(encodedDestId);
			crc = updateCRC(destinationId, crc);

			// compute length of data to be sent (including encoded tag)
			int rawMessageLength = (message == null) ? 0 : message.length;
			if (tag >= 0) {
				rawMessageLength += 2;
			}

			// create raw message content
			final int[] rawMessage = new int[rawMessageLength];
			int writeIdx = 0;
			if (message != null) {
				for (final byte b : message) {
					rawMessage[writeIdx] = b & 0xff;
					writeIdx++;
				}
			}

			if (tag >= 0) {
				rawMessage[writeIdx] = tag / 256;
				writeIdx++;
				rawMessage[writeIdx] = tag % 256;
			}

			// write message content with each byte encoded as two nibbles
			final int[] twoNibbles = new int[2];
			for (final int b : rawMessage) {
				crc = updateCRC((short) b, crc);
				twoNibbles[0] = b / 16;
				twoNibbles[1] = b % 16;
				twoNibbles[0] = twoNibbles[0] * 16 + ((twoNibbles[0] ^ 0x0F) & 0x0F);
				twoNibbles[1] = twoNibbles[1] * 16 + ((twoNibbles[1] ^ 0x0F) & 0x0F);

				outputStream.write(twoNibbles[0]);
				outputStream.write(twoNibbles[1]);
			}

			if (tag >= 0) {
				outputStream.write(MESSAGE_END_WITH_TAG_BYTE);
			} else {
				outputStream.write(MESSAGE_END_BYTE);
			}

			outputStream.write(crc);
			outputStream.flush();
			return true;
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Core method implementing communication using the protocol. The method
	 * opens the session and reads incoming data (messages).
	 */
	private void connectAndReadMessages() {
		// specify maximal buffer size (message length + 2 bytes for tag)
		final int maxBufferSize = maxMessageLength + 2;
		// full duplex stream used in this session.
		FullDuplexStream sessionStream = null;
		try {
			// open session as a full-duplex stream
			try {
				sessionStream = socket.createStream();
			} catch (Exception e) {
				logger.log(Level.SEVERE, "Unable to open connection.", e);
				return;
			}
			final InputStream inputStream = sessionStream.getInputStream();

			// save that messenger is connected (has a stream).
			synchronized (lock) {
				firstConnecting = false;
				connected = true;
				this.ioStream = sessionStream;
				lock.notifyAll();
			}

			// start waiting for a message start
			ProtocolState state = ProtocolState.WAIT_START;

			byte[] inputBuffer = new byte[512];
			short[] messageBuffer = new short[maxBufferSize];
			int receivedMessageBytes = 0;
			short crc = 0;

			while (!stopFlag) {
				// read received data
				int receivedBytes;
				try {
					receivedBytes = inputStream.read(inputBuffer);
				} catch (Exception e) {
					// handle exception as end of data
					receivedBytes = -1;
				}

				if (receivedBytes < 0) {
					break;
				}
		
				// process received bytes
				dataloop: for (int i = 0; i < receivedBytes; i++) {
					int receivedByte = inputBuffer[i] & 0xFF;
					switch (state) {
					case WAIT_START: {
						if (receivedByte == MESSAGE_START_BYTE) {
							state = ProtocolState.WAIT_DESTINATION_ID;
						}
						continue dataloop;
					}

					case WAIT_DESTINATION_ID: {
						final int nibble = receivedByte / 16;
						if (nibble == ((receivedByte ^ 0x0F) & 0x0F)) {
							// skip message, if this messenger is not
							// destination of the message
							if ((messengerId > 0) && (nibble > 0) && (nibble != messengerId)) {
								state = ProtocolState.WAIT_START;
								continue dataloop;
							}

							// initialize receive of message content
							crc = updateCRC((short) nibble, (short) 0);
							receivedMessageBytes = 0;
							state = ProtocolState.WAIT_HIGH_NIBBLE;
						} else {
							state = ProtocolState.WAIT_START;
						}
						break;
					}

					case WAIT_HIGH_NIBBLE: {
						if (receivedByte == MESSAGE_END_BYTE) {
							state = ProtocolState.WAIT_CRC;
						} else if (receivedByte == MESSAGE_END_WITH_TAG_BYTE) {
							state = ProtocolState.WAIT_CRC_WITH_TAG;
						} else if (receivedMessageBytes >= maxBufferSize) {
							state = ProtocolState.WAIT_START;
						} else {
							final int nibble = receivedByte / 16;
							if (nibble == ((receivedByte ^ 0x0F) & 0x0F)) {
								messageBuffer[receivedMessageBytes] = (short) (nibble * 16);
								receivedMessageBytes++;
								state = ProtocolState.WAIT_LOW_NIBBLE;
							} else {
								state = ProtocolState.WAIT_START;
							}
						}
						break;
					}

					case WAIT_LOW_NIBBLE: {
						final int nibble = receivedByte / 16;
						if (nibble == ((receivedByte ^ 0x0F) & 0x0F)) {
							messageBuffer[receivedMessageBytes - 1] += nibble;
							crc = updateCRC(messageBuffer[receivedMessageBytes - 1], crc);
							state = ProtocolState.WAIT_HIGH_NIBBLE;
						} else {
							state = ProtocolState.WAIT_START;
						}
						break;
					}

					case WAIT_CRC: {
						if (receivedByte == crc) {
							handleReceivedMessage(messageBuffer, receivedMessageBytes, -1);
							state = ProtocolState.WAIT_START;
							continue dataloop;
						}
						state = ProtocolState.WAIT_START;
						break;
					}

					case WAIT_CRC_WITH_TAG: {
						if ((receivedByte == crc) && (receivedMessageBytes >= 2)) {
							final int messageTag = messageBuffer[receivedMessageBytes - 2] * 256
									+ messageBuffer[receivedMessageBytes - 1];
							receivedMessageBytes -= 2;
							handleReceivedMessage(messageBuffer, receivedMessageBytes, messageTag);
							state = ProtocolState.WAIT_START;
							continue dataloop;
						}
						state = ProtocolState.WAIT_START;
						break;
					}

					} // end of switch

					// make progress from WAIT_START, if the received by is
					// MESSAGE_START_BYTE
					if ((state == ProtocolState.WAIT_START) && (receivedByte == MESSAGE_START_BYTE)) {
						state = ProtocolState.WAIT_DESTINATION_ID;
					}
				}
			}
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Communication failed or disconnected.", e);
		} finally {
			if (sessionStream != null) {
				try {
					sessionStream.close();
				} catch (Exception e) {
					logger.log(Level.SEVERE, "Closing of session stream failed.", e);
				}
			}

			// set disconnected state
			synchronized (lock) {
				firstConnecting = false;
				connected = false;
				this.ioStream = null;
				lock.notifyAll();
			}
		}
	}

	/**
	 * Handles a received message.
	 * 
	 * @param messageBuffer
	 *            the buffer that stores the received message.
	 * @param messageLength
	 *            the length of the received message.
	 * @param tag
	 *            the tag associated with the message.
	 */
	private void handleReceivedMessage(short[] messageBuffer, int messageLength, int tag) {
		byte[] message = new byte[messageLength];
		for (int i = 0; i < messageLength; i++) {
			message[i] = (byte) messageBuffer[i];
		}

		if (messageListener != null) {
			messageListener.onMessageReceived(tag, message);
		}
	}

	/**
	 * Updates CRC8 checksum after adding a new byte of data.
	 * 
	 * @param data
	 *            the new byte of data
	 * @param crc
	 *            the current CRC8 checksum
	 * @return updates CRC8 checksum
	 */
	private short updateCRC(short aByte, short crc) {
		return (short) (crcTable[(aByte ^ crc) & 0xFF]);
	}

	/**
	 * Initializes table for fast computation of CRC8 checksums
	 */
	private void initializeCRCTable() {
		// based on:
		// http://stackoverflow.com/questions/25284556/translate-crc8-from-c-to-java
		final int polynomial = 0x8C;
		for (int dividend = 0; dividend < 256; dividend++) {
			int remainder = dividend;// << 8;
			for (int bit = 0; bit < 8; ++bit) {
				if ((remainder & 0x01) != 0) {
					remainder = (remainder >>> 1) ^ polynomial;
				} else {
					remainder >>>= 1;
				}
			}
			crcTable[dividend] = (short) remainder;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy