diff --git a/settings.gradle b/settings.gradle
index f6ec52ab..6ca3c545 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -15,6 +15,7 @@ if(build_simulator_java)
include 'snobot_sim_utilities'
include 'snobot_sim_gui'
+include 'snobot_sim_gui_javafx'
include 'snobot_sim_joysticks'
include 'snobot_sim_example_robot'
diff --git a/snobot_sim_gui/build.gradle b/snobot_sim_gui/build.gradle
index 37b6d2b6..0b515f43 100644
--- a/snobot_sim_gui/build.gradle
+++ b/snobot_sim_gui/build.gradle
@@ -29,7 +29,8 @@ dependencies {
// 3rd Party
- native3rdPartyDeps 'net.java.jinput:jinput:2.0.9'
+ compile 'net.java.jinput:jinput:2.0.9'
+ wpilibNativeDeps 'net.java.jinput:jinput:2.0.9:natives-all'
compile 'jfree:jcommon:1.0.16'
compile 'jfree:jfreechart:1.0.13'
compile 'org.apache.logging.log4j:log4j-api:2.12.0'
diff --git a/snobot_sim_gui_javafx/build.gradle b/snobot_sim_gui_javafx/build.gradle
new file mode 100644
index 00000000..ed0f4249
--- /dev/null
+++ b/snobot_sim_gui_javafx/build.gradle
@@ -0,0 +1,128 @@
+
+evaluationDependsOn(':snobot_sim_utilities')
+evaluationDependsOn(':sim_adx_family')
+evaluationDependsOn(':sim_extension_navx')
+
+ext
+{
+ baseId = "snobot_sim_gui_javafx"
+}
+
+apply from: "${rootDir}/common/base_java_script.gradle"
+
+configurations {
+ native3rdPartyDeps
+ compile.extendsFrom(native3rdPartyDeps)
+}
+
+configurations.maybeCreate("wpilibNativeDeps")
+dependencies {
+
+ compile "org.openjfx:javafx-base:11:win"
+ compile "org.openjfx:javafx-graphics:11:win"
+ compile "org.openjfx:javafx-controls:11:win"
+ compile "org.openjfx:javafx-fxml:11:win"
+
+ // WPILib
+ compile 'edu.wpi.first.wpilibj:wpilibj-java:' + allwpilibVersion()
+ compile 'edu.wpi.first.wpiutil:wpiutil-java:' + getWpiUtilVersion()
+ compile 'edu.wpi.first.cscore:cscore-java:' + getCsCoreVersion()
+ runtime 'edu.wpi.first.cscore:cscore-jni:' + getCsCoreVersion() + ':all'
+ compile 'edu.wpi.first.ntcore:ntcore-java:' + getNtCoreVersion()
+ runtime 'edu.wpi.first.ntcore:ntcore-jni:' + getNtCoreVersion() + ':all'
+ runtime 'edu.wpi.first.hal:hal-jni:' + allwpilibVersion() + ':all'
+ compile 'org.opencv:opencv-java:' + getWpilibOpencvVersion()
+ runtime 'org.opencv:opencv-jni:' + getWpilibOpencvVersion() + ':all'
+
+ // 3rd Party
+ compile 'net.java.jinput:jinput:2.0.9'
+ wpilibNativeDeps 'net.java.jinput:jinput:2.0.9:natives-all'
+ compile 'jfree:jcommon:1.0.16'
+ compile 'jfree:jfreechart:1.0.13'
+ compile 'org.apache.logging.log4j:log4j-api:2.11.0'
+ compile 'org.apache.logging.log4j:log4j-core:2.11.0'
+ compile 'org.yaml:snakeyaml:1.18'
+ compile 'com.miglayout:miglayout-swing:4.2'
+ //compile 'org.python:jython:2.7.1b3'
+
+ // Internal
+ compile project(":snobot_sim_utilities")
+ compile project(":snobot_sim_joysticks")
+
+ if(build_simulator_cpp)
+ {
+ compile project(":snobot_sim_jni")
+ wpilibNativeDeps project(':snobot_sim_jni').packageNativeFiles.outputs.files
+ }
+
+ if(build_simulator_java)
+ {
+ compile project(":snobot_sim_java")
+ wpilibNativeDeps project(':sim_extension_navx').packageNativeFiles.outputs.files
+ wpilibNativeDeps project(':sim_adx_family').packageNativeFiles.outputs.files
+ }
+
+ // Test
+ testCompile 'org.junit.jupiter:junit-jupiter-api:5.2.0'
+ testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
+ testRuntime 'org.junit.platform:junit-platform-launcher:1.2.0'
+ runtime 'com.snobot.simulator:ctre_sim_override:' + getCtreSimVersion() + ':all'
+ runtime 'com.snobot.simulator:rev_simulator:' + getRevRoboticsSimVersion() + ':all'
+}
+
+apply from: "${rootDir}/common/extract_native_libraries.gradle"
+test.dependsOn extract_wpilib
+
+
+task unzipNativeLibraries(type: Copy) {
+
+ configurations.native3rdPartyDeps.each {
+ from zipTree(it)
+ into "build/native_libs"
+ include "**/*.dll"
+ include "**/*.lib"
+ include "**/*.pdb"
+ include "**/*.so*"
+ include "**/*.a"
+ include "**/*.dylib*"
+ }
+
+ includeEmptyDirs = false
+
+}
+
+eclipse.classpath.file {
+ whenMerged { classpath ->
+ classpath.entries.each {
+ if(it.path.contains("jinput") && !it.path.contains("natives")) {
+ it.setNativeLibraryLocation("snobot_sim_gui_javafx/build/native_libs")
+ }
+ }
+ }
+}
+
+build.dependsOn unzipNativeLibraries
+
+if(build_simulator_cpp)
+{
+ compileJava.dependsOn(":snobot_sim_jni:build")
+}
+if(build_simulator_java)
+{
+ compileJava.dependsOn(":snobot_sim_java:build")
+}
+
+sourceSets.main.java.srcDir "${buildDir}/generated/java/"
+compileJava {
+ apply from: "${rootDir}/common/create_version_file.gradle"
+ createJavaVersion("com/snobot/simulator", "SnobotSimGuiVersion", "com.snobot.simulator", getVersionName())
+}
+
+clean {
+ delete "src/main/java/com/snobot/simulator/SnobotSimGuiVersion.java"
+}
+
+
+spotbugs {
+ ignoreFailures = true
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/BaseSimulator.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/BaseSimulator.java
new file mode 100644
index 00000000..c06f8ba6
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/BaseSimulator.java
@@ -0,0 +1,45 @@
+package com.snobot.simulator;
+
+import com.snobot.simulator.config.v1.SimulatorConfigReaderV1;
+import com.snobot.simulator.robot_container.IRobotClassContainer;
+
+/**
+ * Base class for a custom simulator.
+ *
+ * @author PJ
+ *
+ */
+public class BaseSimulator implements ISimulatorUpdater
+{
+ private final SimulatorConfigReaderV1 mConfigReader;
+ private String mConfigFile;
+
+ protected BaseSimulator()
+ {
+ mConfigReader = new SimulatorConfigReaderV1();
+ }
+
+ public boolean loadConfig(String aConfigFile)
+ {
+ mConfigFile = aConfigFile;
+ return mConfigReader.loadConfig(mConfigFile);
+ }
+
+
+ @Override
+ public void update()
+ {
+ // Nothing to do
+ }
+
+ @Override
+ public void setRobot(IRobotClassContainer aRobot)
+ {
+ // Nothing to do
+ }
+
+ public String getConfigFile()
+ {
+ return mConfigFile;
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/DefaultDataAccessorFactory.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/DefaultDataAccessorFactory.java
new file mode 100644
index 00000000..1c1923e6
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/DefaultDataAccessorFactory.java
@@ -0,0 +1,28 @@
+package com.snobot.simulator;
+
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+import com.snobot.simulator.wrapper_accessors.java.JavaDataAccessor;
+
+/**
+ * Helper class that sets up the data accessor abstraction layer
+ *
+ * @author PJ
+ *
+ */
+public final class DefaultDataAccessorFactory
+{
+ private static final boolean sINITIALIZED = false;
+
+ private DefaultDataAccessorFactory()
+ {
+
+ }
+
+ public static void initalize()
+ {
+ if (!sINITIALIZED)
+ {
+ DataAccessorFactory.setAccessor(new JavaDataAccessor());
+ }
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/ISimulatorUpdater.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/ISimulatorUpdater.java
new file mode 100644
index 00000000..b06ce30a
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/ISimulatorUpdater.java
@@ -0,0 +1,11 @@
+package com.snobot.simulator;
+
+import com.snobot.simulator.robot_container.IRobotClassContainer;
+
+public interface ISimulatorUpdater
+{
+
+ public abstract void update();
+
+ public abstract void setRobot(IRobotClassContainer aRobot);
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Main.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Main.java
new file mode 100644
index 00000000..749c5101
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Main.java
@@ -0,0 +1,22 @@
+package com.snobot.simulator;
+
+import com.sun.javafx.application.LauncherImpl;
+
+public final class Main
+{
+ private Main()
+ {
+
+ }
+
+ public static void main(String[] aArgs)
+ {
+ DefaultDataAccessorFactory.initalize();
+
+ // JavaFX 11+ uses GTK3 by default, and has problems on some display
+ // servers
+ // This flag forces JavaFX to use GTK2
+ // System.setProperty("jdk.gtk.version", "2");
+ LauncherImpl.launchApplication(SimulatorApplication.class, SimulatorPreloader.class, aArgs);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/RobotContainerFactory.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/RobotContainerFactory.java
new file mode 100644
index 00000000..95b7170d
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/RobotContainerFactory.java
@@ -0,0 +1,51 @@
+package com.snobot.simulator;
+
+import java.io.File;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+
+import com.snobot.simulator.robot_container.CppRobotContainer;
+import com.snobot.simulator.robot_container.IRobotClassContainer;
+import com.snobot.simulator.robot_container.JavaRobotContainer;
+
+import edu.wpi.first.networktables.NetworkTableInstance;
+
+public final class RobotContainerFactory
+{
+ private RobotContainerFactory()
+ {
+
+ }
+
+ public static IRobotClassContainer createRobotContainer(File aConfigDirectory, String aRobotType, String aRobotClassName)
+ throws ReflectiveOperationException
+ {
+ LogManager.getLogger(RobotContainerFactory.class).log(Level.INFO, "Starting Robot Code");
+
+ IRobotClassContainer mRobot;
+
+ if (aRobotType == null || "java".equals(aRobotType))
+ {
+ mRobot = new JavaRobotContainer(aRobotClassName);
+ }
+ else if ("cpp".equals(aRobotType))
+ {
+ mRobot = new CppRobotContainer(aRobotClassName);
+ }
+ else
+ {
+ throw new RuntimeException("Unsupported robot type " + aRobotType);
+ }
+
+ mRobot.constructRobot();
+
+ // Change the network table preferences path. Need to start
+ // the robot, stop the server and restart it
+ NetworkTableInstance inst = NetworkTableInstance.getDefault();
+ inst.stopServer();
+ inst.startServer(aConfigDirectory.toString() + "/networktables.ini");
+
+ return mRobot;
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Simulator.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Simulator.java
new file mode 100644
index 00000000..fb18850e
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Simulator.java
@@ -0,0 +1,197 @@
+package com.snobot.simulator;
+
+import java.io.File;
+import java.util.function.Consumer;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.snobot.simulator.SimulatorPreloader.StateNotification.StateNotificationType;
+import com.snobot.simulator.gui.SimulatorFrameController;
+import com.snobot.simulator.joysticks.IJoystickInterface;
+import com.snobot.simulator.joysticks.NullJoystickInterface;
+import com.snobot.simulator.joysticks.SnobotSimJoystickInterface;
+import com.snobot.simulator.robot_container.IRobotClassContainer;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+import com.snobot.simulator.wrapper_accessors.SimulatorDataAccessor.SnobotLogLevel;
+
+import edu.wpi.first.wpilibj.DriverStation;
+import javafx.application.Platform;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+
+public class Simulator
+{
+ private static final Logger LOGGER = LogManager.getLogger(Simulator.class);
+
+ private IRobotClassContainer mRobot; // The robot code to run
+ private BaseSimulator mSimulator; // The simulator for the robot
+ private final File mUserConfigDirectory;
+ private final SimulatorFrameController mController;
+ private final IJoystickInterface mJoystickInterface;
+
+ protected Thread mRobotThread;
+ protected Thread mSimulatorThread;
+ protected boolean mRunningSimulator;
+ protected final boolean mUseSnobotSimDriverStation;
+
+ /**
+ * Constructor
+ *
+ * @param aLogLevel
+ * The log level to set up the simulator with
+ * @param aUserConfigDir
+ * The config directory where settings are saved
+ * @throws Exception
+ * Throws an exception if the plugin loading failed
+ */
+ public Simulator(SimulatorFrameController aController, SnobotLogLevel aLogLevel, String aUserConfigDir, boolean aUseSnobotSimDriverstation)
+ throws Exception
+ {
+ mUserConfigDirectory = new File(aUserConfigDir);
+ mController = aController;
+ mUseSnobotSimDriverStation = aUseSnobotSimDriverstation;
+
+ if (mUseSnobotSimDriverStation)
+ {
+ mJoystickInterface = new SnobotSimJoystickInterface();
+ }
+ else
+ {
+ mJoystickInterface = new NullJoystickInterface();
+ }
+ }
+
+ protected void showInitializationMessage()
+ {
+ String message = DataAccessorFactory.getInstance().getInitializationErrors();
+ if (message != null && !message.isEmpty())
+ {
+ System.out.println("Initialization warning");
+// String message = "Some simulator components were specified in the config file, but not in the robot.
"
+// + "They will be removed from the simulator registery, to make it easier to fix your config file.
";
+// JLabel label = new JLabel(message);
+// label.setFont(new Font("serif", Font.PLAIN, 14));
+//
+// JOptionPane.showMessageDialog(null, label, "Config file mismatch", JOptionPane.ERROR_MESSAGE);
+ }
+ }
+
+ public void setupSimulator(Consumer aProgressCallback) throws ReflectiveOperationException
+ {
+ aProgressCallback.accept(new SimulatorPreloader.StateNotification(StateNotificationType.LoadingSimulatorProperties));
+ SimulatorPropertiesLoader propertyLoader = new SimulatorPropertiesLoader();
+ propertyLoader.loadProperties(mUserConfigDirectory);
+
+ // Force the jinput libraries to load
+ mJoystickInterface.sendJoystickUpdate();
+
+ mSimulator = propertyLoader.getSimulator();
+
+ aProgressCallback.accept(new SimulatorPreloader.StateNotification(StateNotificationType.CreatingRobot));
+ mRobot = RobotContainerFactory.createRobotContainer(mUserConfigDirectory, propertyLoader.getRobotType(), propertyLoader.getRobotClassName());
+
+ if (mRobot == null)
+ {
+ throw new IllegalArgumentException("Cannot start, could not create robot class");
+ }
+ else if (mSimulator == null)
+ {
+ throw new IllegalArgumentException("Cannot start, could not create simulator class");
+ }
+ else
+ {
+ mRobotThread = new Thread(createRobotThread(), "RobotThread");
+
+ mRunningSimulator = true;
+ LOGGER.log(Level.INFO, "Starting simulator");
+
+ aProgressCallback.accept(new SimulatorPreloader.StateNotification(StateNotificationType.StartingRobotThread));
+ mRobotThread.start();
+
+ aProgressCallback.accept(new SimulatorPreloader.StateNotification(StateNotificationType.WaitingForProgramToStart));
+ DataAccessorFactory.getInstance().getDriverStationAccessor().waitForProgramToStart();
+
+ showInitializationMessage();
+ mSimulator.setRobot(mRobot);
+
+ mController.initialize(mUserConfigDirectory + "/simulator_config.yml", mUseSnobotSimDriverStation);
+
+ aProgressCallback.accept(new SimulatorPreloader.StateNotification(StateNotificationType.Finished));
+ }
+ }
+
+ public void startSimulation()
+ {
+ createSimulatorBackgroundService().start();
+ }
+
+ private Runnable createRobotThread()
+ {
+ return new Runnable()
+ {
+
+ @Override
+ public void run()
+ {
+
+ try
+ {
+ DriverStation.getInstance();
+ mJoystickInterface.waitForLoop();
+ mRobot.startCompetition();
+ }
+ catch (UnsatisfiedLinkError e)
+ {
+ throw new RuntimeException(
+ "Unsatisfied link error. This likely means that there is a native "
+ + "call in WpiLib or the NetworkTables libraries. Please tell PJ so he can mock it out.\n\nError Message: " + e,
+ e);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("Unexpected exception, shutting down simulator", e);
+ }
+ }
+ };
+ }
+
+ private Service createSimulatorBackgroundService()
+ {
+ return new Service()
+ {
+ @Override
+ protected Task createTask()
+ {
+ return new Task()
+ {
+ @Override
+ protected Void call() throws Exception
+ {
+ DataAccessorFactory.getInstance().getDriverStationAccessor().setDisabled(false);
+
+ while (mRunningSimulator)
+ {
+ mJoystickInterface.waitForLoop();
+ DataAccessorFactory.getInstance().getSimulatorDataAccessor().updateSimulatorComponents();
+ mSimulator.update();
+ mJoystickInterface.sendJoystickUpdate();
+
+ Platform.runLater(() -> mController.updateLoop());
+ }
+
+ return null;
+ }
+ };
+ }
+ };
+ }
+
+ public void stop()
+ {
+ mRunningSimulator = false;
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorApplication.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorApplication.java
new file mode 100644
index 00000000..a92ca553
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorApplication.java
@@ -0,0 +1,63 @@
+package com.snobot.simulator;
+
+import com.snobot.simulator.SimulatorPreloader.StateNotification.StateNotificationType;
+import com.snobot.simulator.gui.SimulatorFrameController;
+import com.snobot.simulator.wrapper_accessors.SimulatorDataAccessor.SnobotLogLevel;
+
+import javafx.application.Application;
+import javafx.application.Preloader.PreloaderNotification;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.Pane;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+
+public class SimulatorApplication extends Application
+{
+ private static final String USER_CONFIG_DIR = "simulator_config/";
+
+ private Simulator mSimulator;
+ private Pane mMainPane;
+
+ @Override
+ public void init() throws Exception
+ {
+ boolean useSnobotSimDriverstation = !getParameters().getRaw().contains("--use_native_ds");
+
+ notifyPreloader(new SimulatorPreloader.StateNotification(StateNotificationType.Starting));
+
+ FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/snobot/simulator/gui/simulator_frame.fxml"));
+ mMainPane = loader.load();
+ SimulatorFrameController controller = loader.getController();
+
+ mSimulator = new Simulator(controller, SnobotLogLevel.DEBUG, USER_CONFIG_DIR, useSnobotSimDriverstation);
+ mSimulator.setupSimulator(this::notifyPreloader2);
+
+ }
+
+ public final void notifyPreloader2(PreloaderNotification aInfo)
+ {
+ notifyPreloader(aInfo);
+ }
+
+ @Override
+ public void start(Stage aPrimaryStage) throws Exception
+ {
+ aPrimaryStage.setScene(new Scene(mMainPane));
+ aPrimaryStage.setTitle("SnobotSim");
+ aPrimaryStage.setMinWidth(300);
+ aPrimaryStage.setMinHeight(480);
+ aPrimaryStage.setWidth(300);
+ aPrimaryStage.setHeight(Screen.getPrimary().getVisualBounds().getHeight());
+
+ aPrimaryStage.show();
+
+ mSimulator.startSimulation();
+
+ aPrimaryStage.setOnCloseRequest(closeEvent ->
+ {
+ // There is no way to stop the robot thread, so kill it with fire
+ System.exit(0);
+ });
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloader.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloader.java
new file mode 100644
index 00000000..bc6da9cb
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloader.java
@@ -0,0 +1,85 @@
+package com.snobot.simulator;
+
+import javafx.application.Preloader;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+
+public class SimulatorPreloader extends Preloader
+{
+ private Stage mPreloaderStage;
+ private SimulatorPreloaderController mController;
+
+ @Override
+ public void start(Stage aStage) throws Exception
+ {
+ mPreloaderStage = aStage;
+
+ FXMLLoader loader = new FXMLLoader(SimulatorPreloader.class.getResource("preloader.fxml"));
+
+ Pane preloaderPane = loader.load();
+ mController = loader.getController();
+
+ Scene scene = new Scene(preloaderPane);
+
+ aStage.setScene(scene);
+ aStage.initStyle(StageStyle.UNDECORATED);
+ aStage.show();
+ }
+
+ @Override
+ public void handleApplicationNotification(PreloaderNotification aInfo)
+ {
+ StateNotification notification = (StateNotification) aInfo;
+ System.out.println("Getting notification...." + aInfo);
+ mController.setStateText(notification.getState());
+ mController.setProgress(notification.getProgress());
+ }
+
+ @Override
+ public void handleStateChangeNotification(StateChangeNotification aInfo)
+ {
+ if (aInfo.getType() == StateChangeNotification.Type.BEFORE_START)
+ {
+ mPreloaderStage.close();
+ }
+ }
+
+ public static final class StateNotification implements PreloaderNotification
+ {
+ public static enum StateNotificationType
+ {
+ Starting,
+ LoadingSimulatorProperties,
+ CreatingRobot,
+ StartingRobotThread,
+ WaitingForProgramToStart,
+ Finished;
+ }
+
+ private final StateNotificationType mState;
+
+ public StateNotification(StateNotificationType aState)
+ {
+ mState = aState;
+ }
+
+ public double getProgress()
+ {
+ return mState.ordinal() / (StateNotificationType.values().length - 1.0);
+ }
+
+ public String getState()
+ {
+ return mState.toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ return mState.toString();
+ }
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloaderController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloaderController.java
new file mode 100644
index 00000000..307b2803
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloaderController.java
@@ -0,0 +1,38 @@
+package com.snobot.simulator;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.layout.Pane;
+
+public class SimulatorPreloaderController
+{
+ @FXML
+ private Pane mRoot;
+ @FXML
+ private Pane mBackgroundContainer;
+ @FXML
+ private Label mVersionLabel;
+ @FXML
+ private Label mStateLabel;
+ @FXML
+ private ProgressBar mProgressBar;
+
+ @FXML
+ private void initialize()
+ {
+ mProgressBar.setProgress(-1);
+ mVersionLabel.setText(SnobotSimGuiVersion.Version);
+ }
+
+ public void setStateText(String aText)
+ {
+ mStateLabel.setText(aText);
+ }
+
+ public void setProgress(double aProgress)
+ {
+ mProgressBar.setProgress(aProgress);
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPropertiesLoader.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPropertiesLoader.java
new file mode 100644
index 00000000..7d4966c6
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPropertiesLoader.java
@@ -0,0 +1,120 @@
+package com.snobot.simulator;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class SimulatorPropertiesLoader
+{
+ private static final Logger LOGGER = LogManager.getLogger(SimulatorPropertiesLoader.class);
+
+ private String mRobotClassName;
+ private String mRobotType;
+ private BaseSimulator mSimulator;
+
+ public void loadProperties(File aConfigDirectory)
+ {
+ File propertiesFile = new File(aConfigDirectory, "simulator_config.properties");
+
+ try
+ {
+ if (!propertiesFile.exists())
+ {
+ createDefaultConfig(aConfigDirectory, propertiesFile);
+ }
+
+ Properties p = new Properties();
+
+ try (FileInputStream fis = new FileInputStream(propertiesFile))
+ {
+ p.load(fis);
+ }
+
+ mRobotClassName = p.getProperty("robot_class");
+ mRobotType = p.getProperty("robot_type");
+
+ String simulatorClassName = p.getProperty("simulator_class");
+
+ createSimulator(simulatorClassName, p.getProperty("simulator_config"));
+ }
+ catch (IOException ex)
+ {
+ LOGGER.log(Level.WARN, "Could not read properties file", ex);
+ }
+ }
+
+ private void createDefaultConfig(File aConfigDirectory, File aPropertiesFile) throws IOException
+ {
+ LOGGER.log(Level.WARN, "Could not read properties file, will use defaults and will overwrite the file if it exists");
+
+ if (!aConfigDirectory.exists() && !aConfigDirectory.mkdirs())
+ {
+ throw new IllegalStateException();
+ }
+
+ String defaultSimConfig = aConfigDirectory + "/simulator_config.yml";
+ Properties defaults = new Properties();
+
+ defaults.putIfAbsent("robot_class", "com.snobot.simulator.example_robot.ExampleRobot");
+ defaults.putIfAbsent("robot_type", "java");
+ defaults.putIfAbsent("simulator_config", defaultSimConfig);
+
+ File defaultConfigFile = new File(defaultSimConfig);
+ if (!defaultConfigFile.exists() && !defaultConfigFile.createNewFile())
+ {
+ LOGGER.log(Level.WARN, "Could not create default config file at " + defaultConfigFile);
+ }
+
+ try (OutputStreamWriter fw = new OutputStreamWriter(new FileOutputStream(aPropertiesFile), StandardCharsets.UTF_8))
+ {
+ defaults.store(fw, "");
+ }
+ }
+
+ private void createSimulator(String aSimulatorClassName, String aSimulatorConfig)
+ {
+ try
+ {
+ if (aSimulatorClassName == null || aSimulatorClassName.isEmpty())
+ {
+ mSimulator = new BaseSimulator();
+ mSimulator.loadConfig(aSimulatorConfig);
+
+ LOGGER.log(Level.DEBUG, "Created default simulator");
+ }
+ else
+ {
+ mSimulator = (BaseSimulator) Class.forName(aSimulatorClassName).getDeclaredConstructor().newInstance();
+ mSimulator.loadConfig(aSimulatorConfig);
+ LOGGER.log(Level.INFO, aSimulatorClassName);
+ }
+ }
+ catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e)
+ {
+ LOGGER.log(Level.FATAL, "Could not find simulator class " + aSimulatorClassName, e);
+ }
+ }
+
+ public String getRobotType()
+ {
+ return mRobotType;
+ }
+
+ public String getRobotClassName()
+ {
+ return mRobotClassName;
+ }
+
+ public BaseSimulator getSimulator()
+ {
+ return mSimulator;
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/TestMain.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/TestMain.java
new file mode 100644
index 00000000..299d10f2
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/TestMain.java
@@ -0,0 +1,65 @@
+package com.snobot.simulator;
+
+import com.snobot.simulator.gui.motor_graphs.MotorCurveDisplayController;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.event.EventHandler;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+
+public final class TestMain
+{
+ private TestMain()
+ {
+
+ }
+
+ public static class PseudoMain extends Application
+ {
+ @Override
+ public void start(Stage aPrimaryStage) throws Exception
+ {
+ FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/snobot/simulator/gui/motor_graphs/motor_curve_display.fxml"));
+ Pane root = loader.load();
+ MotorCurveDisplayController controller = loader.getController();
+
+
+ Scene scene = new Scene(root);
+ aPrimaryStage.setScene(scene);
+ aPrimaryStage.show();
+ controller.setCurveParams("CIM", 12, 5330, 131, 2.7, 2.410);
+
+ aPrimaryStage.setOnHidden(new EventHandler()
+ {
+
+ @Override
+ public void handle(WindowEvent aEvent)
+ {
+ Platform.runLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ System.exit(0);
+ }
+ });
+ }
+ });
+ }
+ }
+
+ public static void main(String[] aArgs)
+ {
+ DefaultDataAccessorFactory.initalize();
+
+ // JavaFX 11+ uses GTK3 by default, and has problems on some display
+ // servers
+ // This flag forces JavaFX to use GTK2
+ // System.setProperty("jdk.gtk.version", "2");
+ Application.launch(PseudoMain.class, aArgs);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/example_robot/ExampleRobot.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/example_robot/ExampleRobot.java
new file mode 100644
index 00000000..4e958e90
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/example_robot/ExampleRobot.java
@@ -0,0 +1,153 @@
+package com.snobot.simulator.example_robot;
+
+import edu.wpi.first.wpilibj.ADXL345_I2C;
+import edu.wpi.first.wpilibj.ADXRS450_Gyro;
+import edu.wpi.first.wpilibj.AnalogGyro;
+import edu.wpi.first.wpilibj.AnalogOutput;
+import edu.wpi.first.wpilibj.DriverStation;
+import edu.wpi.first.wpilibj.Encoder;
+import edu.wpi.first.wpilibj.I2C;
+import edu.wpi.first.wpilibj.Joystick;
+import edu.wpi.first.wpilibj.Relay;
+import edu.wpi.first.wpilibj.Relay.Value;
+import edu.wpi.first.wpilibj.Solenoid;
+import edu.wpi.first.wpilibj.SpeedController;
+import edu.wpi.first.wpilibj.TimedRobot;
+import edu.wpi.first.wpilibj.Timer;
+import edu.wpi.first.wpilibj.VictorSP;
+import edu.wpi.first.wpilibj.interfaces.Accelerometer;
+import edu.wpi.first.wpilibj.interfaces.Gyro;
+import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
+
+/**
+ * Example robot used in the event that no robot to simulate is specified. Does
+ * a simple simulation by hooking up a few commonly used components
+ *
+ * @author PJ
+ *
+ */
+public class ExampleRobot extends TimedRobot
+{
+ private Joystick mJoystick;
+ private Solenoid mSolenoid;
+ private SpeedController mLeftDrive;
+ private SpeedController mRightDrive;
+ private Relay mTestRelay;
+ private AnalogOutput mAnalogOut;
+ private Encoder mLeftDriveEncoder;
+ private Encoder mRightDriveEncoder;
+ private Gyro mAnalogGyro;
+ private Gyro mSpiGyro;
+ private ADXL345_I2C mAdxAccelerometer;
+
+ private Timer mAutoTimer;
+
+ @Override
+ public void robotInit()
+ {
+ mJoystick = new Joystick(0);
+
+ mSolenoid = new Solenoid(0);
+ mLeftDrive = new VictorSP(0);
+ mRightDrive = new VictorSP(1);
+ mTestRelay = new Relay(0);
+ mLeftDriveEncoder = new Encoder(0, 1);
+ mRightDriveEncoder = new Encoder(2, 3);
+ mAnalogOut = new AnalogOutput(0);
+ mAnalogGyro = new AnalogGyro(0);
+ mSpiGyro = new ADXRS450_Gyro();
+ mAdxAccelerometer = new ADXL345_I2C(I2C.Port.kMXP, Accelerometer.Range.k2G);
+
+ mAutoTimer = new Timer();
+
+ mLeftDriveEncoder.setDistancePerPulse(.01);
+ mRightDriveEncoder.setDistancePerPulse(.01);
+
+ String errorMessage = "Warning, this is the example robot bundled with the simulator!\n";
+ errorMessage += "To configure this for your robot, change /simulator_config/simulator_config.properties, and update the robot_class field"; // NOPMD
+
+ System.err.println(errorMessage); // NOPMD
+ }
+
+ @Override
+ public void autonomousInit()
+ {
+ mLeftDriveEncoder.reset();
+ mRightDriveEncoder.reset();
+ mAutoTimer.start();
+
+ System.out.println("Game Information: "); // NOPMD
+ System.out.println(" Match Number : " + DriverStation.getInstance().getMatchNumber()); // NOPMD
+ System.out.println(" Match Replay : " + DriverStation.getInstance().getReplayNumber()); // NOPMD
+ System.out.println(" Match Type : " + DriverStation.getInstance().getMatchType()); // NOPMD
+ System.out.println(" Event Name : " + DriverStation.getInstance().getEventName()); // NOPMD
+ System.out.println(" Game Info : " + DriverStation.getInstance().getGameSpecificMessage()); // NOPMD
+ }
+
+ @Override
+ public void autonomousPeriodic()
+ {
+ if (mAutoTimer.get() < 2)
+ {
+ mLeftDrive.set(1);
+ mRightDrive.set(-1);
+ }
+ else
+ {
+ mLeftDrive.set(0);
+ mRightDrive.set(0);
+ }
+ }
+
+ @Override
+ public void teleopPeriodic()
+ {
+ mLeftDrive.set(mJoystick.getRawAxis(1));
+ mRightDrive.set(-mJoystick.getRawAxis(5));
+
+ mSolenoid.set(mJoystick.getRawButton(1));
+
+ if (mJoystick.getRawButton(2))
+ {
+ mTestRelay.set(Value.kForward);
+ }
+ else if (mJoystick.getRawButton(3))
+ {
+ mTestRelay.set(Value.kReverse);
+ }
+ else if (mJoystick.getRawButton(4))
+ {
+ mTestRelay.set(Value.kOn);
+ }
+ else
+ {
+ mTestRelay.set(Value.kOff);
+ }
+
+ if (mJoystick.getRawButton(5))
+ {
+ mAnalogOut.setVoltage(2.5);
+ }
+ else if (mJoystick.getRawButton(6))
+ {
+ mAnalogOut.setVoltage(5.0);
+ }
+ else
+ {
+ mAnalogOut.setVoltage(0);
+ }
+
+ SmartDashboard.putNumber("Left Enc", mLeftDriveEncoder.getDistance());
+ SmartDashboard.putNumber("Right Enc", mRightDriveEncoder.getDistance());
+ SmartDashboard.putNumber("Analog Gyro", mAnalogGyro.getAngle());
+ SmartDashboard.putNumber("SPI Gyro", mSpiGyro.getAngle());
+ SmartDashboard.putNumber("I2C Accelerometer", mAdxAccelerometer.getX());
+ }
+
+ @Override
+ public void disabledPeriodic()
+ {
+ // Nothing to do
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/ConfigurationPaneController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/ConfigurationPaneController.java
new file mode 100644
index 00000000..07103437
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/ConfigurationPaneController.java
@@ -0,0 +1,78 @@
+package com.snobot.simulator.gui;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.control.TitledPane;
+import javafx.scene.layout.Pane;
+
+public class ConfigurationPaneController
+{
+ @FXML
+ private Pane mMainPane;
+
+ private final List mControllers;
+
+ public ConfigurationPaneController()
+ {
+ mControllers = new ArrayList<>();
+ }
+
+ public void loadWidgets(Supplier aSaveFunction)
+ {
+ try
+ {
+ initializeWidgetGroup(aSaveFunction, "Speed Controllers", DataAccessorFactory.getInstance().getSpeedControllerAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/speed_controller_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Solenoids", DataAccessorFactory.getInstance().getSolenoidAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/solenoid_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Digital IO", DataAccessorFactory.getInstance().getDigitalAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/digital_io_controller_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Relays", DataAccessorFactory.getInstance().getRelayAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/relay_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Analog In", DataAccessorFactory.getInstance().getAnalogInAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/analog_in_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Analog Out", DataAccessorFactory.getInstance().getAnalogOutAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/analog_out_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Encoders", DataAccessorFactory.getInstance().getEncoderAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/encoder_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Gyros", DataAccessorFactory.getInstance().getGyroAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/gyro_widget.fxml");
+ initializeWidgetGroup(aSaveFunction, "Accelerometers", DataAccessorFactory.getInstance().getAccelerometerAccessor().getPortList(),
+ "/com/snobot/simulator/gui/widgets/accelerometer_widget.fxml");
+ }
+ catch (IOException ex)
+ {
+ ex.printStackTrace();
+ }
+ }
+
+ private void initializeWidgetGroup(Supplier aSaveFunction, String aGroupName, List aIds, String aFxmlConfig) throws IOException
+ {
+ FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/snobot/simulator/gui/widget_group.fxml"));
+ Pane newGroup = loader.load();
+
+ TitledPane titlePane = new TitledPane(aGroupName, newGroup);
+
+ mMainPane.getChildren().add(titlePane);
+
+ WidgetGroupController controller = loader.getController();
+ controller.initialize(aSaveFunction, aIds, aFxmlConfig);
+
+ mControllers.add(controller);
+ }
+
+ public void update()
+ {
+ for (WidgetGroupController controller : mControllers)
+ {
+ controller.update();
+ }
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/EnablePanelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/EnablePanelController.java
new file mode 100644
index 00000000..dee8f3a1
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/EnablePanelController.java
@@ -0,0 +1,50 @@
+package com.snobot.simulator.gui;
+
+import java.text.DecimalFormat;
+
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import edu.wpi.first.wpilibj.DriverStation;
+import javafx.fxml.FXML;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+
+public class EnablePanelController
+{
+ private static final DecimalFormat MATCH_TIME_FORMAT = new DecimalFormat("000.00");
+
+ @FXML
+ private Label mMatchTime;
+
+ @FXML
+ private CheckBox mEnableButton;
+
+ @FXML
+ private CheckBox mAutonButton;
+
+ public void setUseSnobotSim(boolean aUseSnobotSimDriverstation)
+ {
+ mEnableButton.setDisable(!aUseSnobotSimDriverstation);
+ mAutonButton.setDisable(!aUseSnobotSimDriverstation);
+ }
+
+ public void setTime(double aTime)
+ {
+ mMatchTime.setText(MATCH_TIME_FORMAT.format(aTime));
+ }
+
+ public void updateLoop(boolean aUseSnobotSimDriverstation)
+ {
+
+ if (aUseSnobotSimDriverstation)
+ {
+ setTime(DataAccessorFactory.getInstance().getDriverStationAccessor().getTimeSinceEnabled());
+ }
+ else
+ {
+ setTime(DriverStation.getInstance().getMatchTime());
+ mEnableButton.setSelected(DriverStation.getInstance().isEnabled());
+ mAutonButton.setSelected(DriverStation.getInstance().isAutonomous());
+ }
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/SimulatorFrameController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/SimulatorFrameController.java
new file mode 100644
index 00000000..c9b95928
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/SimulatorFrameController.java
@@ -0,0 +1,70 @@
+package com.snobot.simulator.gui;
+
+import com.snobot.simulator.config.SimulatorConfigWriter;
+import com.snobot.simulator.gui.joysticks.JoystickManagerController;
+import com.snobot.simulator.gui.widgets.AdvancedSettingsController;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+
+public class SimulatorFrameController
+{
+ @FXML
+ private ConfigurationPaneController mConfigurationPanelController;
+
+ @FXML
+ private AdvancedSettingsController mAdvancedSettingsWidgetController;
+
+ @FXML
+ private EnablePanelController mEnablePanelController;
+
+ @FXML
+ private Button mJoystickSettingsButton;
+
+ private boolean mUseSnobotSimDriverstation;
+
+ private String mSimulatorConfigFile;
+
+ public void initialize(String aSimulatorConfigFile, boolean aUseSnobotSimDriverstation)
+ {
+ mUseSnobotSimDriverstation = aUseSnobotSimDriverstation;
+ mSimulatorConfigFile = aSimulatorConfigFile;
+ mConfigurationPanelController.loadWidgets(this::saveSettings);
+ mAdvancedSettingsWidgetController.setSaveCallback(this::saveSettings);
+
+ mEnablePanelController.setUseSnobotSim(mUseSnobotSimDriverstation);
+ mJoystickSettingsButton.setVisible(mUseSnobotSimDriverstation);
+ }
+
+ public void updateLoop()
+ {
+ mConfigurationPanelController.update();
+ mEnablePanelController.updateLoop(mUseSnobotSimDriverstation);
+ }
+
+ public boolean saveSettings()
+ {
+ SimulatorConfigWriter writer = new SimulatorConfigWriter();
+ writer.writeConfig(mSimulatorConfigFile);
+
+ return true;
+ }
+
+ public void setTime(double aTime)
+ {
+ mEnablePanelController.setTime(aTime);
+ }
+
+ @FXML
+ public void handleJoystickSettingsButton()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/joysticks/joystick_manager_controller.fxml");
+
+ if (dialog.showAndWait())
+ {
+ System.out.println("XFDF");
+ }
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/Util.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/Util.java
new file mode 100644
index 00000000..7a233efd
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/Util.java
@@ -0,0 +1,43 @@
+package com.snobot.simulator.gui;
+
+import javafx.scene.paint.Color;
+
+/**
+ *
+ * @author PJ
+ */
+public final class Util
+{
+ private Util()
+ {
+ // Class is static helper, don't construct it
+ }
+
+ public static Color getMotorColor(double aSpeed)
+ {
+ return colorGetShadedColor(aSpeed, 1, -1);
+ }
+
+ public static Color colorGetShadedColor(double aSpeed, double aMax, double aMin) // NOPMD
+ {
+ if (Double.isNaN(aSpeed))
+ {
+ aSpeed = 0;
+ }
+ if (aSpeed > aMax)
+ {
+ aSpeed = aMax;
+ }
+ else if (aSpeed < aMin)
+ {
+ aSpeed = aMin;
+ }
+
+ double percent = (aSpeed - aMin) / (aMax - aMin);
+ double hue = percent * 120;
+ double saturation = 1;
+ double brightness = 1;
+
+ return Color.hsb(hue, saturation, brightness);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/WidgetGroupController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/WidgetGroupController.java
new file mode 100644
index 00000000..f70fb8bb
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/WidgetGroupController.java
@@ -0,0 +1,60 @@
+package com.snobot.simulator.gui;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import com.snobot.simulator.gui.widgets.IWidgetController;
+
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.VBox;
+
+public class WidgetGroupController
+{
+ @FXML
+ public VBox mMainPane;
+
+ private final List mControllers;
+
+ public WidgetGroupController()
+ {
+ mControllers = new ArrayList<>();
+ }
+
+ public void initialize(Supplier aSaveFunction, List aIds, String aFxmlPath)
+ {
+ try
+ {
+ for (int id : aIds)
+ {
+
+ FXMLLoader loader = new FXMLLoader(getClass().getResource(aFxmlPath));
+ Pane widgetPane = loader.load();
+ mMainPane.getChildren().add(widgetPane);
+
+ IWidgetController controller = loader.getController();
+ controller.initialize(id);
+ controller.setSaveAction(aSaveFunction);
+
+ mControllers.add(controller);
+ }
+ }
+ catch (IOException e)
+ {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ public void update()
+ {
+ for (IWidgetController controller : mControllers)
+ {
+ controller.update();
+ }
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/BaseGameDataController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/BaseGameDataController.java
new file mode 100644
index 00000000..c9e84dd3
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/BaseGameDataController.java
@@ -0,0 +1,52 @@
+package com.snobot.simulator.gui.game_data;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+import com.snobot.simulator.wrapper_accessors.DriverStationDataAccessor.MatchType;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.TextField;
+
+public abstract class BaseGameDataController
+{
+ @FXML
+ protected TextField mMatchNumber;
+
+ @FXML
+ protected ComboBox mMatchType;
+
+ @FXML
+ protected TextField mEventName;
+
+ @FXML
+ public void initialize()
+ {
+ mMatchType.getItems().addAll(MatchType.values());
+ mMatchType.getSelectionModel().select(MatchType.Practice);
+ }
+
+ @FXML
+ protected void handleUpdate()
+ {
+ int matchNumber = 1;
+
+ try
+ {
+ matchNumber = Integer.parseInt(mMatchNumber.getText());
+ }
+ catch (NumberFormatException ex)
+ {
+ LogManager.getLogger().log(Level.ERROR, "Could not parse match number", ex);
+ }
+
+ System.out.println("Setting game data");
+
+ DataAccessorFactory.getInstance().getDriverStationAccessor().setMatchInfo(mEventName.getText(),
+ mMatchType.getSelectionModel().getSelectedItem(), matchNumber, 0, getGameData());
+ }
+
+ protected abstract String getGameData();
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/GenericGameDataController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/GenericGameDataController.java
new file mode 100644
index 00000000..674ea40e
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/GenericGameDataController.java
@@ -0,0 +1,17 @@
+package com.snobot.simulator.gui.game_data;
+
+
+import javafx.fxml.FXML;
+import javafx.scene.control.TextField;
+
+public class GenericGameDataController extends BaseGameDataController
+{
+ @FXML
+ protected TextField mGameData;
+
+ @Override
+ protected String getGameData()
+ {
+ return mGameData.getText();
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/PowerUpGameDataController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/PowerUpGameDataController.java
new file mode 100644
index 00000000..aaad7f5f
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/PowerUpGameDataController.java
@@ -0,0 +1,24 @@
+package com.snobot.simulator.gui.game_data;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+
+public class PowerUpGameDataController extends BaseGameDataController
+{
+ @FXML
+ protected ComboBox mGameData;
+
+ @Override
+ @FXML
+ public void initialize()
+ {
+ mGameData.getSelectionModel().select("LLL");
+ super.initialize();
+ }
+
+ @Override
+ protected String getGameData()
+ {
+ return mGameData.getSelectionModel().getSelectedItem();
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/ConnectedInputConfigPanelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/ConnectedInputConfigPanelController.java
new file mode 100644
index 00000000..f4e5b294
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/ConnectedInputConfigPanelController.java
@@ -0,0 +1,113 @@
+package com.snobot.simulator.gui.joysticks;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.snobot.simulator.gui.joysticks.sub_panels.RawPanelController;
+import com.snobot.simulator.gui.joysticks.sub_panels.WrappedPanelController;
+import com.snobot.simulator.gui.joysticks.sub_panels.XboxDisplayController;
+import com.snobot.simulator.joysticks.ControllerConfiguration;
+import com.snobot.simulator.joysticks.IMockJoystick;
+import com.snobot.simulator.joysticks.JoystickDiscoverer;
+import com.snobot.simulator.joysticks.JoystickFactory;
+import com.snobot.simulator.joysticks.joystick_specializations.NullJoystick;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+import net.java.games.input.Controller;
+import net.java.games.input.Controller.Type;
+
+public class ConnectedInputConfigPanelController
+{
+ private static final Logger LOGGER = LogManager.getLogger(ConnectedInputConfigPanelController.class);
+
+ @FXML
+ private RawPanelController mRawPanelController;
+ @FXML
+ private WrappedPanelController mWrappedPanelController;
+ @FXML
+ private XboxDisplayController mXboxPanelController;
+ @FXML
+ private ComboBox mInterpretAsComboBox;
+
+ private String mJoystickName;
+ private Controller mController;
+
+ public void update()
+ {
+ mRawPanelController.updateDisplay();
+ mWrappedPanelController.updateDisplay();
+ mXboxPanelController.updateDisplay();
+ }
+
+ public void setJoystick(String aControllerName, IMockJoystick aSelectedJoystick, ControllerConfiguration aConfig)
+ {
+ mJoystickName = aControllerName;
+ mController = aConfig.mController;
+
+ initializeInterpretComboBox(aConfig);
+ // mXboxPanelController.setJoystick(aSelectedJoystick);
+ mRawPanelController.setController(aConfig.mController);
+ }
+
+ private void initializeInterpretComboBox(ControllerConfiguration aConfig)
+ {
+
+ if (aConfig.mController.getType() == Type.KEYBOARD)
+ {
+ mInterpretAsComboBox.getItems().add("Keyboard");
+ }
+ else
+ {
+ for (String name : JoystickDiscoverer.getJoystickNames())
+ {
+ mInterpretAsComboBox.getItems().add(name);
+ }
+ }
+
+ if (JoystickDiscoverer.getSpecializationTypes().contains(aConfig.mSpecialization))
+ {
+ mInterpretAsComboBox.getSelectionModel().select(JoystickDiscoverer.getSpecialization(aConfig.mSpecialization));
+ }
+ handleWrapperSelected(mInterpretAsComboBox.getSelectionModel().getSelectedItem());
+
+ mInterpretAsComboBox.valueProperty().addListener((obsValue, oldValue, newValue) ->
+ {
+ System.out.println(obsValue + ", " + oldValue + ", " + newValue);
+ handleWrapperSelected(newValue);
+ });
+ }
+
+ private void handleWrapperSelected(String aType)
+ {
+ IMockJoystick wrappedJoystick = null;
+
+ // Assuming values are unique as well as keys
+ for (Class extends IMockJoystick> specializationType : JoystickDiscoverer.getSpecializationTypes())
+ {
+ String value = JoystickDiscoverer.getSpecialization(specializationType);
+ if (value.equals(aType))
+ {
+ try
+ {
+ JoystickFactory.getInstance().setSpecialization(mJoystickName, specializationType);
+ wrappedJoystick = specializationType.getDeclaredConstructor(Controller.class).newInstance(mController);
+ }
+ catch (Exception e)
+ {
+ LOGGER.log(Level.ERROR, e);
+ }
+ break;
+ }
+ }
+
+ if (wrappedJoystick == null)
+ {
+ wrappedJoystick = new NullJoystick();
+ }
+
+ mWrappedPanelController.setJoystick(wrappedJoystick);
+ mXboxPanelController.setJoystick(wrappedJoystick);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/CurrentSettingsPanelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/CurrentSettingsPanelController.java
new file mode 100644
index 00000000..d4e5fdb7
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/CurrentSettingsPanelController.java
@@ -0,0 +1,47 @@
+package com.snobot.simulator.gui.joysticks;
+
+import java.util.Set;
+
+import com.snobot.simulator.joysticks.IMockJoystick;
+import com.snobot.simulator.joysticks.JoystickFactory;
+import com.snobot.simulator.joysticks.joystick_specializations.NullJoystick;
+
+import edu.wpi.first.wpilibj.DriverStation;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+
+public class CurrentSettingsPanelController
+{
+ @FXML
+ private GridPane mPane;
+
+ public void setControllerConfig(Set aControllerNames, IMockJoystick[] aSelectedJoysticks)
+ {
+ System.out.println("HELLO");
+ ObservableList controllerNames = FXCollections.observableArrayList();
+ controllerNames.add(NullJoystick.sNAME);
+ controllerNames.addAll(aControllerNames);
+
+ for (int i = 0; i < DriverStation.kJoystickPorts; ++i)
+ {
+ int joystickNum = i;
+
+ ComboBox comboBox = new ComboBox<>(controllerNames);
+ comboBox.getSelectionModel().select(aSelectedJoysticks[i].getName());
+ comboBox.valueProperty().addListener((obsValue, oldValue, newValue) ->
+ {
+ JoystickFactory.getInstance().setJoysticks(joystickNum, newValue);
+ });
+
+ mPane.add(new Label("Joystick " + i), 0, i);
+ mPane.add(comboBox, 1, i);
+
+ }
+
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/JoystickManagerController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/JoystickManagerController.java
new file mode 100644
index 00000000..427b7c31
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/JoystickManagerController.java
@@ -0,0 +1,118 @@
+package com.snobot.simulator.gui.joysticks;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.snobot.simulator.joysticks.ControllerConfiguration;
+import com.snobot.simulator.joysticks.IMockJoystick;
+import com.snobot.simulator.joysticks.JoystickFactory;
+
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.layout.Pane;
+
+public class JoystickManagerController
+{
+ private static final Logger LOGGER = LogManager.getLogger(JoystickManagerController.class);
+ private static final int UPDATE_TIME = 20;
+
+ @FXML
+ private CurrentSettingsPanelController mCurrentSettingsPanelController;
+
+ @FXML
+ private TabPane mInputConfigTabbedPane;
+
+ private final List mInputControllers;
+ private boolean mIsOpen;
+
+ public JoystickManagerController()
+ {
+ mInputControllers = new ArrayList<>();
+ }
+
+ @FXML
+ public void initialize()
+ {
+
+ JoystickFactory joystickFactory = JoystickFactory.getInstance();
+ Map goodControllers = joystickFactory.getControllerConfiguration();
+
+ //
+
+ for (Entry pair : goodControllers.entrySet())
+ {
+ String controllerName = pair.getKey();
+ ControllerConfiguration config = pair.getValue();
+ createAndAddJoystickInput(controllerName, config, joystickFactory.getAll()[0]);
+ }
+
+ mCurrentSettingsPanelController.setControllerConfig(goodControllers.keySet(), joystickFactory.getAll());
+ setVisible(true);
+ }
+
+ private void createAndAddJoystickInput(String aControllerName, ControllerConfiguration aConfig, IMockJoystick aSelectedJoystick)
+ {
+ try
+ {
+ FXMLLoader loader = new FXMLLoader(
+ JoystickManagerController.class.getResource("/com/snobot/simulator/gui/joysticks/connected_input_config_panel.fxml"));
+
+ Pane widgetPane = loader.load();
+ ConnectedInputConfigPanelController controller = loader.getController();
+ controller.setJoystick(aControllerName, aSelectedJoystick, aConfig);
+
+ Tab tab = new Tab(aControllerName, widgetPane);
+ mInputConfigTabbedPane.getTabs().add(tab);
+ mInputControllers.add(controller);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ public void setVisible(boolean aVisible)
+ {
+ if (aVisible && !mIsOpen)
+ {
+ mIsOpen = true;
+ Thread t = new Thread(mUpdateLooper);
+ t.setName("Joystick Updater");
+ t.start();
+ }
+ }
+
+ private final Runnable mUpdateLooper = new Runnable()
+ {
+
+ @Override
+ public void run()
+ {
+ while (mIsOpen)
+ {
+ for (ConnectedInputConfigPanelController controller : mInputControllers)
+ {
+ controller.update();
+ }
+
+ try
+ {
+ Thread.sleep(UPDATE_TIME);
+ }
+ catch (InterruptedException aEvent)
+ {
+ LOGGER.log(Level.ERROR, aEvent);
+ }
+ }
+ }
+ };
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/AnalogInputPanelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/AnalogInputPanelController.java
new file mode 100644
index 00000000..229a2839
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/AnalogInputPanelController.java
@@ -0,0 +1,25 @@
+package com.snobot.simulator.gui.joysticks.sub_panels;
+
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.Slider;
+
+public class AnalogInputPanelController
+{
+ @FXML
+ private Label mLabel;
+ @FXML
+ private Slider mSlider;
+
+
+ public void setValue(double aValue)
+ {
+ mSlider.setValue(aValue);
+ }
+
+ public void setName(String aText)
+ {
+ mLabel.setText(aText);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/BaseJoystickDisplayController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/BaseJoystickDisplayController.java
new file mode 100644
index 00000000..3859cf14
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/BaseJoystickDisplayController.java
@@ -0,0 +1,66 @@
+package com.snobot.simulator.gui.joysticks.sub_panels;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.snobot.simulator.gui.joysticks.JoystickManagerController;
+
+import javafx.fxml.FXMLLoader;
+import javafx.scene.layout.Pane;
+
+public class BaseJoystickDisplayController
+{
+ protected final List mAnalogControllers;
+ protected final List mDigitalDisplays;
+
+ protected BaseJoystickDisplayController()
+ {
+ mAnalogControllers = new ArrayList<>();
+ mDigitalDisplays = new ArrayList<>();
+ }
+
+ protected Pane createAnalogDisplay(String aName)
+ {
+ try
+ {
+ FXMLLoader loader = new FXMLLoader(
+ JoystickManagerController.class.getResource("/com/snobot/simulator/gui/joysticks/sub_panels/analog_input_panel.fxml"));
+
+ Pane widgetPane = loader.load();
+ AnalogInputPanelController controller = loader.getController();
+ controller.setName(aName);
+ mAnalogControllers.add(controller);
+
+ return widgetPane;
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ protected Pane createDigitalDisplay(String aName)
+ {
+ try
+ {
+ FXMLLoader loader = new FXMLLoader(
+ JoystickManagerController.class.getResource("/com/snobot/simulator/gui/joysticks/sub_panels/digital_input_panel.fxml"));
+
+ Pane widgetPane = loader.load();
+ DigitalInputPanelController controller = loader.getController();
+ controller.setName(aName);
+ mDigitalDisplays.add(controller);
+
+ return widgetPane;
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/DigitalInputPanelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/DigitalInputPanelController.java
new file mode 100644
index 00000000..ef2fb402
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/DigitalInputPanelController.java
@@ -0,0 +1,20 @@
+package com.snobot.simulator.gui.joysticks.sub_panels;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.CheckBox;
+
+public class DigitalInputPanelController
+{
+ @FXML
+ protected CheckBox mCheckbox;
+
+ public void setValue(boolean aValue)
+ {
+ mCheckbox.setSelected(aValue);
+ }
+
+ public void setName(String aText)
+ {
+ mCheckbox.setText(aText);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/RawPanelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/RawPanelController.java
new file mode 100644
index 00000000..cc0811b3
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/RawPanelController.java
@@ -0,0 +1,94 @@
+package com.snobot.simulator.gui.joysticks.sub_panels;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javafx.fxml.FXML;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import net.java.games.input.Component;
+import net.java.games.input.Controller;
+
+public class RawPanelController extends BaseJoystickDisplayController
+{
+ private Controller mJoystick;
+
+ private final List mAnalogComponents;
+ private final List mDigitalComponents;
+
+ @FXML
+ private GridPane mAnalogPane;
+ @FXML
+ private GridPane mDigitalPane;
+
+ public RawPanelController()
+ {
+ mAnalogComponents = new ArrayList<>();
+ mDigitalComponents = new ArrayList<>();
+ }
+
+ public void updateDisplay()
+ {
+ if (mJoystick != null)
+ {
+ mJoystick.poll();
+ }
+
+ for (int i = 0; i < mAnalogComponents.size(); ++i)
+ {
+ float rawValue = mAnalogComponents.get(i).getPollData();
+
+ AnalogInputPanelController controller = mAnalogControllers.get(i);
+ controller.setValue((int) (rawValue * 127));
+ }
+ for (int i = 0; i < mDigitalComponents.size(); ++i)
+ {
+ float rawValue = mDigitalComponents.get(i).getPollData();
+
+ DigitalInputPanelController controller = mDigitalDisplays.get(i);
+ controller.setValue(rawValue == 1);
+ }
+ }
+
+ public void setController(Controller aJoystick)
+ {
+ mJoystick = aJoystick;
+
+ if (mJoystick != null)
+ {
+ mAnalogControllers.clear();
+ mDigitalDisplays.clear();
+ mAnalogComponents.clear();
+ mDigitalComponents.clear();
+
+ Component[] components = mJoystick.getComponents();
+ for (int j = 0; j < components.length; j++)
+ {
+ Component component = components[j];
+
+ if (component.isAnalog())
+ {
+ mAnalogComponents.add(component);
+ }
+ else
+ {
+ mDigitalComponents.add(component);
+ }
+ }
+
+ int modulo = 5;
+
+ for (int i = 0; i < mAnalogComponents.size(); ++i)
+ {
+ Pane pane = createAnalogDisplay("Analog " + i);
+ mAnalogPane.add(pane, i % modulo, i / modulo);
+ }
+ for (int i = 0; i < mDigitalComponents.size(); ++i)
+ {
+ Pane pane = createDigitalDisplay("Digital " + i);
+ mDigitalPane.add(pane, i % modulo, i / modulo);
+ }
+ }
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/WrappedPanelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/WrappedPanelController.java
new file mode 100644
index 00000000..5d8b5ce7
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/WrappedPanelController.java
@@ -0,0 +1,73 @@
+package com.snobot.simulator.gui.joysticks.sub_panels;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.snobot.simulator.joysticks.IMockJoystick;
+
+import javafx.fxml.FXML;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+
+public class WrappedPanelController extends BaseJoystickDisplayController
+{
+ private static final Logger LOGGER = LogManager.getLogger(WrappedPanelController.class);
+
+ private IMockJoystick mJoystick;
+
+ @FXML
+ private GridPane mAnalogPane;
+ @FXML
+ private GridPane mDigitalPane;
+
+ public final void setJoystick(IMockJoystick aJoystick)
+ {
+ mJoystick = aJoystick;
+ // mAnalogPanel.removeAll();
+ // mDigitalPanel.removeAll();
+
+ if (mJoystick != null)
+ {
+ mAnalogControllers.clear();
+ mDigitalDisplays.clear();
+
+ for (int i = 0; i < mJoystick.getAxisCount(); ++i)
+ {
+ Pane pane = createAnalogDisplay("Analog " + i);
+ mAnalogPane.add(pane, 0, i);
+ }
+ for (int i = 0; i < mJoystick.getButtonCount(); ++i)
+ {
+ Pane pane = createDigitalDisplay("Digital " + i);
+ mDigitalPane.add(pane, 0, i);
+ }
+ }
+ }
+
+ public void updateDisplay()
+ {
+ if (mJoystick == null)
+ {
+ LOGGER.log(Level.WARN, "Joystick is null");
+ }
+ else
+ {
+ float[] axisValues = mJoystick.getAxisValues();
+ int buttonMask = mJoystick.getButtonMask();
+
+ for (int i = 0; i < axisValues.length; ++i)
+ {
+ AnalogInputPanelController panel = mAnalogControllers.get(i);
+ panel.setValue(axisValues[i]);
+ }
+ for (int i = 0; i < mJoystick.getButtonCount(); ++i)
+ {
+ DigitalInputPanelController panel = mDigitalDisplays.get(i);
+ boolean active = (buttonMask & (1 << i)) != 0;
+ panel.setValue(active);
+ }
+ }
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/XboxDisplayController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/XboxDisplayController.java
new file mode 100644
index 00000000..37411c6a
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/XboxDisplayController.java
@@ -0,0 +1,115 @@
+package com.snobot.simulator.gui.joysticks.sub_panels;
+
+
+import com.snobot.simulator.joysticks.IMockJoystick;
+import com.snobot.simulator.joysticks.joystick_specializations.XboxButtonMap;
+
+import javafx.fxml.FXML;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Shape;
+
+public class XboxDisplayController
+{
+ private static final Color BTN_PRESSED_COLOR = new Color(0, 1, 0, .5);
+ private static final Color BTN_NOT_PRESSED_COLOR = Color.TRANSPARENT;
+
+ @FXML
+ private Circle mXButton;
+ @FXML
+ private Circle mYButton;
+ @FXML
+ private Circle mBButton;
+ @FXML
+ private Circle mAButton;
+ @FXML
+ private Circle mBackButton;
+ @FXML
+ private Circle mStartButton;
+ @FXML
+ private Circle mXboxButton;
+
+ @FXML
+ private Rectangle mLeftBumper;
+ @FXML
+ private Rectangle mRightBumper;
+
+ @FXML
+ private Rectangle mL3;
+ @FXML
+ private Rectangle mR3;
+
+ @FXML
+ private Circle mLeftJoystickCircle;
+ @FXML
+ private Circle mRightJoystickCircle;
+
+ @FXML
+ private Line mLeftJoystickLine;
+ @FXML
+ private Line mRightJoystickLine;
+
+ private IMockJoystick mJoystick;
+
+ public void setJoystick(IMockJoystick aSelectedJoystick)
+ {
+ mJoystick = aSelectedJoystick;
+ }
+
+ public void updateDisplay()
+ {
+ colorButton(mXButton, mJoystick.getRawButton(XboxButtonMap.X_BUTTON - 1));
+ colorButton(mYButton, mJoystick.getRawButton(XboxButtonMap.Y_BUTTON - 1));
+ colorButton(mBButton, mJoystick.getRawButton(XboxButtonMap.B_BUTTON - 1));
+ colorButton(mAButton, mJoystick.getRawButton(XboxButtonMap.A_BUTTON - 1));
+ colorButton(mLeftBumper, mJoystick.getRawButton(XboxButtonMap.LB_BUTTON - 1));
+ colorButton(mRightBumper, mJoystick.getRawButton(XboxButtonMap.RB_BUTTON - 1));
+
+ colorButton(mL3, mJoystick.getRawButton(XboxButtonMap.L3_BUTTON - 1));
+ colorButton(mR3, mJoystick.getRawButton(XboxButtonMap.R3_BUTTON - 1));
+
+ colorButton(mBackButton, mJoystick.getRawButton(XboxButtonMap.BACK_BUTTON - 1));
+ colorButton(mStartButton, mJoystick.getRawButton(XboxButtonMap.START_BUTTON - 1));
+ colorButton(mXboxButton, mJoystick.getRawButton(XboxButtonMap.XBOX_BUTTON));
+
+ drawJoystick(mLeftJoystickLine, mLeftJoystickCircle, mJoystick.getRawAxis(XboxButtonMap.LEFT_X_AXIS),
+ mJoystick.getRawAxis(XboxButtonMap.LEFT_Y_AXIS), 162, 270);
+
+ drawJoystick(mRightJoystickLine, mRightJoystickCircle, mJoystick.getRawAxis(XboxButtonMap.RIGHT_X_AXIS),
+ mJoystick.getRawAxis(XboxButtonMap.RIGHT_Y_AXIS), 468, 372);
+ //
+ // drawTrigger(aGraphics,
+ // mJoystick.getRawAxis(XboxButtonMap.LEFT_TRIGGER), 155, 40);
+ // drawTrigger(aGraphics,
+ // mJoystick.getRawAxis(XboxButtonMap.RIGHT_TRIGGER), 530, 40);
+ //
+ // drawPOV(aGraphics, mJoystick.getPovValues());
+ }
+
+ private void drawJoystick(Line aJoystickVector, Circle aJoystickCircle, double aXAxis, double aYAxis, double aCircleHomeX, double aCircleHomeY)
+ {
+ double width = 98 / 2;
+ double height = 80 / 2;
+
+ aJoystickVector.setEndX(aJoystickVector.getStartX() + aXAxis * width);
+ aJoystickVector.setEndY(aJoystickVector.getStartY() + aYAxis * height);
+
+ aJoystickCircle.setCenterX(aCircleHomeX + aXAxis * width);
+ aJoystickCircle.setCenterY(aCircleHomeY + aYAxis * height);
+
+ }
+
+ private void colorButton(Shape aShape, boolean aPressed)
+ {
+ if (aPressed)
+ {
+ aShape.setFill(BTN_PRESSED_COLOR);
+ }
+ else
+ {
+ aShape.setFill(BTN_NOT_PRESSED_COLOR);
+ }
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/motor_graphs/MotorCurveDisplayController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/motor_graphs/MotorCurveDisplayController.java
new file mode 100644
index 00000000..8162749c
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/motor_graphs/MotorCurveDisplayController.java
@@ -0,0 +1,81 @@
+package com.snobot.simulator.gui.motor_graphs;
+
+import com.snobot.simulator.motor_sim.DcMotorModelConfig;
+
+import javafx.fxml.FXML;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.XYChart;
+
+public class MotorCurveDisplayController
+{
+ protected static final int POINT_RESOLUTION = 600;
+
+ @FXML
+ private LineChart mChart;
+
+ private final XYChart.Series mCurrent = new XYChart.Series<>();
+ private final XYChart.Series mTorque = new XYChart.Series<>();
+ private final XYChart.Series mPower = new XYChart.Series<>();
+ private final XYChart.Series mEfficiency = new XYChart.Series<>();
+
+ public MotorCurveDisplayController()
+ {
+ mCurrent.setName("Current");
+ mTorque.setName("Torque");
+ mPower.setName("Power");
+ mEfficiency.setName("Efficiency");
+ }
+
+ @FXML
+ public void initialize()
+ {
+ mChart.setAnimated(false);
+ mChart.getData().addAll(mCurrent, mTorque, mPower, mEfficiency);
+ }
+
+ public void setCurveParams(DcMotorModelConfig aModel)
+ {
+ setCurveParams(aModel.mFactoryParams.mMotorType, aModel.mMotorParams.mNominalVoltage, aModel.mMotorParams.mFreeSpeedRpm,
+ aModel.mMotorParams.mStallCurrent, aModel.mMotorParams.mFreeCurrent, aModel.mMotorParams.mStallTorque);
+ }
+
+ public void setCurveParams(String aMotorName, double aNominalVoltage, double aFreeSpeedRpm, double aStallCurrent, double aFreeCurrent,
+ double aStallTorque)
+ {
+ mChart.setTitle(aMotorName);
+
+ mCurrent.getData().clear();
+ mTorque.getData().clear();
+ mPower.getData().clear();
+ mEfficiency.getData().clear();
+
+ double currentSlope = (aFreeCurrent - aStallCurrent) / aFreeSpeedRpm;
+ double torqueSlope = (0 - aStallTorque) / aFreeSpeedRpm;
+
+ int dRpm = (int) Math.ceil(aFreeSpeedRpm / POINT_RESOLUTION);
+
+ for (int rpm = 0; rpm < aFreeSpeedRpm; rpm += dRpm)
+ {
+ addPoint(aNominalVoltage, aStallCurrent, aStallTorque, rpm, currentSlope, torqueSlope);
+ }
+
+ // Add the last point always
+ addPoint(aNominalVoltage, aStallCurrent, aStallTorque, (int) aFreeSpeedRpm, currentSlope, torqueSlope);
+ }
+
+ private void addPoint(double aNominalVoltage, double aStallCurrent, double aStallTorque, int aRpm, double aCurrentSlope, double aTorqueSlope)
+ {
+ final double omega = 2 * aRpm * Math.PI / 60;
+ final double current = aStallCurrent + aRpm * aCurrentSlope;
+ final double torque = aStallTorque + aRpm * aTorqueSlope;
+ final double inputPower = aNominalVoltage * current;
+ final double outputPower = torque * omega;
+ final double efficiency = outputPower / inputPower * 100;
+
+ mCurrent.getData().add(new XYChart.Data(aRpm, current));
+ mTorque.getData().add(new XYChart.Data(aRpm, torque));
+ mPower.getData().add(new XYChart.Data(aRpm, outputPower));
+ mEfficiency.getData().add(new XYChart.Data(aRpm, efficiency));
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AccelerometerWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AccelerometerWidgetController.java
new file mode 100644
index 00000000..ad7f9c3c
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AccelerometerWidgetController.java
@@ -0,0 +1,97 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.text.DecimalFormat;
+
+import com.snobot.simulator.gui.widgets.settings.BasicSettingsDialog;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.shape.Rectangle;
+
+public class AccelerometerWidgetController extends BaseWidgetController
+{
+ private static final double WIDTH = 80;
+ private static final double GRAVITY_FPS = 32.2;
+ private static final double GRAVITY_IPS = GRAVITY_FPS * 12;
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.###");
+
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Rectangle mAccelerationBar;
+
+ @FXML
+ private TextField mAccelerationText;
+
+ private int mId;
+
+ private final double mMaxAcceleration;
+
+ public AccelerometerWidgetController()
+ {
+ mMaxAcceleration = 2 * GRAVITY_IPS;
+ }
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+ }
+
+ @Override
+ public void update()
+ {
+ set(DataAccessorFactory.getInstance().getAccelerometerAccessor().getAcceleration(mId));
+ }
+
+ private void set(double aAcceleration)
+ {
+ mAccelerationText.setText(DECIMAL_FORMAT.format(aAcceleration));
+
+ double acceleration = Math.min(mMaxAcceleration, aAcceleration);
+ acceleration = Math.max(-mMaxAcceleration, acceleration);
+
+ double width = acceleration * WIDTH / 2 / mMaxAcceleration;
+ double x;
+ if (acceleration < 0)
+ {
+ x = WIDTH / 2 + width;
+ }
+ else
+ {
+ x = WIDTH / 2;
+ }
+
+ mAccelerationBar.setWidth(Math.abs(width));
+ mAccelerationBar.setX(x);
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml");
+ dialog.getController().setName(getName());
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getAccelerometerAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getAccelerometerAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AdvancedSettingsController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AdvancedSettingsController.java
new file mode 100644
index 00000000..2fda29de
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AdvancedSettingsController.java
@@ -0,0 +1,42 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.util.function.Supplier;
+
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.gui.widgets.settings.advanced.SpiI2cSettingsController;
+import com.snobot.simulator.gui.widgets.settings.advanced.TankDriveSettingsController;
+
+import javafx.fxml.FXML;
+
+public class AdvancedSettingsController
+{
+ private Supplier mSaveFunction;
+
+ @FXML
+ protected void handleSpiI2cButton()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/advanced/spi_i2c_settings.fxml");
+ if (dialog.showAndWait())
+ {
+ dialog.getController().onSubmit();
+ mSaveFunction.get();
+ }
+ }
+
+ @FXML
+ protected void handleTankDriveButton()
+ {
+ DialogRunner dialog = new DialogRunner<>(
+ "/com/snobot/simulator/gui/widgets/settings/advanced/tank_drive_settings.fxml");
+ if (dialog.showAndWait())
+ {
+ dialog.getController().onSubmit();
+ mSaveFunction.get();
+ }
+ }
+
+ public void setSaveCallback(Supplier aSaveFunction)
+ {
+ mSaveFunction = aSaveFunction;
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogInWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogInWidgetController.java
new file mode 100644
index 00000000..60c660d7
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogInWidgetController.java
@@ -0,0 +1,101 @@
+package com.snobot.simulator.gui.widgets;
+
+import com.snobot.simulator.gui.Util;
+import com.snobot.simulator.gui.widgets.settings.BasicSettingsDialog;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.shape.Circle;
+
+public class AnalogInWidgetController extends BaseWidgetController
+{
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Circle mValueIcon;
+
+ @FXML
+ private TextField mValueField;
+
+ private int mId;
+
+ private boolean mEditing;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+
+ mValueField.focusedProperty().addListener((obs, oldVal, newVal) ->
+ {
+ System.out.println("Focus listener" + newVal);
+ if (newVal)
+ {
+ mEditing = true;
+ }
+ else
+ {
+ handleUserSetting();
+ }
+ });
+ }
+
+ @Override
+ public void update()
+ {
+ if (!mEditing)
+ {
+ double voltage = DataAccessorFactory.getInstance().getAnalogInAccessor().getVoltage(mId);
+
+ mValueIcon.setFill(Util.colorGetShadedColor(voltage, 5, 0));
+ mValueField.setText(Double.toString(voltage));
+ }
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml");
+ dialog.getController().setName(getName());
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getAnalogInAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getAnalogInAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+
+ @FXML
+ public void handleAction()
+ {
+ handleUserSetting();
+ }
+
+ private void handleUserSetting()
+ {
+ mEditing = false;
+ double newVoltage = Double.parseDouble(mValueField.getText());
+ newVoltage = Math.min(5, newVoltage);
+ newVoltage = Math.max(0, newVoltage);
+
+ DataAccessorFactory.getInstance().getAnalogInAccessor().setVoltage(mId, newVoltage);
+
+ mLabel.requestFocus();
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogOutWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogOutWidgetController.java
new file mode 100644
index 00000000..2b9f327e
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogOutWidgetController.java
@@ -0,0 +1,69 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.text.DecimalFormat;
+
+import com.snobot.simulator.gui.Util;
+import com.snobot.simulator.gui.widgets.settings.BasicSettingsDialog;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.shape.Circle;
+
+public class AnalogOutWidgetController extends BaseWidgetController
+{
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.###");
+
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Circle mValueIcon;
+
+ @FXML
+ private TextField mValueField;
+
+ private int mId;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+ mValueField.setEditable(false);
+ }
+
+ @Override
+ public void update()
+ {
+ double voltage = DataAccessorFactory.getInstance().getAnalogOutAccessor().getVoltage(mId);
+ mValueIcon.setFill(Util.colorGetShadedColor(voltage, 5, 0));
+ mValueField.setText(DECIMAL_FORMAT.format(voltage));
+
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml");
+ dialog.getController().setName(getName());
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getAnalogOutAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getAnalogOutAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/BaseWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/BaseWidgetController.java
new file mode 100644
index 00000000..b882795e
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/BaseWidgetController.java
@@ -0,0 +1,21 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.util.function.Supplier;
+
+public abstract class BaseWidgetController implements IWidgetController
+{
+ private Supplier mSaveSettingsFunction;
+
+ @Override
+ public boolean saveSettings()
+ {
+ return mSaveSettingsFunction.get();
+ }
+
+ @Override
+ public void setSaveAction(Supplier aSaveFunction)
+ {
+ mSaveSettingsFunction = aSaveFunction;
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/DigitalIOWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/DigitalIOWidgetController.java
new file mode 100644
index 00000000..54134b48
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/DigitalIOWidgetController.java
@@ -0,0 +1,59 @@
+package com.snobot.simulator.gui.widgets;
+
+
+import com.snobot.simulator.gui.widgets.settings.BasicSettingsDialog;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+
+public class DigitalIOWidgetController extends BaseWidgetController
+{
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Circle mValueIcon;
+
+ private int mId;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+ }
+
+ @Override
+ public void update()
+ {
+ boolean value = DataAccessorFactory.getInstance().getDigitalAccessor().getState(mId);
+ mValueIcon.setFill(value ? Color.GREEN : Color.RED);
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml");
+ dialog.getController().setName(getName());
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getDigitalAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getDigitalAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/EncoderWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/EncoderWidgetController.java
new file mode 100644
index 00000000..1bec59a4
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/EncoderWidgetController.java
@@ -0,0 +1,70 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.text.DecimalFormat;
+
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.gui.widgets.settings.EncoderSettingsDialog;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+
+public class EncoderWidgetController extends BaseWidgetController
+{
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.###");
+
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private TextField mDistanceField;
+
+ private int mId;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+ }
+
+ @Override
+ public void update()
+ {
+ boolean isConnected = DataAccessorFactory.getInstance().getEncoderAccessor().isHookedUp(mId);
+ if (isConnected)
+ {
+ mDistanceField.setText(DECIMAL_FORMAT.format(DataAccessorFactory.getInstance().getEncoderAccessor().getDistance(mId)));
+ }
+ else
+ {
+ mDistanceField.setText("No SC Connected");
+ }
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/encoder_settings.fxml");
+ dialog.getController().setName(getName());
+ dialog.getController().setEncoderHandle(mId);
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ DataAccessorFactory.getInstance().getEncoderAccessor().connectSpeedController(mId, dialog.getController().getSelectedId());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getEncoderAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getEncoderAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/GyroWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/GyroWidgetController.java
new file mode 100644
index 00000000..3bde8bed
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/GyroWidgetController.java
@@ -0,0 +1,78 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.text.DecimalFormat;
+
+import com.snobot.simulator.gui.widgets.settings.BasicSettingsDialog;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import javafx.scene.transform.Rotate;
+
+public class GyroWidgetController extends BaseWidgetController
+{
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.###");
+
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Circle mAngleIndicator;
+
+ @FXML
+ private Line mAngleArm;
+
+ @FXML
+ private TextField mAngleText;
+
+ private Rotate mAngleArmRotation;
+
+ private int mId;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+
+ mAngleArmRotation = new Rotate();
+ mAngleArm.getTransforms().add(mAngleArmRotation);
+ }
+
+ @Override
+ public void update()
+ {
+ double angle = DataAccessorFactory.getInstance().getGyroAccessor().getAngle(mId);
+ mAngleText.setText(DECIMAL_FORMAT.format(angle));
+
+ mAngleArmRotation.setAngle(angle);
+
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml");
+ dialog.getController().setName(getName());
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getGyroAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getGyroAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/IWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/IWidgetController.java
new file mode 100644
index 00000000..fb11ea2e
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/IWidgetController.java
@@ -0,0 +1,18 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.util.function.Supplier;
+
+public interface IWidgetController
+{
+
+ void initialize(int aId);
+
+ void update();
+
+ void openSettings();
+
+ boolean saveSettings();
+
+ void setSaveAction(Supplier aSaveFunction);
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/RelayWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/RelayWidgetController.java
new file mode 100644
index 00000000..ba1a3391
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/RelayWidgetController.java
@@ -0,0 +1,64 @@
+package com.snobot.simulator.gui.widgets;
+
+import com.snobot.simulator.gui.widgets.settings.BasicSettingsDialog;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+
+public class RelayWidgetController extends BaseWidgetController
+{
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Rectangle mForwardIndicator;
+
+ @FXML
+ private Rectangle mReverseIndicator;
+
+ private int mId;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+ }
+
+ @Override
+ public void update()
+ {
+ boolean forward = DataAccessorFactory.getInstance().getRelayAccessor().getFowardValue(mId);
+ boolean reverse = DataAccessorFactory.getInstance().getRelayAccessor().getReverseValue(mId);
+
+ mForwardIndicator.setFill(forward ? Color.GREEN : Color.RED);
+ mReverseIndicator.setFill(reverse ? Color.GREEN : Color.RED);
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml");
+ dialog.getController().setName(getName());
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getRelayAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getRelayAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SolenoidWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SolenoidWidgetController.java
new file mode 100644
index 00000000..b79ed8ef
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SolenoidWidgetController.java
@@ -0,0 +1,75 @@
+package com.snobot.simulator.gui.widgets;
+
+import com.snobot.simulator.gui.widgets.settings.BasicSettingsDialog;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.shape.Rectangle;
+
+public class SolenoidWidgetController extends BaseWidgetController
+{
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Rectangle mPole;
+
+ @FXML
+ private Rectangle mPlunger;
+
+ private int mId;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+ }
+
+ @Override
+ public void update()
+ {
+ boolean state = DataAccessorFactory.getInstance().getSolenoidAccessor().get(mId);
+ set(state);
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml");
+ dialog.getController().setName(getName());
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getSolenoidAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getSolenoidAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+
+ private void set(boolean aState)
+ {
+ if (aState)
+ {
+ mPole.setX(30);
+ mPlunger.setX(80);
+ }
+ else
+ {
+ mPole.setX(0);
+ mPlunger.setX(50);
+ }
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SpeedControllerWidgetController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SpeedControllerWidgetController.java
new file mode 100644
index 00000000..c3b4f0b1
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SpeedControllerWidgetController.java
@@ -0,0 +1,73 @@
+package com.snobot.simulator.gui.widgets;
+
+import java.text.DecimalFormat;
+
+import com.snobot.simulator.gui.Util;
+import com.snobot.simulator.gui.widgets.settings.DialogRunner;
+import com.snobot.simulator.gui.widgets.settings.SpeedControllerSettingsDialog;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+import com.snobot.simulator.wrapper_accessors.SpeedControllerWrapperAccessor.MotorSimType;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.shape.Circle;
+
+public class SpeedControllerWidgetController extends BaseWidgetController
+{
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.###");
+
+ @FXML
+ private Label mLabel;
+
+ @FXML
+ private Circle mMotorSpeedIndicator;
+
+ @FXML
+ private TextField mValueField;
+
+ private int mId;
+
+ @Override
+ public void initialize(int aId)
+ {
+ mId = aId;
+ mLabel.setText(getName());
+ }
+
+ @Override
+ public void update()
+ {
+ double speed = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getVoltagePercentage(mId);
+ mMotorSpeedIndicator.setFill(Util.getMotorColor(speed));
+ mValueField.setText(DECIMAL_FORMAT.format(speed));
+ }
+
+ @Override
+ public void openSettings()
+ {
+ DialogRunner dialog = new DialogRunner<>("/com/snobot/simulator/gui/widgets/settings/speed_controller_settings.fxml");
+ dialog.getController().setName(getName());
+
+ MotorSimType mode = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getMotorSimType(mId);
+ dialog.getController().initialize(mId, mode);
+
+ if (dialog.showAndWait())
+ {
+ setName(dialog.getController().getDisplayName());
+ dialog.getController().saveMotorSim(mId);
+ saveSettings();
+ }
+ }
+
+ private String getName()
+ {
+ return DataAccessorFactory.getInstance().getSpeedControllerAccessor().getName(mId);
+ }
+
+ private void setName(String aName)
+ {
+ DataAccessorFactory.getInstance().getSpeedControllerAccessor().setName(mId, aName);
+ mLabel.setText(aName);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/BasicSettingsDialog.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/BasicSettingsDialog.java
new file mode 100644
index 00000000..f2c86bd4
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/BasicSettingsDialog.java
@@ -0,0 +1,24 @@
+package com.snobot.simulator.gui.widgets.settings;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.TextField;
+
+public class BasicSettingsDialog
+{
+ @FXML
+ private TextField mNameField;
+
+ public BasicSettingsDialog()
+ {
+ }
+
+ public void setName(String aName)
+ {
+ mNameField.setText(aName);
+ }
+
+ public String getDisplayName()
+ {
+ return mNameField.getText();
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/DialogRunner.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/DialogRunner.java
new file mode 100644
index 00000000..786d318c
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/DialogRunner.java
@@ -0,0 +1,57 @@
+package com.snobot.simulator.gui.widgets.settings;
+
+import java.io.IOException;
+
+import javafx.fxml.FXMLLoader;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.ButtonType;
+import javafx.scene.layout.Pane;
+
+public class DialogRunner
+{
+ private boolean mOkSelected;
+ private DialogController mDialogController;
+ private Alert mAlert;
+
+ public DialogRunner(String aFxmlPath)
+ {
+ try
+ {
+ mAlert = new Alert(AlertType.NONE);
+ mAlert.setTitle("Error alert");
+
+ mAlert.getDialogPane().getButtonTypes().add(ButtonType.OK);
+ mAlert.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
+
+ FXMLLoader loader = new FXMLLoader(DialogRunner.class.getResource(aFxmlPath));
+ Pane widgetPane = loader.load();
+ mDialogController = loader.getController();
+
+ mAlert.getDialogPane().setContent(widgetPane);
+ }
+ catch (IOException ex)
+ {
+ throw new RuntimeException("Could not load dialog", ex);
+ }
+ }
+
+ public boolean showAndWait()
+ {
+ mAlert.showAndWait().ifPresent(response ->
+ {
+ if (response == ButtonType.OK)
+ {
+ mOkSelected = true;
+ }
+ });
+
+ return mOkSelected;
+ }
+
+ public DialogController getController()
+ {
+ return mDialogController;
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/EncoderSettingsDialog.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/EncoderSettingsDialog.java
new file mode 100644
index 00000000..b3928fea
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/EncoderSettingsDialog.java
@@ -0,0 +1,55 @@
+package com.snobot.simulator.gui.widgets.settings;
+
+import java.util.List;
+
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+
+public class EncoderSettingsDialog extends BasicSettingsDialog
+{
+ @FXML
+ private ComboBox mSpeedControllerComboBox;
+
+ @FXML
+ public void initialize()
+ {
+ List speedControllers = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getPortList();
+ for (int handle : speedControllers)
+ {
+ SensorHandleOption option = new SensorHandleOption(handle,
+ DataAccessorFactory.getInstance().getSpeedControllerAccessor().getName(handle));
+ mSpeedControllerComboBox.getItems().add(option);
+ }
+ }
+
+ public void setEncoderHandle(int aEncoderHandle)
+ {
+ int connectedSc = -1;
+ if (DataAccessorFactory.getInstance().getEncoderAccessor().isHookedUp(aEncoderHandle))
+ {
+ connectedSc = DataAccessorFactory.getInstance().getEncoderAccessor().getHookedUpId(aEncoderHandle);
+ }
+
+ for (int i = 0; i < mSpeedControllerComboBox.getItems().size(); ++i)
+ {
+ SensorHandleOption option = mSpeedControllerComboBox.getItems().get(i);
+
+ if (option.mHandle == connectedSc)
+ {
+ mSpeedControllerComboBox.getSelectionModel().select(i);
+ }
+ if (option.mHandle != -1)
+ {
+ option.mName = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getName(option.mHandle);
+ }
+ }
+ }
+
+ public int getSelectedId()
+ {
+ SensorHandleOption option = (SensorHandleOption) mSpeedControllerComboBox.getSelectionModel().getSelectedItem();
+ return option == null ? -1 : option.mHandle;
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SensorHandleOption.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SensorHandleOption.java
new file mode 100644
index 00000000..f9858453
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SensorHandleOption.java
@@ -0,0 +1,19 @@
+package com.snobot.simulator.gui.widgets.settings;
+
+public class SensorHandleOption
+{
+ public int mHandle;
+ public String mName;
+
+ public SensorHandleOption(int aHandle, String aName)
+ {
+ mHandle = aHandle;
+ mName = aName;
+ }
+
+ @Override
+ public String toString()
+ {
+ return mName + "(" + mHandle + ")";
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SpeedControllerSettingsDialog.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SpeedControllerSettingsDialog.java
new file mode 100644
index 00000000..7da425b1
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SpeedControllerSettingsDialog.java
@@ -0,0 +1,100 @@
+package com.snobot.simulator.gui.widgets.settings;
+
+import com.snobot.simulator.gui.widgets.settings.motor_sim.GravitationalLoadMotorSimController;
+import com.snobot.simulator.gui.widgets.settings.motor_sim.IMotorSimController;
+import com.snobot.simulator.gui.widgets.settings.motor_sim.RotationalLoadMotorSimController;
+import com.snobot.simulator.gui.widgets.settings.motor_sim.SimpleMotorSimController;
+import com.snobot.simulator.gui.widgets.settings.motor_sim.StaticLoadMotorSimController;
+import com.snobot.simulator.wrapper_accessors.SpeedControllerWrapperAccessor.MotorSimType;
+
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.ComboBox;
+import javafx.scene.layout.StackPane;
+
+public class SpeedControllerSettingsDialog extends BasicSettingsDialog
+{
+ @FXML
+ private ComboBox mSelectionBox;
+
+ @FXML
+ private StackPane mLayoutManager;
+
+ @FXML
+ private Node mSimpleConfigPanel;
+
+ @FXML
+ private SimpleMotorSimController mSimpleConfigPanelController;
+
+ @FXML
+ private Node mStaticLoadConfigPanel;
+
+ @FXML
+ private StaticLoadMotorSimController mStaticLoadConfigPanelController;
+
+ @FXML
+ private Node mRotationalLoadConfigPanel;
+
+ @FXML
+ private RotationalLoadMotorSimController mRotationalLoadConfigPanelController;
+
+ @FXML
+ private Node mGravitationalLoadConfigPanel;
+
+ @FXML
+ private GravitationalLoadMotorSimController mGravitationalLoadConfigPanelController;
+
+ private IMotorSimController mActiveController;
+
+ @FXML
+ public void initialize()
+ {
+ mSelectionBox.getItems().addAll(MotorSimType.values());
+ }
+
+ public void initialize(int aSpeedControllerHandle, MotorSimType aMode)
+ {
+ mSelectionBox.getSelectionModel().select(aMode);
+ handleSimType(aMode);
+ mActiveController.populate(aSpeedControllerHandle);
+ }
+
+ @FXML
+ public void handleSimType()
+ {
+ MotorSimType selectedType = mSelectionBox.getSelectionModel().getSelectedItem();
+ handleSimType(selectedType);
+ }
+
+ public void handleSimType(MotorSimType aSelectedType)
+ {
+ mLayoutManager.getChildren().clear();
+
+ switch (aSelectedType)
+ {
+ case Simple:
+ mLayoutManager.getChildren().add(mSimpleConfigPanel);
+ mActiveController = mSimpleConfigPanelController;
+ break;
+ case StaticLoad:
+ mLayoutManager.getChildren().add(mStaticLoadConfigPanel);
+ mActiveController = mStaticLoadConfigPanelController;
+ break;
+ case RotationalLoad:
+ mLayoutManager.getChildren().add(mRotationalLoadConfigPanel);
+ mActiveController = mRotationalLoadConfigPanelController;
+ break;
+ case GravitationalLoad:
+ mLayoutManager.getChildren().add(mGravitationalLoadConfigPanel);
+ mActiveController = mGravitationalLoadConfigPanelController;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown type " + aSelectedType);
+ }
+ }
+
+ public void saveMotorSim(int aSpeedControllerHandle)
+ {
+ mActiveController.saveSettings(aSpeedControllerHandle);
+ }
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/SpiI2cSettingsController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/SpiI2cSettingsController.java
new file mode 100644
index 00000000..4d669c49
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/SpiI2cSettingsController.java
@@ -0,0 +1,128 @@
+package com.snobot.simulator.gui.widgets.settings.advanced;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import edu.wpi.first.wpilibj.I2C;
+import edu.wpi.first.wpilibj.SPI;
+import javafx.collections.FXCollections;
+import javafx.fxml.FXML;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+
+public class SpiI2cSettingsController
+{
+ private static final String DEFAULT_ITEM = "None";
+
+ private final Map mSpiSettings;
+ private final Map mI2CSettings;
+
+ @FXML
+ private GridPane mSpiPane;
+
+ @FXML
+ private GridPane mI2CPane;
+
+ public SpiI2cSettingsController()
+ {
+ mSpiSettings = new HashMap<>();
+ mI2CSettings = new HashMap<>();
+ }
+
+ @FXML
+ public void initialize()
+ {
+ Map defaultSpiMapping = DataAccessorFactory.getInstance().getSpiAccessor().getSpiWrapperTypes();
+ Map defaultI2CMapping = DataAccessorFactory.getInstance().getI2CAccessor().getI2CWrapperTypes();
+ Collection availableSpiOptions = new ArrayList<>();
+ Collection availableI2COptions = new ArrayList<>();
+
+ availableSpiOptions.add(DEFAULT_ITEM);
+ availableSpiOptions.addAll(DataAccessorFactory.getInstance().getSpiAccessor().getAvailableSpiSimulators());
+
+ availableI2COptions.add(DEFAULT_ITEM);
+ availableI2COptions.addAll(DataAccessorFactory.getInstance().getI2CAccessor().getAvailableI2CSimulators());
+
+ int rowCtr = 0;
+ for (SPI.Port port : SPI.Port.values())
+ {
+ String selectedValue = defaultSpiMapping.get(port.value);
+
+ ComponentRow row = new ComponentRow(port.name(), "(" + port.ordinal() + ")", availableSpiOptions, selectedValue);
+ mSpiSettings.put(port.value, row);
+
+ mSpiPane.add(row.mNameLabel, 0, rowCtr);
+ mSpiPane.add(row.mIndexLabel, 1, rowCtr);
+ mSpiPane.add(row.mSimType, 2, rowCtr);
+ ++rowCtr;
+ }
+
+ rowCtr = 0;
+ for (I2C.Port port : I2C.Port.values())
+ {
+ String selectedValue = defaultI2CMapping.get(port.value);
+
+ ComponentRow row = new ComponentRow(port.name(), "(" + port.ordinal() + ")", availableSpiOptions, selectedValue);
+ mI2CSettings.put(port.value, row);
+
+ mI2CPane.add(row.mNameLabel, 0, rowCtr);
+ mI2CPane.add(row.mIndexLabel, 1, rowCtr);
+ mI2CPane.add(row.mSimType, 2, rowCtr);
+ ++rowCtr;
+ }
+
+ }
+
+ public void onSubmit()
+ {
+ for (Entry pair : mSpiSettings.entrySet())
+ {
+ Object selected = pair.getValue().mSimType.getSelectionModel().getSelectedItem();
+ String value = null;
+ if (selected != null && !DEFAULT_ITEM.equals(selected))
+ {
+ value = selected.toString();
+ }
+ DataAccessorFactory.getInstance().getSpiAccessor().createSpiSimulator(pair.getKey(), value);
+ }
+
+ for (Entry pair : mI2CSettings.entrySet())
+ {
+ Object selected = pair.getValue().mSimType.getSelectionModel().getSelectedItem();
+ String value = null;
+ if (selected != null && !DEFAULT_ITEM.equals(selected))
+ {
+ value = selected.toString();
+ }
+ DataAccessorFactory.getInstance().getI2CAccessor().createI2CSimulator(pair.getKey(), value);
+ }
+
+ Alert closeAlert = new Alert(AlertType.WARNING,
+ "Most SPI and I2C simulators are required to be initialized on startup, so it is recommended that you save your updates and restart the simulator");
+ closeAlert.showAndWait();
+ }
+
+ private static class ComponentRow
+ {
+ private final Label mNameLabel;
+ private final Label mIndexLabel;
+ private final ComboBox mSimType;
+
+ private ComponentRow(String aName, String aIndex, Collection aOptions, String aSelectedValue)
+ {
+ mNameLabel = new Label(aName);
+ mIndexLabel = new Label(aIndex);
+ mSimType = new ComboBox<>(FXCollections.observableArrayList(aOptions));
+ mSimType.getSelectionModel().select(aSelectedValue);
+ }
+ }
+
+}
diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/TankDriveSettingsController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/TankDriveSettingsController.java
new file mode 100644
index 00000000..d1e15444
--- /dev/null
+++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/TankDriveSettingsController.java
@@ -0,0 +1,197 @@
+package com.snobot.simulator.gui.widgets.settings.advanced;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.snobot.simulator.gui.widgets.settings.SensorHandleOption;
+import com.snobot.simulator.simulator_components.config.TankDriveConfig;
+import com.snobot.simulator.wrapper_accessors.DataAccessorFactory;
+
+import javafx.collections.FXCollections;
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TitledPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.VBox;
+
+public class TankDriveSettingsController
+{
+ private static final Logger LOGGER = LogManager.getLogger(TankDriveSettingsController.class);
+
+ private final List mWidgetPanels;
+
+ @FXML
+ private GridPane mSpiPane;
+
+ @FXML
+ private VBox mPane;
+
+ public TankDriveSettingsController()
+ {
+ mWidgetPanels = new ArrayList<>();
+ }
+
+ @FXML
+ public void initialize()
+ {
+ Collection