public class CameraDriver implements CameraOperatorListener, ImageReader.OnImageAvailableListener {

  private final static String PHOTO_FILENAME = "cv.jpg";

  private AdafruitMotorHat motorHat;
  private CameraOperator cameraOperator;

  public CameraDriver(Context context, AdafruitMotorHat motorHat) {
    this.motorHat = motorHat;
    cameraOperator = new CameraOperator(context, this);
  }

  public void start() {
    cameraOperator.startSession(this);
  }

  public void stop() {
    cameraOperator.endSession();
  }

  @Override
  public void sessionStarted() {
    Timber.d("Camera is ready, taking a picture.");
    cameraOperator.takePicture();
  }

  @Override
  public void onImageAvailable(final ImageReader reader) {
    Timber.d("Picture is available.");
    new Thread(new Runnable() {
      @Override
      public void run() {
        File root = ImageSaver.getRoot(CameraOperator.ROBOCAR_FOLDER);
        new ImageSaver(reader.acquireLatestImage(), root, PHOTO_FILENAME).run();
        processPhoto(root, PHOTO_FILENAME);
      }
    }).start();
  }

  private void processPhoto(File root, String filename) {
    File path = new File(root, filename);
    Timber.d("TODO: Process %s", path.getAbsolutePath());
  }
} public class TensorFlowTrainer implements
    CameraOperatorListener, ImageReader.OnImageAvailableListener {

  private AdafruitMotorHat motorHat;
  private CameraOperator cameraOperator;

  private String sessionId;
  private int sessionCount;
  private Timer timer;

  public TensorFlowTrainer(Context context, AdafruitMotorHat motorHat) {
    this.motorHat = motorHat;
    cameraOperator = new CameraOperator(context, this);
  }

  public int[] getSpeeds() {
    return new int[] {
        motorHat.getMotor(1).getLastSpeed(),
        motorHat.getMotor(2).getLastSpeed(),
        motorHat.getMotor(3).getLastSpeed(),
        motorHat.getMotor(4).getLastSpeed()};
  }

  public void startSession() {
    if (!cameraOperator.isInSession()) {
      sessionId = UUID.randomUUID().toString().replace("-", "");
      sessionCount = 0;
      cameraOperator.startSession(this);
    }
  }

  @Override
  public void sessionStarted() {
    timer = new Timer("CAMERA_TRAINING");
    timer.schedule(new TimerTask() {
      @Override
      public void run() {
        cameraOperator.takePicture();
      }
    }, 0, 250); // 0 delay, 250 period
  }

  public void endSession() {
    if (timer != null) {
      timer.cancel();
    }
    cameraOperator.endSession();
  }

  @Override
  public void onImageAvailable(final ImageReader reader) {
    Timber.d("Image available.");
    new Thread(new Runnable() {
      @Override
      public void run() {
        File root = ImageSaver.getRoot(CameraOperator.ROBOCAR_FOLDER);
        ImageSaver imageSaver = new ImageSaver(reader.acquireLatestImage(), root, getFilename());
        imageSaver.run();
      }
    }).start();
  }

  private String getFilename() {
    String timestamp = CameraOperator.DATE_FORMAT.format(new Date());
    int[] speeds = getSpeeds();
    String speedState = String.format(Locale.US, "%d-%d-%d-%d",
        speeds[0], speeds[1], speeds[2], speeds[3]);
    return String.format(Locale.US, "robocar-%s-%d-%s-%s.jpg",
        sessionId, sessionCount++, timestamp, speedState);
  }
} public class RCDriver {

  // Set the speed, from 0 (off) to 255 (max speed)
  private static final int MOTOR_SPEED_SLOW = 95;
  private static final int MOTOR_SPEED = 255;

  private AdafruitDcMotor motorFrontLeft;
  private AdafruitDcMotor motorFrontRight;
  private AdafruitDcMotor motorBackLeft;
  private AdafruitDcMotor motorBackRight;

  private boolean isUpPressed = false;
  private boolean isDownPressed = false;
  private boolean isLeftPressed = false;
  private boolean isRightPressed = false;
  private boolean isUpOrDownPressed = false;
  private boolean allButtonsReleased = true;

  private boolean isMoving = false;

  public RCDriver(AdafruitMotorHat motorHat) {
    motorFrontLeft = motorHat.getMotor(1);
    motorBackLeft = motorHat.getMotor(2);
    motorFrontRight = motorHat.getMotor(3);
    motorBackRight = motorHat.getMotor(4);
  }

  public void updateButtonPressedStates(@Nes30Manager.ButtonCode int keyCode, boolean isDown) {
    switch (keyCode) {
      case Nes30Manager.BUTTON_UP_CODE:
        isUpPressed = isDown;
        break;
      case Nes30Manager.BUTTON_DOWN_CODE:
        isDownPressed = isDown;
        break;
      case Nes30Manager.BUTTON_LEFT_CODE:
        isLeftPressed = isDown;
        break;
      case Nes30Manager.BUTTON_RIGHT_CODE:
        isRightPressed = isDown;
        break;
      default:
        // No action
        break;
    }

    isUpOrDownPressed = isUpPressed || isDownPressed;
    allButtonsReleased = !(isUpPressed || isDownPressed || isLeftPressed || isRightPressed);
  }

  private void preCheck(@Nes30Manager.ButtonCode int keyCode, boolean isDown) {
    updateButtonPressedStates(keyCode, isDown);
    if (allButtonsReleased && isMoving) {
      isMoving = false;
      release();
      return;
    }

    // We start moving the moment the key is pressed
    isMoving = true;

    setMotorSpeedsBasedOnButtonsPressed();
  }

  public void moveForward(@Nes30Manager.ButtonCode int keyCode, boolean isDown) {
    preCheck(keyCode, isDown);

    Timber.d("Moving forward.");
    motorFrontLeft.run(AdafruitMotorHat.FORWARD);
    motorFrontRight.run(AdafruitMotorHat.BACKWARD);
    motorBackLeft.run(AdafruitMotorHat.BACKWARD);
    motorBackRight.run(AdafruitMotorHat.FORWARD);
  }

  public void moveBackward(@Nes30Manager.ButtonCode int keyCode, boolean isDown) {
    preCheck(keyCode, isDown);

    Timber.d("Moving backward.");
    motorFrontLeft.run(AdafruitMotorHat.BACKWARD);
    motorFrontRight.run(AdafruitMotorHat.FORWARD);
    motorBackLeft.run(AdafruitMotorHat.FORWARD);
    motorBackRight.run(AdafruitMotorHat.BACKWARD);
  }

  public void turnLeft(@Nes30Manager.ButtonCode int keyCode, boolean isDown) {
    preCheck(keyCode, isDown);
    if (isUpOrDownPressed) {
      return;
    }

    Timber.d("Turning left.");
    motorFrontLeft.run(AdafruitMotorHat.BACKWARD);
    motorFrontRight.run(AdafruitMotorHat.BACKWARD);
    motorBackLeft.run(AdafruitMotorHat.FORWARD);
    motorBackRight.run(AdafruitMotorHat.FORWARD);
  }

  public void turnRight(@Nes30Manager.ButtonCode int keyCode, boolean isDown) {
    preCheck(keyCode, isDown);
    if (isUpOrDownPressed) {
      return;
    }

    Timber.d("Turning right.");
    motorFrontLeft.run(AdafruitMotorHat.FORWARD);
    motorFrontRight.run(AdafruitMotorHat.FORWARD);
    motorBackLeft.run(AdafruitMotorHat.BACKWARD);
    motorBackRight.run(AdafruitMotorHat.BACKWARD);
  }

  public void release() {
    Timber.d("Release.");
    motorFrontLeft.run(AdafruitMotorHat.RELEASE);
    motorFrontRight.run(AdafruitMotorHat.RELEASE);
    motorBackLeft.run(AdafruitMotorHat.RELEASE);
    motorBackRight.run(AdafruitMotorHat.RELEASE);
  }

  private void setMotorSpeedsBasedOnButtonsPressed() {
    boolean isLowSpeedOnLeft = isLeftPressed && isUpOrDownPressed;
    boolean isLowSpeedOnRight = isRightPressed && isUpOrDownPressed;

    int speedLeft = isLowSpeedOnLeft ? MOTOR_SPEED_SLOW : MOTOR_SPEED;
    int speedRight = isLowSpeedOnRight ? MOTOR_SPEED_SLOW : MOTOR_SPEED;

    motorFrontLeft.setSpeed(speedLeft);
    motorBackLeft.setSpeed(speedLeft);
    motorFrontRight.setSpeed(speedRight);
    motorBackRight.setSpeed(speedRight);
  }
} public class LocalhostDriver {

  // If speed was set too low, the motor could burn.
  private static final int MIN_SPEED = 64;

  private AdafruitDcMotor motorFrontLeft;
  private AdafruitDcMotor motorFrontRight;
  private AdafruitDcMotor motorBackLeft;
  private AdafruitDcMotor motorBackRight;

  public LocalhostDriver(AdafruitMotorHat motorHat) {
    motorFrontLeft = motorHat.getMotor(1);
    motorBackLeft = motorHat.getMotor(2);
    motorFrontRight = motorHat.getMotor(3);
    motorBackRight = motorHat.getMotor(4);
  }

  public void changeSpeed(RobocarSpeed speed) {
    if (speed.getLeft() != null) {
      setLeftSpeed(speed.getLeft());
    }

    if (speed.getRight() != null) {
      setRightSpeed(speed.getRight());
    }
  }

  public void setLeftSpeed(int speed) {
    int unsignedSpeed = Math.abs(speed);
    if (unsignedSpeed < MIN_SPEED) {
      unsignedSpeed = 0;
    }

    motorBackLeft.setSpeed(unsignedSpeed);
    motorFrontLeft.setSpeed(unsignedSpeed);

    if (speed > 0) { // Positive speed motor directions
      motorFrontLeft.run(AdafruitMotorHat.FORWARD);
      motorBackLeft.run(AdafruitMotorHat.BACKWARD);
    } else { // Negative speed motor directions
      motorFrontLeft.run(AdafruitMotorHat.BACKWARD);
      motorBackLeft.run(AdafruitMotorHat.FORWARD);
    }
  }

  public void setRightSpeed(int speed) {
    int unsignedSpeed = Math.abs(speed);
    if (unsignedSpeed < MIN_SPEED) {
      unsignedSpeed = 0;
    }

    motorBackRight.setSpeed(unsignedSpeed);
    motorFrontRight.setSpeed(unsignedSpeed);

    if (speed > 0) {
      // Positive speed motor directions
      motorFrontRight.run(AdafruitMotorHat.BACKWARD);
      motorBackRight.run(AdafruitMotorHat.FORWARD);
    } else {
      // Negative speed motor directions
      motorFrontRight.run(AdafruitMotorHat.FORWARD);
      motorBackRight.run(AdafruitMotorHat.BACKWARD);
    }
  }
} public class AdafruitMotorHat {

  public static final int MOTOR_FREQUENCY = 1600;

  public static final int FORWARD = 1;
  public static final int BACKWARD = 2;
  public static final int BRAKE = 3;
  public static final int RELEASE = 4;

  public static final int SINGLE = 1;
  public static final int DOUBLE = 2;
  public static final int INTERLEAVE = 3;
  public static final int MICROSTEP = 4;

  private AdafruitPwm pwm;
  private AdafruitDcMotor[] motors;

  /**
   * Public constructor.
   */
  public AdafruitMotorHat(String i2cName, int i2cAddress, boolean debug) {
    pwm = new AdafruitPwm(i2cName, i2cAddress, debug);
    pwm.setPwmFreq(MOTOR_FREQUENCY);
    motors = new AdafruitDcMotor[]{
            new AdafruitDcMotor(this, 0),
            new AdafruitDcMotor(this, 1),
            new AdafruitDcMotor(this, 2),
            new AdafruitDcMotor(this, 3)
    };
  }

  /**
   * Get current PWM value.
   */
  public AdafruitPwm getPwm() {
    return pwm;
  }

  /**
   * Set pin to specified value.
   */
  public void setPin(int pin, int value) {
    if ((pin < 0) || (pin > 15)) {
      throw new RuntimeException("PWM pin must be between 0 and 15 inclusive");
    }
    if ((value != 0) && (value != 1)) {
      throw new RuntimeException("Pin value must be 0 or 1!");
    }
    if ((value == 0)) {
      pwm.setPwm(pin, 0, 4096);
    }
    if ((value == 1)) {
      pwm.setPwm(pin, 4096, 0);
    }
  }

  /**
   * Get the specific motor.
   */
  public AdafruitDcMotor getMotor(int num) {
    if ((num < 1) || (num > 4)) {
      throw new RuntimeException("MotorHAT Motor must be between 1 and 4 inclusive");
    }
    return motors[num - 1];
  }

  public void close() {
    pwm.close();
  }

} public class Nes30Connection {

  private Context context;
  private String deviceAddress;

  private BluetoothAdapter bluetoothAdapter;

  /**
   * Public constructor.
   */
  public Nes30Connection(Context context, String deviceAddress) {
    this.context = context;
    this.deviceAddress = deviceAddress;
    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (bluetoothAdapter == null) {
      Timber.e("This device does not support Bluetooth.");
    } else if (!isEnabled()) {
      Timber.d("Bluetooth isn't enabled, enabling: %b", bluetoothAdapter.enable());
    }
  }

  public boolean isEnabled() {
    return (bluetoothAdapter != null && bluetoothAdapter.isEnabled());
  }

  public Set<BluetoothDevice> getPairedDevices() {
    return bluetoothAdapter.getBondedDevices();
  }

  /**
   * Checks whether the device is already paired.
   */
  public BluetoothDevice getSelectedDevice() {
    Set<BluetoothDevice> pairedDevices = getPairedDevices();
    for (BluetoothDevice pairedDevice : pairedDevices) {
      if (isSelectedDevice(pairedDevice.getAddress())) {
        return pairedDevice;
      }
    }

    return null;
  }

  public boolean startDiscovery() {
    registerReceiver();
    return bluetoothAdapter.startDiscovery();
  }

  // Create a BroadcastReceiver for ACTION_FOUND.
  private final BroadcastReceiver receiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
      String action = intent.getAction();
      // Discovery has found a device.
      if (BluetoothDevice.ACTION_FOUND.equals(action)) {
        // Get the BluetoothDevice object and its info from the Intent.
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        int bondState = device.getBondState();
        String foundName = device.getName();
        String foundAddress = device.getAddress(); // MAC address
        Timber.d("Discovery has found a device: %d/%s/%s", bondState, foundName, foundAddress);
        if (isSelectedDevice(foundAddress)) {
          createBond(device);
        } else {
          Timber.d("Unknown device, skipping bond attempt.");
        }
      } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
        int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
        switch (state) {
          case BluetoothDevice.BOND_NONE:
            Timber.d("The remote device is not bonded.");
            break;
          case BluetoothDevice.BOND_BONDING:
            Timber.d("Bonding is in progress with the remote device.");
            break;
          case BluetoothDevice.BOND_BONDED:
            Timber.d("The remote device is bonded.");
            break;
          default:
            Timber.d("Unknown remote device bonding state.");
            break;
        }
      }
    }
  };

  private void registerReceiver() {
    // Register for broadcasts when a device is discovered.
    IntentFilter filter = new IntentFilter();
    filter.addAction(BluetoothDevice.ACTION_FOUND);
    filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
    context.registerReceiver(receiver, filter);
  }

  public void cancelDiscovery() {
    bluetoothAdapter.cancelDiscovery();
    context.unregisterReceiver(receiver);
  }

  private boolean isSelectedDevice(String foundAddress) {
    // MAC address is set and recognized
    return !TextUtils.isEmpty(deviceAddress) && deviceAddress.equals(foundAddress);
  }

  /**
   * Pair with the specific device.
   */
  public boolean createBond(BluetoothDevice device) {
    boolean result = device.createBond();
    Timber.d("Creating bond with: %s/%s/%b", device.getName(), device.getAddress(), result);
    return result;
  }

  /**
   * Remove bond with the specific device.
   */
  public void removeBond(BluetoothDevice device) {
    try {
      Timber.w("Removing bond.");
      Method m = device.getClass().getMethod("removeBond", (Class[]) null);
      m.invoke(device, (Object[]) null);
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
      Timber.e(e, "Failed to remove bond.");
    }
  }
} public interface Nes30Listener {

  void onKeyPress(@Nes30Manager.ButtonCode int keyCode, boolean isDown);

} public class Nes30Manager implements KeyEvent.Callback {

  @Retention(RetentionPolicy.SOURCE)
  @IntDef({BUTTON_LEFT_CODE, BUTTON_RIGHT_CODE, BUTTON_UP_CODE, BUTTON_DOWN_CODE,
      BUTTON_SELECT_CODE, BUTTON_START_CODE, BUTTON_A_CODE, BUTTON_B_CODE,
      BUTTON_X_CODE, BUTTON_Y_CODE, BUTTON_L_CODE, BUTTON_R_CODE, BUTTON_KONAMI})
  public @interface ButtonCode {
  }

  public static final int BUTTON_LEFT_CODE = KeyEvent.KEYCODE_DPAD_LEFT;
  public static final int BUTTON_RIGHT_CODE = KeyEvent.KEYCODE_DPAD_RIGHT;
  public static final int BUTTON_UP_CODE = KeyEvent.KEYCODE_DPAD_UP;
  public static final int BUTTON_DOWN_CODE = KeyEvent.KEYCODE_DPAD_DOWN;

  public static final int BUTTON_SELECT_CODE = KeyEvent.KEYCODE_BUTTON_11;
  public static final int BUTTON_START_CODE = KeyEvent.KEYCODE_BUTTON_12;

  public static final int BUTTON_A_CODE = KeyEvent.KEYCODE_BUTTON_A;
  public static final int BUTTON_B_CODE = KeyEvent.KEYCODE_BUTTON_B;
  public static final int BUTTON_X_CODE = KeyEvent.KEYCODE_BUTTON_X;
  public static final int BUTTON_Y_CODE = KeyEvent.KEYCODE_BUTTON_Y;

  public static final int BUTTON_L_CODE = KeyEvent.KEYCODE_BUTTON_7;
  public static final int BUTTON_R_CODE = KeyEvent.KEYCODE_BUTTON_8;

  public static final int BUTTON_KONAMI = -1;

  private Deque<Integer> history = null;
  private Nes30Listener listener = null;

  private Deque<Integer> konami = new ArrayDeque<>(Arrays.asList(
      BUTTON_UP_CODE, BUTTON_UP_CODE, BUTTON_DOWN_CODE, BUTTON_DOWN_CODE,
      BUTTON_LEFT_CODE, BUTTON_RIGHT_CODE, BUTTON_LEFT_CODE, BUTTON_RIGHT_CODE,
      BUTTON_B_CODE, BUTTON_A_CODE));

  public Nes30Manager() {
    history = new ArrayDeque<>();
  }

  public Nes30Manager(Nes30Listener listener) {
    this();
    this.listener = listener;
  }

  public Nes30Listener getListener() {
    return listener;
  }

  public void setListener(Nes30Listener listener) {
    this.listener = listener;
  }

  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    addKeyToHistory(keyCode);
    if (listener != null) {
      listener.onKeyPress(keyCode, true);
      return true;
    }

    return false;
  }

  @Override
  public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (listener != null) {
      listener.onKeyPress(keyCode, false);
      return true;
    }

    return false;
  }

  @Override
  public boolean onKeyLongPress(int keyCode, KeyEvent event) {
    // NOTE: Doesn't seem to be triggered
    Timber.d("onKeyLongPress: %d", keyCode);
    return false;
  }

  @Override
  public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
    // NOTE: Doesn't seem to be triggered
    Timber.d("onKeyMultiple: %d", keyCode);
    return false;
  }

  private void addKeyToHistory(int keyCode) {
    history.add(keyCode);
    if (history.size() > 10) {
      history.pop();
    }

    if (Arrays.equals(history.toArray(), konami.toArray())) {
      if (listener != null) {
        listener.onKeyPress(Nes30Manager.BUTTON_KONAMI, true);
      }
    }
  }
} public class LocalWebServer extends NanoHTTPD {

  private static final String APPLICATION_JSON = "application/json";

  public static final String ENDPOINT_ROOT = "/";
  public static final String ENDPOINT_GET_STATUS = "api/status";
  public static final String ENDPOINT_POST_MOVE = "api/move";
  public static final String ENDPOINT_POST_SPEED = "api/speed";

  private HTTPRequestListener requestListener;

  public LocalWebServer(HTTPRequestListener requestListener) {
    super(8080);
    this.requestListener = requestListener;
  }

  public LocalWebServer(HTTPRequestListener requestListener, int port) {
    super(port);
    this.requestListener = requestListener;
  }

  @Override
  public Response serve(IHTTPSession session) {
    requestListener.onRequest(session);

    Object result = null;
    switch (session.getMethod()) {
      case GET:
        switch (session.getUri()) {
          case ENDPOINT_ROOT + ENDPOINT_GET_STATUS:
            result = requestListener.onStatus();
            break;
          default:
            // No action.
            break;
        }
        break;
      case POST:
        switch (session.getUri()) {
          case ENDPOINT_ROOT + ENDPOINT_POST_MOVE:
            RobocarMove move = (RobocarMove) readPostAsObject(session, RobocarMove.class);
            result = requestListener.onMove(move);
            break;
          case ENDPOINT_ROOT + ENDPOINT_POST_SPEED:
            RobocarSpeed speed = (RobocarSpeed)readPostAsObject(session, RobocarSpeed.class);
            result = requestListener.onSpeed(speed);
            break;
          default:
            // No action.
            break;
        }
        break;
      default:
        // No action.
        break;
    }

    if (result == null) {
      result = new RobocarResponse(404, String.format(
          "Unknown %s endpoint: %s", session.getMethod(), session.getUri()));
    }

    return buildResponse(result);
  }

  private Response buildResponse(Object object) {
    return newFixedLengthResponse(Response.Status.OK, APPLICATION_JSON,
        new GsonBuilder().create().toJson(object));
  }

  public static String getIpAddress(Context context) {
    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    return Formatter.formatIpAddress(wm.getConnectionInfo().getIpAddress());
  }

  /**
   * Print out session log information.
   */
  public static void logSession(IHTTPSession session) {
    // E.g.: http://localhost:8080/foo.json?echo=true&foo=bar
    Timber.d("getMethod: %s", session.getMethod()); // GET
    Timber.d("getQueryParameterString: %s", session.getQueryParameterString()); // echo=true&foo=bar
    Timber.d("getUri: %s", session.getUri()); // /foo.json
    Timber.d("getRemoteIpAddress: %s", session.getRemoteIpAddress());
    Timber.d("getRemoteHostName: %s", session.getRemoteHostName());
    Timber.d("Cookies present: %b", session.getCookies() != null);
    Timber.d("Headers present: %b", session.getHeaders() != null);
    Timber.d("InputStream present: %b", session.getInputStream() != null);
    Timber.d("Parameters present: %b", session.getParameters() != null);
  }

  private static Object readPostAsObject(IHTTPSession session, Class clazz) {
    Map<String, String> files = new HashMap<>();

    try {
      session.parseBody(files);
    } catch (IOException | ResponseException e) {
      Timber.e(e, "Failed to parse POST response.");
      return null;
    }

    String postBody = files.get("postData");
    return new GsonBuilder().create().fromJson(postBody, clazz);
  }
} public interface HTTPRequestListener {

  void onRequest(NanoHTTPD.IHTTPSession session);

  RobocarStatus onStatus();

  RobocarResponse onMove(RobocarMove move);

  RobocarResponse onSpeed(RobocarSpeed speed);
} public class RobocarMove {

  @Nes30Manager.ButtonCode
  private int keyCode;

  public RobocarMove() {
  }

  public RobocarMove(@Nes30Manager.ButtonCode int keyCode) {
    this.keyCode = keyCode;
  }

  @Nes30Manager.ButtonCode public int getKeyCode() {
    return keyCode;
  }

  public void setKeyCode(@Nes30Manager.ButtonCode int keyCode) {
    this.keyCode = keyCode;
  }
} public class RobocarResponse {

  private int code;
  private String message;

  public RobocarResponse() {
  }

  public RobocarResponse(int code, String message) {
    this.code = code;
    this.message = message;
  }

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }
} public class RobocarSpeed {

  private Integer left;
  private Integer right;

  public Integer getLeft() {
    return left;
  }

  public void setLeft(Integer speed) {
    left = speed;
  }

  public Integer getRight() {
    return right;
  }

  public void setRight(Integer speed) {
    right = speed;
  }

} public class RobocarStatus extends RobocarResponse {
  public RobocarStatus(int code, String message) {
    super(code, message);
  }
}
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;
import com.zugaldia.robocar.app.autonomous.CameraDriver;
import com.zugaldia.robocar.app.autonomous.TensorFlowTrainer;
import com.zugaldia.robocar.app.manual.RCDriver;
import com.zugaldia.robocar.app.manual.LocalhostDriver;
import com.zugaldia.robocar.hardware.adafruit2348.AdafruitMotorHat;
import com.zugaldia.robocar.software.controller.nes30.Nes30Connection;
import com.zugaldia.robocar.software.controller.nes30.Nes30Listener;
import com.zugaldia.robocar.software.controller.nes30.Nes30Manager;
import com.zugaldia.robocar.software.webserver.LocalWebServer;
import com.zugaldia.robocar.software.webserver.HTTPRequestListener;
import com.zugaldia.robocar.software.webserver.models.RobocarMove;
import com.zugaldia.robocar.software.webserver.models.RobocarResponse;
import com.zugaldia.robocar.software.webserver.models.RobocarSpeed;
import com.zugaldia.robocar.software.webserver.models.RobocarStatus;
import java.io.IOException;
import fi.iki.elonen.NanoHTTPD;
import timber.log.Timber;
    }
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    rcDriver.release();
    motorHat.close();
    nes30Connection.cancelDiscovery();
  }

  /*
   * Handle keyboard (controller) events
   */

  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    return nes30Manager.onKeyDown(keyCode, event);
  }

  @Override
  public boolean onKeyLongPress(int keyCode, KeyEvent event) {
    return nes30Manager.onKeyLongPress(keyCode, event);
  }

  @Override
  public boolean onKeyUp(int keyCode, KeyEvent event) {
    return nes30Manager.onKeyUp(keyCode, event);
  }

  @Override
  public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
    return nes30Manager.onKeyMultiple(keyCode, count, event);
  }

  /*
   * Implements Nes30Listener
   */

  @Override
  public void onKeyPress(@Nes30Manager.ButtonCode int keyCode, boolean isDown) {
    switch (keyCode) {
      case Nes30Manager.BUTTON_UP_CODE:
        rcDriver.moveForward(keyCode, isDown);
        break;
      case Nes30Manager.BUTTON_DOWN_CODE:
        rcDriver.moveBackward(keyCode, isDown);
        break;
      case Nes30Manager.BUTTON_LEFT_CODE:
        rcDriver.turnLeft(keyCode, isDown);
        break;
      case Nes30Manager.BUTTON_RIGHT_CODE:
        rcDriver.turnRight(keyCode, isDown);
        break;
      case Nes30Manager.BUTTON_X_CODE:
        if (isDown) {
          Timber.d("Starting camera session for single pics.");
        }
        break;
      case Nes30Manager.BUTTON_Y_CODE:
        if (isDown) {
          Timber.d("Starting camera session for multiple pics.");
          if (tensorFlowTrainer == null) {
            tensorFlowTrainer = new TensorFlowTrainer(this, motorHat);
          }
          tensorFlowTrainer.startSession();
        }
        break;
      case Nes30Manager.BUTTON_A_CODE:
        if (isDown) {
          Timber.d("Stopping camera session.");
          tensorFlowTrainer.endSession();
        }
        break;
      case Nes30Manager.BUTTON_B_CODE:
        Timber.d("Button B pressed.");
        break;
      case Nes30Manager.BUTTON_L_CODE:
        if (cameraDriver == null) {
          cameraDriver = new CameraDriver(this, motorHat);
        }
        cameraDriver.start();
        break;
      case Nes30Manager.BUTTON_R_CODE:
        if (cameraDriver != null) {
          cameraDriver.stop();
        }
        break;
      case Nes30Manager.BUTTON_SELECT_CODE:
        Timber.d("Select button pressed.");
        break;
      case Nes30Manager.BUTTON_START_CODE:
        Timber.d("Start button pressed.");
        break;
      case Nes30Manager.BUTTON_KONAMI:
        // Do your magic here ;-)
        break;
    }
  }

  /*
   * Implement RequestListener (web server)
   */

  @Override
  public void onRequest(NanoHTTPD.IHTTPSession session) {
    LocalWebServer.logSession(session);
  }

  @Override
  public RobocarStatus onStatus() {
    return new RobocarStatus(200, "OK");
  }

  @Override
  public RobocarResponse onMove(RobocarMove move) {
    return new RobocarResponse(200, "TODO");
  }

  @Override
  public RobocarResponse onSpeed(RobocarSpeed speed) {