package co.flyver.androidrc.server; import android.app.IntentService; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.util.Base64; import android.util.Log; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.lang.reflect.Type; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Timer; import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; import co.flyver.utils.containers.SharedIPCKeys; import co.flyver.utils.JSONUtils; import co.flyver.androidrc.server.interfaces.ServerCallback; import co.flyver.dataloggerlib.LoggerService; import co.flyver.utils.containers.Tuples; import static co.flyver.utils.containers.Tuples.Quadruple; import static co.flyver.utils.containers.Tuples.Triple; /** * Created by Petar Petrov on 10/2/14. */ public class Server extends IntentService { private static final String SERVER = "SERVER"; private static final String EV_DEBUG = "DEBUG"; private static final String EV_RAWDATA = "RAWDATA"; public static Status sCurrentStatus = new Status(); Gson mGson = new Gson(); static BufferedReader mStreamFromClient; static PrintWriter mStreamToClient; Quadruple<String, Float, Float, Float> mJsonCoordinates = new Quadruple<>(); Triple<String, String, Float> mJsonAction = new Triple<>(); Quadruple<String, Float, Float, Float> mJsonPid = new Quadruple<>(); Tuples.Tuple<String, String> mTuple = new Tuples.Tuple<>(); static HashMap<String, ServerCallback> mCallbacks = new HashMap<>(); IBinder mBinder = new LocalBinder(); CameraProvider mCameraProvider; byte[] mPicture; ServerSocket mServerSocket; Socket mConnection; Timer mHeartbeat = new Timer(); LoggerService logger; public Server() { super("Server"); } public void setCameraProvider(CameraProvider cameraProvider) { this.mCameraProvider = cameraProvider; } @Override public IBinder onBind(Intent intent) { return mBinder; } public class LocalBinder extends Binder { public Server getServerInstance() { return Server.this; } } /** * Supply a runnable to be executed when a JSON with the appropriate key is received * Refer to the IPC/IPCKeys class for the valid keys * @param key - String, key associated with the runnable * @param callback - Runnable to be executed */ public static void registerCallback(String key, ServerCallback callback) { if(callback == null) { Log.d(SERVER, "Runnable provided as callback is null"); return; } mCallbacks.put(key, callback); } private void initLogger() { logger = new LoggerService(this.getApplicationContext()); logger.Start(); logger.LogData("EV_DEBUG", "Server", "Server initialized the Logger."); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(SERVER, "OnStart executed"); super.onStartCommand(intent, flags, startId); //initLogger(); return START_NOT_STICKY; } @Override /** * Entry point of the server, starts a socket server, initializes the connection and starts an event loop * Provides a pictureReady callback to the CameraProvider */ protected void onHandleIntent(Intent intent) { try { Log.d(SERVER, "Server Started"); openSockets(); initConnection(mConnection); loop(); } catch (IOException e) { e.printStackTrace(); } } /** * Opens a socket associated with the ServerSocket * @throws IOException */ private void openSockets() throws IOException { if(mServerSocket != null && mServerSocket.isBound()) { mServerSocket.close(); } mServerSocket = new ServerSocket(51342); mServerSocket.setSoTimeout(0); mServerSocket.setReuseAddress(true); mConnection = mServerSocket.accept(); Log.d(SERVER, "Socket opened"); } /** * Opens the input/output streams with a Socket * as a BufferedReader/PrintWriter * @param connection - Socket * @throws IOException */ private void initConnection(Socket connection) throws IOException { Log.d(SERVER, "Streams opened"); //logger.LogData(EV_DEBUG, SERVER, "Streams opened"); mStreamFromClient = new BufferedReader(new InputStreamReader(connection.getInputStream())); mStreamToClient = new PrintWriter(connection.getOutputStream(), true); sCurrentStatus.setEmergency(new Tuples.Tuple<>("emergency", "stop")); //initialize a heartbeat to the client mHeartbeat.scheduleAtFixedRate(new TimerTask() { @Override public void run() { long currentTime = System.currentTimeMillis(); Tuples.Tuple<String, String> tuple = new Tuples.Tuple<>(SharedIPCKeys.HEARTBEAT, String.valueOf(currentTime / 1000)); Type type = new TypeToken<Tuples.Tuple<String, String>>() {}.getType(); String json = mGson.toJson(tuple, type); mStreamToClient.println(json); mStreamToClient.flush(); } }, 1, 1000); onClientConnected(); } /** * Deserializes a JSON string in a JSONCoordinates/JsonAction class, depending on the key of the JSON * Checks if the key has a callback associated with it, and runs it. * @param json - String describing a JSON object */ private String deserialize(String json) { if(json.isEmpty()) { Log.w(SERVER, "JSON is empty"); return null; } String mKey; Log.d(SERVER, json); //returns the next string encountered after "key" in the JSON Pattern mPattern = Pattern.compile("\\{.+(?=key)\\w+\":(\"\\w+\")\\}?.+\\}?$"); Matcher matcher = mPattern.matcher(json); if(JSONUtils.validateJson(json) && matcher.matches()) { mKey = matcher.group(1); } else { Log.e(SERVER, "Invalid JSON!"); mStreamToClient.println("Invalid JSON!"); mStreamToClient.flush(); return null; } mKey = mKey.replace('"', ' ').trim(); switch (mKey) { case SharedIPCKeys.COORDINATES: { //TypeToken must be passed to the fromJson method to avoid type erasure problems Type type = new TypeToken<Quadruple<String, Float, Float, Float>>() {}.getType(); mJsonCoordinates = JSONUtils.deserialize(json, type); sCurrentStatus.setAzimuth(mJsonCoordinates.getValue1()); sCurrentStatus.setPitch(mJsonCoordinates.getValue2()); sCurrentStatus.setRoll(mJsonCoordinates.getValue3()); fireCallbacks(mKey, json); } break; case SharedIPCKeys.YAW: { Type type = new TypeToken<Triple<String, String, Float>>() {}.getType(); mJsonAction = JSONUtils.deserialize(json, type); sCurrentStatus.setYaw(mJsonAction); fireCallbacks(mKey, json); } break; case SharedIPCKeys.THROTTLE: { Type type = new TypeToken<Triple<String, String, Float>>() {}.getType(); mJsonAction = JSONUtils.deserialize(json, type); sCurrentStatus.setThrottle(mJsonAction); fireCallbacks(mKey, json); } break; case SharedIPCKeys.EMERGENCY: { Type type = new TypeToken<Tuples.Tuple<String, String>>() {}.getType(); mTuple = JSONUtils.deserialize(json, type); sCurrentStatus.setEmergency(mTuple); fireCallbacks(mKey, json); mStreamToClient.println("Emergency " + mTuple.value); mStreamToClient.flush(); } break; case SharedIPCKeys.PIDYAW: { Type type = new TypeToken<Quadruple<String, Float, Float, Float>>() {}.getType(); mJsonPid = JSONUtils.deserialize(json, type); sCurrentStatus.mPidYaw.setP(mJsonPid.getValue1()); sCurrentStatus.mPidYaw.setI(mJsonPid.getValue2()); sCurrentStatus.mPidYaw.setD(mJsonPid.getValue3()); fireCallbacks(mKey, json); } break; case SharedIPCKeys.PIDPITCH: { Type type = new TypeToken<Quadruple<String, Float, Float, Float>>() {}.getType(); mJsonPid = JSONUtils.deserialize(json, type); sCurrentStatus.mPidPitch.setP(mJsonPid.getValue1()); sCurrentStatus.mPidPitch.setI(mJsonPid.getValue2()); sCurrentStatus.mPidPitch.setD(mJsonPid.getValue3()); fireCallbacks(mKey, json); } break; case SharedIPCKeys.PIDROLL: { Type type = new TypeToken<Quadruple<String, Float, Float, Float>>() {}.getType(); mJsonPid = JSONUtils.deserialize(json, type); sCurrentStatus.mPidRoll.setP(mJsonPid.getValue1()); sCurrentStatus.mPidRoll.setI(mJsonPid.getValue2()); sCurrentStatus.mPidRoll.setD(mJsonPid.getValue3()); fireCallbacks(mKey, json); } break; case SharedIPCKeys.PICTURE: { mCameraProvider.snapIt(); fireCallbacks(mKey, json); } break; default: { if(mCallbacks.containsKey(mKey)) { fireCallbacks(mKey, json); } else { mStreamToClient.println("Unknown key " + mKey); mStreamToClient.flush(); } } break; } return mKey; } /** * Waits for input on the socket, and returns it as a string * If the string representing the JSON object is null * Resets the socketServer and waits for a new connection * Also resets the heartbeat * @return - String in JSON notation, describing a command * @throws java.io.IOException */ private String readStream() throws IOException { String mJson; if((mJson = mStreamFromClient.readLine()) == null) { mHeartbeat.cancel(); mHeartbeat.purge(); mHeartbeat = new Timer(); sCurrentStatus.setEmergency(new Tuples.Tuple<>("emergency", "start")); mConnection = mServerSocket.accept(); initConnection(mConnection); loop(); } return mJson; } /** * Camera provider callback, called every time a new picture is ready * Sends a JSON with picture metadata and base64 encoded byte array of the picture */ private void newPictureReady() { Log.d(SERVER, "New picture is ready"); mPicture = mCameraProvider.getLastPicture(); String base64Pic = Base64.encodeToString(mPicture, Base64.DEFAULT); Triple<String, String, String> mJsonBitmap = new Triple<>(SharedIPCKeys.PICTURE, SharedIPCKeys.PICREADY, base64Pic); String mJson = JSONUtils.serialize(mJsonBitmap, new TypeToken<Triple<String, String, String>>() {}.getType()); mStreamToClient.println(mJson); Log.d(SERVER, mJson); mStreamToClient.flush(); } private void fireCallbacks(String key, String json) { if(mCallbacks.containsKey(key)) { mCallbacks.get(key).run(json); } else { Log.w(SERVER, "Key: " + key + " has no associated callback"); } } /** * Used to send custom messages to the client app * Intended for usage with custom callbacks. * @param msg - String * @return - true, if successful, false otherwise */ public static boolean sendMsgToClient(String msg) { if(msg != null && !msg.isEmpty() && mStreamToClient != null) { Log.d(SERVER, "Sent to client: " + msg); mStreamToClient.println(msg); mStreamToClient.flush(); return true; } else { return false; } } private void onClientConnected() { String json; Type type = new TypeToken<Quadruple<String, Float, Float, Float>>() {}.getType(); mJsonPid = new Quadruple<>("pidyaw", sCurrentStatus.getPidYaw().getP(), sCurrentStatus.getPidYaw().getI(), sCurrentStatus.getPidYaw().getD()); json = mGson.toJson(mJsonPid, type); mStreamToClient.println(json); mStreamToClient.flush(); mJsonPid = new Quadruple<>("pidpitch", sCurrentStatus.getPidPitch().getP(), sCurrentStatus.getPidPitch().getI(), sCurrentStatus.getPidPitch().getD()); json = mGson.toJson(mJsonPid, type); mStreamToClient.println(json); mStreamToClient.flush(); mJsonPid = new Quadruple<>("pidroll", sCurrentStatus.getPidRoll().getP(), sCurrentStatus.getPidRoll().getI(), sCurrentStatus.getPidRoll().getD()); json = mGson.toJson(mJsonPid, type); mStreamToClient.println(json); mStreamToClient.flush(); } /** * Main worker, deserializes the JSON data and notifies the registered components for changes * @throws java.io.IOException */ private void loop() throws IOException { while(true) { Log.d(SERVER, "Waiting for input"); String mJson = readStream(); String mKey; if(mJson == null) { Log.w(SERVER, "JSON is null"); break; } mKey = deserialize(mJson); // fireCallbacks(mKey, mJson); } } }