Package mpe.client

Source Code of mpe.client.TCPClient

/**
* The MPE Client
* The Client class registers itself with a server
* and receives messages related to frame rendering and data input
* <http://mostpixelsever.com>
* @author Shiffman and Kairalla
*/

package mpe.client;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.UnknownHostException;

import processing.core.PApplet;
import processing.core.PConstants;
import processing.data.XML;
import processing.opengl.PGraphics3D;

public class TCPClient extends Thread {
 
  public static final String version = "2.0.2";

  /** If VERBOSE is true, the client will print lots of messages about what it is doing.
   * Set with debug=true; in your XML file. */
  public static boolean VERBOSE = false;

  boolean connected = false;
  int waittime = 1; // in seconds

  boolean asynchronous = false;
  boolean asynchreceive = false;

  boolean offsetWindow = false;

  // TCP stuff
  String hostName;
  int serverPort = 9002;
  Socket socket;
  InputStream is;
  BufferedReader brin;
  DataOutputStream dos;
  OutputStream os;

  PApplet p5parent;
  Method frameEventMethod;
  Method resetEventMethod;
  Method dataEventMethod;

  boolean dataEventEnabled = false;

  /** The id is used for communication with the server, to let it know which
   *  client is speaking and how to order the screens. */
  int id = 0;

  String name = "noname";

  /** The master width. */
  protected int mWidth = -1;
  /** The master height. */
  protected int mHeight = -1;

  /** The local width. */
  protected int lWidth = 640;
  /** The local height. */
  protected int lHeight = 480;

  int xOffset = 0;
  int yOffset = 0;

  boolean running = false;
  boolean rendering = false;
  boolean autoMode = false;

  boolean simulation = false;
  float simFPS = 30.0f;

  int   frameCount = 0;
  float fps = 0.f;
  long  lastMs = 0;

  boolean reset = false;

  protected boolean messageAvailable;      // Is a message available?
  protected String[] dataMessage;          // data that has come in
  protected String rawMessage;       // This is the full raw frame message

  // 3D variables
  protected boolean enable3D = false;
  protected float fieldOfView = 30.0f;
  protected float cameraZ;

  /**
   * Client is constructed with an init file location, and the parent PApplet.
   * The parent PApplet must have a method called "frameEvent(Client c)".
   *
   * The frameEvent handles syncing up the frame rate on the
   * multiple screens and should be implemented like draw().
   *
   *
   */
  public TCPClient(PApplet _p, String _fileString) {
    this(_p, _fileString, true);
  }

  public TCPClient(PApplet _p, String _fileString, boolean _autoMode) {
    p5parent = _p;

    PApplet.println("MPE CLIENT VERSION " + version);
   
    // Autodetecting if we should use 3D or not
    enable3D = p5parent.g instanceof PGraphics3D;

    autoMode = _autoMode;
    cameraZ = (p5parent.height/2.0f) / PApplet.tan(PConstants.PI * fieldOfView/360.0f);

    loadSettings(_fileString);

    setServer(hostName, serverPort, id);


    if (!asynchronous) {

      // look for a method called "frameEvent" in the parent PApplet, with one
      // argument of type Client
      try {
        frameEventMethod = p5parent.getClass().getMethod("frameEvent",new Class[] { TCPClient.class });
        if (!autoMode) {
          System.out.println("frameEvent() will not be used in manual mode. ");
        }
      } catch (Exception e) {
        if (autoMode) {
          System.out.println("You are missing the frameEvent() method. " + e);
        }
      }

      try {
        resetEventMethod = p5parent.getClass().getMethod("resetEvent",new Class[] { TCPClient.class });
      } catch (Exception e) {
        System.out.println("You are missing the resetEvent() method. " + e);
      }

      try {
        dataEventMethod = p5parent.getClass().getMethod("dataEvent",new Class[] { TCPClient.class });
        if (!autoMode) {
          System.out.println("dataEvent() will not be used in manual mode. ");
          dataEventEnabled = false;
        } else {
          dataEventEnabled = true;
        }
      } catch (Exception e) {
        dataEventEnabled = false;
      }


      // Now we are always registering draw(), we still need to deal with reset() in manual mode
      //if (autoMode) {
        p5parent.registerMethod("draw", this);
      //}
    } else {
      if (asynchreceive) {
        try {
          dataEventMethod = p5parent.getClass().getMethod("dataEvent", new Class[] { TCPClient.class });
        } catch (Exception e) {
          System.out.println("You are missing the dataEvent() method. " + e);
        }
      }

    }

  }

  /**
   * Called automatically by PApplet.draw() when using auto mode.
   */
  public void draw() {
    if (offsetWindow) {
      p5parent.frame.setLocation(id*lWidth,0);
    }

    // Simulation mode just trigger frameEvent
    if (simulation) {
      placeScreen();
      p5parent.frameRate(simFPS);
      // Just keep moving
      try {
        frameEventMethod.invoke(p5parent, new Object[] { this });
        frameCount++;
      } catch (Exception e) {
        e.printStackTrace();
      }
      // Otherwise typical MPE stuff
    } else if (running && rendering) {
     
      // Only place screen in auto mode
      if (autoMode) {
        placeScreen();
      }

      if (!asynchronous) {
        if (reset) {
          try {
            resetEventMethod.invoke(p5parent, new Object[] { this });
          } catch (Exception e) {
            err("Could not invoke the \"resetEvent()\" method for some reason.");
            e.printStackTrace();
            resetEventMethod = null;
          }
        } else if (frameEventMethod != null) {
          try {
            // First see if dataEvent should be trigged
            if (dataEventEnabled && messageAvailable()) {
              dataEventMethod.invoke(p5parent, new Object[] { this });
            }
            // Then trigger the frame event
            frameEventMethod.invoke(p5parent, new Object[] { this });
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
       
        // Say done in auto mode only
        if (autoMode) {
          done();
        }
      }
    }
  }

  /**
   * Loads the settings from the Client XML file.
   *
   * @param filename the path to the XML file
   */
  private void loadSettings(String filename) {
    // parse XML file
    XML xml = p5parent.loadXML(filename);
    setServer(xml.getChild("server/ip").getContent());
    setPort(xml.getChild("server/port").getIntContent());
    setID(xml.getChild("id").getIntContent());

    XML name = xml.getChild("name");
    if (name != null) {
      setClientName(name.getContent());
    }


    XML asynch = xml.getChild("asynchronous");
    if (asynch != null) {
      String a = asynch.getContent();
      asynchronous = Boolean.parseBoolean(a);
      if (asynchronous) {
        XML receive = xml.getChild("asynchreceive");
        if (receive != null) {
          String r = receive.getContent();
          asynchreceive = Boolean.parseBoolean(r);
        }
      }
    }



    String v = xml.getChild("verbose").getContent();
    VERBOSE = Boolean.parseBoolean(v);

    // Implement name
    if (!asynchronous) {
      int w = xml.getChild("local_dimensions/width").getIntContent();
      int h = xml.getChild("local_dimensions/height").getIntContent();
      setLocalDimensions(w,h);

      int x = xml.getChild("local_location/x").getIntContent();
      int y = xml.getChild("local_location/y").getIntContent();
      setOffsets(x,y);

      int mw = xml.getChild("master_dimensions/width").getIntContent();
      int mh = xml.getChild("master_dimensions/height").getIntContent();
      setLocalDimensions(w,h);

      XML offset = xml.getChild("offset_window");
      if (offset != null) {
        offsetWindow = Boolean.parseBoolean(offset.getContent());
      }

      XML simxml = xml.getChild("simulation");
      if (simxml != null) {
        simulation = Boolean.parseBoolean(simxml.getContent());
        simFPS = simxml.getInt("fps");
        if (simFPS < 1) simFPS = 30;
      }

      setMasterDimensions(mw,mh);

      out("Settings: server = " + hostName + ":" + serverPort + ",  id = " + id
          + ", local dimensions = " + lWidth + ", " + lHeight
          + ", location = " + xOffset + ", " + yOffset);
    }
  }


  /**
   * Connects to the server.
   *
   * @param _hostName the server host name
   * @param _serverPort the server port
   * @param _id the client id
   */
  private void setServer(String _hostName, int _serverPort, int _id) {
    // set the server address and port
    setServer(_hostName);
    setPort(_serverPort);
    setID(_id);
  }

  /**
   * Sets the server address.
   *
   * @param _hostName the server host name
   */
  protected void setServer(String _hostName) {
    if (_hostName != null)
      hostName = _hostName;
  }

  /**
   * Sets the server port.
   *
   * @param _serverPort the server port
   */
  protected void setPort(int _serverPort) {
    if (_serverPort > -1)
      serverPort = _serverPort;
  }

  /** @return the server port */
  public int getPort() { return serverPort; }

  /**
   * Sets the client ID.
   *
   * @param _id the client id
   */
  protected void setID(int _id) {
    if (_id > -1)
      id = _id;
  }

  /** @return the client ID */
  public int getID() { return id; }

  /**
   * Sets the dimensions for the local display.
   *
   * @param _lWidth The local width
   * @param _lHeight The local height
   */
  protected void setLocalDimensions(int _lWidth, int _lHeight) {
    if (_lWidth > -1 && _lHeight > -1) {
      lWidth = _lWidth;
      lHeight = _lHeight;
    }
  }

  /**
   * Sets the offsets for the local display.
   *
   * @param _xOffset Offsets the display along x axis
   * @param _yOffset Offsets the display along y axis
   */
  protected void setOffsets(int _xOffset, int _yOffset) {
    if (_xOffset > -1 && _yOffset > -1) {
      xOffset = _xOffset;
      yOffset = _yOffset;
    }
  }

  /**
   * Sets the dimensions for the local display.
   * The offsets are used to determine what part of the Master Dimensions to render.
   * For example, if you have two screens, each 100x100, and the master dimensions are 200x100
   * then you would set
   *  client 0: setLocalDimensions(0, 0, 100, 100);
   *  client 1: setLocalDimensions(100, 0, 100, 100);
   * for a 10 pixel overlap you would do:
   *  client 0: setLocalDimensions(0, 0, 110, 100);
   *  client 1: setLocalDimensions(90, 0, 110, 100);
   *
   * @param _xOffset Offsets the display along x axis
   * @param _yOffset Offsets the display along y axis
   * @param _lWidth The local width
   * @param _lHeight The local height
   */
  public void setLocalDimensions(int _xOffset, int _yOffset, int _lWidth, int _lHeight) {
    setOffsets(_xOffset, _yOffset);
    setLocalDimensions(_lWidth, _lHeight);
  }

  /**
   * Sets the master dimensions for the Video Wall. This is used to calculate
   * what is rendered.
   *
   * @param _mWidth The master width
   * @param _mHeight he master height
   */
  public void setMasterDimensions(int _mWidth, int _mHeight) {
    if (_mWidth > -1 && _mHeight > -1) {
      mWidth = _mWidth;
      mHeight = _mHeight;
    }
  }

  /** @return the local width in pixels */
  public int getLWidth() { return lWidth; }

  /** @return the local height in pixels */
  public int getLHeight() { return lHeight; }

  /** @return the x-offset of frame in pixels */
  public int getXoffset() { return xOffset; }

  /** @return the y-offset of frame in pixels */
  public int getYoffset() { return yOffset; }

  /** @return the master width in pixels */
  public int getMWidth() { return mWidth; }

  /** @return the master height in pixels */
  public int getMHeight() { return mHeight; }

  /** @return the total number of frames rendered */ 
  public int getFrameCount() { return frameCount; }

  /** @return the client framerate */ 
  public float getFPS() { return fps; }

  public String getClientName() {
    return name;
  }

  public void setClientName(String n) {
    name = n;
  }

  /** @return whether or not the client is rendering */ 
  public boolean isRendering() { return rendering; }

  /**
   * Sets the field of view of the camera when rendering in 3D.
   * Note that this has no effect when rendering in 2D.
   *
   * @param val the value of the field of view
   */
  public void setFieldOfView(float val) {
    fieldOfView = val;

    if (p5parent != null) {
      cameraZ = (p5parent.height/2.0f) / PApplet.tan(PConstants.PI * fieldOfView/360.0f);

      if (!(p5parent.g instanceof PGraphics3D)) {
        out("MPE Warning: Rendering in 2D! fieldOfView has no effect!");
      }
    } else {
      out("MPE Warning: Not using Processing! fieldOfView has no effect!");
    }
  }

  /** @return the value of the field of view */
  public float getFieldOfView() { return fieldOfView; }

  /**
   * Places the viewing area for this screen. This must be called at the
   * beginning of the render loop.  If you are using Processing, you would
   * typically place it at the beginning of your draw() function.
   */
  public void placeScreen() {
    if (enable3D) {
      placeScreen3D();
    } else {
      placeScreen2D();
    }
  }

  /**
   * If you want to enable or disable 3D manually in automode
   */
  public void enable3D(boolean b) {
    enable3D = b;
  }

  /**
   * Places the viewing area for this screen when rendering in 2D.
   */
  public void placeScreen2D() {
    p5parent.translate(xOffset * -1, yOffset * -1);
  }

  /**
   * Places the viewing area for this screen when rendering in 3D.
   */
  public void placeScreen3D() {
    p5parent.camera(mWidth/2.0f, mHeight/2.0f, cameraZ,
        mWidth/2.0f, mHeight/2.0f, 0,
        0, 1, 0);


    // The frustum defines the 3D clipping plane for each Client window!
    float mod = 1f/10f;
    float left   = (xOffset - mWidth/2)*mod;
    float right  = (xOffset + lWidth - mWidth/2)*mod;
    float top    = (yOffset - mHeight/2)*mod;
    float bottom = (yOffset + lHeight-mHeight/2)*mod;
    float near   = cameraZ*mod;
    float far    = 10000;
    p5parent.frustum(left,right,top,bottom,near,far);
  }

  /**
   * Restores the viewing area for this screen when rendering in 3D.
   */
  public void restoreCamera() {
    p5parent.camera(p5parent.width/2.0f, p5parent.height/2.0f, cameraZ,
        p5parent.width/2.0f, p5parent.height/2.0f, 0,
        0, 1, 0);

    float mod = 1/10.0f;
    p5parent.frustum(-(p5parent.width/2)*mod, (p5parent.width/2)*mod,
        -(p5parent.height/2)*mod, (p5parent.height/2)*mod,
        cameraZ*mod, 10000);
  }

  /**
   * Checks whether the given point is on screen.
   */
  public boolean isOnScreen(float x, float y) {
    return (x > xOffset &&
        x < (xOffset + lWidth) &&
        y > yOffset &&
        y < (yOffset + lHeight));
  }

  /**
   * Checks whether the given rectangle is on screen.
   */
  public boolean isOnScreen(float x, float y, float w, float h) {
    return (isOnScreen(x, y) ||
        isOnScreen(x + w, y) ||
        isOnScreen(x + w, y + h) ||
        isOnScreen(x, y + h));
  }

  /**
   * Outputs a message to the console.
   *
   * @param _str the message to output.
   */
  private void out(String _str) {
    print(_str);
  }

  /**
   * Outputs a message to the console.
   *
   * @param _str the message to output.
   */
  private void print(String _str) {
    System.out.println("Client: " + _str);
  }

  /**
   * Outputs an error message to the console.
   *
   * @param _str the message to output.
   */
  private void err(String _str) {
    System.err.println("Client: " + _str);
  }

  private void connect() throws UnknownHostException, IOException {
    socket = new Socket(hostName, serverPort);
    is = socket.getInputStream();
    brin = new BufferedReader(new InputStreamReader(is));
    os = socket.getOutputStream();
    dos = new DataOutputStream(os);
  }

  /**
   * This method must be called when the client PApplet starts up. It will
   * tell the server it is ready.
   */
  public void start() {

    running = true;
    super.start();
  }

  /**
   * This method should only be called internally by Thread.start().
   */
  public void run() {

    if (VERBOSE) {
      if (simulation) {
        out("Running in simulation mode (no server connection, will not receive data)!");
      } else {
        out("Starting!");
      }
    }

    // Don't bother to connect to server if you are in simulation mode
    if (!simulation) {
      // Try until server responds
      while (!connected) {
        try {
          connect();
          connected = true;
        } catch (IOException e) {
          //e.printStackTrace();
          connected = false;
          System.out.println("Cannot connect to server, retrying in " + waittime + " second");
        }

        try {
          Thread.sleep(waittime*1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }

      if (VERBOSE) out("Connected to server!");

      // let the server know that this client is ready to start.
      if (asynchronous) {
        send("A|" + id + "|" + asynchreceive);
      } else {
        send("S|" + id);
      }



      try {
        while (running) {
          // read packet
          String msg = brin.readLine();
          if (msg == null) {
            //running = false;
            break;
          } else {
            read(msg);
          }
        }
        is.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    } else {
      while (running) {
        //System.out.println("GOGOGO");
      }
    }
  }

  /**
   * Reads and parses a message from the server.
   *
   * @param _serverInput the server message
   */
  // Synchronized b/c the reset method could come anytime strange?
  private synchronized void read(String msg) {
    if (VERBOSE) out("Receiving: " + msg);

    rawMessage = msg;

    // a "G" startbyte will trigger a frameEvent.
    char c = msg.charAt(0);
    if (c == 'G' || c == 'R') {

      if (c == 'R') reset = true;

      String[] tokens = msg.split("\\|");
      int fc = Integer.parseInt(tokens[1]);
      if (tokens.length > 2) {
        // there is a message here with the frameEvent
        String[] dataInfo = new String[tokens.length-2];
        for (int k = 0; k < dataInfo.length; k++){
          // Grabbing the message as everything after first comma
          // TODO offer ID
          int comma = tokens[k+2].indexOf(",");
          dataInfo[k] = tokens[k+2].substring(comma+1,tokens[k+2].length());
        }
        dataMessage = null// clear
        dataMessage = dataInfo;
        messageAvailable = true;
      } else {
        messageAvailable = false;
      }

      if (reset) {
        frameCount = 0;
      }

      if (fc == frameCount) {
        rendering = true;
        // calculate new framerate
        float ms = System.currentTimeMillis() - lastMs;
        fps = 1000.f / ms;
        lastMs = System.currentTimeMillis();
      } else if (!asynchronous) {
        if (VERBOSE) print("Extra message, frameCount: " + frameCount + " received from server: " + fc);
      }
    }

    // If we're asynchronous and should receive trigger dataEvent now?
    if (asynchronous && asynchreceive) {
      try {
        // call the method with this object as the argument!
        dataEventMethod.invoke(p5parent, new Object[] { this });
      } catch (Exception e) {
        err("Could not invoke the \"dataEvent()\" method for some reason.");
        e.printStackTrace();
      }
    }
  }

  /**
   * Send a message to the server using UDP.
   *
   * @param _msg the message to send
   */
  private void send(String _msg) {

    // Don't actually send if you are in simulation mode!
    if (!simulation) {
      if (VERBOSE) out("Sending: " + _msg);
      _msg += "\n";
      try {
        dos.write(_msg.getBytes());
        dos.flush();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  /**
   * Format a broadcast message and send it.
   * Do not use a colon '|' in your message!!!
   *
   * @param _msg the message to broadcast
   */
  public void broadcast(String msg) {
    if (msg.contains("|")) {
      msg.replaceAll("|", "_");
      System.out.println("MPE messages cannot contain a '|' character. '|' has been replaced with '_' in your message.");
    }
    // prepend the message with a "T"
    msg = "T|"+ msg;
    send(msg);
  }


  /**
   * Returns true of false based on whether a String message is available from
   *  the server.
   * This should be used inside "frameEvent()" since messages are tied to
   * specific frames.
   *
   * @return true if a new String message is available
   */
  public boolean messageAvailable() {
    return messageAvailable;
  }

  /**
   * Returns an array of messages from the server.
   * This should be used inside "frameEvent()" since messages are tied to
   * specific frames. It also should only be called after checking that
   * {@link #messageAvailable()} returns true.
   *
   * @return an array of messages from the server
   */
  public String[] getDataMessage() {
    return dataMessage;
  }

  public String getRawMessage() {
    return rawMessage;
  }

  /**
   * Sends a "Done" command to the server. This must be called at the end of
   * the draw loop.
   */
  public void done() {
    rendering = false;
    reset = false;
    String msg = "D|" + id + "|" + frameCount;
    send(msg);
    frameCount++;
  }

  public void togglePause() {
    // Let's send id along with P just in case
    String msg = "P|" + id;
    send(msg);
  }

  /**
   * Stops the client thread.  You don't really need to do this ever.
   */ 
  public void quit() {
    out("Quitting.");
    running = false// Setting running to false ends the loop in run()
    interrupt();      // In case the thread is waiting. . .
  }

  public boolean isAsynchronous() {
    return asynchronous;
  }

  public boolean isReceiver() {
    return (!asynchronous || asynchreceive);
  }

}
TOP

Related Classes of mpe.client.TCPClient

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.