diff --git a/src/ve/ucv/ciens/ccg/nxtar/Main.java b/src/ve/ucv/ciens/ccg/nxtar/Main.java deleted file mode 100644 index 943d07b..0000000 --- a/src/ve/ucv/ciens/ccg/nxtar/Main.java +++ /dev/null @@ -1,66 +0,0 @@ -package ve.ucv.ciens.ccg.nxtar; - -import com.badlogic.gdx.ApplicationListener; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.graphics.GL10; -import com.badlogic.gdx.graphics.OrthographicCamera; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.Texture.TextureFilter; -import com.badlogic.gdx.graphics.g2d.Sprite; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.graphics.g2d.TextureRegion; - -public class Main implements ApplicationListener { - private OrthographicCamera camera; - private SpriteBatch batch; - private Texture texture; - private Sprite sprite; - - @Override - public void create() { - float w = Gdx.graphics.getWidth(); - float h = Gdx.graphics.getHeight(); - - camera = new OrthographicCamera(1, h/w); - batch = new SpriteBatch(); - - texture = new Texture(Gdx.files.internal("data/libgdx.png")); - texture.setFilter(TextureFilter.Linear, TextureFilter.Linear); - - TextureRegion region = new TextureRegion(texture, 0, 0, 512, 275); - - sprite = new Sprite(region); - sprite.setSize(0.9f, 0.9f * sprite.getHeight() / sprite.getWidth()); - sprite.setOrigin(sprite.getWidth()/2, sprite.getHeight()/2); - sprite.setPosition(-sprite.getWidth()/2, -sprite.getHeight()/2); - } - - @Override - public void dispose() { - batch.dispose(); - texture.dispose(); - } - - @Override - public void render() { - Gdx.gl.glClearColor(1, 1, 1, 1); - Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); - - batch.setProjectionMatrix(camera.combined); - batch.begin(); - sprite.draw(batch); - batch.end(); - } - - @Override - public void resize(int width, int height) { - } - - @Override - public void pause() { - } - - @Override - public void resume() { - } -} diff --git a/src/ve/ucv/ciens/ccg/nxtar/NxtARCore.java b/src/ve/ucv/ciens/ccg/nxtar/NxtARCore.java new file mode 100644 index 0000000..453e808 --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/NxtARCore.java @@ -0,0 +1,120 @@ +package ve.ucv.ciens.ccg.nxtar; + +import ve.ucv.ciens.ccg.nxtar.interfaces.MulticastEnabler; +import ve.ucv.ciens.ccg.nxtar.interfaces.NetworkConnectionListener; +import ve.ucv.ciens.ccg.nxtar.interfaces.Toaster; +import ve.ucv.ciens.ccg.nxtar.network.RobotControlThread; +import ve.ucv.ciens.ccg.nxtar.network.ServiceDiscoveryThread; +import ve.ucv.ciens.ccg.nxtar.network.VideoStreamingThread; +import ve.ucv.ciens.ccg.nxtar.utils.ProjectConstants; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL10; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.Texture.TextureFilter; +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; + +public class NxtARCore implements ApplicationListener, NetworkConnectionListener { + private static final String TAG = "NXTAR_CORE_MAIN"; + private static final String CLASS_NAME = NxtARCore.class.getSimpleName(); + + private OrthographicCamera camera; + private SpriteBatch batch; + private Texture texture; + private Sprite sprite; + private Toaster toaster; + private MulticastEnabler mcastEnabler; + private int connections; + + private ServiceDiscoveryThread udpThread; + private VideoStreamingThread videoThread; + private RobotControlThread robotThread; + + public NxtARCore(Application concreteApp){ + super(); + connections = 0; + try{ + this.toaster = (Toaster)concreteApp; + this.mcastEnabler = (MulticastEnabler)concreteApp; + }catch(ClassCastException cc){ + Gdx.app.debug(TAG, CLASS_NAME + ".Main() :: concreteApp does not implement any of the required interfaces."); + System.exit(ProjectConstants.EXIT_FAILURE); + } + } + + @Override + public void create(){ + float w = Gdx.graphics.getWidth(); + float h = Gdx.graphics.getHeight(); + + Gdx.app.setLogLevel(Application.LOG_DEBUG); + + camera = new OrthographicCamera(1, h/w); + batch = new SpriteBatch(); + + texture = new Texture(Gdx.files.internal("data/libgdx.png")); + texture.setFilter(TextureFilter.Linear, TextureFilter.Linear); + + TextureRegion region = new TextureRegion(texture, 0, 0, 512, 275); + + sprite = new Sprite(region); + sprite.setSize(0.9f, 0.9f * sprite.getHeight() / sprite.getWidth()); + sprite.setOrigin(sprite.getWidth()/2, sprite.getHeight()/2); + sprite.setPosition(-sprite.getWidth()/2, -sprite.getHeight()/2); + + Gdx.app.debug(TAG, CLASS_NAME + ".create() :: Creating network threads"); + mcastEnabler.enableMulticast(); + udpThread = ServiceDiscoveryThread.getInstance(); + videoThread = VideoStreamingThread.getInstance().setToaster(toaster); + robotThread = RobotControlThread.getInstance().setToaster(toaster); + + udpThread.start(); + videoThread.start(); + robotThread.start(); + } + + @Override + public void dispose() { + batch.dispose(); + texture.dispose(); + } + + @Override + public void render(){ + Gdx.gl.glClearColor(1, 1, 1, 1); + Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); + + batch.setProjectionMatrix(camera.combined); + batch.begin(); + sprite.draw(batch); + batch.end(); + } + + @Override + public void resize(int width, int height){ + } + + @Override + public void pause(){ + } + + @Override + public void resume(){ + } + + @Override + public synchronized void networkStreamConnected(String streamName){ + if(streamName.compareTo(VideoStreamingThread.THREAD_NAME) == 0 || streamName.compareTo(RobotControlThread.THREAD_NAME) == 0) + connections += 1; + if(connections >= 2){ + Gdx.app.debug(TAG, CLASS_NAME + ".networkStreamConnected() :: Stopping service broadcast."); + udpThread.finish(); + mcastEnabler.disableMulticast(); + } + } +} diff --git a/src/ve/ucv/ciens/ccg/nxtar/interfaces/MulticastEnabler.java b/src/ve/ucv/ciens/ccg/nxtar/interfaces/MulticastEnabler.java new file mode 100644 index 0000000..5967c01 --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/interfaces/MulticastEnabler.java @@ -0,0 +1,6 @@ +package ve.ucv.ciens.ccg.nxtar.interfaces; + +public interface MulticastEnabler { + public void enableMulticast(); + public void disableMulticast(); +} diff --git a/src/ve/ucv/ciens/ccg/nxtar/interfaces/NetworkConnectionListener.java b/src/ve/ucv/ciens/ccg/nxtar/interfaces/NetworkConnectionListener.java new file mode 100644 index 0000000..ab3d546 --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/interfaces/NetworkConnectionListener.java @@ -0,0 +1,5 @@ +package ve.ucv.ciens.ccg.nxtar.interfaces; + +public interface NetworkConnectionListener { + public void networkStreamConnected(String streamName); +} diff --git a/src/ve/ucv/ciens/ccg/nxtar/interfaces/Toaster.java b/src/ve/ucv/ciens/ccg/nxtar/interfaces/Toaster.java new file mode 100644 index 0000000..0999526 --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/interfaces/Toaster.java @@ -0,0 +1,6 @@ +package ve.ucv.ciens.ccg.nxtar.interfaces; + +public interface Toaster { + public void showShortToast(String msg); + public void showLongToast(String msg); +} diff --git a/src/ve/ucv/ciens/ccg/nxtar/network/RobotControlThread.java b/src/ve/ucv/ciens/ccg/nxtar/network/RobotControlThread.java new file mode 100644 index 0000000..b0bb090 --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/network/RobotControlThread.java @@ -0,0 +1,64 @@ +package ve.ucv.ciens.ccg.nxtar.network; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import ve.ucv.ciens.ccg.nxtar.interfaces.NetworkConnectionListener; +import ve.ucv.ciens.ccg.nxtar.interfaces.Toaster; +import ve.ucv.ciens.ccg.nxtar.utils.ProjectConstants; + +import com.badlogic.gdx.Gdx; + +public class RobotControlThread extends Thread { + public static final String THREAD_NAME = "RobotControlThread"; + private static final String TAG = "NXTAR_CORE_ROBOTTHREAD"; + private static final String CLASS_NAME = RobotControlThread.class.getSimpleName(); + + private NetworkConnectionListener netListener; + private ServerSocket server; + private Socket client; + private Toaster toaster; + + private RobotControlThread(){ + super(THREAD_NAME); + + netListener = null; + + try{ + server = new ServerSocket(ProjectConstants.SERVER_TCP_PORT_2); + }catch(IOException io){ + Gdx.app.error(TAG, CLASS_NAME + ".RobotControlThread() :: Error creating server: " + io.getMessage(), io); + } + } + + private static class SingletonHolder{ + public static final RobotControlThread INSTANCE = new RobotControlThread(); + } + + public static RobotControlThread getInstance(){ + return SingletonHolder.INSTANCE; + } + + public RobotControlThread setToaster(Toaster toaster){ + this.toaster = toaster; + return this; + } + + public void addNetworkConnectionListener(NetworkConnectionListener listener){ + netListener = listener; + } + + @Override + public void run(){ + try{ + client = server.accept(); + if(netListener != null) + netListener.networkStreamConnected(THREAD_NAME); + toaster.showShortToast("Client connected to RobotControlThread"); + client.close(); + }catch(IOException io){ + Gdx.app.error(TAG, CLASS_NAME + ".run() :: Error accepting client: " + io.getMessage(), io); + } + } +} diff --git a/src/ve/ucv/ciens/ccg/nxtar/network/ServiceDiscoveryThread.java b/src/ve/ucv/ciens/ccg/nxtar/network/ServiceDiscoveryThread.java new file mode 100644 index 0000000..3a3a09b --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/network/ServiceDiscoveryThread.java @@ -0,0 +1,162 @@ +package ve.ucv.ciens.ccg.nxtar.network; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import ve.ucv.ciens.ccg.nxtar.utils.ProjectConstants; + +import com.badlogic.gdx.Gdx; + +/** + * Ad hoc service discovery server thread. + * + *
This thread performs an ad hoc service discovery protocol. A multicast datagram packet is sent every + * 250 miliseconds carrying the string "NxtAR server is here!" on the multicast address defined + * in {@link ve.ucv.ciens.ccg.nxtar.utils.ProjectConstants#MULTICAST_ADDRESS}. The port defined in + * {@link ve.ucv.ciens.ccg.nxtar.utils.ProjectConstants#SERVER_UDP_PORT} is used for the transmissions. The server stops + * when another thread calls the {@link #finish()} method or the server fails to transmit {@link #MAX_RETRIES} packets in + * a row, whichever happens first.
+ * + * @author miky + */ +public class ServiceDiscoveryThread extends Thread { + /** + * The name used to identify this thread. + */ + public static final String THREAD_NAME = "ServiceDiscoveryThread"; + /** + * Tag used for logging. + */ + private static final String TAG = "NXTAR_CORE_UDPTHREAD"; + /** + * Class name used for logging. + */ + private static final String CLASS_NAME = ServiceDiscoveryThread.class.getSimpleName(); + /** + * Maximum number of transmission attempts before ending the thread abruptly. + */ + private static final int MAX_RETRIES = 5; + + /** + * A semaphore object used to synchronize acces to this thread finish flag. + */ + private Object semaphore; + /** + * The finish flag. + */ + private boolean done; + /** + * The UDP server socket used for the ad hoc service discovery protocol. + */ + private DatagramSocket udpServer; + /** + * Holder for the multicast address used in the protocol. + */ + private InetAddress group; + + private ServiceDiscoveryThread(){ + // Setup this thread name. + super(THREAD_NAME); + + done = false; + semaphore = new Object(); + + // Try to get the InetAddress defined by the IP address defined in ProjectConstants.MULTICAST_ADDRESS. + try{ + group = InetAddress.getByName(ProjectConstants.MULTICAST_ADDRESS); + }catch(UnknownHostException uh){ + group = null; + } + + // Create a UDP socket at the port defined in ProjectConstants.SERVER_UDP_PORT. + Gdx.app.debug(TAG, CLASS_NAME + ".ServiceDiscoveryThread() :: Creating multicast server."); + try{ + udpServer = new DatagramSocket(ProjectConstants.SERVER_UDP_PORT); + }catch(IOException io){ + Gdx.app.error(TAG, CLASS_NAME + ".ServiceDiscoveryThread() :: Error creating UDP socket: " + io.getMessage()); + udpServer = null; + } + Gdx.app.debug(TAG, CLASS_NAME + ".ServiceDiscoveryThread() :: Multicast server created."); + } + + /** + * Singleton holder for this class. + */ + private static class SingletonHolder{ + public static final ServiceDiscoveryThread INSTANCE = new ServiceDiscoveryThread(); + } + + /** + * Get the singleton instance of this class. + * + * @return The singleton instance. + */ + public static ServiceDiscoveryThread getInstance(){ + return SingletonHolder.INSTANCE; + } + + /** + * This thread's run method. + * + *This method executes the ad hoc service discovery protocol implemented by this class, as + * described in the class introduction.
+ */ + @Override + public void run(){ + int retries = 0; + byte[] buffer = (new String("NxtAR server here!")).getBytes(); + + // If failed to get any of the required network elements then end the thread right away. + if(group == null || udpServer == null){ + Gdx.app.error(TAG, CLASS_NAME + ".run() :: No multicast address defined, ending thread."); + return; + } + if(group == null){ + Gdx.app.error(TAG, CLASS_NAME + ".run() :: No server available, ending thread."); + return; + } + + while(true){ + // Verify if the thread should end. If that is the case, close the server. + synchronized(semaphore){ + if(done){ + udpServer.close(); + break; + } + } + try{ + // End the thread if already at the last retry attempt after too many failed transmissions. + if(retries >= MAX_RETRIES){ + Gdx.app.error(TAG, CLASS_NAME + ".run() :: Too many failed transmissions, ending thread."); + udpServer.close(); + break; + } + // Send the packet and reset the retry counter. + DatagramPacket packet = new DatagramPacket(buffer, buffer.length, group, ProjectConstants.SERVER_UDP_PORT); + udpServer.send(packet); + retries = 0; + try{ sleep(250L); }catch(InterruptedException ie){ } + }catch(IOException io){ + Gdx.app.error(TAG, CLASS_NAME + ".run() :: Error sending packet: " + io.getMessage()); + retries += 1; + } + } + if(retries < MAX_RETRIES) + Gdx.app.debug(TAG, CLASS_NAME + ".run() :: Service discovery successfully terminated."); + else + Gdx.app.debug(TAG, CLASS_NAME + ".run() :: Service discovery terminated after too many failed transmissions."); + } + + /** + * Marks this thread as ready to end. + */ + public void finish(){ + synchronized(semaphore){ + Gdx.app.debug(TAG, CLASS_NAME + ".finish() :: Finishing service discovery thread."); + done = true; + } + } +} diff --git a/src/ve/ucv/ciens/ccg/nxtar/network/VideoStreamingThread.java b/src/ve/ucv/ciens/ccg/nxtar/network/VideoStreamingThread.java new file mode 100644 index 0000000..fc5fd8f --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/network/VideoStreamingThread.java @@ -0,0 +1,63 @@ +package ve.ucv.ciens.ccg.nxtar.network; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import ve.ucv.ciens.ccg.nxtar.interfaces.NetworkConnectionListener; +import ve.ucv.ciens.ccg.nxtar.interfaces.Toaster; +import ve.ucv.ciens.ccg.nxtar.utils.ProjectConstants; + +import com.badlogic.gdx.Gdx; + +public class VideoStreamingThread extends Thread { + public static final String THREAD_NAME = "VideoStreamingThread"; + private static final String TAG = "NXTAR_CORE_VIDEOTHREAD"; + private static final String CLASS_NAME = VideoStreamingThread.class.getSimpleName(); + + private NetworkConnectionListener netListener; + private ServerSocket server; + private Socket client; + private Toaster toaster; + + private VideoStreamingThread(){ + super(THREAD_NAME); + + netListener = null; + try{ + server = new ServerSocket(ProjectConstants.SERVER_TCP_PORT_1); + }catch(IOException io){ + Gdx.app.error(TAG, CLASS_NAME + ".VideoStreamingThread() :: Error creating server: " + io.getMessage(), io); + } + } + + private static class SingletonHolder{ + public static final VideoStreamingThread INSTANCE = new VideoStreamingThread(); + } + + public static VideoStreamingThread getInstance(){ + return SingletonHolder.INSTANCE; + } + + public VideoStreamingThread setToaster(Toaster toaster){ + this.toaster = toaster; + return this; + } + + public void addNetworkConnectionListener(NetworkConnectionListener listener){ + netListener = listener; + } + + @Override + public void run(){ + try{ + client = server.accept(); + if(netListener != null) + netListener.networkStreamConnected(THREAD_NAME); + toaster.showShortToast("Client connected to VideoStreamingThread"); + client.close(); + }catch(IOException io){ + Gdx.app.error(TAG, CLASS_NAME + ".run() :: Error accepting client: " + io.getMessage(), io); + } + } +} diff --git a/src/ve/ucv/ciens/ccg/nxtar/utils/ProjectConstants.java b/src/ve/ucv/ciens/ccg/nxtar/utils/ProjectConstants.java new file mode 100644 index 0000000..280b9c5 --- /dev/null +++ b/src/ve/ucv/ciens/ccg/nxtar/utils/ProjectConstants.java @@ -0,0 +1,12 @@ +package ve.ucv.ciens.ccg.nxtar.utils; + +public abstract class ProjectConstants { + public static final int SERVER_UDP_PORT = 8889; + public static final int SERVER_TCP_PORT_1 = 9989; + public static final int SERVER_TCP_PORT_2 = 9990; + public static final String MULTICAST_ADDRESS = "230.0.0.1"; + public static final int EXIT_SUCCESS = 0; + public static final int EXIT_FAILURE = 1; + + public static final boolean DEBUG = true; +}