package eu.hgross.blaubot.ethernet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import eu.hgross.blaubot.core.Blaubot;
import eu.hgross.blaubot.core.BlaubotConstants;
import eu.hgross.blaubot.core.BlaubotDevice;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener;
import eu.hgross.blaubot.core.acceptor.IBlaubotListeningStateListener;
import eu.hgross.blaubot.core.acceptor.discovery.BeaconMessage;
import eu.hgross.blaubot.core.acceptor.discovery.BlaubotBeaconService;
import eu.hgross.blaubot.core.acceptor.discovery.ExchangeStatesTask;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeacon;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotDiscoveryEventListener;
import eu.hgross.blaubot.core.acceptor.discovery.TimeoutList;
import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper;
import eu.hgross.blaubot.core.statemachine.states.FreeState;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotState;
import eu.hgross.blaubot.util.KingdomCensusLifecycleListener;
import eu.hgross.blaubot.util.Log;
/**
* Beacon for ethernet using broadcasts. It consists of two broadcast specific threads (Broadcaster and
* BroadcastDiscoverer) and a beacon accept thread. The broadcasts simply shout the current beaconUUID out in the world.
* If a BroadcastReceiver gets to recongnize the beaconUUID (which must be the same as its own) the device is added
* to a {@link TimeoutList}.
*
* While the discovery is activated, a BeaconScanner goes through all devices known as alive and tries to connect to
* their beacon. On a successful connection the resulting {@link IBlaubotConnection} is handed to the registered
* {@link IBlaubotIncomingConnectionListener}. From here the {@link BlaubotBeaconService} will handle the beacon conversation
* via the {@link ExchangeStatesTask} (exchanging {@link BeaconMessage}s).
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
public class BlaubotEthernetMulticastBeacon implements IBlaubotBeacon, IEthernetBeacon {
private static final String LOG_TAG = BlaubotEthernetMulticastBeacon.class.getSimpleName();
/**
* The broadcasting interval for the beacon broadcaster thread
*/
private static final int BROADCASTER_INTERVAL = 6500;
/**
* Probe interval if in FreeState
*/
private static final long BEACON_PROBE_INTERVAL_AGGRESSIVE = 1000;
/**
* PROBE-INTERVAL if not in FreeState
*/
private static final long BEACON_PROBE_INTERVAL_DECENT = 5000;
/**
* The alive interval for the TimeoutList of known active devices.
*/
private static final int ALIVE_TIMEOUT = BROADCASTER_INTERVAL * 5;
/**
* The TCP-port on which this beacon will accept connections for the beacon message exchange.
*/
private final int beaconPort;
/**
* The UDP-port on which the udp broadcast message will be sent by the broadcaster thread.
*/
private final int beaconBroadcastPort;
/**
* This beacon's uuid.
*/
private UUID beaconUUID;
/**
* The own device for which this beacon announces it's presence.
*/
private IBlaubotDevice ownDevice;
private volatile IBlaubotState currentState;
private volatile IBlaubotDiscoveryEventListener discoveryEventListener;
private volatile IBlaubotIncomingConnectionListener incomingConnectionListener;
private volatile IBlaubotListeningStateListener listeningStateListener;
private volatile boolean discoveryActive = true;
/**
* Contains all devices which sent us a broadcast in between the last x seconds.
*/
private final TimeoutList<IBlaubotDevice> knownActiveDevices;
private volatile EthernetBeaconAcceptThread acceptThread;
private volatile BroadcasterThread broadcaster;
private volatile BroadcastDiscovererThread broadcastDiscoverer;
private volatile EthernetBeaconScanner beaconScanner;
private Object startStopMonitor;
private IBlaubotBeaconStore beaconStore;
private Blaubot blaubot;
private KingdomCensusLifecycleListener kingdomCensusLifecycleListener;
/**
* @param beaconPort the port to accept beacon connections
* @param beaconBroadcastPort the broadcast port for announcements
*/
public BlaubotEthernetMulticastBeacon(int beaconPort, int beaconBroadcastPort) {
this.startStopMonitor = new Object();
this.beaconPort = beaconPort;
this.beaconBroadcastPort = beaconBroadcastPort;
this.knownActiveDevices = new TimeoutList<>((long) ALIVE_TIMEOUT);
}
@Override
public IBlaubotAdapter getAdapter() {
return null;
}
@Override
public void startListening() {
synchronized (startStopMonitor) {
if (isStarted()) {
return;
}
acceptThread = new EthernetBeaconAcceptThread(incomingConnectionListener, this);
broadcaster = new BroadcasterThread();
broadcastDiscoverer = new BroadcastDiscovererThread();
beaconScanner = new EthernetBeaconScanner();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon is starting to listen for incoming connections on port " + beaconPort);
}
acceptThread.start();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "BroadcastDiscoverer is starting");
}
broadcastDiscoverer.start();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Broadcaster is starting");
}
broadcaster.start();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "EthernetBeaconScanner is starting");
}
beaconScanner.start();
if (listeningStateListener != null)
listeningStateListener.onListeningStarted(this);
}
}
@Override
public void stopListening() {
synchronized (startStopMonitor) {
if (!isStarted()) {
return;
}
if (beaconScanner != null && beaconScanner.isAlive()) {
beaconScanner.interrupt();
// try {
// beaconScanner.join();
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
}
beaconScanner = null;
if (broadcaster != null && broadcaster.isAlive()) {
broadcaster.interrupt();
try {
broadcaster.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
broadcaster = null;
if (broadcastDiscoverer != null && broadcastDiscoverer.isAlive()) {
broadcastDiscoverer.interrupt();
try {
broadcastDiscoverer.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
broadcastDiscoverer = null;
if (acceptThread != null && acceptThread.isAlive()) {
try {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Waiting for beacon accept thread to finish ...");
}
acceptThread.interrupt();
acceptThread.join();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon accept thread to finished ...");
}
} catch (InterruptedException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, e);
}
}
acceptThread = null;
}
if (listeningStateListener != null)
listeningStateListener.onListeningStopped(this);
}
}
@Override
public boolean isStarted() {
return acceptThread != null && acceptThread.isAlive();
}
@Override
public void setListeningStateListener(IBlaubotListeningStateListener stateListener) {
this.listeningStateListener = stateListener;
}
@Override
public void setAcceptorListener(IBlaubotIncomingConnectionListener acceptorListener) {
this.incomingConnectionListener = acceptorListener;
}
@Override
public ConnectionMetaDataDTO getConnectionMetaData() {
// TODO: maybe beacons should not derive from acceptors anymore
return null;
}
@Override
public void setDiscoveryEventListener(IBlaubotDiscoveryEventListener discoveryEventListener) {
this.discoveryEventListener = discoveryEventListener;
}
@Override
public void onConnectionStateMachineStateChanged(IBlaubotState state) {
this.currentState = state;
}
@Override
public void setDiscoveryActivated(boolean active) {
this.discoveryActive = active;
}
/**
* Broadcasts the beacon's existence (BeaconUUID) periodically to the network.
* The message has the format BEACON_UUID;acceptorPort;beaconPort
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
private byte[] broadcastMessage;
private byte[] createBroadcastMessage() {
byte[] uuidBytes = beaconUUID.toString().getBytes(BlaubotConstants.STRING_CHARSET);
// -- UUIDs have fixed lengths
final String ownDeviceUniqueDeviceID = ownDevice.getUniqueDeviceID();
ByteBuffer bb = ByteBuffer.allocate(uuidBytes.length + 2 * 4 + ownDeviceUniqueDeviceID.length()); // app uuid, integer for beacon, integer for uniqueDeviceId length, and uniqueDeviceId
bb.order(ByteOrder.BIG_ENDIAN);
bb.put(uuidBytes);
bb.putInt(beaconPort);
bb.putInt(ownDeviceUniqueDeviceID.length());
bb.put(ownDeviceUniqueDeviceID.getBytes(BlaubotConstants.STRING_CHARSET));
bb.flip();
return bb.array();
}
@Override
public Thread getAcceptThread() {
return acceptThread;
}
@Override
public int getBeaconPort() {
return beaconPort;
}
/**
* Used to store some beacon related data for the discoverer and scanner.
*/
private class MulticastBeaconBlaubotDevice extends BlaubotDevice {
private final InetAddress inetAddress;
private final int beaconPort;
private MulticastBeaconBlaubotDevice(String uniqueId, InetAddress inetAddress, int beaconPort) {
super(uniqueId);
this.inetAddress = inetAddress;
this.beaconPort = beaconPort;
}
private int getBeaconPort() {
return beaconPort;
}
private InetAddress getInetAddress() {
return inetAddress;
}
}
/**
* A thread that periodically broadcasts a beacon message over udp.
*/
class BroadcasterThread extends Thread {
private static final String LOG_TAG = "MulticastBroadcaster";
private static final int SEND_INTERVAL = BROADCASTER_INTERVAL;
@Override
public void run() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Broadcaster started ...");
}
DatagramSocket serverSocket = null;
try {
serverSocket = new DatagramSocket();
serverSocket.setBroadcast(true);
} catch (SocketException e1) {
return;
}
while (!isInterrupted() && Thread.currentThread() == broadcaster) {
// try to send to 255.255.255.255 first
try {
DatagramPacket packetToSend = new DatagramPacket(broadcastMessage, broadcastMessage.length, InetAddress.getByName("255.255.255.255"), beaconBroadcastPort);
serverSocket.send(packetToSend);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Broadcast message sent to: 255.255.255.255");
}
} catch (IOException e) {
// Log.e(LOG_TAG, "Failed to broadcast to 255.255.255.255", e);
}
Enumeration<NetworkInterface> interfaces;
try {
interfaces = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e1) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to get network interfaces", e1);
}
break;
}
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
try {
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
continue;
}
} catch (SocketException e1) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Failed to get network information", e1);
}
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress broadcast = interfaceAddress.getBroadcast();
if (broadcast == null) {
continue;
}
try {
DatagramPacket sendPacket = new DatagramPacket(broadcastMessage, broadcastMessage.length, broadcast, beaconPort);
serverSocket.send(sendPacket);
} catch (Exception e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Failed to send broadcast message to " + broadcast.getHostAddress() + "; Interface: " + networkInterface.getDisplayName());
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Broadcast message sent to: " + broadcast.getHostAddress() + " over interface: " + networkInterface.getDisplayName());
}
}
}
try {
Thread.sleep(SEND_INTERVAL);
} catch (InterruptedException e) {
break;
}
}
serverSocket.close();
}
}
/**
* Listens to Broadcasts on the network (if the discovery is active, see setDiscoveryActivated(). If a broadcast is
* discovered, a connection to the beacon will be established to exchange states.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
class BroadcastDiscovererThread extends Thread {
private static final int BROADCAST_DISCOVERER_SOCKET_TIMEOUT = 150;
@Override
public void run() {
DatagramSocket receivingSocket = null;
try {
receivingSocket = new DatagramSocket(beaconBroadcastPort);
receivingSocket.setSoTimeout(BROADCAST_DISCOVERER_SOCKET_TIMEOUT);
} catch (SocketException e) {
return;
}
byte[] buffer = new byte[broadcastMessage.length];
while (!isInterrupted() && Thread.currentThread() == broadcastDiscoverer) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
receivingSocket.receive(packet);
// if (isOwnInetAddr(packet.getAddress())) {
// ignore inet addresses that belong to one of our own interfaces!
// continue;
// }
} catch (SocketTimeoutException e) {
// no connection for BROADCAST_DISCOVERER_SOCKET_TIMEOUT ms
continue;
} catch (IOException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Receive failed.", e);
}
continue;
}
// wrap the data
final ByteBuffer bb = ByteBuffer.wrap(packet.getData());
bb.order(BlaubotConstants.BYTE_ORDER);
// read uuid
byte[] uuidBytes = new byte[beaconUUID.toString().length()];
bb.get(uuidBytes, 0, uuidBytes.length);
String uuidStr = new String(uuidBytes, BlaubotConstants.STRING_CHARSET);
UUID receivedUUID = UUID.fromString(uuidStr);
// get the beacon port
int beaconPort = bb.getInt();
// get the uniqueDeviceId length
int uniqueDeviceIdLength = bb.getInt();
// get the uniqueDeviceId bytes
byte[] uniqueDeviceIdBytes = new byte[uniqueDeviceIdLength];
bb.get(uniqueDeviceIdBytes);
String uniqueDeviceId = new String(uniqueDeviceIdBytes, BlaubotConstants.STRING_CHARSET);
final boolean isFromOwnUniqueDeviceId = uniqueDeviceId.equals(ownDevice.getUniqueDeviceID());
try {
final boolean relevantBeaconUUID = receivedUUID.equals(beaconUUID);
if (!isFromOwnUniqueDeviceId && relevantBeaconUUID) {
InetAddress remoteDeviceAddr = packet.getAddress(); // only used for the beacon internally
IBlaubotDevice device = new MulticastBeaconBlaubotDevice(uniqueDeviceId, remoteDeviceAddr, beaconPort);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Received a relevant beaconUUID (" + receivedUUID.toString() + " == " + beaconUUID.toString() + ") via broadcast from " + device + ". Reporting it as active device.");
}
knownActiveDevices.report(device);
} else if(!relevantBeaconUUID) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Received an irrelevant beaconUUID (" + receivedUUID.toString() + " != " + beaconUUID.toString() + ") via broadcast. Ignoring device.");
}
}
} catch (IllegalArgumentException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Received String is not a valid UUID: " + uuidStr, e);
}
}
}
receivingSocket.close();
}
}
/**
* Periodically checks the beacon of all devices known as alive (except devices connected to our network) (added to the {@link TimeoutList})
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
class EthernetBeaconScanner extends Thread {
private static final int BEACON_SCANNER_CONNECT_TIMEOUT = 10000;
private String LOG_TAG = "EthernetBeaconScanner";
private List<IBlaubotDevice> getAliveDevices() {
ArrayList<IBlaubotDevice> devices = new ArrayList<IBlaubotDevice>(knownActiveDevices.getItems());
// do not check the devices connected to the blaubot network
devices.removeAll(kingdomCensusLifecycleListener.getDevices());
// note: we do not sort the collection since ethernet connection creation is beaming fast.
return devices;
}
@Override
public void run() {
boolean exit = false;
while(!isInterrupted() && Thread.currentThread() == beaconScanner && !exit) {
if (isDiscoveryDisabled()) {
// Discovery is not active - not connecting to discovered beacon.
try {
Thread.sleep(200);
} catch (InterruptedException e) {
exit = true;
}
// we don't want to connect if discovery is deactivated.
continue;
}
for(IBlaubotDevice d : getAliveDevices()) {
if (isDiscoveryDisabled()) {
break;
}
MulticastBeaconBlaubotDevice device = (MulticastBeaconBlaubotDevice) d;
InetAddress remoteDeviceAddr = device.getInetAddress();
int remoteBeaconPort = device.getBeaconPort();
// -- we know that remoteDeviceAddr had a running beacon in the recent past as it is in the knownActiveDevices TimeoutList
// try to connect, then exchange states via tcp/ip
Socket clientSocket;
try {
clientSocket = new Socket();
clientSocket.connect(new InetSocketAddress(remoteDeviceAddr, remoteBeaconPort), BEACON_SCANNER_CONNECT_TIMEOUT);
BlaubotEthernetUtils.sendOwnUniqueIdThroughSocket(ownDevice, clientSocket);
BlaubotEthernetConnection connection = new BlaubotEthernetConnection(device, clientSocket);
final List<ConnectionMetaDataDTO> ownAcceptorsMetaDataList = BlaubotAdapterHelper.getConnectionMetaDataList(BlaubotAdapterHelper.getConnectionAcceptors(blaubot.getAdapters()));
ExchangeStatesTask exchangeStatesTask = new ExchangeStatesTask(ownDevice, connection, currentState, ownAcceptorsMetaDataList, beaconStore, discoveryEventListener);
exchangeStatesTask.run();
} catch (IOException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Connection to " + device + "'s beacon (" + remoteDeviceAddr + ":" + remoteBeaconPort + ") failed: " + e.getMessage());
}
}
try {
// if we are in free state, be a little more decent with the interval
final long sleepTime = currentState != null && !(currentState instanceof FreeState) ? BEACON_PROBE_INTERVAL_DECENT : BEACON_PROBE_INTERVAL_AGGRESSIVE;
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
exit = true;
break;
}
}
}
}
/**
* @return true if the discovery is disabled by config or explicitly by {@link #setDiscoveryActivated}
*/
private boolean isDiscoveryDisabled() {
return !discoveryActive;
}
}
@Override
public void setBlaubot(Blaubot blaubot) {
this.blaubot = blaubot;
this.ownDevice = blaubot.getOwnDevice();
this.beaconUUID = blaubot.getUuidSet().getBeaconUUID();
this.broadcastMessage = createBroadcastMessage();
this.kingdomCensusLifecycleListener = new KingdomCensusLifecycleListener(ownDevice);
this.blaubot.addLifecycleListener(this.kingdomCensusLifecycleListener);
}
@Override
public void setBeaconStore(IBlaubotBeaconStore beaconStore) {
this.beaconStore = beaconStore;
}
}