From feab9a273296af4cf22d4daa18a9054f390af274 Mon Sep 17 00:00:00 2001 From: PJ Reiniger Date: Sun, 2 Jun 2019 19:25:42 -0400 Subject: [PATCH] Starting javafx --- settings.gradle | 1 + snobot_sim_gui/build.gradle | 3 +- snobot_sim_gui_javafx/build.gradle | 128 ++++++++++++ .../com/snobot/simulator/BaseSimulator.java | 45 ++++ .../simulator/DefaultDataAccessorFactory.java | 28 +++ .../snobot/simulator/ISimulatorUpdater.java | 11 + .../main/java/com/snobot/simulator/Main.java | 22 ++ .../simulator/RobotContainerFactory.java | 51 +++++ .../java/com/snobot/simulator/Simulator.java | 197 ++++++++++++++++++ .../simulator/SimulatorApplication.java | 63 ++++++ .../snobot/simulator/SimulatorPreloader.java | 85 ++++++++ .../SimulatorPreloaderController.java | 38 ++++ .../simulator/SimulatorPropertiesLoader.java | 120 +++++++++++ .../java/com/snobot/simulator/TestMain.java | 65 ++++++ .../simulator/example_robot/ExampleRobot.java | 153 ++++++++++++++ .../gui/ConfigurationPaneController.java | 78 +++++++ .../simulator/gui/EnablePanelController.java | 50 +++++ .../gui/SimulatorFrameController.java | 70 +++++++ .../java/com/snobot/simulator/gui/Util.java | 43 ++++ .../simulator/gui/WidgetGroupController.java | 60 ++++++ .../gui/game_data/BaseGameDataController.java | 52 +++++ .../game_data/GenericGameDataController.java | 17 ++ .../game_data/PowerUpGameDataController.java | 24 +++ .../ConnectedInputConfigPanelController.java | 113 ++++++++++ .../CurrentSettingsPanelController.java | 47 +++++ .../joysticks/JoystickManagerController.java | 118 +++++++++++ .../AnalogInputPanelController.java | 25 +++ .../BaseJoystickDisplayController.java | 66 ++++++ .../DigitalInputPanelController.java | 20 ++ .../sub_panels/RawPanelController.java | 94 +++++++++ .../sub_panels/WrappedPanelController.java | 73 +++++++ .../sub_panels/XboxDisplayController.java | 115 ++++++++++ .../MotorCurveDisplayController.java | 81 +++++++ .../AccelerometerWidgetController.java | 97 +++++++++ .../widgets/AdvancedSettingsController.java | 42 ++++ .../gui/widgets/AnalogInWidgetController.java | 101 +++++++++ .../widgets/AnalogOutWidgetController.java | 69 ++++++ .../gui/widgets/BaseWidgetController.java | 21 ++ .../widgets/DigitalIOWidgetController.java | 59 ++++++ .../gui/widgets/EncoderWidgetController.java | 70 +++++++ .../gui/widgets/GyroWidgetController.java | 78 +++++++ .../gui/widgets/IWidgetController.java | 18 ++ .../gui/widgets/RelayWidgetController.java | 64 ++++++ .../gui/widgets/SolenoidWidgetController.java | 75 +++++++ .../SpeedControllerWidgetController.java | 73 +++++++ .../widgets/settings/BasicSettingsDialog.java | 24 +++ .../gui/widgets/settings/DialogRunner.java | 57 +++++ .../settings/EncoderSettingsDialog.java | 55 +++++ .../widgets/settings/SensorHandleOption.java | 19 ++ .../SpeedControllerSettingsDialog.java | 100 +++++++++ .../advanced/SpiI2cSettingsController.java | 128 ++++++++++++ .../advanced/TankDriveSettingsController.java | 197 ++++++++++++++++++ ...aseMotorSimWithDcMotorModelController.java | 10 + .../DcMotorModelParamsController.java | 116 +++++++++++ .../GravitationalLoadMotorSimController.java | 34 +++ .../motor_sim/IMotorSimController.java | 10 + .../RotationalLoadMotorSimController.java | 30 +++ .../motor_sim/SimpleMotorSimController.java | 28 +++ .../StaticLoadMotorSimController.java | 32 +++ .../robot_container/CppRobotContainer.java | 55 +++++ .../robot_container/IRobotClassContainer.java | 16 ++ .../robot_container/JavaRobotContainer.java | 37 ++++ .../simulator/gui/configuration_panel.fxml | 7 + .../snobot/simulator/gui/enable_panel.fxml | 14 ++ .../gui/game_data/generic_game_data.fxml | 20 ++ .../gui/game_data/power_up_game_data.fxml | 30 +++ .../connected_input_config_panel.fxml | 34 +++ .../gui/joysticks/current_settings.fxml | 11 + .../joystick_manager_controller.fxml | 26 +++ .../sub_panels/analog_input_panel.fxml | 13 ++ .../sub_panels/digital_input_panel.fxml | 13 ++ .../joysticks/sub_panels/raw_controller.fxml | 18 ++ .../sub_panels/wrapped_controller.fxml | 18 ++ .../joysticks/sub_panels/xbox_controller.png | Bin 0 -> 99960 bytes .../joysticks/sub_panels/xbox_display.fxml | 38 ++++ .../gui/motor_graphs/motor_curve_display.fxml | 22 ++ .../snobot/simulator/gui/simulator_frame.fxml | 31 +++ .../snobot/simulator/gui/widget_group.fxml | 10 + .../gui/widgets/accelerometer_widget.fxml | 51 +++++ .../gui/widgets/advanced_settings_widget.fxml | 23 ++ .../gui/widgets/analog_in_widget.fxml | 45 ++++ .../gui/widgets/analog_out_widget.fxml | 43 ++++ .../widgets/digital_io_controller_widget.fxml | 37 ++++ .../simulator/gui/widgets/encoder_widget.fxml | 33 +++ .../com/snobot/simulator/gui/widgets/gear.png | Bin 0 -> 755 bytes .../simulator/gui/widgets/gyro_widget.fxml | 51 +++++ .../simulator/gui/widgets/relay_widget.fxml | 43 ++++ .../settings/advanced/spi_i2c_settings.fxml | 17 ++ .../advanced/tank_drive_settings.fxml | 15 ++ .../gui/widgets/settings/basic_settings.fxml | 12 ++ .../widgets/settings/encoder_settings.fxml | 14 ++ .../motor_sim/dc_motor_model_panel.fxml | 124 +++++++++++ .../motor_sim/gravitational_load_sim.fxml | 25 +++ .../motor_sim/rotational_load_sim.fxml | 25 +++ .../settings/motor_sim/simple_motor_sim.fxml | 15 ++ .../settings/motor_sim/static_load_sim.fxml | 25 +++ .../settings/speed_controller_settings.fxml | 42 ++++ .../widgets/settings/tank_drive_settings.fxml | 0 .../gui/widgets/solenoid_widget.fxml | 73 +++++++ .../gui/widgets/speed_controller_widget.fxml | 50 +++++ .../com/snobot/simulator/preloader.fxml | 29 +++ .../src/main/resources/themes/snobot_sim.css | 0 .../joysticks/IJoystickInterface.java | 9 + .../JoystickConfigurationReader.java | 88 ++++++++ .../joysticks/NullJoystickInterface.java | 17 ++ .../joysticks/SnobotSimJoystickInterface.java | 25 +++ styleguide/pmd-ruleset.xml | 1 + 107 files changed, 5107 insertions(+), 1 deletion(-) create mode 100644 snobot_sim_gui_javafx/build.gradle create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/BaseSimulator.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/DefaultDataAccessorFactory.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/ISimulatorUpdater.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Main.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/RobotContainerFactory.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/Simulator.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorApplication.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloader.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPreloaderController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/SimulatorPropertiesLoader.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/TestMain.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/example_robot/ExampleRobot.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/ConfigurationPaneController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/EnablePanelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/SimulatorFrameController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/Util.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/WidgetGroupController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/BaseGameDataController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/GenericGameDataController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/game_data/PowerUpGameDataController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/ConnectedInputConfigPanelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/CurrentSettingsPanelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/JoystickManagerController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/AnalogInputPanelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/BaseJoystickDisplayController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/DigitalInputPanelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/RawPanelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/WrappedPanelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/joysticks/sub_panels/XboxDisplayController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/motor_graphs/MotorCurveDisplayController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AccelerometerWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AdvancedSettingsController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogInWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/AnalogOutWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/BaseWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/DigitalIOWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/EncoderWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/GyroWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/IWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/RelayWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SolenoidWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/SpeedControllerWidgetController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/BasicSettingsDialog.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/DialogRunner.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/EncoderSettingsDialog.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SensorHandleOption.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/SpeedControllerSettingsDialog.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/SpiI2cSettingsController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/advanced/TankDriveSettingsController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/BaseMotorSimWithDcMotorModelController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/DcMotorModelParamsController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/GravitationalLoadMotorSimController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/IMotorSimController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/RotationalLoadMotorSimController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/SimpleMotorSimController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/StaticLoadMotorSimController.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/CppRobotContainer.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/IRobotClassContainer.java create mode 100644 snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/JavaRobotContainer.java create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/configuration_panel.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/enable_panel.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/generic_game_data.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/power_up_game_data.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/connected_input_config_panel.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/current_settings.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/joystick_manager_controller.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/analog_input_panel.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/digital_input_panel.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/raw_controller.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/wrapped_controller.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/xbox_controller.png create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/xbox_display.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/motor_graphs/motor_curve_display.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/simulator_frame.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widget_group.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/accelerometer_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/advanced_settings_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/analog_in_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/analog_out_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/digital_io_controller_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/encoder_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/gear.png create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/gyro_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/relay_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/spi_i2c_settings.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/tank_drive_settings.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/basic_settings.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/encoder_settings.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/motor_sim/dc_motor_model_panel.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/motor_sim/gravitational_load_sim.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/motor_sim/rotational_load_sim.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/motor_sim/simple_motor_sim.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/motor_sim/static_load_sim.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/speed_controller_settings.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/tank_drive_settings.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/solenoid_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/speed_controller_widget.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/preloader.fxml create mode 100644 snobot_sim_gui_javafx/src/main/resources/themes/snobot_sim.css create mode 100644 snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/IJoystickInterface.java create mode 100644 snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/JoystickConfigurationReader.java create mode 100644 snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/NullJoystickInterface.java create mode 100644 snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/SnobotSimJoystickInterface.java 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 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 simulatorComponents = DataAccessorFactory.getInstance().getSimulatorDataAccessor().getSimulatorComponentConfigs(); + for (Object comp : simulatorComponents) + { + if (comp instanceof TankDriveConfig) + { + addPanel((TankDriveConfig) comp); + } + } + + } + + @FXML + public void handleAddButton() + { + addPanel(new TankDriveConfig()); + } + + private void addPanel(TankDriveConfig aConfig) + { + SingleSettingWidget panel = new SingleSettingWidget(aConfig); + mWidgetPanels.add(panel); + + TitledPane titledPane = new TitledPane("Tank Drive Config", panel); + mPane.getChildren().add(titledPane); + } + + public void onSubmit() + { + Collection toRemove = new ArrayList<>(); + Collection simulatorComponents = DataAccessorFactory.getInstance().getSimulatorDataAccessor().getSimulatorComponentConfigs(); + + for (Object comp : simulatorComponents) + { + if (comp instanceof TankDriveConfig) + { + toRemove.add(comp); + } + } + + for (Object comp : toRemove) + { + DataAccessorFactory.getInstance().getSimulatorDataAccessor().removeSimulatorComponent(comp); + } + + for (SingleSettingWidget panel : mWidgetPanels) + { + TankDriveConfig config = panel.getConfig(); + if (config == null) + { + throw new IllegalArgumentException("Could not create tank drive simulator"); + } + DataAccessorFactory.getInstance().getSimulatorDataAccessor().connectTankDriveSimulator(config.getmLeftMotorHandle(), + config.getmRightMotorHandle(), config.getmGyroHandle(), config.getmTurnKp()); + } + } + + private static class SingleSettingWidget extends GridPane + { + private final ComboBox mLeftMotorSelection; + private final ComboBox mRightMotorSelection; + private final ComboBox mGyroSelection; + private final TextField mKpField; + + private SingleSettingWidget(TankDriveConfig aConfig) + { + SensorHandleOption leftSelection = null; + SensorHandleOption rightSelection = null; + SensorHandleOption gyroSelection = null; + + List speedControllerOptions = new ArrayList<>(); + List speedControllers = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getPortList(); + for (int handle : speedControllers) + { + SensorHandleOption option = new SensorHandleOption(handle, + DataAccessorFactory.getInstance().getSpeedControllerAccessor().getName(handle)); + speedControllerOptions.add(option); + + if (option.mHandle == aConfig.getmLeftMotorHandle()) + { + leftSelection = option; + } + + if (option.mHandle == aConfig.getmRightMotorHandle()) + { + rightSelection = option; + } + } + + List gyroOptions = new ArrayList<>(); + List gyros = DataAccessorFactory.getInstance().getGyroAccessor().getPortList(); + for (int handle : gyros) + { + SensorHandleOption option = new SensorHandleOption(handle, DataAccessorFactory.getInstance().getGyroAccessor().getName(handle)); + gyroOptions.add(option); + + if (option.mHandle == aConfig.getmGyroHandle()) + { + gyroSelection = option; + } + } + + mLeftMotorSelection = new ComboBox<>(FXCollections.observableList(speedControllerOptions)); + mRightMotorSelection = new ComboBox<>(FXCollections.observableList(speedControllerOptions)); + mGyroSelection = new ComboBox<>(FXCollections.observableList(gyroOptions)); + + mLeftMotorSelection.getSelectionModel().select(leftSelection == null ? speedControllerOptions.get(0) : leftSelection); + mRightMotorSelection.getSelectionModel().select(rightSelection == null ? speedControllerOptions.get(0) : rightSelection); + mGyroSelection.getSelectionModel().select(gyroSelection == null ? gyroOptions.get(0) : gyroSelection); + + mKpField = new TextField(Double.toString(aConfig.getmTurnKp())); + + add(new Label("Left Motor"), 0, 0); + add(mLeftMotorSelection, 1, 0); + + add(new Label("Right Motor"), 0, 1); + add(mRightMotorSelection, 1, 1); + + add(new Label("Gyroscope"), 0, 2); + add(mGyroSelection, 1, 2); + + add(new Label("Turning kP"), 0, 3); + add(mKpField, 1, 3); + } + + public TankDriveConfig getConfig() + { + SensorHandleOption left = mLeftMotorSelection.getSelectionModel().getSelectedItem(); + SensorHandleOption right = mRightMotorSelection.getSelectionModel().getSelectedItem(); + SensorHandleOption gyro = mGyroSelection.getSelectionModel().getSelectedItem(); + + if (left == null || right == null || gyro == null) + { + return null; + } + + int leftHandle = left.mHandle; + int rightHanle = right.mHandle; + int gyroHandle = gyro.mHandle; + double kp = 1; + + try + { + kp = Double.parseDouble(mKpField.getText()); + } + catch (NumberFormatException e) + { + LOGGER.log(Level.ERROR, e); + } + + return new TankDriveConfig(leftHandle, rightHanle, gyroHandle, kp); + } + } + +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/BaseMotorSimWithDcMotorModelController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/BaseMotorSimWithDcMotorModelController.java new file mode 100644 index 00000000..95b461dd --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/BaseMotorSimWithDcMotorModelController.java @@ -0,0 +1,10 @@ +package com.snobot.simulator.gui.widgets.settings.motor_sim; + +import javafx.fxml.FXML; + +public abstract class BaseMotorSimWithDcMotorModelController implements IMotorSimController // NOPMD.AbstractClassWithoutAnyMethod +{ + @FXML + protected DcMotorModelParamsController mMotorPanelController; + +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/DcMotorModelParamsController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/DcMotorModelParamsController.java new file mode 100644 index 00000000..d3af4818 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/DcMotorModelParamsController.java @@ -0,0 +1,116 @@ +package com.snobot.simulator.gui.widgets.settings.motor_sim; + +import java.text.DecimalFormat; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.snobot.simulator.gui.motor_graphs.MotorCurveDisplayController; +import com.snobot.simulator.motor_sim.DcMotorModelConfig; +import com.snobot.simulator.wrapper_accessors.DataAccessorFactory; + +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextField; + +public class DcMotorModelParamsController +{ + private static final Logger sLOGGER = LogManager.getLogger(DcMotorModelParamsController.class); + + @FXML + private ComboBox mMotorType; + + @FXML + private Spinner mNumMotors; + + @FXML + private TextField mGearRatio; + + @FXML + private TextField mEfficiency; + + @FXML + private CheckBox mInverted; + + @FXML + private CheckBox mBrake; + + @FXML + private TextField mNominalVoltage; + + @FXML + private TextField mFreeSpeedRpm; + + @FXML + private TextField mNominalCurrent; + + @FXML + private TextField mStallTorque; + + @FXML + private TextField mStallCurrent; + + @FXML + private MotorCurveDisplayController mMotorChartController; + + private final DecimalFormat mDecimalFormat; + + public DcMotorModelParamsController() + { + mDecimalFormat = new DecimalFormat("0.000"); + } + + @FXML + protected void handleMotorType() + { + updateMotorConfig(); + } + + public DcMotorModelConfig getMotorConfig() + { + DcMotorModelConfig output = null; + + try + { + String selectedMotor = mMotorType.getSelectionModel().getSelectedItem(); + int numMotors = (Integer) (mNumMotors.getValue()); + double gearReduction = Double.parseDouble(mGearRatio.getText()); + double efficiency = Double.parseDouble(mEfficiency.getText()); + + output = DataAccessorFactory.getInstance().getSimulatorDataAccessor().createMotor(selectedMotor, numMotors, gearReduction, efficiency, + mInverted.isSelected(), mBrake.isSelected()); + } + catch (NumberFormatException e) + { + sLOGGER.log(Level.ERROR, e); + } + + return output; + } + + private void updateMotorConfig() + { + setModelConfig(getMotorConfig()); + } + + public void setModelConfig(DcMotorModelConfig aConfig) + { + mMotorType.getSelectionModel().select(aConfig.mFactoryParams.mMotorType); + mNumMotors.getValueFactory().setValue(aConfig.mFactoryParams.mNumMotors); + mGearRatio.setText(Double.toString(aConfig.mFactoryParams.mGearReduction)); + mEfficiency.setText(Double.toString(aConfig.mFactoryParams.mGearboxEfficiency)); + + mNominalVoltage.setText(mDecimalFormat.format(aConfig.mMotorParams.mNominalVoltage)); + mFreeSpeedRpm.setText(mDecimalFormat.format(aConfig.mMotorParams.mFreeSpeedRpm)); + mNominalCurrent.setText(mDecimalFormat.format(aConfig.mMotorParams.mFreeCurrent)); + mStallTorque.setText(mDecimalFormat.format(aConfig.mMotorParams.mStallTorque)); + mStallCurrent.setText(mDecimalFormat.format(aConfig.mMotorParams.mStallCurrent)); + mInverted.setSelected(aConfig.mFactoryParams.ismInverted()); + mBrake.setSelected(aConfig.mFactoryParams.ismHasBrake()); + + mMotorChartController.setCurveParams(aConfig); + } +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/GravitationalLoadMotorSimController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/GravitationalLoadMotorSimController.java new file mode 100644 index 00000000..4a4f4495 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/GravitationalLoadMotorSimController.java @@ -0,0 +1,34 @@ +package com.snobot.simulator.gui.widgets.settings.motor_sim; + +import com.snobot.simulator.motor_sim.DcMotorModelConfig; +import com.snobot.simulator.motor_sim.GravityLoadMotorSimulationConfig; +import com.snobot.simulator.wrapper_accessors.DataAccessorFactory; + +import javafx.fxml.FXML; +import javafx.scene.control.TextField; + +public class GravitationalLoadMotorSimController extends BaseMotorSimWithDcMotorModelController +{ + @FXML + private TextField mLoad; + + @Override + public void saveSettings(int aHandle) + { + double load = Double.parseDouble(mLoad.getText()); + DataAccessorFactory.getInstance().getSimulatorDataAccessor().setSpeedControllerModel_Gravitational(aHandle, + mMotorPanelController.getMotorConfig(), + new GravityLoadMotorSimulationConfig(load)); + } + + @Override + public void populate(int aHandle) + { + DcMotorModelConfig modelConfig = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getMotorConfig(aHandle); + mMotorPanelController.setModelConfig(modelConfig); + + GravityLoadMotorSimulationConfig config = DataAccessorFactory.getInstance().getSpeedControllerAccessor() + .getMotorSimGravitationalModelConfig(aHandle); + mLoad.setText(Double.toString(config.getLoad())); + } +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/IMotorSimController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/IMotorSimController.java new file mode 100644 index 00000000..d0f36f12 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/IMotorSimController.java @@ -0,0 +1,10 @@ +package com.snobot.simulator.gui.widgets.settings.motor_sim; + +public interface IMotorSimController +{ + + void saveSettings(int aHandle); + + void populate(int aHandle); + +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/RotationalLoadMotorSimController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/RotationalLoadMotorSimController.java new file mode 100644 index 00000000..41bcef9a --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/RotationalLoadMotorSimController.java @@ -0,0 +1,30 @@ +package com.snobot.simulator.gui.widgets.settings.motor_sim; + +import com.snobot.simulator.motor_sim.DcMotorModelConfig; +import com.snobot.simulator.motor_sim.RotationalLoadMotorSimulationConfig; +import com.snobot.simulator.wrapper_accessors.DataAccessorFactory; + +import javafx.fxml.FXML; +import javafx.scene.control.TextField; + +public class RotationalLoadMotorSimController extends BaseMotorSimWithDcMotorModelController +{ + @FXML + private TextField mLoad; + + @Override + public void saveSettings(int aHandle) + { + DataAccessorFactory.getInstance().getSimulatorDataAccessor().setSpeedControllerModel_Rotational(aHandle, + mMotorPanelController.getMotorConfig(), + new RotationalLoadMotorSimulationConfig(0, 0)); + } + + @Override + public void populate(int aHandle) + { + DcMotorModelConfig modelConfig = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getMotorConfig(aHandle); + mMotorPanelController.setModelConfig(modelConfig); + + } +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/SimpleMotorSimController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/SimpleMotorSimController.java new file mode 100644 index 00000000..e8c0b82b --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/SimpleMotorSimController.java @@ -0,0 +1,28 @@ +package com.snobot.simulator.gui.widgets.settings.motor_sim; + +import com.snobot.simulator.motor_sim.SimpleMotorSimulationConfig; +import com.snobot.simulator.wrapper_accessors.DataAccessorFactory; + +import javafx.fxml.FXML; +import javafx.scene.control.TextField; + +public class SimpleMotorSimController implements IMotorSimController +{ + @FXML + private TextField mMaxSpeed; + + @Override + public void saveSettings(int aHandle) + { + double maxSpeed = Double.parseDouble(mMaxSpeed.getText()); + DataAccessorFactory.getInstance().getSimulatorDataAccessor().setSpeedControllerModel_Simple(aHandle, + new SimpleMotorSimulationConfig(maxSpeed)); + } + + @Override + public void populate(int aHandle) + { + SimpleMotorSimulationConfig config = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getMotorSimSimpleModelConfig(aHandle); + mMaxSpeed.setText(Double.toString(config.mMaxSpeed)); + } +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/StaticLoadMotorSimController.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/StaticLoadMotorSimController.java new file mode 100644 index 00000000..4c2bc5a0 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/gui/widgets/settings/motor_sim/StaticLoadMotorSimController.java @@ -0,0 +1,32 @@ +package com.snobot.simulator.gui.widgets.settings.motor_sim; + +import com.snobot.simulator.motor_sim.DcMotorModelConfig; +import com.snobot.simulator.motor_sim.StaticLoadMotorSimulationConfig; +import com.snobot.simulator.wrapper_accessors.DataAccessorFactory; + +import javafx.fxml.FXML; +import javafx.scene.control.TextField; + +public class StaticLoadMotorSimController extends BaseMotorSimWithDcMotorModelController +{ + @FXML + private TextField mLoad; + + @Override + public void saveSettings(int aHandle) + { + double load = Double.parseDouble(mLoad.getText()); + DataAccessorFactory.getInstance().getSimulatorDataAccessor().setSpeedControllerModel_Static(aHandle, mMotorPanelController.getMotorConfig(), + new StaticLoadMotorSimulationConfig(load)); + } + + @Override + public void populate(int aHandle) + { + DcMotorModelConfig modelConfig = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getMotorConfig(aHandle); + mMotorPanelController.setModelConfig(modelConfig); + + StaticLoadMotorSimulationConfig config = DataAccessorFactory.getInstance().getSpeedControllerAccessor().getMotorSimStaticModelConfig(aHandle); + mLoad.setText(Double.toString(config.mLoad)); + } +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/CppRobotContainer.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/CppRobotContainer.java new file mode 100644 index 00000000..26e784c7 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/CppRobotContainer.java @@ -0,0 +1,55 @@ +package com.snobot.simulator.robot_container; + +import java.lang.reflect.Method; + +import com.snobot.simulator.JniLibraryResourceLoader; + +/** + * Wrapper class around a C++ robot's code + * + * @author PJ + * + */ +public class CppRobotContainer implements IRobotClassContainer +{ + private final String mRobotClassName; + private Class mJniClass; + + public CppRobotContainer(String aRobotClassName) + { + mRobotClassName = aRobotClassName; + } + + @Override + public void constructRobot() + throws ReflectiveOperationException + { + mJniClass = Class.forName(mRobotClassName); + + String libraryName = (String) mJniClass.getMethod("getLibraryName").invoke(null); + + String openCvVersion = "343"; + + JniLibraryResourceLoader.loadLibrary("ntcore"); + JniLibraryResourceLoader.loadLibrary("opencv_core" + openCvVersion); + JniLibraryResourceLoader.loadLibrary("opencv_imgproc" + openCvVersion); + JniLibraryResourceLoader.loadLibrary("opencv_imgcodecs" + openCvVersion); + JniLibraryResourceLoader.loadLibrary("cscore"); + JniLibraryResourceLoader.loadLibrary("wpiutil"); + JniLibraryResourceLoader.loadLibrary("cameraserver"); + JniLibraryResourceLoader.loadLibrary("wpilibc"); + JniLibraryResourceLoader.loadLibrary("snobot_sim"); + JniLibraryResourceLoader.loadLibrary(libraryName); + + Method method = mJniClass.getMethod("createRobot"); + method.invoke(null); + } + + @Override + public void startCompetition() + throws ReflectiveOperationException + { + Method method = mJniClass.getMethod("startCompetition"); + method.invoke(null); + } +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/IRobotClassContainer.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/IRobotClassContainer.java new file mode 100644 index 00000000..76a795f2 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/IRobotClassContainer.java @@ -0,0 +1,16 @@ +package com.snobot.simulator.robot_container; + +/** + * Interface that provides a wrapper around a robot class + * + * @author PJ + * + */ +public interface IRobotClassContainer +{ + public void constructRobot() + throws ReflectiveOperationException; + + public void startCompetition() + throws ReflectiveOperationException; +} diff --git a/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/JavaRobotContainer.java b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/JavaRobotContainer.java new file mode 100644 index 00000000..50932f80 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/java/com/snobot/simulator/robot_container/JavaRobotContainer.java @@ -0,0 +1,37 @@ +package com.snobot.simulator.robot_container; + +import edu.wpi.first.wpilibj.RobotBase; + +/** + * Wrapper class around a Java robot + * + * @author PJ + * + */ +public class JavaRobotContainer implements IRobotClassContainer +{ + private final String mRobotClassName; + private RobotBase mRobot; + + public JavaRobotContainer(String aRobotClassName) + { + mRobotClassName = aRobotClassName; + } + + @Override + public void constructRobot() throws ReflectiveOperationException + { + mRobot = (RobotBase) Class.forName(mRobotClassName).newInstance(); + } + + @Override + public void startCompetition() + { + mRobot.startCompetition(); + } + + public RobotBase getJavaRobot() + { + return mRobot; + } +} diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/configuration_panel.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/configuration_panel.fxml new file mode 100644 index 00000000..33069158 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/configuration_panel.fxml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/enable_panel.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/enable_panel.fxml new file mode 100644 index 00000000..df2cae8a --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/enable_panel.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/generic_game_data.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/generic_game_data.fxml new file mode 100644 index 00000000..1eb7d975 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/generic_game_data.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/power_up_game_data.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/power_up_game_data.fxml new file mode 100644 index 00000000..4e682b30 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/game_data/power_up_game_data.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/connected_input_config_panel.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/connected_input_config_panel.fxml new file mode 100644 index 00000000..e21c4c97 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/connected_input_config_panel.fxml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/current_settings.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/current_settings.fxml new file mode 100644 index 00000000..34753b18 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/current_settings.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/joystick_manager_controller.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/joystick_manager_controller.fxml new file mode 100644 index 00000000..fcb1c1ad --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/joystick_manager_controller.fxml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+
+
diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/analog_input_panel.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/analog_input_panel.fxml new file mode 100644 index 00000000..8d0c08ec --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/analog_input_panel.fxml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/digital_input_panel.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/digital_input_panel.fxml new file mode 100644 index 00000000..a203a079 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/digital_input_panel.fxml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/raw_controller.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/raw_controller.fxml new file mode 100644 index 00000000..40ec0f17 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/raw_controller.fxml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/wrapped_controller.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/wrapped_controller.fxml new file mode 100644 index 00000000..f2ef25f1 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/wrapped_controller.fxml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/xbox_controller.png b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/xbox_controller.png new file mode 100644 index 0000000000000000000000000000000000000000..2756d16ae3dfb27d28bbe5c5a18d6110c7aa1a12 GIT binary patch literal 99960 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 N6T?G$^9>v>{SUco`s)Ay literal 0 HcmV?d00001 diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/xbox_display.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/xbox_display.fxml new file mode 100644 index 00000000..568fa35b --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/joysticks/sub_panels/xbox_display.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/motor_graphs/motor_curve_display.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/motor_graphs/motor_curve_display.fxml new file mode 100644 index 00000000..40ab5b57 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/motor_graphs/motor_curve_display.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + +
+ + + + + + + + +
+
diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/simulator_frame.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/simulator_frame.fxml new file mode 100644 index 00000000..d69c9ce5 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/simulator_frame.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + +
diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/advanced_settings_widget.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/advanced_settings_widget.fxml new file mode 100644 index 00000000..71821b0e --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/advanced_settings_widget.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/analog_out_widget.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/analog_out_widget.fxml new file mode 100644 index 00000000..dd109cb4 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/analog_out_widget.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/digital_io_controller_widget.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/digital_io_controller_widget.fxml new file mode 100644 index 00000000..54729f12 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/digital_io_controller_widget.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/encoder_widget.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/encoder_widget.fxml new file mode 100644 index 00000000..e2f625d1 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/encoder_widget.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/gear.png b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/gear.png new file mode 100644 index 0000000000000000000000000000000000000000..b84d8ec91a2183cae7b468a70c2ed8996484246c GIT binary patch literal 755 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf02y>eSaefwW^{L9 za%BKPWN%_+AW3auXJt}lVPtu6$z?nM00Lu4L_t(IPnA^5>uFIGUY?0$Af+gz$Rh(2 z0|NtL;O{l?7*nDcc;yvQo|zzT*SDPQ9`fsV?^nBX_Bv~S>sybr754||2OJIu(&=<` zy5yKpC#%)Uwx6Jso^R;&dX&v(DWA{tez90kx7($9y-t(Kgl)Usj-Q|Ss<2wE z(rh*(l}aU99*znH-EKEBo6Y2QyQx$vk-=b~Xf#S%tyc1qDMWgeN`=!y45d=ZB=Us- z>-Cz+G#U-7Y&04Y!xJxsg+hTzg(|taZ@@9EWHQOY1^DfJKJ!McR`bW-$N(q?VXao9 za=A?7@tEyQ2>Fpngf5p0I||?cfX2t1$9X}Mf-(*VVOzrRnXQ(CQ7wApN4 z>TnPN0)YTIoldq51_NrfTAYap3_mQ?K9x$5VmKV$F%IaIJXNR(27}~qI3&vx4}WSl zo17JZ&f`5EkMmx+-EP?^FD!tFpJV{k;+p`JA~q|?0=Ca7K!yMYUGeDNJ(tUI9Yi!hG{x`tlgs7eJruOt?UyfrG%XejnM@`w z5f3&9Pp1<{mPZ$)qGtFe?=XOKu~>|ZMkDW;a5Ngx@p$A}!_SXH{{jRQ3th&fa19~V zAr#;+{!d&_a7m}roK_~2;TzlQ^^)Cgmrne}S3&`F l5PtyfJrDp;ELZdnqCe@9iVTG}`^x|T002ovPDHLkV1mt~J+}Y= literal 0 HcmV?d00001 diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/gyro_widget.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/gyro_widget.fxml new file mode 100644 index 00000000..701eee23 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/gyro_widget.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/relay_widget.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/relay_widget.fxml new file mode 100644 index 00000000..e4beda70 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/relay_widget.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/spi_i2c_settings.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/spi_i2c_settings.fxml new file mode 100644 index 00000000..4da57ab8 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/spi_i2c_settings.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/tank_drive_settings.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/tank_drive_settings.fxml new file mode 100644 index 00000000..21cf502d --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/settings/advanced/tank_drive_settings.fxml @@ -0,0 +1,15 @@ + + + + + + + +
+ + +
+ + + + + +
diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/speed_controller_widget.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/speed_controller_widget.fxml new file mode 100644 index 00000000..ff7dd22b --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/gui/widgets/speed_controller_widget.fxml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/preloader.fxml b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/preloader.fxml new file mode 100644 index 00000000..6cdc0ad8 --- /dev/null +++ b/snobot_sim_gui_javafx/src/main/resources/com/snobot/simulator/preloader.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + +
+ + + + +
+ + + + +
diff --git a/snobot_sim_gui_javafx/src/main/resources/themes/snobot_sim.css b/snobot_sim_gui_javafx/src/main/resources/themes/snobot_sim.css new file mode 100644 index 00000000..e69de29b diff --git a/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/IJoystickInterface.java b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/IJoystickInterface.java new file mode 100644 index 00000000..f36a1269 --- /dev/null +++ b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/IJoystickInterface.java @@ -0,0 +1,9 @@ +package com.snobot.simulator.joysticks; + +public interface IJoystickInterface +{ + + void sendJoystickUpdate(); + + void waitForLoop(); +} diff --git a/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/JoystickConfigurationReader.java b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/JoystickConfigurationReader.java new file mode 100644 index 00000000..175b2430 --- /dev/null +++ b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/JoystickConfigurationReader.java @@ -0,0 +1,88 @@ +//package com.snobot.simulator.joysticks; +// +//import java.io.File; +//import java.io.FileInputStream; +//import java.io.FileOutputStream; +//import java.io.IOException; +//import java.io.InputStream; +//import java.nio.file.Files; +//import java.nio.file.Paths; +//import java.util.Map; +//import java.util.Map.Entry; +//import java.util.Properties; +// +//import org.apache.logging.log4j.Level; +// +//import edu.wpi.first.wpilibj.DriverStation; +// +//public class JoystickConfigurationReader +//{ +// +// private final Map mControllerConfig; +// +// private void writeJoystickFile() +// { +// try +// { +// Properties p = new Properties(); +// for (int i = 0; i < DriverStation.kJoystickPorts; ++i) +// { +// String joystickName = mJoystickMap[i].getName(); +// +// ControllerConfiguration config = mControllerConfig.get(joystickName); +// String specializationName = config == null ? null : config.mSpecialization.getName(); +// +// p.put(sKEY + i, joystickName + "---" + specializationName); +// } +// +// try (FileOutputStream stream = new FileOutputStream(sJOYSTICK_CONFIG_FILE)) +// { +// p.store(stream, ""); +// } +// +// sLOGGER.log(Level.INFO, "Wrote joystick config file to " + new File(sJOYSTICK_CONFIG_FILE).getAbsolutePath()); +// } +// catch (Exception ex) +// { +// sLOGGER.log(Level.ERROR, ex); +// } +// } +// +// private void readLegacyFile(String aConfigFile) +// { +// if (!Files.exists(Paths.get(aConfigFile))) +// { +// writeJoystickFile(); +// return; +// } +// +// try +// { +// InputStream inputStream = new FileInputStream(aConfigFile); +// Properties properties = new Properties(); +// properties.load(inputStream); +// inputStream.close(); +// +// for (Entry i : properties.entrySet()) +// { +// int number = Integer.parseInt(i.getKey().toString().substring(sKEY.length())); +// +// String config = i.getValue().toString(); +// String[] parts = config.split("---"); +// +// String joystickName = parts[0]; +// String specialization = parts[1]; +// +// if (!"null".equals(specialization)) +// { +// setSpecialization(joystickName, (Class) Class.forName(specialization), false); +// } +// setJoysticks(number, joystickName, false); +// } +// } +// catch (IOException | ClassNotFoundException ex) +// { +// sLOGGER.log(Level.ERROR, ex); +// } +// } +//} diff --git a/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/NullJoystickInterface.java b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/NullJoystickInterface.java new file mode 100644 index 00000000..2227e211 --- /dev/null +++ b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/NullJoystickInterface.java @@ -0,0 +1,17 @@ +package com.snobot.simulator.joysticks; + +public class NullJoystickInterface implements IJoystickInterface +{ + @Override + public void sendJoystickUpdate() + { + + } + + @Override + public void waitForLoop() + { + + } + +} diff --git a/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/SnobotSimJoystickInterface.java b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/SnobotSimJoystickInterface.java new file mode 100644 index 00000000..43f4cea6 --- /dev/null +++ b/snobot_sim_joysticks/src/main/java/com/snobot/simulator/joysticks/SnobotSimJoystickInterface.java @@ -0,0 +1,25 @@ +package com.snobot.simulator.joysticks; + +import com.snobot.simulator.wrapper_accessors.DataAccessorFactory; + +public class SnobotSimJoystickInterface implements IJoystickInterface +{ + @Override + public void sendJoystickUpdate() + { + IMockJoystick[] joysticks = JoystickFactory.getInstance().getAll(); + for (int i = 0; i < joysticks.length; ++i) + { + IMockJoystick joystick = joysticks[i]; + DataAccessorFactory.getInstance().getDriverStationAccessor().setJoystickInformation(i, joystick.getAxisValues(), joystick.getPovValues(), + joystick.getButtonCount(), joystick.getButtonMask()); + } + } + + @Override + public void waitForLoop() + { + DataAccessorFactory.getInstance().getDriverStationAccessor().waitForNextUpdateLoop(); + } + +} diff --git a/styleguide/pmd-ruleset.xml b/styleguide/pmd-ruleset.xml index b3c84365..755ea55d 100644 --- a/styleguide/pmd-ruleset.xml +++ b/styleguide/pmd-ruleset.xml @@ -83,6 +83,7 @@ +