diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a7509..7bfef59 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/FtcRobotController/build.gradle b/FtcRobotController/build.gradle index de6d53f..9aff81d 100644 --- a/FtcRobotController/build.gradle +++ b/FtcRobotController/build.gradle @@ -21,6 +21,10 @@ android { } } +dependencies { + implementation project(path: ':TimeCraftersConfigurationTool') +} + repositories { maven { url = "https://dl.bintray.com/first-tech-challenge/ftcsdk/" } diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java index c4dd90c..93c11dc 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java @@ -121,6 +121,7 @@ import org.firstinspires.ftc.robotcore.internal.ui.UILocation; import org.firstinspires.ftc.robotcore.internal.webserver.RobotControllerWebInfo; import org.firstinspires.ftc.robotserver.internal.programmingmode.ProgrammingModeManager; import org.firstinspires.inspection.RcInspectionActivity; +import org.timecrafters.TimeCraftersConfigurationTool.backend.Backend; import java.util.List; import java.util.Queue; @@ -379,6 +380,12 @@ public class FtcRobotControllerActivity extends Activity } FtcAboutActivity.setBuildTimeFromBuildConfig(BuildConfig.BUILD_TIME); + + // TODO: Allow disabling TAC Server when in a competition + if (true) { + new Backend(); + Backend.instance().startServer(); + } } protected UpdateUI createUpdateUI() { @@ -458,6 +465,8 @@ public class FtcRobotControllerActivity extends Activity if (preferencesHelper != null) preferencesHelper.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener); RobotLog.cancelWriteLogcatToDisk(); + + Backend.instance().stopServer(); } protected void bindToService() { diff --git a/TimeCraftersConfigurationTool/.gitignore b/TimeCraftersConfigurationTool/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/TimeCraftersConfigurationTool/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/TimeCraftersConfigurationTool/build.gradle b/TimeCraftersConfigurationTool/build.gradle new file mode 100644 index 0000000..5e397f8 --- /dev/null +++ b/TimeCraftersConfigurationTool/build.gradle @@ -0,0 +1,42 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion "30.0.1" + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + // NOTE: Keep RobotCore version in sync with FtcRobotController modules version + implementation 'org.firstinspires.ftc:RobotCore:6.0.1' + implementation 'androidx.appcompat:appcompat:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} + +repositories { + maven { url = "https://dl.bintray.com/first-tech-challenge/ftcsdk/" } + + flatDir { + dirs '../libs' + } +} \ No newline at end of file diff --git a/TimeCraftersConfigurationTool/consumer-rules.pro b/TimeCraftersConfigurationTool/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/TimeCraftersConfigurationTool/proguard-rules.pro b/TimeCraftersConfigurationTool/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/TimeCraftersConfigurationTool/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/TimeCraftersConfigurationTool/src/androidTest/java/org/timecrafters/TimeCraftersConfigurationTool/ExampleInstrumentedTest.java b/TimeCraftersConfigurationTool/src/androidTest/java/org/timecrafters/TimeCraftersConfigurationTool/ExampleInstrumentedTest.java new file mode 100644 index 0000000..a597539 --- /dev/null +++ b/TimeCraftersConfigurationTool/src/androidTest/java/org/timecrafters/TimeCraftersConfigurationTool/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package org.timecrafters.TimeCraftersConfigurationTool; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("org.timecrafters.TimeCraftersConfigurationTool.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/TimeCraftersConfigurationTool/src/main/AndroidManifest.xml b/TimeCraftersConfigurationTool/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4c4c45f --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/TimeCraftersConfiguration.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/TimeCraftersConfiguration.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/TimeCraftersConfiguration.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/TimeCraftersConfiguration.java diff --git a/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Backend.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Backend.java new file mode 100644 index 0000000..f398ab0 --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Backend.java @@ -0,0 +1,394 @@ +package org.timecrafters.TimeCraftersConfigurationTool.backend; + +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.SoundPool; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +import org.json.JSONException; +import org.timecrafters.TimeCraftersConfigurationTool.R; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Action; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Configuration; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Group; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Presets; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Variable; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.ActionDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.ActionSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.ConfigDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.ConfigSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.ConfigurationDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.ConfigurationSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.GroupDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.GroupSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.PresetsDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.PresetsSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.SettingsDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.SettingsSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.VariableDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.VariableSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.tacnet.PacketHandler; +import org.timecrafters.TimeCraftersConfigurationTool.tacnet.Server; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; + +public class Backend { + private static final String TAG = "Backend"; + static private HashMap storage = new HashMap<>(); + static private Backend instance; + public Context applicationContext; + private TACNET tacnet; + private Server server; + private Exception lastServerError; + private Config config; + private Settings settings; + private boolean configChanged, settingsChanged; + private MediaPlayer mediaPlayer; + + public static HashMap getStorage() { + return storage; + } + + public Backend() { + if (Backend.instance() != null) { + throw(new RuntimeException("Backend instance already exists!")); + } else { + instance = this; + } + + loadSettings(); + if (!settings.config.isEmpty()) { + loadConfig(settings.config); + } + tacnet = new TACNET(); + + configChanged = false; + settingsChanged = false; + } + + static public Backend instance() { + return instance; + } + + public TACNET tacnet() { + return tacnet; + } + + public Server getServer() { + return server; + } + + public void startServer() { + try { + server = new Server(settings.port); + server.start(); + } catch (IOException error) { + lastServerError = error; + } + } + + public void stopServer() { + if (server != null) { + try { + server.stop(); + server = null; + } catch (IOException error) { + lastServerError = error; + } + } + } + + public Exception getLastServerError() { + return lastServerError; + } + + public Config getConfig() { + return config; + } + + public Settings getSettings() { + return settings; + } + + public void configChanged() { + config.getConfiguration().updatedAt = new Date(); + config.getConfiguration().revision += 1; + configChanged = true; + + saveConfig(); + + /* Automatically upload whole config to server + * TODO: Implement a more atomic remote config updating + * */ + if (config != null && tacnet.isConnected()) { + String json = gsonForConfig().toJson(config); + tacnet.puts(PacketHandler.packetUploadConfig(config.getName(), json).toString()); + } + } + + public boolean hasConfigChanged() { return configChanged; } + + public String configPath(String name) { + return TAC.CONFIGS_PATH + File.separator + name + ".json"; + } + + public void loadConfig(String name) { + if (name.equals("")) { + config = null; + return; + } + + String path = configPath(name); + File file = new File(path); + + if (file.exists() && file.isFile()) { + config = gsonForConfig().fromJson(readFromFile(path), Config.class); + config.setName(name); + } + } + + public Config loadConfigWithoutMutatingBackend(String name) { + if (name.equals("")) { + return null; + } + + String path = configPath(name); + File file = new File(path); + + if (file.exists() && file.isFile()) { + Config config = gsonForConfig().fromJson(readFromFile(path), Config.class); + config.setName(name); + + return config; + } + + return null; + } + + public boolean isConfigValid(String json) { + try { + gsonForConfig().fromJson(json, Config.class); + + return true; + } catch (JsonSyntaxException ignored) { + return false; + } + } + + public boolean saveConfig() { + if (config == null) { return false; } + + final String path = configPath(getConfig().getName()); + configChanged = false; + + return writeToFile(path, gsonForConfig().toJson(config)); + } + + public boolean moveConfig(String oldName, String newName) { + final String oldPath = configPath(oldName); + final String newPath = configPath(newName); + + final File oldFile = new File(oldPath); + final File newFile = new File(newPath); + + if (!oldFile.exists() || !oldFile.isFile()) { + Log.e(TAG, "moveConfig: Can not move config file \"" + oldPath + "\" does not exists!"); + return false; + } + + if (newFile.exists() && newFile.isFile()) { + Log.e(TAG, "moveConfig: Config file \"" + newPath + "\" already exists!"); + return false; + } + + return oldFile.renameTo(newFile); + } + + public boolean deleteConfig(String name) { + File file = new File(configPath(name)); + + return file.delete(); + } + + public void writeNewConfig(String name) { + String path = configPath(name); + File file = new File(path); + + Config config = new Config(name); + + Gson gson = new Gson(); + + try { + FileWriter fileWriter = new FileWriter(file); + fileWriter.write(gsonForConfig().toJson(config)); + fileWriter.close(); + } catch (IOException error) { + /* TODO */ + Log.d(TAG, "writeNewConfig: IO Error: " + error.toString()); + } + } + + public ArrayList configsList() { + ArrayList list = new ArrayList<>(); + + File directory = new File(TAC.CONFIGS_PATH); + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".json"); + } + }; + File fileList[] = directory.listFiles(filter); + for (File file : fileList) { + list.add(file.getName().replace(".json", "")); + } + + Collections.sort(list, new Comparator() { + @Override + public int compare(String o1, String o2) { + return o1.toLowerCase().compareTo(o2.toLowerCase()); + } + }); + + return list; + } + + public Gson gsonForConfig() { + return new GsonBuilder() + .registerTypeAdapter(Config.class, new ConfigSerializer()) + .registerTypeAdapter(Config.class, new ConfigDeserializer()) + + .registerTypeAdapter(Configuration.class, new ConfigurationSerializer()) + .registerTypeAdapter(Configuration.class, new ConfigurationDeserializer()) + + .registerTypeAdapter(Group.class, new GroupSerializer()) + .registerTypeAdapter(Group.class, new GroupDeserializer()) + + .registerTypeAdapter(Action.class, new ActionSerializer()) + .registerTypeAdapter(Action.class, new ActionDeserializer()) + + .registerTypeAdapter(Variable.class, new VariableSerializer()) + .registerTypeAdapter(Variable.class, new VariableDeserializer()) + + .registerTypeAdapter(Presets.class, new PresetsSerializer()) + .registerTypeAdapter(Presets.class, new PresetsDeserializer()) + .create(); + } + + public void settingsChanged() { + settingsChanged = true; + } + + public boolean haveSettingsChanged() { + return settingsChanged; + } + + public void loadSettings() { + File settingsFile = new File(TAC.SETTINGS_PATH); + + if (!settingsFile.exists()) { + Log.i(TAG, "Writing default settings.json"); + writeDefaultSettings(); + } + + try { + settings = gsonForSettings().fromJson(new FileReader(settingsFile), Settings.class); + } catch (FileNotFoundException e) { + // TODO + Log.e(TAG, "Unable to load settings.json"); + } + } + + public void saveSettings() { + Log.i(TAG, "Settings: " + gsonForSettings().toJson(settings)); + writeToFile(TAC.SETTINGS_PATH, gsonForSettings().toJson(settings)); + } + + public void writeDefaultSettings() { + settings = new Settings(TACNET.DEFAULT_HOSTNAME, TACNET.DEFAULT_PORT, ""); + saveSettings(); + } + + public Gson gsonForSettings() { + return new GsonBuilder() + .registerTypeAdapter(Settings.class, new SettingsSerializer()) + .registerTypeAdapter(Settings.class, new SettingsDeserializer()) + .create(); + } + + public String readFromFile(String path) { + StringBuilder text = new StringBuilder(); + + try { + BufferedReader br = new BufferedReader( new FileReader(path) ); + String line; + + while((line = br.readLine()) != null) { + text.append(line); + text.append("\n"); + } + + br.close(); + } catch (IOException e) { + // TODO + } + + return text.toString(); + } + + public boolean writeToFile(String filePath, String content) { + try { + if (filePath.startsWith(TAC.ROOT_PATH)) { + createFolders(filePath); + + FileWriter writer = new FileWriter(filePath); + writer.write(content); + writer.close(); + + return true; + } else { + Log.e(TAG, "writeToFile disallowed path: " + filePath); + return false; + } + + } catch (IOException e) { + Log.e(TAG, e.getLocalizedMessage()); + return false; + } + } + + private void createFolders(String filePath) throws IOException { + File rootPath = new File(TAC.ROOT_PATH); + File configsPath = new File(TAC.CONFIGS_PATH); + + if (!rootPath.exists()) { + rootPath.mkdir(); + } + + if (!configsPath.exists()) { + configsPath.mkdir(); + } + } +} diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Config.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Config.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Config.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Config.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Settings.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Settings.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Settings.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Settings.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/TAC.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/TAC.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/TAC.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/TAC.java diff --git a/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/TACNET.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/TACNET.java new file mode 100644 index 0000000..ff7f26e --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/TACNET.java @@ -0,0 +1,106 @@ +package org.timecrafters.TimeCraftersConfigurationTool.backend; + +import android.os.SystemClock; +import android.util.Log; + +import org.timecrafters.TimeCraftersConfigurationTool.tacnet.Client; +import org.timecrafters.TimeCraftersConfigurationTool.tacnet.Connection; + +import java.io.IOException; + +public class TACNET { + private final static String TAG = "TACNET|TACNET"; + public static final String DEFAULT_HOSTNAME = "192.168.49.1"; + public static final int DEFAULT_PORT = 8962; + + public static final int SYNC_INTERVAL = 250; // ms + public static final int HEARTBEAT_INTERVAL = 1_500; // ms + + public enum Status { + CONNECTED, + CONNECTING, + CONNECTION_ERROR, + NOT_CONNECTED, + } + + private Connection connection; + + public void connect(String hostname, int port) { + if (connection != null && connection.isConnected()) { + return; + } + + connection = new Connection(hostname, port); + + connection.connect(new Runnable() { + @Override + public void run() { + Log.d(TAG, "run: " + connection.lastSocketError()); + } + }); + } + + public Status status() { + if (isConnecting()) { + return Status.CONNECTING; + } else if (isConnectionError()) { + return Status.CONNECTION_ERROR; + } else if (isConnected()) { + return Status.CONNECTED; + } else { + return Status.NOT_CONNECTED; + } + } + + public boolean isConnected() { + return connection != null && connection.isConnected(); + } + + public boolean isConnecting() { + return connection != null && !connection.isConnected() && !connection.socketError(); + } + + public boolean isConnectionError() { + return connection != null && connection.socketError(); + } + + public void close() { + if (connection != null) { + try { + connection.close(); + } catch (IOException e) {} + + connection = null; + } + } + + public Client getClient() { + if (isConnected()) { + return connection.getClient(); + } else { + return null; + } + } + + public Connection getConnection() { + return connection; + } + + public void puts(String message) { + if (isConnected()) { + connection.puts(message); + } + } + + public String gets() { + if (isConnected()) { + return connection.gets(); + } else { + return null; + } + } + + public static long milliseconds() { + return SystemClock.elapsedRealtime(); + } +} diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Action.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Action.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Action.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Action.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Configuration.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Configuration.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Configuration.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Configuration.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Group.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Group.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Group.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Group.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Presets.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Presets.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Presets.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Presets.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Variable.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Variable.java similarity index 97% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Variable.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Variable.java index cca427c..2132a55 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Variable.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/config/Variable.java @@ -1,5 +1,9 @@ package org.timecrafters.TimeCraftersConfigurationTool.backend.config; +import android.util.Log; + +import java.util.Arrays; + public class Variable { public String name; private String value; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionDeserializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionDeserializer.java similarity index 94% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionDeserializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionDeserializer.java index d44ef8f..b638505 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionDeserializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionDeserializer.java @@ -7,6 +7,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Action; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Group; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Variable; import java.lang.reflect.Type; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionSerializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionSerializer.java similarity index 93% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionSerializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionSerializer.java index e369e56..5a2c562 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionSerializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ActionSerializer.java @@ -7,6 +7,7 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Action; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Group; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Variable; import java.lang.reflect.Type; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigDeserializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigDeserializer.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigDeserializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigDeserializer.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigSerializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigSerializer.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigSerializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigSerializer.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationDeserializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationDeserializer.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationDeserializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationDeserializer.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationSerializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationSerializer.java similarity index 93% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationSerializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationSerializer.java index 73c1484..6639bc8 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationSerializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/ConfigurationSerializer.java @@ -7,6 +7,7 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Configuration; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Variable; import java.lang.reflect.Type; import java.text.SimpleDateFormat; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupDeserializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupDeserializer.java similarity index 83% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupDeserializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupDeserializer.java index 8398a7c..36b6ced 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupDeserializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupDeserializer.java @@ -6,8 +6,11 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; +import org.timecrafters.TimeCraftersConfigurationTool.backend.Config; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Action; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Configuration; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Group; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Presets; import java.lang.reflect.Type; import java.util.ArrayList; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupSerializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupSerializer.java similarity index 84% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupSerializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupSerializer.java index f555cc0..5f22240 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupSerializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/GroupSerializer.java @@ -6,7 +6,9 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; +import org.timecrafters.TimeCraftersConfigurationTool.backend.Config; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Action; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Configuration; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Group; import java.lang.reflect.Type; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsDeserializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsDeserializer.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsDeserializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsDeserializer.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsSerializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsSerializer.java similarity index 96% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsSerializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsSerializer.java index 21276a2..297440e 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsSerializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/PresetsSerializer.java @@ -2,6 +2,7 @@ package org.timecrafters.TimeCraftersConfigurationTool.serializers; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsDeserializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsDeserializer.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsDeserializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsDeserializer.java diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java similarity index 87% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java index fd101a5..9a10bb8 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java @@ -1,7 +1,10 @@ package org.timecrafters.TimeCraftersConfigurationTool.serializers; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableDeserializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableDeserializer.java similarity index 78% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableDeserializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableDeserializer.java index 698dc5d..8d8d2ad 100644 --- a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableDeserializer.java +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableDeserializer.java @@ -6,9 +6,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Action; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Group; import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Variable; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; public class VariableDeserializer implements JsonDeserializer { @Override diff --git a/TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableSerializer.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableSerializer.java similarity index 100% rename from TeamCode/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableSerializer.java rename to TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/VariableSerializer.java diff --git a/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Client.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Client.java new file mode 100755 index 0000000..7dea793 --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Client.java @@ -0,0 +1,252 @@ +package org.timecrafters.TimeCraftersConfigurationTool.tacnet; + +import android.util.Log; + +import org.timecrafters.TimeCraftersConfigurationTool.backend.Backend; +import org.timecrafters.TimeCraftersConfigurationTool.backend.TACNET; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class Client { + private Socket socket; + private BufferedReader bufferedReader; + private BufferedWriter bufferedWriter; + private String uuid; + + private ArrayList readQueue; + private ArrayList writeQueue; + + final private Object readQueueLock = new Object(); + final private Object writeQueueLock = new Object(); + + private long syncInterval = TACNET.SYNC_INTERVAL; + + private int packetsSent, packetsReceived = 0; + private long dataSent, dataReceived = 0; + + private String TAG = "TACNET|Client"; + + private boolean socketError = false; + private String lastSocketError; + + public Client() { + this.uuid = (UUID.randomUUID()).toString(); + + this.readQueue = new ArrayList<>(); + this.writeQueue = new ArrayList<>(); + } + + public void setSyncInterval(long milliseconds) { + syncInterval = milliseconds; + } + + public void setSocket(Socket socket) throws IOException { + this.socket = socket; + + // This socket is for a "Connection" thus set a connect timeout + if (!this.socket.isBound()) { + this.socket.connect(new InetSocketAddress(Backend.instance().getSettings().hostname, Backend.instance().getSettings().port), 1500); + } + + this.bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + this.bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + + startReader(); + startWriter(); + } + + public ArrayList readQueue() { + return readQueue; + } + + public ArrayList writeQueue() { + return writeQueue; + } + + private void startReader() { + new Thread(new Runnable() { + @Override + public void run() { + while(!socket.isClosed()) { + // READER + try { + String message = read(); + if (!message.equals("")) { + Log.i(TAG, "Read: " + message); + + synchronized (readQueueLock) { + readQueue.add(message); + + packetsReceived++; + dataReceived += message.length(); + } + } + + } catch (IOException e) { + Log.e(TAG, "Read error: " + e.getLocalizedMessage()); + } + + try { + TimeUnit.MILLISECONDS.sleep(syncInterval); + } catch (InterruptedException e) {} + } + } + }).start(); + } + + private void startWriter() { + new Thread(new Runnable() { + @Override + public void run() { + while(!socket.isClosed()) { + // WRITER + String message; + + synchronized (writeQueueLock) { + for (Iterator itr = writeQueue.iterator(); itr.hasNext(); ) { + try { + message = (String) itr.next(); + + write(message); + + packetsSent++; + dataSent += message.length(); + + Log.i(TAG, "Write: " + message); + itr.remove(); + + } catch (IOException e) { + Log.e(TAG, "Write error: " + e.getLocalizedMessage()); + socketError = true; + lastSocketError = e.getLocalizedMessage(); + try { + socket.close(); + } catch (IOException k) { + Log.e(TAG, "Failed to close socket: " + e.getLocalizedMessage()); + } + } + } + } + + try { + TimeUnit.MILLISECONDS.sleep(syncInterval); + } catch (InterruptedException e) {} + } + } + }).start(); + } + + public void sync(Runnable runner) { + runner.run(); + } + + public void handleReadQueue() { + String message = this.gets(); + + while (message != null) { + Log.i(TAG, "Writing to Queue: " + message); + this.puts(message); + + message = this.gets(); + + } + } + + public String uuid() { + return this.uuid; + } + + public boolean isConnected() { + return this.socket != null && !this.socket.isClosed(); + } + + public boolean isBound() { + return this.socket == null || this.socket.isBound(); + } + + public boolean isClosed() { + return this.socket == null || this.socket.isClosed(); + } + + public void write(String message) throws IOException { + bufferedWriter.write(message + "\r\n\n"); + bufferedWriter.flush(); + } + + public String read() throws IOException { + String message = ""; + String readLine; + + while((readLine = bufferedReader.readLine()) != null) { + message+=readLine; + if (readLine.isEmpty()) { break; } + } + + return message; + } + + public void puts(String message) { + synchronized (writeQueueLock) { + writeQueue.add(message); + } + } + + public String gets() { + String message = null; + + synchronized (readQueueLock) { + if (readQueue.size() > 0) { + message = readQueue.get(0); + + readQueue.remove(0); + } + } + + return message; + } + + public String encode(String message) { + return message; + } + + public String decode(String blob) { + return blob; + } + + public int getPacketsSent() { return packetsSent; } + public int getPacketsReceived() { return packetsReceived; } + public long getDataSent() { return dataSent; } + public long getDataReceived() { return dataReceived; } + + public boolean socketError() { + return socketError; + } + public String lastSocketError() { + return lastSocketError; + } + + public void flush() throws IOException { + this.bufferedWriter.flush(); + } + + public void close(String reason) throws IOException { + write(reason); + this.socket.close(); + } + + public void close() throws IOException { + if (this.socket != null) { + this.socket.close(); + } + } +} diff --git a/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Connection.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Connection.java new file mode 100755 index 0000000..11f0742 --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Connection.java @@ -0,0 +1,126 @@ +package org.timecrafters.TimeCraftersConfigurationTool.tacnet; + +import android.util.Log; + +import org.timecrafters.TimeCraftersConfigurationTool.backend.TACNET; + +import java.io.IOException; +import java.net.Socket; + +public class Connection { + private PacketHandler packetHandler; + private Client client; + private String hostname; + private int port; + private String lastSocketError = null; + private boolean socketError = false; + + private long lastSyncTime = 0; + private long syncInterval = TACNET.SYNC_INTERVAL; + + private Runnable connectionHandlingRunner; + private long lastHeartBeatSent = 0; + private long heartBeatInterval = TACNET.HEARTBEAT_INTERVAL; + + private String TAG = "TACNET|Connection"; + + public Connection(String hostname, int port) { + this.hostname = hostname; + this.port = port; + this.packetHandler = new PacketHandler(true); + + this.connectionHandlingRunner = new Runnable() { + @Override + public void run() { + handleConnection(); + } + }; + } + + public void connect(final Runnable errorCallback) { + if (client != null) { + return; + } + + client = new Client(); + + new Thread(new Runnable() { + @Override + public void run() { + try { + client.setSocket(new Socket()); + Log.i(TAG, "Connected to: " + hostname + ":" + port); + + while(client != null && !client.isClosed()) { + if (System.currentTimeMillis() > lastSyncTime + syncInterval) { + lastSyncTime = System.currentTimeMillis(); + + client.sync(connectionHandlingRunner); + } + } + } catch (IOException e) { + socketError = true; + lastSocketError = e.getLocalizedMessage(); + + errorCallback.run(); + + Log.e(TAG, e.toString()); + } + } + }).start(); + } + + private void handleConnection() { + if (client != null && !client.isClosed()) { + String message = client.gets(); + + if (message != null) { + packetHandler.handle(message); + } + + if (System.currentTimeMillis() > lastHeartBeatSent + heartBeatInterval) { + lastHeartBeatSent = System.currentTimeMillis(); + + client.puts(PacketHandler.packetHeartBeat().toString()); + } + + try { + Thread.sleep(syncInterval); + } catch (InterruptedException e) { + // Failed to sleep I suppose. + } + + } else { + client = null; + } + } + + public void puts(String message) { + this.client.puts(message); + } + + public String gets() { + return this.client.gets(); + } + + public Client getClient() { + return client; + } + + public boolean isClosed() { + return this.client == null || this.client.isClosed(); + } + public boolean isConnected() { + return this.client != null && this.client.isConnected(); + } + public boolean socketError() { + return socketError ? socketError : client.socketError(); + } + public String lastSocketError() { + return lastSocketError != null ? lastSocketError : client.lastSocketError(); + } + + public void close() throws IOException { + this.client.close(); + } +} diff --git a/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Packet.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Packet.java new file mode 100755 index 0000000..ef6fe2a --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Packet.java @@ -0,0 +1,152 @@ +package org.timecrafters.TimeCraftersConfigurationTool.tacnet; + +import android.util.Log; + +import java.util.Arrays; + +public class Packet { + final static public String PROTOCOL_VERSION = "1"; + final static public String PROTOCOL_SEPERATOR = "|"; + final static public String PROTOCOL_HEARTBEAT = "heartbeat"; + private static final String TAG = "TACNET|Packet"; + + + // NOTE: PacketType is cast to a char, no more than 255 packet types can exist unless + // header is updated. + public enum PacketType { + HANDSHAKE(0), + HEARTBEAT(1), + ERROR(2), + + DOWNLOAD_CONFIG(10), + UPLOAD_CONFIG(11), + LIST_CONFIGS(12), + SELECT_CONFIG(13), + ADD_CONFIG(14), + UPDATE_CONFIG(15), + DELETE_CONFIG(16), + + ADD_GROUP(20), + UPDATE_GROUP(21), + DELETE_GROUP(22), + + ADD_ACTION(30), + UPDATE_ACTION(31), + DELETE_ACTION(32), + + ADD_VARIABLE(40), + UPDATE_VARIABLE(41), + DELETE_VARIABLE(42); + + private int id; + final public int getId() { + return id; + } + + PacketType(int id) { + this.id = id; + } + } + + private String protocolVersion; + private PacketType packetType; + private int contentLength; + private String content; + + String rawMessage; + + Packet(String protocolVersion, PacketType packetType, int contentLength, String content) { + this.protocolVersion = protocolVersion; + this.packetType = packetType; + this.contentLength = contentLength; + this.content = content; + } + + static public Packet fromStream(String message) { + String version; + PacketType type = null; + int length; + String body; + + String[] slice = message.split("\\|", 4); + + if (slice.length < 4) { + Log.i(TAG, "Failed to split packet along first 4 " + PROTOCOL_SEPERATOR + ". Raw return: " + Arrays.toString(slice)); + return null; + } + + if (!slice[0].equals(PROTOCOL_VERSION)) { + Log.i(TAG, "Incompatible protocol version received, expected: " + PROTOCOL_VERSION + " got: " + slice[0]); + return null; + } + + version = slice[0]; +// type = PacketType.values()[Integer.parseInt(slice[1])]; + length = Integer.parseInt(slice[2]); + body = slice[slice.length - 1]; + + int typeId = Integer.parseInt(slice[1]); + for (PacketType packetType : PacketType.values()) { + if (packetType.getId() == typeId) { + type = packetType; + break; + } + } + + if (type == null) { + return null; + } + + return new Packet(version, type, length, body); + } + + static public Packet create(PacketType packetType, String message) { + return new Packet(PROTOCOL_VERSION, packetType, message.length(), message); + } + + public boolean isValid() { + if (rawMessage == null) { + return true; + } + + String[] parts = rawMessage.split(PROTOCOL_SEPERATOR); + + return parts[0].equals(PROTOCOL_VERSION) && + isPacketTypeValid( Integer.parseInt(parts[1])); + } + + public boolean isPacketTypeValid(int rawPacketType) { + return PacketType.values().length >= rawPacketType && PacketType.values()[rawPacketType] != null; + } + + public String encodeHeader() { + String string = ""; + string += PROTOCOL_VERSION; + string += PROTOCOL_SEPERATOR; + string += packetType.getId(); + string += PROTOCOL_SEPERATOR; + string += contentLength; + string += PROTOCOL_SEPERATOR; + return string; + } + + public String toString() { + return ("" + encodeHeader() + content); + } + + public String getProtocolVersion() { + return protocolVersion; + } + + public PacketType getPacketType() { + return packetType; + } + + public int getContentLength() { + return contentLength; + } + + public String getContent() { + return content; + } +} diff --git a/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/PacketHandler.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/PacketHandler.java new file mode 100755 index 0000000..aa24e7f --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/PacketHandler.java @@ -0,0 +1,377 @@ +package org.timecrafters.TimeCraftersConfigurationTool.tacnet; + +import android.util.Log; + +import org.timecrafters.TimeCraftersConfigurationTool.backend.Backend; +import org.timecrafters.TimeCraftersConfigurationTool.backend.Config; +import org.timecrafters.TimeCraftersConfigurationTool.backend.TAC; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Action; +import org.timecrafters.TimeCraftersConfigurationTool.backend.config.Group; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; + +public class PacketHandler { + private static final String TAG = "TACNET|PacketHandler"; + private boolean hostIsAConnection = false; + + public PacketHandler(boolean isHostAConnection) { + this.hostIsAConnection = isHostAConnection; + } + + public void handle(String message) { + Packet packet = Packet.fromStream(message); + + if (packet != null && packet.isValid()) { + Log.i(TAG, "Received packet of type: " + packet.getPacketType()); + handOff(packet); + } else { + if (packet == null) { + Log.i(TAG, "Rejected raw packet: " + message); + } else { + Log.i(TAG, "Rejected packet: " + packet.toString()); + } + } + } + + public void handOff(Packet packet) { + switch(packet.getPacketType()) { + case HANDSHAKE: { + handleHandShake(packet); + return; + } + + case HEARTBEAT: { + handleHeartBeat(packet); + return; + } + + case ERROR: { + handleError(packet); + return; + } + + case DOWNLOAD_CONFIG: { + handleDownloadConfig(packet); + return; + } + + case UPLOAD_CONFIG: { + handleUploadConfig(packet); + return; + } + + case LIST_CONFIGS: { + handleListConfigs(packet); + return; + } + + case ADD_CONFIG: { + handleAddConfig(packet); + return; + } + + case UPDATE_CONFIG: { + handleUpdateConfig(packet); + return; + } + + case DELETE_CONFIG: { + handleDeleteConfig(packet); + return; + } + +// case CHANGE_ACTION: { +// handleChangeAction(packet); +// return; +// } + + default: { + } + } + } + + // NO-OP + private void handleHandShake(Packet packet) {} + // NO-OP + private void handleHeartBeat(Packet packet) {} + // NO-OP + private void handleError(Packet packet) {} + + private void handleUploadConfig(Packet packet) { + String[] split = packet.getContent().split("\\" + Packet.PROTOCOL_SEPERATOR, 2); + final String configName = split[0]; + final String json = split[1]; + + if (configName.length() == 0 && !Backend.instance().isConfigValid(json)) { + return; + } + + final String path = TAC.CONFIGS_PATH + File.separator + configName + ".json"; + + Backend.instance().writeToFile(path, json); + + if (Backend.instance().getConfig().getName().equals(configName)) { + Backend.instance().loadConfig(configName); + } + } + + private void handleDownloadConfig(Packet packet) { + final String configName = packet.getContent(); + + Log.i(TAG, "Got request for config: " + packet.getContent()); + Packet pkt; + if (Backend.instance().configsList().contains(configName)) { + final String path = TAC.CONFIGS_PATH + File.separator + configName + ".json"; + + String content = Backend.instance().readFromFile(path); + pkt = packetUploadConfig(configName, content); + } else { // Error + pkt = packetError("Remote config not found", "The requested config " + configName + " does not exist over here."); + } + + if (hostIsAConnection) { + Backend.instance().tacnet().puts(pkt.toString()); + } else { + Backend.instance().getServer().getActiveClient().puts(pkt.toString()); + } + } + + // TODO: reply with config_name,456|other_config,10 (config name,revision) + private void handleListConfigs(Packet packet) { + if (hostIsAConnection) { + final String[] remoteConfigs = packet.getContent().split("\\" + Packet.PROTOCOL_SEPERATOR); + ArrayList diff = Backend.instance().configsList(); + + for (String part : remoteConfigs) { + final String[] configInfo = part.split(",", 2); + final String name = configInfo[0]; + final int revision = Integer.parseInt(configInfo[1]); + + diff.remove(name); + + File file = new File(Backend.instance().configPath(name)); + + if (file.exists()) { + final Config config = Backend.instance().loadConfigWithoutMutatingBackend(name); + + if (config.getConfiguration().revision < revision) { + Log.i(TAG, "handleListConfigs: requesting config: " + name + " since local " + config.getName() + " is @ " + config.getConfiguration().revision); + Backend.instance().tacnet().puts(PacketHandler.packetDownloadConfig(name).toString()); + } else if (config.getConfiguration().revision > revision) { + Log.i(TAG, "handleListConfigs: sending config: " + name + " since local " + config.getName() + " is @ " + config.getConfiguration().revision); + Backend.instance().tacnet().puts(PacketHandler.packetUploadConfig(name, Backend.instance().gsonForConfig().toJson(config)).toString()); + } + } else { + Log.i(TAG, "handleListConfigs: requesting config: " + name + " since there is no local file with that name"); + + Backend.instance().tacnet().puts( PacketHandler.packetDownloadConfig(name).toString() ); + } + } + + for (String name : diff) { + final Config config = Backend.instance().loadConfigWithoutMutatingBackend(name); + + Backend.instance().tacnet().puts(PacketHandler.packetUploadConfig(name, Backend.instance().gsonForConfig().toJson(config)).toString()); + } + + } else { + Backend.instance().getServer().getActiveClient().puts(PacketHandler.packetListConfigs().toString()); + } + } + + private void handleSelectConfig(Packet packet) { + final String configName = packet.getContent(); + + Backend.instance().getSettings().config = configName; + Backend.instance().saveSettings(); + Backend.instance().loadConfig(configName); + } + + private void handleAddConfig(Packet packet) { + final String configName = packet.getContent(); + + if (Backend.instance().configsList().contains(configName)) { + if (!hostIsAConnection) { + Backend.instance().getServer().getActiveClient().puts( + packetError("Config already exists!", "A config with the name " + + configName + " already exists here.").toString() + ); + } + } else { + Backend.instance().writeNewConfig(configName); + } + } + + private void handleUpdateConfig(Packet packet) { + final String[] split = packet.getContent().split("\\" + Packet.PROTOCOL_SEPERATOR, 2); + final String oldConfigName = split[0]; + final String newConfigName = split[1]; + + if (Backend.instance().configsList().contains(newConfigName)) { + if (!hostIsAConnection) { + Backend.instance().getServer().getActiveClient().puts( + packetError("Config already exists!", "A config with the name " + + newConfigName + " already exists here.").toString() + ); + } + } else { + Backend.instance().moveConfig(oldConfigName, newConfigName); + } + } + + private void handleDeleteConfig(Packet packet) { + final String configName = packet.getContent(); + + Backend.instance().deleteConfig(configName); + } + + private void handleAddGroup(Packet packet) { + final String[] split = packet.getContent().split("\\" + Packet.PROTOCOL_SEPERATOR, 2); + final String configName = packet.getContent(); + final String groupName = packet.getContent(); + + if (Backend.instance().getConfig().getName().equals(configName)) { + if (Group.nameIsUnique(Backend.instance().getConfig().getGroups(), groupName)) { + Group group = new Group(groupName, new ArrayList()); + Backend.instance().getConfig().getGroups().add(group); + } else { + Backend.instance().getServer().getActiveClient().puts( + packetError("Group name collision", "A group with the name " + groupName + " already exists").toString() + ); + } + } else { + Backend.instance().getServer().getActiveClient().puts( + packetError("Active config mismatch", "Active config is not " + configName).toString() + ); + } + } + + private void handleUpdateGroup(Packet packet) {} + + private void handleDeleteGroup(Packet packet) {} + + private void handleAddAction(Packet packet) {} + + private void handleChangeAction(Packet packet) { + // TODO: Handle renaming action and updating comment. + } + + private void handleDeleteAction(Packet packet) {} + + private void handleAddVariable(Packet packet) {} + + private void handleUpdateVariable(Packet packet) {} + + private void handleChangeVariable(Packet packet) {} + + private void handleDeleteVariable(Packet packet) {} + + /************************************** + PACKET HELPER FUNCTIONS + **************************************/ + + static public Packet packetHandShake(String clientUUID) { + return Packet.create(Packet.PacketType.HANDSHAKE, clientUUID); + } + + static public Packet packetHeartBeat() { + return Packet.create(Packet.PacketType.HEARTBEAT, Packet.PROTOCOL_HEARTBEAT); + } + + static private Packet packetError(String errorTitle, String errorMessage) { + return Packet.create(Packet.PacketType.ERROR, errorTitle + Packet.PROTOCOL_SEPERATOR + errorMessage); + } + + static public Packet packetUploadConfig(String configName, String json) { + return Packet.create(Packet.PacketType.UPLOAD_CONFIG, configName + Packet.PROTOCOL_SEPERATOR + json); + } + + static public Packet packetDownloadConfig(String configName) { + return Packet.create(Packet.PacketType.DOWNLOAD_CONFIG, configName); + } + + static public Packet packetListConfigs() { + String configsList = ""; + final ArrayList configs = Backend.instance().configsList(); + + int i = 0; + for (final String configName : configs) { + final String path = Backend.instance().configPath(configName); + Config config = Backend.instance().gsonForConfig().fromJson(Backend.instance().readFromFile(path), Config.class); + + configsList += configName + "," + config.getConfiguration().revision; + + if (i != configs.size() - 1) { + configsList += Packet.PROTOCOL_SEPERATOR; + } + + i++; + } + + return Packet.create(Packet.PacketType.LIST_CONFIGS, configsList); + } + + static public Packet packetSelectConfig(String configName) { + return Packet.create(Packet.PacketType.SELECT_CONFIG, configName); + } + + static public Packet packetAddConfig(String configName) { + return Packet.create(Packet.PacketType.ADD_CONFIG, configName); + } + + static public Packet packetUpdateConfig(String oldConfigName, String newConfigName) { + return Packet.create(Packet.PacketType.UPDATE_CONFIG, oldConfigName + Packet.PROTOCOL_SEPERATOR + newConfigName); + } + + static public Packet packetDeleteConfig(String configName) { + return Packet.create(Packet.PacketType.DELETE_CONFIG, configName); + } + + static public Packet packetAddGroup(String configName, String groupName) { + return Packet.create(Packet.PacketType.ADD_GROUP, configName + Packet.PROTOCOL_SEPERATOR + groupName); + } + + static public Packet packetUpdateGroup(String configName, String oldGroupName, String newGroupName) { + return Packet.create(Packet.PacketType.UPDATE_GROUP, configName + Packet.PROTOCOL_SEPERATOR + + oldGroupName + Packet.PROTOCOL_SEPERATOR + newGroupName); + } + + static public Packet packetDeleteGroup(String configName, String groupName) { + return Packet.create(Packet.PacketType.DELETE_GROUP, configName + Packet.PROTOCOL_SEPERATOR + groupName); + } + + // TODO + static public Packet packetAddAction(String configName, String groupName, String actionName) { + return Packet.create(Packet.PacketType.ADD_ACTION, configName + Packet.PROTOCOL_SEPERATOR + groupName); + } + + // TODO + static public Packet packetUpdateAction(String configName, String oldGroupName, String newGroupName) { + return Packet.create(Packet.PacketType.UPDATE_ACTION, configName + Packet.PROTOCOL_SEPERATOR + + oldGroupName + Packet.PROTOCOL_SEPERATOR + newGroupName); + } + + // TODO + static public Packet packetDeleteAction(String configName, String groupName) { + return Packet.create(Packet.PacketType.DELETE_ACTION, configName + Packet.PROTOCOL_SEPERATOR + groupName); + } + + // TODO + static public Packet packetAddVariable(String configName, String groupName) { + return Packet.create(Packet.PacketType.ADD_VARIABLE, configName + Packet.PROTOCOL_SEPERATOR + groupName); + } + + // TODO + static public Packet packetUpdateVariable(String configName, String oldGroupName, String newGroupName) { + return Packet.create(Packet.PacketType.UPDATE_VARIABLE, configName + Packet.PROTOCOL_SEPERATOR + + oldGroupName + Packet.PROTOCOL_SEPERATOR + newGroupName); + } + + static public Packet packetDeleteVariable(String configName, String groupName, String actionName, String variableName) { + return Packet.create(Packet.PacketType.DELETE_VARIABLE, configName + Packet.PROTOCOL_SEPERATOR + groupName + + Packet.PROTOCOL_SEPERATOR + actionName + + Packet.PROTOCOL_SEPERATOR + variableName); + } +} diff --git a/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Server.java b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Server.java new file mode 100755 index 0000000..0ea1590 --- /dev/null +++ b/TimeCraftersConfigurationTool/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/tacnet/Server.java @@ -0,0 +1,195 @@ +package org.timecrafters.TimeCraftersConfigurationTool.tacnet; + +import android.util.Log; + +import org.timecrafters.TimeCraftersConfigurationTool.backend.TACNET; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; + +public class Server { + private ServerSocket server; + private int port; + private Client activeClient; + private long lastSyncTime = 0; + private long syncInterval = TACNET.SYNC_INTERVAL; + + private String TAG = "TACNET|Server"; + + private int packetsSent, packetsReceived, clientLastPacketsSent, clientLastPacketsReceived = 0; + private long dataSent, dataReceived, clientLastDataSent, clientLastDataReceived = 0; + + private Runnable handleClientRunner; + private PacketHandler packetHandler; + + private long lastHeartBeatSent = 0; + private long heartBeatInterval = TACNET.HEARTBEAT_INTERVAL; + + public Server(int port) throws IOException { + this.server = new ServerSocket(); + this.port = port; + this.packetHandler = new PacketHandler(false); + this.handleClientRunner = new Runnable() { + @Override + public void run() { + handleClient(); + } + }; + } + + public void start() throws IOException { + new Thread(new Runnable() { + @Override + public void run() { + int connectionAttempts = 0; + + while(!server.isBound() && connectionAttempts < 10) { + try { + server.bind(new InetSocketAddress(port)); + Log.i(TAG, "Server bound and ready!"); + } catch (IOException e) { + connectionAttempts++; + Log.e(TAG, "Server failed to bind: " + e.getMessage()); + } + } + + while (!server.isClosed()) { + try { + runServer(); + } catch (IOException e) { + Log.e(TAG, "Error running server: " + e.getMessage()); + } + + } + } + }).start(); + } + + private void runServer() throws IOException { + while (!isClosed()) { + + final Client client = new Client(); + client.setSyncInterval(syncInterval); + client.setSocket(this.server.accept()); + + if (activeClient != null && !activeClient.isClosed()) { + Log.i(TAG, "Too many clients, already have one connected!"); + client.close("Too many clients!"); + + } else { + this.activeClient = client; + + activeClient.puts(PacketHandler.packetHandShake( activeClient.uuid() ).toString()); + activeClient.puts(PacketHandler.packetListConfigs().toString()); + + Log.i(TAG, "Client connected!"); + + new Thread(new Runnable() { + @Override + public void run() { + while(activeClient != null && !activeClient.isClosed()) { + if (System.currentTimeMillis() > lastSyncTime + syncInterval) { + lastSyncTime = System.currentTimeMillis(); + + activeClient.sync(handleClientRunner); + updateNetStats(); + } + + try { + Thread.sleep(syncInterval); + } catch (InterruptedException e) { + // Failed to sleep, i guess. + } + } + + updateNetStats(); + activeClient = null; + + clientLastPacketsSent = 0; + clientLastPacketsReceived = 0; + clientLastDataSent = 0; + clientLastDataReceived = 0; + +// AppSync.getMainActivity().clientDisconnected(); + } + }).start(); + + } + } + } + + private void handleClient() { + if (activeClient != null && !activeClient.isClosed()) { + String message = activeClient.gets(); + + if (message != null) { + packetHandler.handle(message); + } + + if (System.currentTimeMillis() > lastHeartBeatSent + heartBeatInterval) { + lastHeartBeatSent = System.currentTimeMillis(); + + activeClient.puts(PacketHandler.packetHeartBeat().toString()); + } + } + } + + public void stop() throws IOException { + if (this.activeClient != null) { + this.activeClient.close(); + this.activeClient = null; + } + + this.server.close(); + } + + public boolean hasActiveClient() { + return activeClient != null; + } + + public Client getActiveClient() { + return activeClient; + } + + public int getPacketsSent() { + return packetsSent; + } + + public int getPacketsReceived() { + return packetsReceived; + } + + public long getDataSent() { + return dataSent; + } + + public long getDataReceived() { + return dataReceived; + } + + private void updateNetStats() { + if (activeClient != null) { + // NOTE: In and Out are reversed for Server stats + + packetsSent += activeClient.getPacketsReceived() - clientLastPacketsReceived; + packetsReceived += activeClient.getPacketsSent() - clientLastPacketsSent; + + dataSent += activeClient.getDataReceived() - clientLastDataReceived; + dataReceived += activeClient.getDataSent() - clientLastDataSent; + + clientLastPacketsSent = activeClient.getPacketsSent(); + clientLastPacketsReceived = activeClient.getPacketsReceived(); + clientLastDataSent = activeClient.getDataSent(); + clientLastDataReceived = activeClient.getDataReceived(); + } + } + + public boolean isBound() { + return this.server.isBound(); + } + + public boolean isClosed() { + return this.server.isClosed(); + } +} diff --git a/TimeCraftersConfigurationTool/src/test/java/org/timecrafters/TimeCraftersConfigurationTool/ExampleUnitTest.java b/TimeCraftersConfigurationTool/src/test/java/org/timecrafters/TimeCraftersConfigurationTool/ExampleUnitTest.java new file mode 100644 index 0000000..0b03566 --- /dev/null +++ b/TimeCraftersConfigurationTool/src/test/java/org/timecrafters/TimeCraftersConfigurationTool/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package org.timecrafters.TimeCraftersConfigurationTool; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9e2cfb3..16e75b4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ +include ':TimeCraftersConfigurationTool' include ':FtcRobotController' include ':TeamCode'