From ae4c4524af33904a35996b6ba6e48522918a1c80 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 31 Dec 2022 15:30:34 -0800 Subject: [PATCH 01/60] fix build and add code for swerve simulation --- WPIWebSockets | 2 +- example/build.gradle | 189 +++++++++--------- example/src/main/java/frc/robot/Robot.java | 2 +- plugin/build.gradle | 2 +- .../controller/src/main/java/DeepBlueSim.java | 36 +++- .../team199/deepbluesim/BaseSimConfig.java | 61 ------ .../org/team199/deepbluesim/Constants.java | 17 +- .../org/team199/deepbluesim/SimConfig.java | 9 - .../team199/deepbluesim/SimRegisterer.java | 8 +- .../deepbluesim/mediators/MockGyro.java | 14 +- .../deepbluesim/mediators/MockedCANCoder.java | 45 +++++ .../deepbluesim/mediators/MockedEncoder.java | 4 +- .../mediators/MockedSparkEncoder.java | 20 +- .../mediators/WebotsMotorForwarder.java | 11 +- 14 files changed, 212 insertions(+), 208 deletions(-) delete mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/BaseSimConfig.java delete mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/SimConfig.java create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java diff --git a/WPIWebSockets b/WPIWebSockets index e131336b..fd957641 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit e131336bfa9dd2333dad0237f03315c0d80fc4ff +Subproject commit fd9576414abb3b4f1d2073e22271f6334b40cd61 diff --git a/example/build.gradle b/example/build.gradle index 990b20e4..b04caaf0 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,6 +1,6 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2021.1.2" + id "edu.wpi.first.GradleRIO" version "2022.4.1" id "org.team199.deepbluesim" version "0.0.12" } @@ -13,27 +13,27 @@ def ROBOT_MAIN_CLASS = "frc.robot.Main" // This is added by GradleRIO's backing project EmbeddedTools. deploy { targets { - roboRIO("roborio") { + roborio(getTargetTypeClass('RoboRIO')) { // Team number is loaded either from the .wpilib/wpilib_preferences.json // or from command line. If not found an exception will be thrown. // You can use getTeamOrDefault(team) instead of getTeamNumber if you // want to store a team number in this file. - team = frc.getTeamNumber() - } - } - artifacts { - frcJavaArtifact('frcJava') { - targets << "roborio" - // Debug can be overridden by command line, for use with VSCode - debug = frc.getDebugOrDefault(false) - } - // Built in artifact to deploy arbitrary files to the roboRIO. - fileTreeArtifact('frcStaticFileDeploy') { - // The directory below is the local directory to deploy - files = fileTree(dir: 'src/main/deploy') - // Deploy to RoboRIO target, into /home/lvuser/deploy - targets << "roborio" - directory = '/home/lvuser/deploy' + team = project.frc.getTeamNumber() + debug = project.frc.getDebugOrDefault(false) + + artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + + frcJava(getArtifactTypeClass('FRCJavaArtifact')) { + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/lvuser/deploy' + } + } } } } @@ -44,35 +44,42 @@ def includeDesktopSupport = true // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. // Also defines JUnit 4. dependencies { - implementation wpi.deps.wpilib() - nativeZip wpi.deps.wpilibJni(wpi.platforms.roborio) - nativeDesktopZip wpi.deps.wpilibJni(wpi.platforms.desktop) + implementation wpi.java.deps.wpilib() + implementation wpi.java.vendor.java() + roborioDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.roborio) + roborioDebug wpi.java.vendor.jniDebug(wpi.platforms.roborio) - implementation wpi.deps.vendor.java() - nativeZip wpi.deps.vendor.jni(wpi.platforms.roborio) - nativeDesktopZip wpi.deps.vendor.jni(wpi.platforms.desktop) + roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio) + roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio) - testImplementation 'junit:junit:4.12' + nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) + nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) + simulationDebug wpi.sim.enableDebug() - // Enable simulation gui support (except during Continuous Integration). - // Must check the box in vscode to enable support upon debugging - if (System.getenv()['CI'] == null) { - simulation wpi.deps.sim.gui(wpi.platforms.desktop, false) - simulation wpi.deps.sim.driverstation(wpi.platforms.desktop, false) - } + nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) + nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) + simulationRelease wpi.sim.enableRelease() - // Websocket extensions require additional configuration. - simulation wpi.deps.sim.ws_server(wpi.platforms.desktop, false) - // simulation wpi.deps.sim.ws_client(wpi.platforms.desktop, false) + testImplementation 'junit:junit:4.12' } +// Enable simulation gui support (except during Continuous Integration). +// Must check the box in vscode to enable support upon debugging +if (System.getenv()['CI'] == null) { + wpi.sim.addGui().defaultEnabled = true + wpi.sim.addDriverstation() +} + +wpi.sim.addWebsocketsServer() + // Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') // in order to make them all available at runtime. Also adding the manifest so WPILib // knows where to look for our Robot Class. jar { from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } // Support a systemTest task that runs a system level functional test in the simulator @@ -97,64 +104,64 @@ task('systemTestJar', type: Jar) { manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest("frc.robot.SystemTestRobot") } -task('systemTest', type: SynchronousJavaSimulationTask) { - dependsOn 'systemTestJar' - dependsOn 'extractTestJNI' -} +// task('systemTest', type: SynchronousJavaSimulationTask) { +// dependsOn 'systemTestJar' +// dependsOn 'extractTestJNI' +// } assemble.dependsOn installDeepBlueSim -check.dependsOn 'systemTest' - -class SynchronousJavaSimulationTask extends edu.wpi.first.gradlerio.test.JavaSimulationTask { - @Override - Process launch(List cmd) { - // Let the base class handle these situations - if (scriptOnly || project.hasProperty('headless')) { - return super.launch(cmd) - } - // Remove the extraneous double quotes added by the caller - cmd = cmd*.replace('"','') - def builder = new ProcessBuilder(cmd) - // Incorporate the task's env and the simulation extension's env (e.g. HALSIM_EXTENSIONS) - def env = builder.environment() - env.putAll environment - env.putAll project.extensions.getByType(edu.wpi.first.gradlerio.wpi.simulation.SimulationExtension).environment - // Set the working dir if specified - if (workingDir != null) { - workingDir.mkdirs() - builder.directory(workingDir) - } - // Redirect stderr to stdout - builder.redirectErrorStream(true) - // Start the process - Process p = builder.start() - println "Waiting for simulation to finish..." - // Copy the output asynchronously so that the current thread can be interrupted - // (e.g. if the task times out) - try { - p.inputStream.withStream { stream -> - def executor = java.util.concurrent.Executors.newFixedThreadPool(1) - def copyStreamCallable = new java.util.concurrent.Callable() { - def buf = new byte[8192] - @Override - Integer call() { - int length = stream.read(buf) - if (length > 0) - System.out.write(buf, 0, length) - return length - } - } - while (executor.submit(copyStreamCallable).get() > 0) { - } - } - } catch (InterruptedException) { - p.destroy() - } - p.waitFor() - if (p.exitValue() != 0) - throw new RuntimeException("Simulation failed with exit code ${p.exitValue()}.") - return p - } -} +// check.dependsOn 'systemTest' + +// class SynchronousJavaSimulationTask extends edu.wpi.first.gradlerio.simulation.JavaSi mulationTask { +// @Override +// Process launch(List cmd) { +// // Let the base class handle these situations +// if (scriptOnly || project.hasProperty('headless')) { +// return super.launch(cmd) +// } +// // Remove the extraneous double quotes added by the caller +// cmd = cmd*.replace('"','') +// def builder = new ProcessBuilder(cmd) +// // Incorporate the task's env and the simulation extension's env (e.g. HALSIM_EXTENSIONS) +// def env = builder.environment() +// env.putAll environment +// env.putAll project.extensions.getByType(edu.wpi.first.gradlerio.wpi.simulation.SimulationExtension).environment +// // Set the working dir if specified +// if (workingDir != null) { +// workingDir.mkdirs() +// builder.directory(workingDir) +// } +// // Redirect stderr to stdout +// builder.redirectErrorStream(true) +// // Start the process +// Process p = builder.start() +// println "Waiting for simulation to finish..." +// // Copy the output asynchronously so that the current thread can be interrupted +// // (e.g. if the task times out) +// try { +// p.inputStream.withStream { stream -> +// def executor = java.util.concurrent.Executors.newFixedThreadPool(1) +// def copyStreamCallable = new java.util.concurrent.Callable() { +// def buf = new byte[8192] +// @Override +// Integer call() { +// int length = stream.read(buf) +// if (length > 0) +// System.out.write(buf, 0, length) +// return length +// } +// } +// while (executor.submit(copyStreamCallable).get() > 0) { +// } +// } +// } catch (InterruptedException) { +// p.destroy() +// } +// p.waitFor() +// if (p.exitValue() != 0) +// throw new RuntimeException("Simulation failed with exit code ${p.exitValue()}.") +// return p +// } +// } diff --git a/example/src/main/java/frc/robot/Robot.java b/example/src/main/java/frc/robot/Robot.java index 940229fd..6ce1ec07 100644 --- a/example/src/main/java/frc/robot/Robot.java +++ b/example/src/main/java/frc/robot/Robot.java @@ -8,7 +8,7 @@ package frc.robot; import edu.wpi.first.wpilibj.Joystick; -import edu.wpi.first.wpilibj.PWMVictorSPX; +import edu.wpi.first.wpilibj.motorcontrol.PWMVictorSPX; import edu.wpi.first.wpilibj.TimedRobot; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.drive.DifferentialDrive; diff --git a/plugin/build.gradle b/plugin/build.gradle index 3c99101e..90edf6a3 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -30,7 +30,7 @@ repositories { dependencies { // Use the awesome Spock testing and specification framework - testImplementation 'org.spockframework:spock-core:1.3-groovy-2.5' + testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0' testImplementation 'edu.wpi.first:GradleRIO:2021.1.2' implementation 'commons-io:commons-io:2.8.0' } diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 9f66f9a2..11bb5744 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -1,3 +1,4 @@ +import java.lang.Thread.UncaughtExceptionHandler; import java.net.URISyntaxException; import java.util.concurrent.ConcurrentLinkedDeque; @@ -7,11 +8,11 @@ import org.team199.wpiws.Pair; import org.team199.wpiws.ScopedObject; import org.team199.wpiws.connection.ConnectionProcessor; +import org.team199.wpiws.connection.RunningObject; import org.team199.wpiws.connection.WSConnection; import org.team199.wpiws.devices.SimDeviceSim; import org.team199.wpiws.interfaces.StringCallback; - -import org.team199.deepbluesim.SimConfig; +import org.java_websocket.client.WebSocketClient; import org.team199.deepbluesim.Simulation; // NOTE: Webots expects the controller class to *not* be in a package and have a name that matches the @@ -21,13 +22,25 @@ public class DeepBlueSim { private static final ConcurrentLinkedDeque queuedMessages = new ConcurrentLinkedDeque<>(); private static ScopedObject> callbackStore = null; + private static RunningObject wsConnection = null; + public static void main(String[] args) { + UncaughtExceptionHandler eh = new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread arg0, Throwable arg1) { + arg1.printStackTrace(System.err); + System.err.flush(); + System.exit(1); + } + }; + Thread.setDefaultUncaughtExceptionHandler(eh); + Thread.currentThread().setUncaughtExceptionHandler(eh); + ConnectionProcessor.setThreadExecutor(queuedMessages::add); final Supervisor robot = new Supervisor(); Runtime.getRuntime().addShutdownHook(new Thread(robot::delete)); int basicTimeStep = (int)Math.round(robot.getBasicTimeStep()); - - SimConfig.initConfig(); + Simulation.init(robot, robot.getBasicTimeStep()); // Use a SimDeviceSim to coordinate with robot code tests @@ -54,7 +67,7 @@ public void run() { @Override public void callback(String name, String value) { System.out.println("Telling the robot we're ready"); - webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); + webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); } }, true); @@ -74,7 +87,7 @@ public void callback(String name, String value) { } try { System.out.println("Trying to connect to robot..."); - WSConnection.connectHALSim(true); + wsConnection = WSConnection.connectHALSim(true); } catch(URISyntaxException e) { System.err.println("Error occured connecting to server:"); e.printStackTrace(System.err); @@ -83,11 +96,22 @@ public void callback(String name, String value) { return; } + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + wsConnection.object.closeBlocking(); + } catch(InterruptedException e) {} + })); + while(robot.step(basicTimeStep) != -1) { queuedMessages.forEach(Runnable::run); queuedMessages.clear(); Simulation.runPeriodicMethods(); } + + System.out.println("Shutting down DeepBlueSim..."); + System.out.flush(); + + System.exit(0); } } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/BaseSimConfig.java b/plugin/controller/src/main/java/org/team199/deepbluesim/BaseSimConfig.java deleted file mode 100644 index 4cfc2cd4..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/BaseSimConfig.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.team199.deepbluesim; - -import java.util.HashMap; - -/** - * Stores basic properties about how to configure the robot simulation - */ -public class BaseSimConfig { - private static int sensorTimestep = 20; - private static double defaultMotorGearing = 1; - private static HashMap motorGearings = new HashMap<>(); - - /** - * Sets the sampling period to be used when enabling Webots sensors - * @param timestep the sampling period - * @see #getSensorTimestep() - */ - protected static void setSensorTimestep(int timestep) { - sensorTimestep = timestep; - } - - /** - * Retrieves the sampling period to be used when enabling Webots sensors - * @see #setSensorTimestep(int) - */ - public static int getSensorTimestep() { - return sensorTimestep; - } - - /** - * Sets the default motor gearing to be used when a specific one is not set - * @param gearing the new default motor gearing - * @see #setMotorGearing(String, double) - * @see #getMotorGearing(String) - */ - protected static void setDefaultMotorGearing(double gearing) { - defaultMotorGearing = gearing; - } - - /** - * Sets the motor gearing for a specific motor - * @param motor the name of the motor - * @param diameter the gearing of the specified motor - * @see #setDefaultMotorGearing(double) - * @see #getMotorGearing(String) - */ - protected static void setMotorGearing(String motor, double diameter) { - motorGearings.put(motor, diameter); - } - - /** - * Retrieves the motor gearing for a specific motor - * @param motor the name of the motor - * @return the motor gearing for the specified motor - * @see #setMotorGearing(String, double) - * @see #setDefaultMotorGearing(double) - */ - public static double getMotorGearing(String motor) { - return motorGearings.containsKey(motor) ? motorGearings.get(motor) : defaultMotorGearing; - } -} \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/Constants.java b/plugin/controller/src/main/java/org/team199/deepbluesim/Constants.java index 73f359fd..73e710a8 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/Constants.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/Constants.java @@ -8,20 +8,5 @@ package org.team199.deepbluesim; public final class Constants { - public static final double motorGearing = 6.8; - // Max speed of a NEO in rad/s. Used for specifying motor velocity in Webots - public static final double neoMotorConstant = (5676 / motorGearing) * (Math.PI * 2) / 60.; - public static final double wheelDiameter = 5 * 0.0254; - public static final double maxSpeed = neoMotorConstant * wheelDiameter / 2; - public static final double wheelBase = 0.46101; - public static final double trackWidth = 0.45085; - - public static final int joystickPort = 0; - - public static class CANPorts { - public static final int dtFrontLeft = 0; - public static final int dtFrontRight = 1; - public static final int dtBackLeft = 2; - public static final int dtBackRight = 3; - } + public static int sensorTimestep = 20; } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimConfig.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimConfig.java deleted file mode 100644 index 6503767a..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.team199.deepbluesim; - -public class SimConfig extends BaseSimConfig { - - public static void initConfig() { - setDefaultMotorGearing(Constants.motorGearing); - } - -} \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java index d134e51c..d6489031 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java @@ -58,9 +58,11 @@ private static void callback(String deviceName) { // If a navX is registered, try to link its SimDevice to the Webots robot MockGyro.linkGyro(); } - if(deviceName.startsWith("CANEncoder_")) { - //deviceName should be CANEncoder_ - new MockedSparkEncoder(new SimDeviceSim(deviceName), deviceName.substring(11)); + if(deviceName.startsWith("RelativeEncoder")) { + new MockedSparkEncoder(new SimDeviceSim(deviceName), deviceName); + } + if(deviceName.startsWith("CANCoder")) { + new MockedCANCoder(new SimDeviceSim(deviceName), deviceName); } } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java index 009dc896..45552879 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java @@ -2,7 +2,7 @@ import com.cyberbotics.webots.controller.Gyro; -import org.team199.deepbluesim.BaseSimConfig; +import org.team199.deepbluesim.Constants; import org.team199.deepbluesim.Simulation; import org.team199.wpiws.devices.SimDeviceSim; @@ -28,17 +28,19 @@ public static void linkGyro() { // Create Sims gyroSim = new SimDeviceSim("navX-Sensor[0]"); webotsGyro = Simulation.getRobot().getGyro("gyro"); - webotsGyro.enable(BaseSimConfig.getSensorTimestep()); - Simulation.registerPeriodicMethod(new MockGyro()); + if(webotsGyro != null) { + webotsGyro.enable(Constants.sensorTimestep); + Simulation.registerPeriodicMethod(new MockGyro()); + } } @Override public void run() { /* getValues() returns angular speeds about each axis (x, y, z). - reading represents the change in angular position about the y axis. - getValues()[1] is negated to convert from Webot's coordinate system (counter-clockwise = positive) to WPIlib's coordinate system (counter-clockwise = negative). + reading represents the change in angular position about the z axis. + getValues()[2] is negated to convert from Webot's coordinate system (counter-clockwise = positive) to WPIlib's coordinate system (counter-clockwise = negative). */ - double reading = -webotsGyro.getValues()[1] * Simulation.getBasicTimeStep(); + double reading = -webotsGyro.getValues()[2] * Simulation.getBasicTimeStep(); // In testing, reading was sometimes NAN in the first second of the simulation. // Also convert from radians to degrees angle += Double.isNaN(reading) ? 0 : (180 * reading / Math.PI); diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java new file mode 100644 index 00000000..b548ca49 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java @@ -0,0 +1,45 @@ +package org.team199.deepbluesim.mediators; + +import com.cyberbotics.webots.controller.PositionSensor; + +import org.team199.deepbluesim.Constants; +import org.team199.deepbluesim.Simulation; +import org.team199.wpiws.devices.SimDeviceSim; +import org.team199.wpiws.interfaces.StringCallback; + +public class MockedCANCoder implements Runnable, StringCallback { + + private SimDeviceSim device; + private PositionSensor webotsEncoder; + private double gearing; + + public MockedCANCoder(SimDeviceSim device, String name) { + this.device = device; + gearing = 1; + device.registerValueChangedCallback("gearing", this, true); + webotsEncoder = Simulation.getRobot().getPositionSensor(name); + if(webotsEncoder != null) { + webotsEncoder.enable(Constants.sensorTimestep); + Simulation.registerPeriodicMethod(this); + } + } + + @Override + public void callback(String name, String value) { + if(value == null) return; // Value has not yet been set + try { + gearing = Double.parseDouble(value); + } catch(NumberFormatException e) {} + } + + @Override + public void run() { + if(webotsEncoder == null) return; + // Get the position of the Webots encoders and set the position of the WPIlib encoders + // getValue() returns radians + // revoultions = radians * gearing / 2pi + double revolutions = (webotsEncoder.getValue() * gearing) / (2*Math.PI); + device.set("count", revolutions); + } + +} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java index 6da2839d..89dedb95 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java @@ -2,7 +2,7 @@ import com.cyberbotics.webots.controller.PositionSensor; -import org.team199.deepbluesim.BaseSimConfig; +import org.team199.deepbluesim.Constants; import org.team199.deepbluesim.Simulation; import org.team199.wpiws.ScopedObject; import org.team199.wpiws.UniqueArrayList; @@ -54,7 +54,7 @@ private void tryToConnectWebotsPositionSensor() { webotsEncoder = Simulation.getRobot().getPositionSensor(newName); if(webotsEncoder != null) { name = newName; - webotsEncoder.enable(BaseSimConfig.getSensorTimestep()); + webotsEncoder.enable(Constants.sensorTimestep); Simulation.registerPeriodicMethod(this); } } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java index ad75b8a6..ddebbbe7 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java @@ -2,7 +2,7 @@ import com.cyberbotics.webots.controller.PositionSensor; -import org.team199.deepbluesim.BaseSimConfig; +import org.team199.deepbluesim.Constants; import org.team199.deepbluesim.Simulation; import org.team199.wpiws.devices.SimDeviceSim; @@ -13,15 +13,21 @@ public class MockedSparkEncoder implements Runnable { // Default value for a CANEncoder private final int countsPerRevolution = 4096; private double position; + private double distancePerPulse; public MockedSparkEncoder(SimDeviceSim sim, String name) { this.name = name; - // Match motor on CAN 0 with channels [0, 1], CAN 1 to channels [2, 3], etc. - // Probably not the best way to do it but it works encoder = sim; - webotsEncoder = Simulation.getRobot().getMotor(name).getPositionSensor(); + webotsEncoder = Simulation.getRobot().getPositionSensor(name); + distancePerPulse = 1; + sim.registerValueChangedCallback("distancePerPulse", (valueName, value) -> { + if(value == null) return; // Value has not yet been set + try { + distancePerPulse = Double.parseDouble(value); + } catch(NumberFormatException e) {} + }, true); if(webotsEncoder != null) { - webotsEncoder.enable(BaseSimConfig.getSensorTimestep()); + webotsEncoder.enable(Constants.sensorTimestep); Simulation.registerPeriodicMethod(this); } } @@ -34,8 +40,8 @@ public double getPosition() { public void run() { // Get the position of the Webots encoders and set the position of the WPIlib encoders // getValue() returns radians - // revoultions = radians * gearing / pi - double revolutions = (webotsEncoder.getValue() * BaseSimConfig.getMotorGearing(name)) / (2*Math.PI); + // revoultions = radians * gearing / 2pi + double revolutions = (webotsEncoder.getValue() * distancePerPulse) / (2*Math.PI); int count = (int) Math.floor(revolutions * countsPerRevolution); encoder.set("count", "" + count); } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index c190be0a..4fe395d2 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -24,16 +24,19 @@ public WebotsMotorForwarder(Robot robot, String motorName) { motor = robot.getMotor(motorName); currentOutput = 0; // Make sure that the motor can rotate any number of times - motor.setPosition(Double.POSITIVE_INFINITY); - motor.setVelocity(0); - Simulation.registerPeriodicMethod(this); + if(motor != null) { + motor.setPosition(Double.POSITIVE_INFINITY); + motor.setVelocity(0); + Simulation.registerPeriodicMethod(this); + } } @Override public void callback(String name, String value) { + if(value == null) return; // Value has not yet been set try { callback(name, Double.parseDouble(value)); - } catch(NullPointerException | NumberFormatException e) {} + } catch(NumberFormatException e) {} } @Override From 38a67895762a0f7fe8e390353c0dccfe40ac6c1f Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Wed, 4 Jan 2023 20:30:45 -0800 Subject: [PATCH 02/60] workaround for webots velocity encoder mismatch --- .../mediators/WebotsMotorForwarder.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index 4fe395d2..4e8a595d 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -1,7 +1,10 @@ package org.team199.deepbluesim.mediators; +import com.cyberbotics.webots.controller.Field; import com.cyberbotics.webots.controller.Motor; +import com.cyberbotics.webots.controller.Node; import com.cyberbotics.webots.controller.Robot; +import com.cyberbotics.webots.controller.Supervisor; import org.team199.deepbluesim.Simulation; import org.team199.wpiws.interfaces.DoubleCallback; @@ -12,8 +15,10 @@ */ public class WebotsMotorForwarder implements DoubleCallback, Runnable, StringCallback { - private double currentOutput; + private double currentOutput, pos, timer; private Motor motor; + private Node jointParameters; + private Field position; /** * Creates a new WebotsMotorForwarder @@ -27,6 +32,7 @@ public WebotsMotorForwarder(Robot robot, String motorName) { if(motor != null) { motor.setPosition(Double.POSITIVE_INFINITY); motor.setVelocity(0); + position = (jointParameters = Supervisor.getSupervisorInstance().getFromDevice(motor).getParentNode()).getField("jointParameters").getSFNode().getField("position"); Simulation.registerPeriodicMethod(this); } } @@ -46,7 +52,18 @@ public void callback(String name, double value) { @Override public void run() { - motor.setVelocity(motor.getMaxVelocity() * currentOutput); + if(timer == 0) { + timer = System.currentTimeMillis(); + return; + } + + double velocity = motor.getMaxVelocity() * currentOutput; + pos += velocity * (System.currentTimeMillis() - timer) / 1000; + + if(motor.getPositionSensor().getName().contains("CANCoder")) jointParameters.setJointPosition(pos, 1); + else motor.setVelocity(velocity); + + timer = System.currentTimeMillis(); } } \ No newline at end of file From 1591de8c6550c65081b5512ddaa650cec8d392bf Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Wed, 4 Jan 2023 22:40:45 -0800 Subject: [PATCH 03/60] remove unnecessary position field --- .../org/team199/deepbluesim/mediators/WebotsMotorForwarder.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index 4e8a595d..4df995a0 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -18,7 +18,6 @@ public class WebotsMotorForwarder implements DoubleCallback, Runnable, StringCal private double currentOutput, pos, timer; private Motor motor; private Node jointParameters; - private Field position; /** * Creates a new WebotsMotorForwarder @@ -32,7 +31,6 @@ public WebotsMotorForwarder(Robot robot, String motorName) { if(motor != null) { motor.setPosition(Double.POSITIVE_INFINITY); motor.setVelocity(0); - position = (jointParameters = Supervisor.getSupervisorInstance().getFromDevice(motor).getParentNode()).getField("jointParameters").getSFNode().getField("position"); Simulation.registerPeriodicMethod(this); } } From 6e6fc9779ccf54e2fa2db1049e048178365c5202 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Thu, 5 Jan 2023 15:27:29 -0800 Subject: [PATCH 04/60] fix jointParameters assignment --- .../org/team199/deepbluesim/mediators/WebotsMotorForwarder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index 4df995a0..c790db9b 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -31,6 +31,7 @@ public WebotsMotorForwarder(Robot robot, String motorName) { if(motor != null) { motor.setPosition(Double.POSITIVE_INFINITY); motor.setVelocity(0); + jointParameters = Supervisor.getSupervisorInstance().getFromDevice(motor).getParentNode(); Simulation.registerPeriodicMethod(this); } } From f3d5ef6c8d8ee70ddf1d7c61e9ff2374c80275a3 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 6 Jan 2023 12:20:44 -0800 Subject: [PATCH 05/60] remove unused import --- .../org/team199/deepbluesim/mediators/WebotsMotorForwarder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index c790db9b..ea17d073 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -1,6 +1,5 @@ package org.team199.deepbluesim.mediators; -import com.cyberbotics.webots.controller.Field; import com.cyberbotics.webots.controller.Motor; import com.cyberbotics.webots.controller.Node; import com.cyberbotics.webots.controller.Robot; From 2f4d78c755e1264c28a9c8ba467f0487a846c5de Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sun, 5 Feb 2023 01:46:23 -0800 Subject: [PATCH 06/60] fix usage of motor multiplier in Webots workaround --- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 257 +++++++++++------- .../mediators/WebotsMotorForwarder.java | 1 + 4 files changed, 155 insertions(+), 105 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da9702f9..84d1f85f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..c53aefaa 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index ea17d073..05f043df 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -56,6 +56,7 @@ public void run() { } double velocity = motor.getMaxVelocity() * currentOutput; + if(motor.getPositionSensor().getName().contains("CANCoder")) velocity *= motor.getMultiplier(); pos += velocity * (System.currentTimeMillis() - timer) / 1000; if(motor.getPositionSensor().getName().contains("CANCoder")) jointParameters.setJointPosition(pos, 1); From 7d939d849de52f31383989a4145e70f5943a910a Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Mon, 13 Feb 2023 01:19:48 -0800 Subject: [PATCH 07/60] better MockedSparkEncoder implementation --- .../mediators/MockedSparkEncoder.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java index ddebbbe7..368a22fe 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java @@ -12,18 +12,17 @@ public class MockedSparkEncoder implements Runnable { private PositionSensor webotsEncoder; // Default value for a CANEncoder private final int countsPerRevolution = 4096; - private double position; - private double distancePerPulse; + private double gearing; public MockedSparkEncoder(SimDeviceSim sim, String name) { this.name = name; encoder = sim; webotsEncoder = Simulation.getRobot().getPositionSensor(name); - distancePerPulse = 1; - sim.registerValueChangedCallback("distancePerPulse", (valueName, value) -> { + gearing = 1; + sim.registerValueChangedCallback("gearing", (valueName, value) -> { if(value == null) return; // Value has not yet been set try { - distancePerPulse = Double.parseDouble(value); + gearing = Double.parseDouble(value); } catch(NumberFormatException e) {} }, true); if(webotsEncoder != null) { @@ -32,16 +31,12 @@ public MockedSparkEncoder(SimDeviceSim sim, String name) { } } - public double getPosition() { - return position; - } - @Override public void run() { - // Get the position of the Webots encoders and set the position of the WPIlib encoders + // Get the position of the Webots encoders and set the position of the WPILib encoders // getValue() returns radians - // revoultions = radians * gearing / 2pi - double revolutions = (webotsEncoder.getValue() * distancePerPulse) / (2*Math.PI); + // revolutions = radians * gearing / 2pi + double revolutions = (webotsEncoder.getValue() * gearing) / (2*Math.PI); int count = (int) Math.floor(revolutions * countsPerRevolution); encoder.set("count", "" + count); } From 00c879e4cc5c0a3f1c76c5cf11e054607e3e399d Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Mon, 13 Feb 2023 01:21:00 -0800 Subject: [PATCH 08/60] remove gyro 360 yaw limit --- .../main/java/org/team199/deepbluesim/mediators/MockGyro.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java index 45552879..56185a85 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java @@ -45,7 +45,7 @@ public void run() { // Also convert from radians to degrees angle += Double.isNaN(reading) ? 0 : (180 * reading / Math.PI); // Make sure angle is between 0 and 359 inclusive - angle = Math.copySign(Math.abs(angle) % 360, angle); + // angle = Math.copySign(Math.abs(angle) % 360, angle); // Update the WPIlib gyro gyroSim.set("Yaw", angle + ""); } From 727b8caf3c010ea1c7fb8d93030b0fd7bf060692 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Mon, 13 Feb 2023 01:21:12 -0800 Subject: [PATCH 09/60] add deepbluesim to launch.json --- .vscode/launch.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 941f7511..7c5c4507 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,13 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - + { + "type": "java", + "name": "Launch DeepBlueSim", + "request": "launch", + "mainClass": "DeepBlueSim", + "projectName": "controller" + }, { "type": "java", "name": "WPILibWebSocketsToWebots", From c309812de3c97254c9fe37ac2ea47cfbaa9780f1 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Thu, 3 Aug 2023 19:24:45 -0700 Subject: [PATCH 10/60] bump gradle, WPIWebSockets, and WPILib --- WPIWebSockets | 2 +- example/build.gradle | 7 +- example/gradle/wrapper/gradle-wrapper.jar | Bin 58702 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/gradlew | 257 +++++++++++------- example/gradlew.bat | 25 +- example/settings.gradle | 2 +- example/src/main/java/frc/robot/Robot.java | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- plugin/build.gradle | 27 +- plugin/controller/build.gradle | 17 +- .../team199/deepbluesim/SimRegisterer.java | 8 +- .../deepbluesim/mediators/MockedEncoder.java | 4 +- plugin/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- plugin/gradlew | 257 +++++++++++------- 16 files changed, 349 insertions(+), 265 deletions(-) diff --git a/WPIWebSockets b/WPIWebSockets index fd957641..1d86497f 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit fd9576414abb3b4f1d2073e22271f6334b40cd61 +Subproject commit 1d86497fcae4b2c49b55d66bacc607713a4f734f diff --git a/example/build.gradle b/example/build.gradle index b04caaf0..5d8389fa 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,6 +1,6 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2022.4.1" + id "edu.wpi.first.GradleRIO" version "2023.4.3" id "org.team199.deepbluesim" version "0.0.12" } @@ -93,7 +93,7 @@ sourceSets { configurations { systemTestImplementation.extendsFrom testImplementation - systemTestRuntimeOnly.extendsFrom runtimeOnly + systemTestRuntimeOnly.extendsFrom runtimeOnly } task('systemTestJar', type: Jar) { @@ -147,7 +147,7 @@ assemble.dependsOn installDeepBlueSim // @Override // Integer call() { // int length = stream.read(buf) -// if (length > 0) +// if (length > 0) // System.out.write(buf, 0, length) // return length // } @@ -164,4 +164,3 @@ assemble.dependsOn installDeepBlueSim // return p // } // } - diff --git a/example/gradle/wrapper/gradle-wrapper.jar b/example/gradle/wrapper/gradle-wrapper.jar index cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 22213 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 delta 21233 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -105,79 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/example/gradlew.bat b/example/gradlew.bat index 9618d8d9..107acd32 100644 --- a/example/gradlew.bat +++ b/example/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/example/settings.gradle b/example/settings.gradle index 0bc697ad..48c039ed 100644 --- a/example/settings.gradle +++ b/example/settings.gradle @@ -4,7 +4,7 @@ pluginManagement { repositories { mavenLocal() gradlePluginPortal() - String frcYear = '2021' + String frcYear = '2023' File frcHome if (OperatingSystem.current().isWindows()) { String publicFolder = System.getenv('PUBLIC') diff --git a/example/src/main/java/frc/robot/Robot.java b/example/src/main/java/frc/robot/Robot.java index 6ce1ec07..388c0fd8 100644 --- a/example/src/main/java/frc/robot/Robot.java +++ b/example/src/main/java/frc/robot/Robot.java @@ -8,10 +8,10 @@ package frc.robot; import edu.wpi.first.wpilibj.Joystick; -import edu.wpi.first.wpilibj.motorcontrol.PWMVictorSPX; import edu.wpi.first.wpilibj.TimedRobot; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.drive.DifferentialDrive; +import edu.wpi.first.wpilibj.motorcontrol.PWMVictorSPX; /** * The VM is configured to automatically run this class, and to call the diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84d1f85f..84a0b92f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugin/build.gradle b/plugin/build.gradle index 90edf6a3..28c1f143 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -14,30 +14,25 @@ plugins { id 'maven-publish' // Support publishing to the Gradle Plugin Portal - id 'com.gradle.plugin-publish' version '0.12.0' + id 'com.gradle.plugin-publish' version '1.1.0' // Apply the Groovy plugin to add support for Groovy id 'groovy' } repositories { - // Use JCenter for resolving dependencies. - jcenter() + mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } -dependencies { - // Use the awesome Spock testing and specification framework - testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0' - testImplementation 'edu.wpi.first:GradleRIO:2021.1.2' - implementation 'commons-io:commons-io:2.8.0' -} - group 'org.team199' +// Info for publishing to the Gradle Plugin Portal gradlePlugin { + website = 'https://github.com/DeepBlueRobotics/DeepBlueSim' + vcsUrl = 'https://github.com/DeepBlueRobotics/DeepBlueSim.git' // Define the plugin plugins { deepbluesim { @@ -45,17 +40,11 @@ gradlePlugin { displayName = 'DeepBlueSim Plugin' description = 'A plugin that simplifies developing WPILib projects that use Webots for simulation.' implementationClass = 'org.team199.deepbluesim.gradle.DeepBlueSimPlugin' + tags.set(['FRC', 'WPILib', 'Webots']) } } } -// Info for publishing to the Gradle Plugin Portal -pluginBundle { - website = 'https://github.com/DeepBlueRobotics/DeepBlueSim' - vcsUrl = 'https://github.com/DeepBlueRobotics/DeepBlueSim.git' - tags = ['FRC', 'WPILib', 'Webots'] -} - // Add a source set for the functional test suite sourceSets { functionalTest { @@ -82,6 +71,10 @@ configurations { } dependencies { + // Use the awesome Spock testing and specification framework + testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0' + testImplementation 'edu.wpi.first:GradleRIO:2021.1.2' + implementation 'commons-io:commons-io:2.8.0' // include the Webots.zip artifact produced by the controller's webotsFolder configuration extraFiles project(path: ':controller', configuration: 'webotsFolder') } diff --git a/plugin/controller/build.gradle b/plugin/controller/build.gradle index b64b07db..724ab4b3 100644 --- a/plugin/controller/build.gradle +++ b/plugin/controller/build.gradle @@ -16,7 +16,7 @@ plugins { id 'distribution' // Support building a fat jar (aka shadowJar) containing needed dependencies - id 'com.github.johnrengelman.shadow' version '6.1.0' + id 'com.github.johnrengelman.shadow' version '8.1.1' // Support finding the local webots installation and adding it as a dependency id "org.carlmontrobotics.webots" version "0.13.0" @@ -45,19 +45,18 @@ dependencies { testImplementation 'junit:junit:4.13' } -// Configure the building of the fat jar +// Configure the building of the fat jar shadowJar { // Force name, so it doesn't contain the version number. - archiveName = "DeepBlueSim.jar" + archiveBaseName.set("DeepBlueSim") // DeepBlueSim.jar + archiveClassifier.set('') + archiveVersion.set('') // Make sure the licenseReport task runs first dependsOn "licenseReport" // dependsOn jar - - // Don't add "-all" to the jar filename. - archiveClassifier.set('') - // Webots puts the correct version of its libs on the classpath before + // Webots puts the correct version of its libs on the classpath before // launching the controller, so don't include them. exclude 'com/cyberbotics/**/*' @@ -89,7 +88,9 @@ distributions { webotsFolderDistZip { // Force name, so it doesn't contain the version number. - archiveName = "Webots.zip" + archiveBaseName.set("Webots") // Webots.zip + archiveClassifier.set('') + archiveVersion.set('') } artifacts { diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java index d6489031..86017b3a 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java @@ -3,18 +3,20 @@ import org.team199.deepbluesim.mediators.*; import org.team199.wpiws.ScopedObject; -import org.team199.wpiws.UniqueArrayList; import org.team199.wpiws.devices.EncoderSim; import org.team199.wpiws.devices.PWMSim; import org.team199.wpiws.devices.SimDeviceSim; import org.team199.wpiws.interfaces.SimDeviceCallback; +import java.util.Set; +import java.util.HashSet; + // Performs automatic registration of callbacks detecting both the initalization of new devices as well as data callbacks for devices such as Motors, Gyros, etc. // This allows us to automatically link these devices to Webots, reducing the amount of code we would have to change from a standard robot project public class SimRegisterer { - + private static final SimDeviceCallback MISC_DEVICE_CALLBACK = SimRegisterer::callback; - private static final UniqueArrayList> CALLBACKS = new UniqueArrayList<>(); + private static final Set> CALLBACKS = new HashSet<>(); static { // Register Initalized Callbacks for Misc Devices diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java index 89dedb95..0c5289b2 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java @@ -5,7 +5,7 @@ import org.team199.deepbluesim.Constants; import org.team199.deepbluesim.Simulation; import org.team199.wpiws.ScopedObject; -import org.team199.wpiws.UniqueArrayList; +import java.util.Set; import org.team199.wpiws.devices.EncoderSim; public class MockedEncoder implements Runnable { @@ -16,7 +16,7 @@ public class MockedEncoder implements Runnable { private int countsPerRevolution = 256; private int channelA = -1, channelB = -1; - public MockedEncoder(UniqueArrayList> callbacks, EncoderSim sim, String wpiLibId) { + public MockedEncoder(Set> callbacks, EncoderSim sim, String wpiLibId) { encoder = sim; this.wpiLibId = wpiLibId; callbacks.add(sim.registerChannelACallback( (id, channel) -> { diff --git a/plugin/gradle/wrapper/gradle-wrapper.jar b/plugin/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/plugin/gradle/wrapper/gradle-wrapper.properties b/plugin/gradle/wrapper/gradle-wrapper.properties index be52383e..84a0b92f 100644 --- a/plugin/gradle/wrapper/gradle-wrapper.properties +++ b/plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugin/gradlew b/plugin/gradlew index 4f906e0c..c53aefaa 100755 --- a/plugin/gradlew +++ b/plugin/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" From 06cc8e6c49c379c617a919a88cd284245a2ca218 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 00:18:24 -0700 Subject: [PATCH 11/60] fix system test --- example/build.gradle | 115 +++++++++--------- .../java/frc/robot/SystemTestRobot.java | 13 +- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/example/build.gradle b/example/build.gradle index 5d8389fa..fb80228f 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -41,6 +41,30 @@ deploy { // Set this to true to enable desktop support. def includeDesktopSupport = true +// repositories { +// mavenLocal() +// gradlePluginPortal() +// String frcYear = '2023' +// File frcHome +// if (OperatingSystem.current().isWindows()) { +// String publicFolder = System.getenv('PUBLIC') +// if (publicFolder == null) { +// publicFolder = "C:\\Users\\Public" +// } +// def homeRoot = new File(publicFolder, "wpilib") +// frcHome = new File(homeRoot, frcYear) +// } else { +// def userFolder = System.getProperty("user.home") +// def homeRoot = new File(userFolder, "wpilib") +// frcHome = new File(homeRoot, frcYear) +// } +// def frcHomeMaven = new File(frcHome, 'maven') +// maven { +// name 'frcHome' +// url frcHomeMaven +// } +// } + // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. // Also defines JUnit 4. dependencies { @@ -102,65 +126,42 @@ task('systemTestJar', type: Jar) { from sourceSets.systemTest.output from { configurations.systemTestRuntimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest("frc.robot.SystemTestRobot") + duplicatesStrategy = DuplicatesStrategy.INCLUDE } -// task('systemTest', type: SynchronousJavaSimulationTask) { -// dependsOn 'systemTestJar' -// dependsOn 'extractTestJNI' -// } +import org.gradle.internal.os.OperatingSystem +task('systemTest', type: JavaExec) { //edu.wpi.first.gradlerio.simulation.JavaExternalSimulationTask + // Run the jar file + dependsOn 'systemTestJar' + // dependsOn 'simulateExternalJavaRelease' + classpath = files(tasks.systemTestJar) + // println(tasks.simulateExternalJavaRelease.getSimulationFile().getAsFile().get().getAbsolutePath()) + + String pathSeparator = File.pathSeparator + + // Load native libraries + dependsOn 'extractReleaseNative' + String nativeDir = tasks.extractReleaseNative.getDestinationDirectory().getAsFile().get().getAbsolutePath() + String defaultLibraryPath; + if(OperatingSystem.current().isWindows()) { + defaultLibraryPath = System.getenv('PATH') + } else if(OperatingSystem.current().isMacOsX()) { + defaultLibraryPath = System.getenv('DYLD_LIBRARY_PATH') + } else { + defaultLibraryPath = System.getenv('LD_LIBRARY_PATH') + } + jvmArgs '-Djava.library.path=' + defaultLibraryPath + pathSeparator + nativeDir + environment 'PATH', System.getenv('PATH') + pathSeparator + nativeDir + + afterEvaluate { // We have to wait until after the project is evaluated so WPILibPlugin can setup the repositories to load the HALSim extensions from + // Load HALSim extensions (See https://github.com/wpilibsuite/GradleRIO/blob/88f3420b1fe554d78b7b682e2adf2080c124ba42/src/main/java/edu/wpi/first/gradlerio/wpi/java/WPIJavaExtension.java#L139) + File ldPath = tasks.extractReleaseNative.getDestinationDirectory().getAsFile().get() + def simExtension = project.extensions.getByType(edu.wpi.first.gradlerio.wpi.WPIExtension).getSim() + def halsimExtensions = simExtension.getHalSimLocations(List.of(ldPath), false) + environment 'HALSIM_EXTENSIONS', halsimExtensions.stream().map { it.libName }.reduce { a, b -> a + pathSeparator + b }.get() + } +} assemble.dependsOn installDeepBlueSim -// check.dependsOn 'systemTest' - -// class SynchronousJavaSimulationTask extends edu.wpi.first.gradlerio.simulation.JavaSi mulationTask { -// @Override -// Process launch(List cmd) { -// // Let the base class handle these situations -// if (scriptOnly || project.hasProperty('headless')) { -// return super.launch(cmd) -// } -// // Remove the extraneous double quotes added by the caller -// cmd = cmd*.replace('"','') -// def builder = new ProcessBuilder(cmd) -// // Incorporate the task's env and the simulation extension's env (e.g. HALSIM_EXTENSIONS) -// def env = builder.environment() -// env.putAll environment -// env.putAll project.extensions.getByType(edu.wpi.first.gradlerio.wpi.simulation.SimulationExtension).environment -// // Set the working dir if specified -// if (workingDir != null) { -// workingDir.mkdirs() -// builder.directory(workingDir) -// } -// // Redirect stderr to stdout -// builder.redirectErrorStream(true) -// // Start the process -// Process p = builder.start() -// println "Waiting for simulation to finish..." -// // Copy the output asynchronously so that the current thread can be interrupted -// // (e.g. if the task times out) -// try { -// p.inputStream.withStream { stream -> -// def executor = java.util.concurrent.Executors.newFixedThreadPool(1) -// def copyStreamCallable = new java.util.concurrent.Callable() { -// def buf = new byte[8192] -// @Override -// Integer call() { -// int length = stream.read(buf) -// if (length > 0) -// System.out.write(buf, 0, length) -// return length -// } -// } -// while (executor.submit(copyStreamCallable).get() > 0) { -// } -// } -// } catch (InterruptedException) { -// p.destroy() -// } -// p.waitFor() -// if (p.exitValue() != 0) -// throw new RuntimeException("Simulation failed with exit code ${p.exitValue()}.") -// return p -// } -// } +check.dependsOn 'systemTest' diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 76e14e62..28330e73 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -1,9 +1,6 @@ package frc.robot; -import edu.wpi.first.wpilibj.RobotBase; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -15,12 +12,13 @@ import edu.wpi.first.hal.SimDevice; import edu.wpi.first.hal.SimDouble; import edu.wpi.first.hal.simulation.SimValueCallback; +import edu.wpi.first.math.Vector; +import edu.wpi.first.math.numbers.N3; import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.simulation.DriverStationSim; import edu.wpi.first.wpilibj.simulation.SimDeviceSim; import edu.wpi.first.wpilibj.simulation.SimHooks; -import edu.wpi.first.wpiutil.math.Vector; -import edu.wpi.first.wpiutil.math.numbers.N3; public class SystemTestRobot extends Robot { @@ -62,7 +60,8 @@ public void simulationInit() { // Wait for the Webots supervisor to be ready final var future = new CompletableFuture(); try (var callback = webotsSupervisorSim.registerValueChangedCallback(simStartMs, new SimValueCallback() { - public void callback(String name, int handle, boolean readonly, HALValue value) { + @Override + public void callback(String name, int handle, int direction, HALValue value) { if (value.getDouble() > 0.0) { System.out.println("WebotsSupervisor is ready"); future.complete(true); From 5b73f8e5bf61c517f5369cace4a20b1a4dde3804 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 00:22:09 -0700 Subject: [PATCH 12/60] remove debugging comments --- example/build.gradle | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/example/build.gradle b/example/build.gradle index fb80228f..406146d9 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -41,30 +41,6 @@ deploy { // Set this to true to enable desktop support. def includeDesktopSupport = true -// repositories { -// mavenLocal() -// gradlePluginPortal() -// String frcYear = '2023' -// File frcHome -// if (OperatingSystem.current().isWindows()) { -// String publicFolder = System.getenv('PUBLIC') -// if (publicFolder == null) { -// publicFolder = "C:\\Users\\Public" -// } -// def homeRoot = new File(publicFolder, "wpilib") -// frcHome = new File(homeRoot, frcYear) -// } else { -// def userFolder = System.getProperty("user.home") -// def homeRoot = new File(userFolder, "wpilib") -// frcHome = new File(homeRoot, frcYear) -// } -// def frcHomeMaven = new File(frcHome, 'maven') -// maven { -// name 'frcHome' -// url frcHomeMaven -// } -// } - // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. // Also defines JUnit 4. dependencies { @@ -130,12 +106,10 @@ task('systemTestJar', type: Jar) { } import org.gradle.internal.os.OperatingSystem -task('systemTest', type: JavaExec) { //edu.wpi.first.gradlerio.simulation.JavaExternalSimulationTask +task('systemTest', type: JavaExec) { // Run the jar file dependsOn 'systemTestJar' - // dependsOn 'simulateExternalJavaRelease' classpath = files(tasks.systemTestJar) - // println(tasks.simulateExternalJavaRelease.getSimulationFile().getAsFile().get().getAbsolutePath()) String pathSeparator = File.pathSeparator From 2630287f29a89c47bc2f4d8b5ed2837a7e35c235 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 01:18:33 -0700 Subject: [PATCH 13/60] bug fixes --- .../java/org/team199/deepbluesim/SimRegisterer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java index 86017b3a..43c60bbe 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java @@ -24,13 +24,13 @@ public class SimRegisterer { // Register Initalized Callbacks for PWM Devices CALLBACKS.add(PWMSim.registerStaticInitializedCallback((name, isInitialized) -> { if(isInitialized) { - callback("PWM", name, 0); + callback("PWM", name); } }, true)); // Register Initalized Callbacks for Encoder Devices CALLBACKS.add(EncoderSim.registerStaticInitializedCallback((name, isInitialized) -> { if(isInitialized) { - callback("Encoder", name, 0); + callback("Encoder", name); } }, true)); } @@ -69,9 +69,9 @@ private static void callback(String deviceName) { } // Callback for when a known device type is registered on a Non-Can port - private static void callback(String type, String port, int storePos) { + private static void callback(String type, String port) { if(type.equals("PWM")) { - // If a new PWM device has been initalized, attempt to link it to a Webots Motor + // If a new PWM device has been initalized, attempt to link it to a Webots motor // Register a speed callback on this device CALLBACKS.add(new PWMSim(port).registerSpeedCallback( // Call a motor forwarder for a callback @@ -80,7 +80,7 @@ private static void callback(String type, String port, int storePos) { true)); } else if(type.equals("Encoder")) { - // If a new PWM device has been initalized, attempt to link it to a Webots Motor + // If a new Encoder device has been initalized, attempt to link it to a Webots encoder // Register a speed callback on this device EncoderSim sim = new EncoderSim(port); new MockedEncoder(CALLBACKS, sim, port); From cb40aa2e12f9b1c0672e8027ed9bfa075597aaa8 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 01:21:20 -0700 Subject: [PATCH 14/60] remove patched workaround --- .../mediators/WebotsMotorForwarder.java | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index 05f043df..0cf514d2 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -1,22 +1,17 @@ package org.team199.deepbluesim.mediators; -import com.cyberbotics.webots.controller.Motor; -import com.cyberbotics.webots.controller.Node; -import com.cyberbotics.webots.controller.Robot; -import com.cyberbotics.webots.controller.Supervisor; - -import org.team199.deepbluesim.Simulation; import org.team199.wpiws.interfaces.DoubleCallback; import org.team199.wpiws.interfaces.StringCallback; +import com.cyberbotics.webots.controller.Motor; +import com.cyberbotics.webots.controller.Robot; + /** * Forwards motor calls from WPILib motor controllers to Webots */ -public class WebotsMotorForwarder implements DoubleCallback, Runnable, StringCallback { +public class WebotsMotorForwarder implements DoubleCallback, StringCallback { - private double currentOutput, pos, timer; private Motor motor; - private Node jointParameters; /** * Creates a new WebotsMotorForwarder @@ -25,14 +20,6 @@ public class WebotsMotorForwarder implements DoubleCallback, Runnable, StringCal */ public WebotsMotorForwarder(Robot robot, String motorName) { motor = robot.getMotor(motorName); - currentOutput = 0; - // Make sure that the motor can rotate any number of times - if(motor != null) { - motor.setPosition(Double.POSITIVE_INFINITY); - motor.setVelocity(0); - jointParameters = Supervisor.getSupervisorInstance().getFromDevice(motor).getParentNode(); - Simulation.registerPeriodicMethod(this); - } } @Override @@ -45,24 +32,7 @@ public void callback(String name, String value) { @Override public void callback(String name, double value) { - currentOutput = value; - } - - @Override - public void run() { - if(timer == 0) { - timer = System.currentTimeMillis(); - return; - } - - double velocity = motor.getMaxVelocity() * currentOutput; - if(motor.getPositionSensor().getName().contains("CANCoder")) velocity *= motor.getMultiplier(); - pos += velocity * (System.currentTimeMillis() - timer) / 1000; - - if(motor.getPositionSensor().getName().contains("CANCoder")) jointParameters.setJointPosition(pos, 1); - else motor.setVelocity(velocity); - - timer = System.currentTimeMillis(); + motor.setVelocity(motor.getMaxVelocity() * value); } } \ No newline at end of file From 0168987e5656cdf5c9d6cbd6824fed50415d58cf Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 01:30:43 -0700 Subject: [PATCH 15/60] cleanup --- .../controller/src/main/java/DeepBlueSim.java | 7 +++-- .../team199/deepbluesim/SimRegisterer.java | 28 ++++++++++--------- .../org/team199/deepbluesim/Simulation.java | 2 +- .../deepbluesim/mediators/MockGyro.java | 2 +- .../deepbluesim/mediators/MockedCANCoder.java | 4 +-- .../deepbluesim/mediators/MockedEncoder.java | 12 ++++---- .../mediators/MockedSparkEncoder.java | 2 -- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 11bb5744..8522a40b 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -21,6 +21,7 @@ public class DeepBlueSim { private static final ConcurrentLinkedDeque queuedMessages = new ConcurrentLinkedDeque<>(); + @SuppressWarnings("unused") private static ScopedObject> callbackStore = null; private static RunningObject wsConnection = null; @@ -73,14 +74,14 @@ public void callback(String name, String value) { }, true); // If the robot code starts before we us, then it might have already tried to tell - // us it was ready and we would have missed it. So, we tell it we're ready when we + // us it was ready and we would have missed it. So, we tell it we're ready when we // connect to it. ConnectionProcessor.addOpenListener(() -> { System.out.println("Telling the robot we're ready"); webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); }); - // Wait until one timestep has completed to ensure that the Webots simulator is + // Wait until one timestep has completed to ensure that the Webots simulator is // not still starting up. if (robot.step(basicTimeStep) == -1) { throw new RuntimeException("Couldn't even do one timestep!"); @@ -89,7 +90,7 @@ public void callback(String name, String value) { System.out.println("Trying to connect to robot..."); wsConnection = WSConnection.connectHALSim(true); } catch(URISyntaxException e) { - System.err.println("Error occured connecting to server:"); + System.err.println("Error occurred connecting to server:"); e.printStackTrace(System.err); System.err.flush(); System.exit(1); diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java index 43c60bbe..f3aa1761 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java @@ -11,23 +11,25 @@ import java.util.Set; import java.util.HashSet; -// Performs automatic registration of callbacks detecting both the initalization of new devices as well as data callbacks for devices such as Motors, Gyros, etc. -// This allows us to automatically link these devices to Webots, reducing the amount of code we would have to change from a standard robot project +/** + * Performs automatic registration of callbacks detecting both the initialization of new devices as well as data callbacks for devices such as Motors, Gyros, etc. + * This allows us to automatically link these devices to Webots, reducing the amount of code we would have to change from a standard robot project + */ public class SimRegisterer { private static final SimDeviceCallback MISC_DEVICE_CALLBACK = SimRegisterer::callback; private static final Set> CALLBACKS = new HashSet<>(); static { - // Register Initalized Callbacks for Misc Devices + // Register Initialized Callbacks for Misc Devices CALLBACKS.add(SimDeviceSim.registerDeviceCreatedCallback("", MISC_DEVICE_CALLBACK, true)); - // Register Initalized Callbacks for PWM Devices + // Register Initialized Callbacks for PWM Devices CALLBACKS.add(PWMSim.registerStaticInitializedCallback((name, isInitialized) -> { if(isInitialized) { callback("PWM", name); } }, true)); - // Register Initalized Callbacks for Encoder Devices + // Register Initialized Callbacks for Encoder Devices CALLBACKS.add(EncoderSim.registerStaticInitializedCallback((name, isInitialized) -> { if(isInitialized) { callback("Encoder", name); @@ -35,25 +37,25 @@ public class SimRegisterer { }, true)); } - // Initalize SimRegisterer. This method exists to ensure that the static block is called + // Initialize SimRegisterer. This method exists to ensure that the static block is called public static void init() {} // Callback methods which place new devices in a processing queue which is processed every robot period // The WPILib callbacks are notified as part of the device creation. This process ensures that the devices complete their setup process - // This is especially important for SimDevice's because their initalized callbacks can be notified before their values have been created - // Queuing also ensures that callbacks (which are usually executed asycronously) are processed syncronously with the rest of the robot code + // This is especially important for SimDevice's because their initialized callbacks can be notified before their values have been created + // Queuing also ensures that callbacks (which are usually executed asynchronously) are processed synchronously with the rest of the robot code // Callback for when a Miscellaneous Device is registered private static void callback(String deviceName) { if(deviceName.startsWith("Talon") || deviceName.startsWith("Victor") || deviceName.startsWith("SparkMax")) { - // If a new Talon or Victor has been initalized, attempt to link it to a Webots Motor + // If a new Talon or Victor has been initialized, attempt to link it to a Webots Motor // Create a WebotsMotorForwarder for this motor final WebotsMotorForwarder fwdr = new WebotsMotorForwarder(Simulation.getRobot(), deviceName); // Register a callback for when the Motor Output changes CALLBACKS.add(new SimDeviceSim(deviceName).registerValueChangedCallback("Motor Output", // Call the callback function fwdr, - // Initalize with current speed + // Initialize with current speed true)); } if(deviceName.startsWith("navX")) { @@ -71,16 +73,16 @@ private static void callback(String deviceName) { // Callback for when a known device type is registered on a Non-Can port private static void callback(String type, String port) { if(type.equals("PWM")) { - // If a new PWM device has been initalized, attempt to link it to a Webots motor + // If a new PWM device has been initialized, attempt to link it to a Webots motor // Register a speed callback on this device CALLBACKS.add(new PWMSim(port).registerSpeedCallback( // Call a motor forwarder for a callback new WebotsMotorForwarder(Simulation.getRobot(), "PWM[" + port + "]"), - // Initalize with current speed + // Initialize with current speed true)); } else if(type.equals("Encoder")) { - // If a new Encoder device has been initalized, attempt to link it to a Webots encoder + // If a new Encoder device has been initialized, attempt to link it to a Webots encoder // Register a speed callback on this device EncoderSim sim = new EncoderSim(port); new MockedEncoder(CALLBACKS, sim, port); diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java b/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java index 01111427..ec07320a 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java @@ -23,7 +23,7 @@ public final class Simulation { * {@link #timeStep} converted into milliseconds. This is equivalent to timeStep * 1000 */ private static double timeStepMillis; - // Use a CopyOnWriteArrayList to prevent syncronization errors + // Use a CopyOnWriteArrayList to prevent synchronization errors private static final CopyOnWriteArrayList periodicMethods; static { diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java index 56185a85..d11ca384 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java @@ -38,7 +38,7 @@ public static void linkGyro() { public void run() { /* getValues() returns angular speeds about each axis (x, y, z). reading represents the change in angular position about the z axis. - getValues()[2] is negated to convert from Webot's coordinate system (counter-clockwise = positive) to WPIlib's coordinate system (counter-clockwise = negative). + getValues()[2] is negated to convert from Webots's coordinate system (counter-clockwise = positive) to WPILib's coordinate system (counter-clockwise = negative). */ double reading = -webotsGyro.getValues()[2] * Simulation.getBasicTimeStep(); // In testing, reading was sometimes NAN in the first second of the simulation. diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java index b548ca49..75cceef4 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java @@ -35,9 +35,9 @@ public void callback(String name, String value) { @Override public void run() { if(webotsEncoder == null) return; - // Get the position of the Webots encoders and set the position of the WPIlib encoders + // Get the position of the Webots encoders and set the position of the WPILib encoders // getValue() returns radians - // revoultions = radians * gearing / 2pi + // revolutions = radians * gearing / 2pi double revolutions = (webotsEncoder.getValue() * gearing) / (2*Math.PI); device.set("count", revolutions); } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java index 0c5289b2..e1ac146d 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java @@ -42,7 +42,7 @@ private void tryToConnectWebotsPositionSensor() { return; String newName = "Encoder[" + channelA + "," + channelB + "]"; if (webotsEncoder != null && !newName.equals(name)) { - System.out.println("WARNING: Ignoring attempt to change PositionSensor of id " + System.out.println("WARNING: Ignoring attempt to change PositionSensor of id " + wpiLibId + " from " + name + " to " + newName); return; } @@ -65,17 +65,17 @@ private void tryToConnectWebotsPositionSensor() { @Override public void run() { - // Get the position of the Webots encoders and set the position of the WPIlib encoders + // Get the position of the Webots encoders and set the position of the WPILib encoders // getValue() returns radians double revolutions = (webotsEncoder.getValue()) / (2*Math.PI); int count = (int) Math.floor(revolutions * countsPerRevolution); encoder.setCount(count); // Compute the period of time since the previous tick. This is a bit more complicated than it would - // seem at first glance because we need to handle both the case where multiple ticks have - // occured since we last checked, the case where no ticks have occurred across multiple - // checks, and the case where only one tick has occured since we last checked. - // For simplicity, we assume that if any number of ticks have occured, then the most recent one + // seem at first glance because we need to handle both the case where multiple ticks have + // occurred since we last checked, the case where no ticks have occurred across multiple + // checks, and the case where only one tick has occurred since we last checked. + // For simplicity, we assume that if any number of ticks have occurred, then the most recent one // happened now. double curTimeSecs = Simulation.getRobot().getTime(); // If no ticks have happened since we last checked, then we know when the the previous tick happened. diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java index 368a22fe..fdd9ddce 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java @@ -7,7 +7,6 @@ import org.team199.wpiws.devices.SimDeviceSim; public class MockedSparkEncoder implements Runnable { - private String name; private SimDeviceSim encoder; private PositionSensor webotsEncoder; // Default value for a CANEncoder @@ -15,7 +14,6 @@ public class MockedSparkEncoder implements Runnable { private double gearing; public MockedSparkEncoder(SimDeviceSim sim, String name) { - this.name = name; encoder = sim; webotsEncoder = Simulation.getRobot().getPositionSensor(name); gearing = 1; From 593f64bbae57dd6af1d91e16da5e507a36144897 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 02:04:10 -0700 Subject: [PATCH 16/60] bump WPIWebSockets --- WPIWebSockets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WPIWebSockets b/WPIWebSockets index 1d86497f..39aca58d 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit 1d86497fcae4b2c49b55d66bacc607713a4f734f +Subproject commit 39aca58d6434e64de72d66bec4a5b2a5aed25223 From 67a8bf4ce454e2a574b266bf39a8c98663183037 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 02:40:55 -0700 Subject: [PATCH 17/60] add identity value to halsim extension reduction --- example/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/build.gradle b/example/build.gradle index 406146d9..a249383b 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -132,7 +132,7 @@ task('systemTest', type: JavaExec) { File ldPath = tasks.extractReleaseNative.getDestinationDirectory().getAsFile().get() def simExtension = project.extensions.getByType(edu.wpi.first.gradlerio.wpi.WPIExtension).getSim() def halsimExtensions = simExtension.getHalSimLocations(List.of(ldPath), false) - environment 'HALSIM_EXTENSIONS', halsimExtensions.stream().map { it.libName }.reduce { a, b -> a + pathSeparator + b }.get() + environment 'HALSIM_EXTENSIONS', halsimExtensions.stream().map { it.libName }.reduce("", { a, b -> a + pathSeparator + b }) } } From 9395f4ad9098becb84d4e9312c683853d7c152af Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 02:52:56 -0700 Subject: [PATCH 18/60] fix gradle task name in workflow --- .github/actions/run-system-test/action.yml | 2 +- .github/workflows/ci.yml | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/actions/run-system-test/action.yml b/.github/actions/run-system-test/action.yml index 2af1bb50..abb3c3e1 100644 --- a/.github/actions/run-system-test/action.yml +++ b/.github/actions/run-system-test/action.yml @@ -7,7 +7,7 @@ runs: run: | # This is also a test that our plugin installs DeepBlueSim if one of WPILib's simulate # tasks is executed. - ./gradlew :example:externalSimulate --info --stacktrace + ./gradlew :example:simulateExternalJavaRelease --info --stacktrace shell: bash - name: Start Webots diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4c77f38..7d960310 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: build: - strategy: + strategy: fail-fast: false matrix: os: [ windows-latest, macos-latest, ubuntu-latest ] @@ -31,7 +31,7 @@ jobs: uses: DeepBlueRobotics/setup-webots@v1 with: install: false - + - name: Cache Webots uses: actions/cache@v2 with: @@ -80,7 +80,7 @@ jobs: uses: DeepBlueRobotics/setup-webots@v1 with: install: false - + - name: Cache Webots uses: actions/cache@v2 with: @@ -108,10 +108,9 @@ jobs: - name: Tag run: git tag ${{ steps.version.outputs.version_tag }} - + - name: Push run: git push origin ${{ steps.version.outputs.version_tag }} - name: Publish to Gradle Plugin Portal run: ./gradlew :plugin:publishPlugins -Pversion=${{ steps.version.outputs.version }} -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} --info --stacktrace - From 1b89bf020d240bd84763bbd42091b590eea25ebb Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 19:50:23 -0700 Subject: [PATCH 19/60] update ci workflow --- .github/workflows/ci.yml | 60 +++++++++++++++------------------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d960310..57df669f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,32 +16,22 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: "recursive" fetch-depth: 0 - - name: Set up JDK 11 - uses: actions/setup-java@v1 + - name: Set up JDK 17 + uses: actions/setup-java@v3 with: - java-version: 11 - - - name: Get Webots cache path - id: getWebotsCachePath - uses: DeepBlueRobotics/setup-webots@v1 - with: - install: false - - - name: Cache Webots - uses: actions/cache@v2 - with: - path: ${{ steps.getWebotsCachePath.outputs.cachePath }} - key: webots-v2021a-install-${{ runner.os }} - + distribution: 'temurin' + java-version: 17 - name: Setup Webots id: setupWebots - uses: DeepBlueRobotics/setup-webots@v1 + uses: DeepBlueRobotics/setup-webots@versioning-and-caching + with: + webotsVersion: R2023b - name: Do the system test uses: ./.github/actions/run-system-test @@ -68,38 +58,29 @@ jobs: steps: - name: Cancel any existing workflow runs - uses: fkirc/skip-duplicate-actions@v3.3.0 + uses: fkirc/skip-duplicate-actions@v5 - - name: Set up JDK 11 - uses: actions/setup-java@v1 + - name: Set up JDK 17 + uses: actions/setup-java@v3 with: - java-version: 11 - - - name: Get Webots cache path - id: getWebotsCachePath - uses: DeepBlueRobotics/setup-webots@v1 - with: - install: false - - - name: Cache Webots - uses: actions/cache@v2 - with: - path: ${{ steps.getWebotsCachePath.outputs.cachePath }} - key: webots-v2021a-install-${{ runner.os }} + distribution: 'temurin' + java-version: 17 - name: Setup Webots id: setupWebots - uses: DeepBlueRobotics/setup-webots@v1 + uses: DeepBlueRobotics/setup-webots@versioning-and-caching + with: + webotsVersion: R2023b - name: Checkout source - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: "recursive" fetch-depth: 0 - name: Compute next semantic version id: version - uses: paulhatch/semantic-version@v3.3.1 + uses: paulhatch/semantic-version@v5 with: branch: "master" @@ -113,4 +94,7 @@ jobs: run: git push origin ${{ steps.version.outputs.version_tag }} - name: Publish to Gradle Plugin Portal - run: ./gradlew :plugin:publishPlugins -Pversion=${{ steps.version.outputs.version }} -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} --info --stacktrace + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + run: ./gradlew :plugin:publishPlugins -Pversion=${{ steps.version.outputs.version }} -Pgradle.publish.key=$GRADLE_PUBLISH_KEY -Pgradle.publish.secret=$GRADLE_PUBLISH_SECRET --info --stacktrace From a6ee1d15476a314b6ed50e4582ce6b6d7b7d99ce Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 00:16:41 -0700 Subject: [PATCH 20/60] use pr version of webots gradle plugin --- plugin/controller/build.gradle | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugin/controller/build.gradle b/plugin/controller/build.gradle index 724ab4b3..227031a7 100644 --- a/plugin/controller/build.gradle +++ b/plugin/controller/build.gradle @@ -25,6 +25,24 @@ plugins { id 'com.jaredsburrows.license' version '0.8.80' } +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.namespace == 'org.carlmontrobotics.webots') { + useModule("com.github.DeepBlueRobotics.webots-gradle-plugin:plugin:PR3-SNAPSHOT") + } + } + } + repositories { + gradlePluginPortal() + mavenCentral() + jcenter() + maven { + url 'jitpack.io' + } + } +} + group 'org.team199' sourceCompatibility = 1.8 From e8dbb65c56f9e546b3e25fa1236ba09a3e6a162e Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 01:02:06 -0700 Subject: [PATCH 21/60] bump WPIWebSockets --- WPIWebSockets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WPIWebSockets b/WPIWebSockets index 39aca58d..6ad03be6 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit 39aca58d6434e64de72d66bec4a5b2a5aed25223 +Subproject commit 6ad03be6cc92503d6bd1e4022f9369c8aef47b10 From fe1eecc027eeb0564ea0974383f50594f492343e Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 01:02:22 -0700 Subject: [PATCH 22/60] move pluginManagement to settings.gradle --- plugin/controller/build.gradle | 18 ------------------ plugin/controller/settings.gradle | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 18 deletions(-) create mode 100644 plugin/controller/settings.gradle diff --git a/plugin/controller/build.gradle b/plugin/controller/build.gradle index 227031a7..724ab4b3 100644 --- a/plugin/controller/build.gradle +++ b/plugin/controller/build.gradle @@ -25,24 +25,6 @@ plugins { id 'com.jaredsburrows.license' version '0.8.80' } -pluginManagement { - resolutionStrategy { - eachPlugin { - if (requested.id.namespace == 'org.carlmontrobotics.webots') { - useModule("com.github.DeepBlueRobotics.webots-gradle-plugin:plugin:PR3-SNAPSHOT") - } - } - } - repositories { - gradlePluginPortal() - mavenCentral() - jcenter() - maven { - url 'jitpack.io' - } - } -} - group 'org.team199' sourceCompatibility = 1.8 diff --git a/plugin/controller/settings.gradle b/plugin/controller/settings.gradle new file mode 100644 index 00000000..3fdf872f --- /dev/null +++ b/plugin/controller/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.namespace == 'org.carlmontrobotics.webots') { + useModule("com.github.DeepBlueRobotics.webots-gradle-plugin:plugin:PR3-SNAPSHOT") + } + } + } + repositories { + gradlePluginPortal() + mavenCentral() + jcenter() + maven { + url 'jitpack.io' + } + } +} From b3f097e340dc88d0a5c40afd061ff7bbe90762c6 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 01:02:38 -0700 Subject: [PATCH 23/60] fix system test --- example/.gitignore | 1 + example/src/main/java/frc/robot/Robot.java | 15 ++++- .../mediators/WebotsMotorForwarder.java | 2 + .../webotsFolder/dist/worlds/DBSExample.wbt | 61 +++++++++++++------ 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/example/.gitignore b/example/.gitignore index 5f50cd6b..0a5323ef 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -163,3 +163,4 @@ imgui.ini # For testing purposes, we want to ensure that this project starts without DeepBlueSim installed. Webots/controllers/DeepBlueSim.jar Webots/*/DBS* +Webots/*/.DBS* diff --git a/example/src/main/java/frc/robot/Robot.java b/example/src/main/java/frc/robot/Robot.java index 388c0fd8..6ccf00e3 100644 --- a/example/src/main/java/frc/robot/Robot.java +++ b/example/src/main/java/frc/robot/Robot.java @@ -11,6 +11,7 @@ import edu.wpi.first.wpilibj.TimedRobot; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.drive.DifferentialDrive; +import edu.wpi.first.wpilibj.motorcontrol.MotorControllerGroup; import edu.wpi.first.wpilibj.motorcontrol.PWMVictorSPX; /** @@ -21,10 +22,10 @@ * directory. */ public class Robot extends TimedRobot { - private final DifferentialDrive m_robotDrive - = new DifferentialDrive(new PWMVictorSPX(0), new PWMVictorSPX(1)); + private final Joystick m_stick = new Joystick(0); private final Timer m_timer = new Timer(); + private DifferentialDrive m_robotDrive; /** * This function is run when the robot is first started up and should be @@ -32,6 +33,16 @@ public class Robot extends TimedRobot { */ @Override public void robotInit() { + MotorControllerGroup leftMotors = new MotorControllerGroup( + new PWMVictorSPX(0), + new PWMVictorSPX(1) + ); + MotorControllerGroup rightMotors = new MotorControllerGroup( + new PWMVictorSPX(2), + new PWMVictorSPX(3) + ); + rightMotors.setInverted(true); + m_robotDrive = new DifferentialDrive(leftMotors, rightMotors); } /** diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java index 0cf514d2..cd80c19d 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java @@ -20,6 +20,8 @@ public class WebotsMotorForwarder implements DoubleCallback, StringCallback { */ public WebotsMotorForwarder(Robot robot, String motorName) { motor = robot.getMotor(motorName); + // Use velocity control + motor.setPosition(Double.POSITIVE_INFINITY); } @Override diff --git a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt index 5036272c..6308a587 100644 --- a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt +++ b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt @@ -1,15 +1,21 @@ -#VRML_SIM R2021a utf8 +#VRML_SIM R2023b utf8 + +EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/floors/protos/RectangleArena.proto" +EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/backgrounds/protos/TexturedBackground.proto" +EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/backgrounds/protos/TexturedBackgroundLight.proto" + WorldInfo { coordinateSystem "NUE" } Viewpoint { - orientation 0.12546424055832162 0.9850338938784661 0.11818186092523109 4.589543198443866 - position -11.848186436085022 3.5024799881936275 -1.1587423512414836 + orientation -0.8834893720712308 0.4661205108595913 0.04667117732793667 1.8807004037332389 + position -2.6740863046422008 1.5141266883477473 2.0717807684941976 } DEF Field Group { children [ RectangleArena { translation 0 0.01 0 + rotation 1 0 0 -1.5707953071795862 name "rectangle arena(1)" floorSize 20 20 floorTileSize 20 20 @@ -21,8 +27,8 @@ TexturedBackground { TexturedBackgroundLight { } Robot { - translation -2.1693054077308728e-12 0.06929473891189548 1.4597477115552345e-08 - rotation 0.9999999976797347 6.81173179794535e-05 7.495205389129735e-07 1.55812926143542e-08 + translation -1.5672499690951603e-12 0.06929667254627217 1.1402531555035887e-07 + rotation 0.9999999999994709 1.0234514950603424e-06 1.054961357222813e-07 1.0472091725023527e-06 children [ Solid { children [ @@ -30,7 +36,7 @@ Robot { } HingeJoint { jointParameters HingeJointParameters { - position 1.667027211877997e-11 + position -4.1511655620378363e-11 axis 0 0 1 anchor -0.167 0 -0.232 } @@ -44,8 +50,8 @@ Robot { } ] endPoint Solid { - translation -0.167 0 -0.232 - rotation 1 4.833061556931681e-10 4.833043804137914e-10 1.5708 + translation -0.1670000000000002 -2.473071410269423e-06 -0.23199999999746604 + rotation 0.9999999896747462 2.2245706338344181e-10 0.00014370284342362158 5.307156870259904e-06 children [ Shape { appearance PBRAppearance { @@ -61,22 +67,28 @@ Robot { boundingObject USE Wheel physics Physics { } + linearVelocity 1.822206059407856e-11 4.9538582186142945e-06 2.6858087780223737e-10 + angularVelocity 1.0140593873494673e-11 8.05902970923126e-16 -2.879076630795684e-10 } } HingeJoint { jointParameters HingeJointParameters { - position 1.6672976088524906e-11 + position 9.225294324560214e-11 axis 0 0 1 anchor 0.167 0 -0.232 } device [ + RotationalMotor { + name "PWM[1]" + maxVelocity 87.4102 + } PositionSensor { name "Front Left Encoder" } ] endPoint Solid { - translation 0.167 0 -0.232 - rotation 1 4.833957821850251e-10 4.833940065764325e-10 1.5708 + translation 0.1670000000000008 -2.47307141028677e-06 -0.231999999997466 + rotation 0.9999999286680492 7.431074700799304e-10 0.0003777087457428224 5.307156870259904e-06 children [ Shape { appearance PBRAppearance { @@ -89,11 +101,13 @@ Robot { boundingObject USE Wheel physics Physics { } + linearVelocity 1.8087846897432735e-11 4.9538604909707546e-06 2.685812324034266e-10 + angularVelocity 1.0168060270765932e-11 8.027475063039239e-16 -2.848409068159918e-10 } } HingeJoint { jointParameters HingeJointParameters { - position -1.6676778847781958e-11 + position 4.150378260475315e-11 axis 0 0 -1 anchor -0.167 0 0.232 } @@ -102,13 +116,13 @@ Robot { name "Back Right Encoder" } RotationalMotor { - name "PWM[1]" + name "PWM[2]" maxVelocity 87.4102 } ] endPoint Solid { - translation -0.167 0 0.232 - rotation 1 -2.168388415894704e-10 -2.1683804509739343e-10 1.5708 + translation -0.1670000000000002 -2.4730714204834747e-06 0.232000000002534 + rotation 0.9999999931301866 -2.2462931806780253e-10 -0.00011721615407189237 5.307156870259904e-06 children [ Shape { appearance PBRAppearance { @@ -121,22 +135,28 @@ Robot { boundingObject USE Wheel physics Physics { } + linearVelocity 1.8221518892736977e-11 4.953855589429738e-06 2.6858193886255167e-10 + angularVelocity 1.0168060270765977e-11 8.059010421989807e-16 -2.878916574611155e-10 } } HingeJoint { jointParameters HingeJointParameters { - position -1.6676110401423026e-11 + position 8.919282517793818e-11 axis 0 0 -1 anchor 0.167 0 0.232 } device [ + RotationalMotor { + name "PWM[3]" + maxVelocity 87.4102 + } PositionSensor { name "Front Right Encoder" } ] endPoint Solid { - translation 0.167 0 0.232 - rotation 1 -2.1683064771089854e-10 -2.168298512489195e-10 1.5708 + translation 0.16699999999999957 -2.4730714204904136e-06 0.23200000000253404 + rotation 0.9999999876420861 -3.2001829594138985e-10 -0.00015721268214391712 5.307156870259904e-06 children [ Shape { appearance PBRAppearance { @@ -148,6 +168,8 @@ Robot { boundingObject USE Wheel physics Physics { } + linearVelocity 1.8087740188949048e-11 4.95385786537571e-06 2.6857994638416105e-10 + angularVelocity 1.0113127476216427e-11 8.027802005714066e-16 -2.848392744175236e-10 } } Shape { @@ -164,6 +186,7 @@ Robot { } Pen { translation 0 0.001 0 + rotation -1 0 0 1.5707963267948966 inkColor 1 0 0 inkDensity 1 leadSize 0.1 @@ -176,4 +199,6 @@ Robot { } controller "DeepBlueSim" supervisor TRUE + linearVelocity 1.809152902427044e-11 4.3220382958373945e-06 2.6856562400292097e-10 + angularVelocity 9.921710062231101e-12 4.970941483132485e-16 6.939801325381416e-12 } From 490d89e794f6f75030b2a3d80d664565a302cb59 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 4 Aug 2023 19:58:28 -0700 Subject: [PATCH 24/60] don't spam log file --- example/src/systemTest/java/frc/robot/SystemTestRobot.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 28330e73..98c632e1 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -79,9 +79,13 @@ public void callback(String name, int handle, int direction, HALValue value) { // Integration servers, it can take over 8 minutes for Webots to start. var startedWaitingTimeMs = System.currentTimeMillis(); var isReady = false; + System.err.println("Waiting for WebotsSupervisor to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 600000) { try { - isReady = future.get(1, TimeUnit.SECONDS); + long elapsedTime = System.currentTimeMillis() - startedWaitingTimeMs; + long remainingTime = 600000 - elapsedTime; + if(remainingTime > 0) isReady = future.get(remainingTime, TimeUnit.MILLISECONDS); + else isReady = true; } catch (TimeoutException ex) { System.err.println("Waiting for WebotsSupervisor to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); } catch (InterruptedException|ExecutionException e) { From 466846c30a0cea60d9fd072a7ee1e807c224188a Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 01:34:15 -0700 Subject: [PATCH 25/60] more version bumps --- example/.gitignore | 5 +++++ example/.wpilib/wpilib_preferences.json | 2 +- example/vendordeps/WPILibNewCommands.json | 8 ++++---- plugin/build.gradle | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/example/.gitignore b/example/.gitignore index 0a5323ef..4e8ecbc9 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -164,3 +164,8 @@ imgui.ini Webots/controllers/DeepBlueSim.jar Webots/*/DBS* Webots/*/.DBS* + +# Ignore Simulation GUI settings +/networktables.json +/simgui.json +/simgui-*.json diff --git a/example/.wpilib/wpilib_preferences.json b/example/.wpilib/wpilib_preferences.json index a387f26c..128942ae 100644 --- a/example/.wpilib/wpilib_preferences.json +++ b/example/.wpilib/wpilib_preferences.json @@ -1,6 +1,6 @@ { "enableCppIntellisense": false, "currentLanguage": "java", - "projectYear": "2021", + "projectYear": "2023", "teamNumber": 199 } \ No newline at end of file diff --git a/example/vendordeps/WPILibNewCommands.json b/example/vendordeps/WPILibNewCommands.json index d7bd9b06..65dcc03c 100644 --- a/example/vendordeps/WPILibNewCommands.json +++ b/example/vendordeps/WPILibNewCommands.json @@ -1,7 +1,7 @@ { "fileName": "WPILibNewCommands.json", "name": "WPILib-New-Commands", - "version": "2020.0.0", + "version": "1.0.0", "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", "mavenUrls": [], "jsonUrl": "", @@ -25,12 +25,12 @@ "skipInvalidPlatforms": true, "binaryPlatforms": [ "linuxathena", - "linuxraspbian", - "linuxaarch64bionic", + "linuxarm32", + "linuxarm64", "windowsx86-64", "windowsx86", "linuxx86-64", - "osxx86-64" + "osxuniversal" ] } ] diff --git a/plugin/build.gradle b/plugin/build.gradle index 28c1f143..b5342636 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -73,7 +73,7 @@ configurations { dependencies { // Use the awesome Spock testing and specification framework testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0' - testImplementation 'edu.wpi.first:GradleRIO:2021.1.2' + testImplementation 'edu.wpi.first:GradleRIO:2023.4.3' implementation 'commons-io:commons-io:2.8.0' // include the Webots.zip artifact produced by the controller's webotsFolder configuration extraFiles project(path: ':controller', configuration: 'webotsFolder') From 4e670e63f8d5131cd1573d151972dd3666df6220 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 02:19:38 -0700 Subject: [PATCH 26/60] [debug] log LD_LIBRARY_PATH --- .github/actions/run-system-test/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/run-system-test/action.yml b/.github/actions/run-system-test/action.yml index abb3c3e1..e0300871 100644 --- a/.github/actions/run-system-test/action.yml +++ b/.github/actions/run-system-test/action.yml @@ -8,6 +8,8 @@ runs: # This is also a test that our plugin installs DeepBlueSim if one of WPILib's simulate # tasks is executed. ./gradlew :example:simulateExternalJavaRelease --info --stacktrace + echo Library Path (Linux): + echo $LD_LIBRARY_PATH shell: bash - name: Start Webots From bc688ddb97ab678dfa5120cb458410578a6e7466 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 02:47:21 -0700 Subject: [PATCH 27/60] move debug location --- .github/actions/run-system-test/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/run-system-test/action.yml b/.github/actions/run-system-test/action.yml index e0300871..00bd8803 100644 --- a/.github/actions/run-system-test/action.yml +++ b/.github/actions/run-system-test/action.yml @@ -5,11 +5,11 @@ runs: steps: - name: Install DeepBlueSim run: | + echo Library Path (Linux): + echo $LD_LIBRARY_PATH # This is also a test that our plugin installs DeepBlueSim if one of WPILib's simulate # tasks is executed. ./gradlew :example:simulateExternalJavaRelease --info --stacktrace - echo Library Path (Linux): - echo $LD_LIBRARY_PATH shell: bash - name: Start Webots From cd143efaf98d2dfed94a0c9be6eb80808bf46b01 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 02:50:08 -0700 Subject: [PATCH 28/60] use quotation marks --- .github/actions/run-system-test/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/run-system-test/action.yml b/.github/actions/run-system-test/action.yml index 00bd8803..4f9aff26 100644 --- a/.github/actions/run-system-test/action.yml +++ b/.github/actions/run-system-test/action.yml @@ -5,7 +5,7 @@ runs: steps: - name: Install DeepBlueSim run: | - echo Library Path (Linux): + echo "Library Path (Linux):" echo $LD_LIBRARY_PATH # This is also a test that our plugin installs DeepBlueSim if one of WPILib's simulate # tasks is executed. From 2946df9f6492415949264227e0d5bc4cd80df190 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 02:52:23 -0700 Subject: [PATCH 29/60] remove LD_LIBRARY_PATH log --- .github/actions/run-system-test/action.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/actions/run-system-test/action.yml b/.github/actions/run-system-test/action.yml index 4f9aff26..abb3c3e1 100644 --- a/.github/actions/run-system-test/action.yml +++ b/.github/actions/run-system-test/action.yml @@ -5,8 +5,6 @@ runs: steps: - name: Install DeepBlueSim run: | - echo "Library Path (Linux):" - echo $LD_LIBRARY_PATH # This is also a test that our plugin installs DeepBlueSim if one of WPILib's simulate # tasks is executed. ./gradlew :example:simulateExternalJavaRelease --info --stacktrace From 9f4f2d36a2e495ea79a2c5c0066a7eafed99e524 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 5 Aug 2023 03:03:42 -0700 Subject: [PATCH 30/60] remove pluginManagement block --- plugin/controller/settings.gradle | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 plugin/controller/settings.gradle diff --git a/plugin/controller/settings.gradle b/plugin/controller/settings.gradle deleted file mode 100644 index 3fdf872f..00000000 --- a/plugin/controller/settings.gradle +++ /dev/null @@ -1,17 +0,0 @@ -pluginManagement { - resolutionStrategy { - eachPlugin { - if (requested.id.namespace == 'org.carlmontrobotics.webots') { - useModule("com.github.DeepBlueRobotics.webots-gradle-plugin:plugin:PR3-SNAPSHOT") - } - } - } - repositories { - gradlePluginPortal() - mavenCentral() - jcenter() - maven { - url 'jitpack.io' - } - } -} From eb21ee060780e337d99416bf853bf1251cc683e5 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Wed, 9 Aug 2023 04:45:54 -0700 Subject: [PATCH 31/60] fix #24 and #25, and start #26 --- example/.gitignore | 4 +- plugin/controller/build.gradle | 24 +++++ .../controller/src/main/java/DeepBlueSim.java | 73 +++++++------ .../org/team199/deepbluesim/ParseUtils.java | 21 ++++ .../team199/deepbluesim/SimRegisterer.java | 92 ---------------- .../org/team199/deepbluesim/Simulation.java | 26 +++-- .../mediators/EncoderMediatorBase.java | 68 ++++++++++++ .../{MockGyro.java => GyroMediator.java} | 35 +++--- .../deepbluesim/mediators/MockedCANCoder.java | 45 -------- .../deepbluesim/mediators/MockedEncoder.java | 100 ------------------ .../mediators/MockedSparkEncoder.java | 41 ------- .../deepbluesim/mediators/MotorMediator.java | 95 +++++++++++++++++ .../mediators/SimDeviceEncoderMediator.java | 31 ++++++ .../mediators/WPILibEncoderMediator.java | 31 ++++++ .../mediators/WebotsMotorForwarder.java | 40 ------- .../dist/protos/AndyMark9015Motor.proto | 29 +++++ .../dist/protos/AndyMarkRs775_125Motor.proto | 29 +++++ .../webotsFolder/dist/protos/BagMotor.proto | 29 +++++ .../dist/protos/BanebotsRs550Motor.proto | 29 +++++ .../dist/protos/BanebotsRs775Motor.proto | 29 +++++ .../webotsFolder/dist/protos/CANCoder.proto | 22 ++++ .../webotsFolder/dist/protos/CIMMotor.proto | 29 +++++ .../dist/protos/Falcon500Motor.proto | 29 +++++ .../dist/protos/MiniCIMMotor.proto | 29 +++++ .../dist/protos/NEO550Motor.proto | 28 +++++ .../webotsFolder/dist/protos/NEOMotor.proto | 28 +++++ .../dist/protos/RomiBuiltinMotor.proto | 28 +++++ .../dist/protos/SparkMaxAbsoluteEncoder.proto | 22 ++++ .../dist/protos/Vex775ProMotor.proto | 29 +++++ .../dist/protos/WPIEncoderBase.proto | 16 +++ .../dist/protos/WPIMotorBase.proto | 28 +++++ .../dist/protos/WPIQuadratureEncoder.proto | 22 ++++ 32 files changed, 797 insertions(+), 384 deletions(-) create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/ParseUtils.java delete mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/EncoderMediatorBase.java rename plugin/controller/src/main/java/org/team199/deepbluesim/mediators/{MockGyro.java => GyroMediator.java} (56%) delete mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java delete mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java delete mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java delete mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java create mode 100644 plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/CANCoder.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/WPIQuadratureEncoder.proto diff --git a/example/.gitignore b/example/.gitignore index 4e8ecbc9..18aed26e 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -161,9 +161,7 @@ imgui.ini # End of https://www.gitignore.io/api/c++,java,linux,macos,gradle,windows,visualstudiocode # For testing purposes, we want to ensure that this project starts without DeepBlueSim installed. -Webots/controllers/DeepBlueSim.jar -Webots/*/DBS* -Webots/*/.DBS* +Webots/* # Ignore Simulation GUI settings /networktables.json diff --git a/plugin/controller/build.gradle b/plugin/controller/build.gradle index 724ab4b3..492a929a 100644 --- a/plugin/controller/build.gradle +++ b/plugin/controller/build.gradle @@ -23,6 +23,10 @@ plugins { // Support creating a license report of all dependencies id 'com.jaredsburrows.license' version '0.8.80' + + // Support WPILib + id "edu.wpi.first.GradleRIO" version "2023.4.3" + id 'edu.wpi.first.WpilibTools' version '1.1.0' } group 'org.team199' @@ -33,6 +37,18 @@ targetCompatibility = 1.8 // The name of the main class in the application jar. mainClassName = 'DeepBlueSim' +wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get() + +def nativeConfigName = 'wpilibNatives' +def nativeConfig = configurations.create(nativeConfigName) + +def nativeTasks = wpilibTools.createExtractionTasks { + configurationName = nativeConfigName +} + +nativeTasks.addToSourceSetResources(sourceSets.main) +nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath") + // Add repositories containing project dependencies repositories { mavenCentral() @@ -43,6 +59,14 @@ repositories { dependencies { implementation 'org.carlmontrobotics:WPIWebSockets:unspecified' testImplementation 'junit:junit:4.13' + + implementation wpilibTools.deps.wpilibJava("wpimath") + + implementation group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: wpi.versions.jacksonVersion.get() + implementation group: "com.fasterxml.jackson.core", name: "jackson-core", version: wpi.versions.jacksonVersion.get() + implementation group: "com.fasterxml.jackson.core", name: "jackson-databind", version: wpi.versions.jacksonVersion.get() + + implementation group: "org.ejml", name: "ejml-simple", version: wpi.versions.ejmlVersion.get() } // Configure the building of the fat jar diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 8522a40b..afa3831b 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -26,28 +26,35 @@ public class DeepBlueSim { private static RunningObject wsConnection = null; public static void main(String[] args) { - UncaughtExceptionHandler eh = new UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread arg0, Throwable arg1) { - arg1.printStackTrace(System.err); - System.err.flush(); - System.exit(1); - } - }; - Thread.setDefaultUncaughtExceptionHandler(eh); - Thread.currentThread().setUncaughtExceptionHandler(eh); + // Set up exception handling to log to stderr and exit + { + UncaughtExceptionHandler eh = new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread arg0, Throwable arg1) { + arg1.printStackTrace(System.err); + System.err.flush(); + System.exit(1); + } + }; + Thread.setDefaultUncaughtExceptionHandler(eh); + Thread.currentThread().setUncaughtExceptionHandler(eh); + } ConnectionProcessor.setThreadExecutor(queuedMessages::add); + final Supervisor robot = new Supervisor(); Runtime.getRuntime().addShutdownHook(new Thread(robot::delete)); - int basicTimeStep = (int)Math.round(robot.getBasicTimeStep()); + if (!robot.getSupervisor()) { + System.err.println("The robot does not have supervisor=true. This is required to detect devices."); + System.exit(1); + } Simulation.init(robot, robot.getBasicTimeStep()); // Use a SimDeviceSim to coordinate with robot code tests - final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); - // Regular report the simulated robot's position - if (robot.getSupervisor()) { + { + final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); + // Regular report the simulated robot's position Simulation.registerPeriodicMethod(new Runnable() { public void run() { Node self = robot.getSelf(); @@ -57,35 +64,37 @@ public void run() { webotsSupervisorSim.set("self.position.z", pos[2]); } }); - } else { - System.err.println("The robot does not have supervisor=true. Reporting is limited."); - } - // If the robot code starts after us, we expect it to tell us it's ready, and we respond - // that we're ready. - callbackStore = webotsSupervisorSim.registerValueChangedCallback("robotStartMs", new StringCallback() { - @Override - public void callback(String name, String value) { + // If the robot code starts after us, we expect it to tell us it's ready, and we respond + // that we're ready. + callbackStore = webotsSupervisorSim.registerValueChangedCallback("robotStartMs", new StringCallback() { + @Override + public void callback(String name, String value) { System.out.println("Telling the robot we're ready"); webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); - } - - }, true); + } + }, true); + + // If the robot code starts before we us, then it might have already tried to tell + // us it was ready and we would have missed it. So, we tell it we're ready when we + // connect to it. + ConnectionProcessor.addOpenListener(() -> { + System.out.println("Telling the robot we're ready"); + webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); + }); + } - // If the robot code starts before we us, then it might have already tried to tell - // us it was ready and we would have missed it. So, we tell it we're ready when we - // connect to it. - ConnectionProcessor.addOpenListener(() -> { - System.out.println("Telling the robot we're ready"); - webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); - }); + // Get the basic timestamp to use for calls to robot.step() + int basicTimeStep = (int)Math.round(robot.getBasicTimeStep()); // Wait until one timestep has completed to ensure that the Webots simulator is // not still starting up. if (robot.step(basicTimeStep) == -1) { throw new RuntimeException("Couldn't even do one timestep!"); } + + // Connect to the robot code try { System.out.println("Trying to connect to robot..."); wsConnection = WSConnection.connectHALSim(true); diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/ParseUtils.java b/plugin/controller/src/main/java/org/team199/deepbluesim/ParseUtils.java new file mode 100644 index 00000000..0e583295 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/ParseUtils.java @@ -0,0 +1,21 @@ +package org.team199.deepbluesim; + +public final class ParseUtils { + + public static final double parseDoubleOrDefault(String str, double defaultValue) { + try { + return Double.parseDouble(str); + } catch(NumberFormatException e) { + return defaultValue; + } + } + + public static final int parseIntOrDefault(String str, int defaultValue) { + try { + return Integer.parseInt(str); + } catch(NumberFormatException e) { + return defaultValue; + } + } + +} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java deleted file mode 100644 index f3aa1761..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.team199.deepbluesim; - -import org.team199.deepbluesim.mediators.*; - -import org.team199.wpiws.ScopedObject; -import org.team199.wpiws.devices.EncoderSim; -import org.team199.wpiws.devices.PWMSim; -import org.team199.wpiws.devices.SimDeviceSim; -import org.team199.wpiws.interfaces.SimDeviceCallback; - -import java.util.Set; -import java.util.HashSet; - -/** - * Performs automatic registration of callbacks detecting both the initialization of new devices as well as data callbacks for devices such as Motors, Gyros, etc. - * This allows us to automatically link these devices to Webots, reducing the amount of code we would have to change from a standard robot project - */ -public class SimRegisterer { - - private static final SimDeviceCallback MISC_DEVICE_CALLBACK = SimRegisterer::callback; - private static final Set> CALLBACKS = new HashSet<>(); - - static { - // Register Initialized Callbacks for Misc Devices - CALLBACKS.add(SimDeviceSim.registerDeviceCreatedCallback("", MISC_DEVICE_CALLBACK, true)); - // Register Initialized Callbacks for PWM Devices - CALLBACKS.add(PWMSim.registerStaticInitializedCallback((name, isInitialized) -> { - if(isInitialized) { - callback("PWM", name); - } - }, true)); - // Register Initialized Callbacks for Encoder Devices - CALLBACKS.add(EncoderSim.registerStaticInitializedCallback((name, isInitialized) -> { - if(isInitialized) { - callback("Encoder", name); - } - }, true)); - } - - // Initialize SimRegisterer. This method exists to ensure that the static block is called - public static void init() {} - - // Callback methods which place new devices in a processing queue which is processed every robot period - // The WPILib callbacks are notified as part of the device creation. This process ensures that the devices complete their setup process - // This is especially important for SimDevice's because their initialized callbacks can be notified before their values have been created - // Queuing also ensures that callbacks (which are usually executed asynchronously) are processed synchronously with the rest of the robot code - - // Callback for when a Miscellaneous Device is registered - private static void callback(String deviceName) { - if(deviceName.startsWith("Talon") || deviceName.startsWith("Victor") || deviceName.startsWith("SparkMax")) { - // If a new Talon or Victor has been initialized, attempt to link it to a Webots Motor - // Create a WebotsMotorForwarder for this motor - final WebotsMotorForwarder fwdr = new WebotsMotorForwarder(Simulation.getRobot(), deviceName); - // Register a callback for when the Motor Output changes - CALLBACKS.add(new SimDeviceSim(deviceName).registerValueChangedCallback("Motor Output", - // Call the callback function - fwdr, - // Initialize with current speed - true)); - } - if(deviceName.startsWith("navX")) { - // If a navX is registered, try to link its SimDevice to the Webots robot - MockGyro.linkGyro(); - } - if(deviceName.startsWith("RelativeEncoder")) { - new MockedSparkEncoder(new SimDeviceSim(deviceName), deviceName); - } - if(deviceName.startsWith("CANCoder")) { - new MockedCANCoder(new SimDeviceSim(deviceName), deviceName); - } - } - - // Callback for when a known device type is registered on a Non-Can port - private static void callback(String type, String port) { - if(type.equals("PWM")) { - // If a new PWM device has been initialized, attempt to link it to a Webots motor - // Register a speed callback on this device - CALLBACKS.add(new PWMSim(port).registerSpeedCallback( - // Call a motor forwarder for a callback - new WebotsMotorForwarder(Simulation.getRobot(), "PWM[" + port + "]"), - // Initialize with current speed - true)); - } - else if(type.equals("Encoder")) { - // If a new Encoder device has been initialized, attempt to link it to a Webots encoder - // Register a speed callback on this device - EncoderSim sim = new EncoderSim(port); - new MockedEncoder(CALLBACKS, sim, port); - } - } - -} \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java b/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java index ec07320a..6753512f 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java @@ -2,7 +2,9 @@ import java.util.concurrent.CopyOnWriteArrayList; +import com.cyberbotics.webots.controller.Node; import com.cyberbotics.webots.controller.Robot; +import com.cyberbotics.webots.controller.Supervisor; /** * Manages control over the robot simulation and Webots connection @@ -13,7 +15,7 @@ public final class Simulation { /** * An object representing the Webots robot */ - private static Robot robot; + private static Supervisor robot; /** * The value of the basicTimeStep field of the WorldInfo node of the Webots robot * @see Robot#getBasicTimeStep() @@ -24,15 +26,9 @@ public final class Simulation { */ private static double timeStepMillis; // Use a CopyOnWriteArrayList to prevent synchronization errors - private static final CopyOnWriteArrayList periodicMethods; + private static final CopyOnWriteArrayList periodicMethods = new CopyOnWriteArrayList<>(); - static { - periodicMethods = new CopyOnWriteArrayList<>(); - // Register callbacks - SimRegisterer.init(); - } - - public static synchronized void init(Robot robot, double basicTimeStepMillis) { + public static synchronized void init(Supervisor robot, double basicTimeStepMillis) { if(init) { return; } @@ -54,6 +50,10 @@ public static Robot getRobot() { return robot; } + public static Supervisor getSupervisor() { + return robot; + } + public static double getBasicTimeStep() { return timeStep; } @@ -66,6 +66,14 @@ public static void runPeriodicMethods() { periodicMethods.forEach(Runnable::run); } + public static Node getPROTOBase(Node node, String baseName) { + if(node == null) { + return null; + } + // while(node.isProto() && node.getBaseTypeName()) + return null; + } + private Simulation() {} } \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/EncoderMediatorBase.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/EncoderMediatorBase.java new file mode 100644 index 00000000..e5aafed1 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/EncoderMediatorBase.java @@ -0,0 +1,68 @@ +package org.team199.deepbluesim.mediators; + +import org.team199.deepbluesim.Constants; +import org.team199.deepbluesim.Simulation; + +import com.cyberbotics.webots.controller.PositionSensor; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.util.Units; + +public abstract class EncoderMediatorBase implements Runnable { + + public final PositionSensor encoder; + public final boolean isOnMotorShaft; + public final boolean isAbsolute; + public final double absoluteOffsetDeg; + public final boolean isInverted; + public final int countsPerRevolution; + public final double gearing; + + private double lastPositionRad = 0; + + public EncoderMediatorBase(PositionSensor encoder) { + this(encoder, false, false, 0, false, 0, 1); // TODO: get these values from the robot + } + + public EncoderMediatorBase(PositionSensor encoder, boolean isOnMotorShaft, boolean isAbsolute, double absoluteOffsetDeg, boolean isInverted, int countsPerRevolution, double gearing) { + this.encoder = encoder; + this.isOnMotorShaft = isOnMotorShaft; + this.isAbsolute = isAbsolute; + this.absoluteOffsetDeg = absoluteOffsetDeg; + this.isInverted = isInverted; + this.countsPerRevolution = countsPerRevolution; + this.gearing = gearing; + + encoder.enable(Constants.sensorTimestep); + Simulation.registerPeriodicMethod(this); + } + + @Override + public void run() { + double positionRad = encoder.getValue(); + + if(isOnMotorShaft) { + positionRad *= gearing; + } + + if(isAbsolute) { + positionRad += Units.degreesToRadians(absoluteOffsetDeg); + MathUtil.inputModulus(positionRad, 0, Math.PI * 2); + } + + if(isInverted) { + positionRad *= -1; + } + + double velocityRadPerSec = (positionRad - lastPositionRad) / Simulation.getBasicTimeStep(); + lastPositionRad = positionRad; + + setPosition((int) Math.round(Units.radiansToRotations(positionRad) * countsPerRevolution)); + setVelocity((int) Math.round(Units.radiansToRotations(velocityRadPerSec) * countsPerRevolution)); + } + + public abstract void setPosition(int positionCounts); + + public abstract void setVelocity(int velocityCountsPerSecond); + +} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java similarity index 56% rename from plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java rename to plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java index d11ca384..98873575 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java @@ -10,28 +10,21 @@ * Handles the linking of the simulated AHRS gyro to Webots * @see com.kauailabs.navx.frc.AHRS */ -public final class MockGyro implements Runnable { +public class GyroMediator implements Runnable { - private static boolean gyroCreated = false; - private static SimDeviceSim gyroSim; - private static Gyro webotsGyro; - private static double angle = 0; + public final Gyro gyro; + public final SimDeviceSim device; + private double angle = 0; /** - * Links the simulated AHRS gyro to Webots if it has not been already + * Links the simulated AHRS gyro to Webots + * @param gyro the Webots gyro to link to */ - public static void linkGyro() { - if(gyroCreated) { - return; - } - gyroCreated = true; - // Create Sims - gyroSim = new SimDeviceSim("navX-Sensor[0]"); - webotsGyro = Simulation.getRobot().getGyro("gyro"); - if(webotsGyro != null) { - webotsGyro.enable(Constants.sensorTimestep); - Simulation.registerPeriodicMethod(new MockGyro()); - } + public GyroMediator(Gyro gyro) { + this.gyro = gyro; + gyro.enable(Constants.sensorTimestep); + device = new SimDeviceSim("navX-Sensor[0]"); + Simulation.registerPeriodicMethod(this); } @Override @@ -40,16 +33,14 @@ public void run() { reading represents the change in angular position about the z axis. getValues()[2] is negated to convert from Webots's coordinate system (counter-clockwise = positive) to WPILib's coordinate system (counter-clockwise = negative). */ - double reading = -webotsGyro.getValues()[2] * Simulation.getBasicTimeStep(); + double reading = -gyro.getValues()[2] * Simulation.getBasicTimeStep(); // In testing, reading was sometimes NAN in the first second of the simulation. // Also convert from radians to degrees angle += Double.isNaN(reading) ? 0 : (180 * reading / Math.PI); // Make sure angle is between 0 and 359 inclusive // angle = Math.copySign(Math.abs(angle) % 360, angle); // Update the WPIlib gyro - gyroSim.set("Yaw", angle + ""); + device.set("Yaw", angle + ""); } - private MockGyro() {} - } \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java deleted file mode 100644 index 75cceef4..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedCANCoder.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.team199.deepbluesim.mediators; - -import com.cyberbotics.webots.controller.PositionSensor; - -import org.team199.deepbluesim.Constants; -import org.team199.deepbluesim.Simulation; -import org.team199.wpiws.devices.SimDeviceSim; -import org.team199.wpiws.interfaces.StringCallback; - -public class MockedCANCoder implements Runnable, StringCallback { - - private SimDeviceSim device; - private PositionSensor webotsEncoder; - private double gearing; - - public MockedCANCoder(SimDeviceSim device, String name) { - this.device = device; - gearing = 1; - device.registerValueChangedCallback("gearing", this, true); - webotsEncoder = Simulation.getRobot().getPositionSensor(name); - if(webotsEncoder != null) { - webotsEncoder.enable(Constants.sensorTimestep); - Simulation.registerPeriodicMethod(this); - } - } - - @Override - public void callback(String name, String value) { - if(value == null) return; // Value has not yet been set - try { - gearing = Double.parseDouble(value); - } catch(NumberFormatException e) {} - } - - @Override - public void run() { - if(webotsEncoder == null) return; - // Get the position of the Webots encoders and set the position of the WPILib encoders - // getValue() returns radians - // revolutions = radians * gearing / 2pi - double revolutions = (webotsEncoder.getValue() * gearing) / (2*Math.PI); - device.set("count", revolutions); - } - -} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java deleted file mode 100644 index e1ac146d..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedEncoder.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.team199.deepbluesim.mediators; - -import com.cyberbotics.webots.controller.PositionSensor; - -import org.team199.deepbluesim.Constants; -import org.team199.deepbluesim.Simulation; -import org.team199.wpiws.ScopedObject; -import java.util.Set; -import org.team199.wpiws.devices.EncoderSim; - -public class MockedEncoder implements Runnable { - private String name; - private String wpiLibId; - private EncoderSim encoder; - private PositionSensor webotsEncoder; - private int countsPerRevolution = 256; - private int channelA = -1, channelB = -1; - - public MockedEncoder(Set> callbacks, EncoderSim sim, String wpiLibId) { - encoder = sim; - this.wpiLibId = wpiLibId; - callbacks.add(sim.registerChannelACallback( (id, channel) -> { - setChannelA(channel); - }, true)); - callbacks.add(sim.registerChannelBCallback( (id, channel) -> { - setChannelB(channel); - }, true)); - } - - private void setChannelA(int channel) { - channelA = channel; - tryToConnectWebotsPositionSensor(); - } - - private void setChannelB(int channel) { - channelB = channel; - tryToConnectWebotsPositionSensor(); - } - - private void tryToConnectWebotsPositionSensor() { - if (channelA < 0 || channelB < 0) - return; - String newName = "Encoder[" + channelA + "," + channelB + "]"; - if (webotsEncoder != null && !newName.equals(name)) { - System.out.println("WARNING: Ignoring attempt to change PositionSensor of id " - + wpiLibId + " from " + name + " to " + newName); - return; - } - - // TODO: Find the position sensor whose name *starts* with the given - // name and use the remained of the name to determine the countsPerRevolutions - // to use. - // For now, we just assume 256 countsPerRevolution - webotsEncoder = Simulation.getRobot().getPositionSensor(newName); - if(webotsEncoder != null) { - name = newName; - webotsEncoder.enable(Constants.sensorTimestep); - Simulation.registerPeriodicMethod(this); - } - } - - private int prevCount = 0; - private double timeCountChangedSecs = 0.0; - private double timeCountCheckedSecs = 0.0; - - @Override - public void run() { - // Get the position of the Webots encoders and set the position of the WPILib encoders - // getValue() returns radians - double revolutions = (webotsEncoder.getValue()) / (2*Math.PI); - int count = (int) Math.floor(revolutions * countsPerRevolution); - encoder.setCount(count); - - // Compute the period of time since the previous tick. This is a bit more complicated than it would - // seem at first glance because we need to handle both the case where multiple ticks have - // occurred since we last checked, the case where no ticks have occurred across multiple - // checks, and the case where only one tick has occurred since we last checked. - // For simplicity, we assume that if any number of ticks have occurred, then the most recent one - // happened now. - double curTimeSecs = Simulation.getRobot().getTime(); - // If no ticks have happened since we last checked, then we know when the the previous tick happened. - double prevTickTimeSecs = timeCountChangedSecs; - // ... but if any ticks have happened since we last checked, then compute the time of the previous - // tick assuming that the ticks were evenly spaced in time. - if (count != prevCount) { - prevTickTimeSecs = curTimeSecs - (curTimeSecs - timeCountCheckedSecs) / (count - prevCount); - } - double periodSecs = curTimeSecs - prevTickTimeSecs; - encoder.setPeriod(periodSecs/1000.0); // WPILib expects ms. - - // Keep track of when we last checked the count and what it was. - timeCountCheckedSecs = curTimeSecs; - prevCount = count; - - // Keep track of when the count actually changed - if (count != prevCount) { - timeCountChangedSecs = curTimeSecs; - } - } -} \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java deleted file mode 100644 index fdd9ddce..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.team199.deepbluesim.mediators; - -import com.cyberbotics.webots.controller.PositionSensor; - -import org.team199.deepbluesim.Constants; -import org.team199.deepbluesim.Simulation; -import org.team199.wpiws.devices.SimDeviceSim; - -public class MockedSparkEncoder implements Runnable { - private SimDeviceSim encoder; - private PositionSensor webotsEncoder; - // Default value for a CANEncoder - private final int countsPerRevolution = 4096; - private double gearing; - - public MockedSparkEncoder(SimDeviceSim sim, String name) { - encoder = sim; - webotsEncoder = Simulation.getRobot().getPositionSensor(name); - gearing = 1; - sim.registerValueChangedCallback("gearing", (valueName, value) -> { - if(value == null) return; // Value has not yet been set - try { - gearing = Double.parseDouble(value); - } catch(NumberFormatException e) {} - }, true); - if(webotsEncoder != null) { - webotsEncoder.enable(Constants.sensorTimestep); - Simulation.registerPeriodicMethod(this); - } - } - - @Override - public void run() { - // Get the position of the Webots encoders and set the position of the WPILib encoders - // getValue() returns radians - // revolutions = radians * gearing / 2pi - double revolutions = (webotsEncoder.getValue() * gearing) / (2*Math.PI); - int count = (int) Math.floor(revolutions * countsPerRevolution); - encoder.set("count", "" + count); - } -} \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java new file mode 100644 index 00000000..2fdfc677 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java @@ -0,0 +1,95 @@ +package org.team199.deepbluesim.mediators; + +import java.util.Collection; + +import org.team199.deepbluesim.ParseUtils; +import org.team199.deepbluesim.Simulation; +import org.team199.wpiws.ScopedObject; +import org.team199.wpiws.devices.SimDeviceSim; + +import com.cyberbotics.webots.controller.Brake; +import com.cyberbotics.webots.controller.Motor; +import com.cyberbotics.webots.controller.PositionSensor; + +import edu.wpi.first.math.system.plant.DCMotor; + +/** + * Links WPILib motor controllers to Webots + */ +public class MotorMediator implements Runnable { + + public final Motor motor; + public final double gearing; + public final DCMotor motorConstants; + public final SimDeviceSim motorDevice; + public final Brake brake; + + private double requestedOutput = 0; + private boolean brakeMode = true; + private double neutralDeadband = 0.04; + + /** + * Creates a new MotorMediator + * @param motor the Webots motor to link to + * @param callbackStore a collection to store callbacks in + * @throws IllegalArgumentException if {@code motor} is not a WPIMotorBase + */ + public MotorMediator(Motor motor, Collection> callbackStore) throws IllegalArgumentException { + this.motor = motor; + gearing = 1; + motorConstants = new DCMotor(0, 0, 0, 0, 0, 1); + motorDevice = new SimDeviceSim(String.format("%s[%d]", motor.getName(), 0 /* motor.getPort() */)); + + if(motor.getName().equals("Spark Max")) { + PositionSensor encoder = motor.getPositionSensor(); + if(encoder == null) { + System.err.println(String.format("WARNING: Spark Max encoder not found for motor: \"%s\", no position data will be reported!", motor.getName())); + } else { + new SimDeviceEncoderMediator(encoder, new SimDeviceSim(String.format("%s[%d]_RelativeEncoder", motor.getName(), 0 /* motor.getPort() */))); + } + } + + this.brake = motor.getBrake(); + if(brake == null) { + System.err.println(String.format("WARNING: Brake not found for motor: \"%s\", braking will be disabled!", motor.getName())); + } + + // Use velocity control + motor.setPosition(Double.POSITIVE_INFINITY); + brake.setDampingConstant(motorConstants.stallTorqueNewtonMeters * gearing); + + callbackStore.add(motorDevice.registerValueChangedCallback("Brake Mode", (name, enabled) -> { + brakeMode = Boolean.parseBoolean(enabled); + }, true)); + callbackStore.add(motorDevice.registerValueChangedCallback("Neutral Deadband", (name, deadband) -> { + neutralDeadband = Math.abs(ParseUtils.parseDoubleOrDefault(deadband, neutralDeadband)); + }, true)); + callbackStore.add(motorDevice.registerValueChangedCallback("Current Speed", (name, speed) -> { + requestedOutput = ParseUtils.parseDoubleOrDefault(speed, requestedOutput); + }, true)); + + Simulation.registerPeriodicMethod(this); + } + + @Override + public void run() { + // Apply the speed changes periodically so that changes to variables (ie brake mode) don't require a speed update to be applied + // Copy requested output so that decreasing the neutral deadband can take effect without a speed update + double currentOutput = requestedOutput; + if(Math.abs(currentOutput) < neutralDeadband) { + currentOutput = 0; + brake.setDampingConstant(brakeMode ? motorConstants.stallTorqueNewtonMeters * gearing : 0); + } else { + brake.setDampingConstant(0); + } + + double velocity = currentOutput * motorConstants.freeSpeedRadPerSec; + motor.setVelocity(velocity / gearing); + + double currentDraw = motorConstants.getCurrent(velocity, currentOutput * motorConstants.nominalVoltageVolts); + motor.setAvailableTorque(motorConstants.getTorque(currentDraw) * gearing); + + motorDevice.set("Current Draw", currentDraw); + } + +} \ No newline at end of file diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java new file mode 100644 index 00000000..dd03cadf --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java @@ -0,0 +1,31 @@ +package org.team199.deepbluesim.mediators; + +import org.team199.wpiws.devices.SimDeviceSim; + +import com.cyberbotics.webots.controller.PositionSensor; + +public class SimDeviceEncoderMediator extends EncoderMediatorBase { + + public final SimDeviceSim device; + + public SimDeviceEncoderMediator(PositionSensor encoder, SimDeviceSim device) { + super(encoder); + this.device = device; + } + + public SimDeviceEncoderMediator(PositionSensor encoder, SimDeviceSim device, boolean isOnMotorShaft, boolean isAbsolute, double absoluteOffsetDeg, boolean isInverted, int countsPerRevolution, double gearing) { + super(encoder, isOnMotorShaft, isAbsolute, absoluteOffsetDeg, isInverted, countsPerRevolution, gearing); + this.device = device; + } + + @Override + public void setPosition(int positionCounts) { + device.set("Position", positionCounts); + } + + @Override + public void setVelocity(int velocityCountsPerSecond) { + device.set("Velocity", velocityCountsPerSecond); + } + +} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java new file mode 100644 index 00000000..c8918c01 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java @@ -0,0 +1,31 @@ +package org.team199.deepbluesim.mediators; + +import org.team199.wpiws.devices.EncoderSim; + +import com.cyberbotics.webots.controller.PositionSensor; + +public class WPILibEncoderMediator extends EncoderMediatorBase { + + public final EncoderSim device; + + public WPILibEncoderMediator(PositionSensor encoder, EncoderSim device) { + super(encoder); + this.device = device; + } + + public WPILibEncoderMediator(PositionSensor encoder, EncoderSim device, boolean isOnMotorShaft, boolean isAbsolute, double absoluteOffsetDeg, boolean isInverted, int countsPerRevolution, double gearing) { + super(encoder, isOnMotorShaft, isAbsolute, absoluteOffsetDeg, isInverted, countsPerRevolution, gearing); + this.device = device; + } + + @Override + public void setPosition(int positionCounts) { + device.setCount(positionCounts); + } + + @Override + public void setVelocity(int velocityCountsPerSecond) { + device.setPeriod(1.0D / velocityCountsPerSecond); + } + +} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java deleted file mode 100644 index cd80c19d..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.team199.deepbluesim.mediators; - -import org.team199.wpiws.interfaces.DoubleCallback; -import org.team199.wpiws.interfaces.StringCallback; - -import com.cyberbotics.webots.controller.Motor; -import com.cyberbotics.webots.controller.Robot; - -/** - * Forwards motor calls from WPILib motor controllers to Webots - */ -public class WebotsMotorForwarder implements DoubleCallback, StringCallback { - - private Motor motor; - - /** - * Creates a new WebotsMotorForwarder - * @param robot the Webots robot - * @param motorName the name of the Webots motor to which to connect - */ - public WebotsMotorForwarder(Robot robot, String motorName) { - motor = robot.getMotor(motorName); - // Use velocity control - motor.setPosition(Double.POSITIVE_INFINITY); - } - - @Override - public void callback(String name, String value) { - if(value == null) return; // Value has not yet been set - try { - callback(name, Double.parseDouble(value)); - } catch(NumberFormatException e) {} - } - - @Override - public void callback(String name, double value) { - motor.setVelocity(motor.getMaxVelocity() * value); - } - -} \ No newline at end of file diff --git a/plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto new file mode 100644 index 00000000..98489251 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of an AndyMark 9015 motor. +PROTO AndyMark9015Motor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 0.36 + stallCurrentAmps 71 + freeCurrentAmps 3.7 + freeSpeedRPM 14270 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto new file mode 100644 index 00000000..96965b66 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of an AndyMark Rs 775_125 motor. +PROTO AndyMarkRs775_125Motor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 0.28 + stallCurrentAmps 18 + freeCurrentAmps 1.6 + freeSpeedRPM 5800 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto new file mode 100644 index 00000000..7f4ae11a --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a Bag Motor +PROTO BagMotor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 0.43 + stallCurrentAmps 53 + freeCurrentAmps 1.8 + freeSpeedRPM 13180 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto new file mode 100644 index 00000000..e3ff8d26 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a Banebots Rs 550 motor. +PROTO BanebotsRs550Motor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + inverted IS inverted + controllerType IS controllerType + gearing IS gearing + nominalVoltageVolts 12 + stallTorqueNewtonMeters 0.38 + stallCurrentAmps 84 + freeCurrentAmps 0.4 + freeSpeedRPM 19000 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto new file mode 100644 index 00000000..005f592a --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a Banebots Rs 775 motor. +PROTO BanebotsRs775Motor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 0.72 + stallCurrentAmps 97 + freeCurrentAmps 2.7 + freeSpeedRPM 13050 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/CANCoder.proto b/plugin/controller/src/webotsFolder/dist/protos/CANCoder.proto new file mode 100644 index 00000000..5fb0995c --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/CANCoder.proto @@ -0,0 +1,22 @@ +#VRML_SIM R2023b utf8 + +EXTERNPROTO "../protos/WPIEncoderBase.proto" + +# A WPIEncoderBase implementation for a CANCoder. +PROTO CANCoder [ + unconnectedField SFInt32 id 0 + field SFString{"Motor Shaft", "Output Shaft"} location "Motor Shaft" + field SFFloat absoluteOffsetDeg 0 + field SFBool inverted FALSE + field SFFloat noiseStdDevRad 0 +] +{ + WPIEncoderBase { + location IS location + absolute TRUE + absoluteOffsetDeg IS absoluteOffsetDeg + inverted IS inverted + CPR 4096 + noiseStdDevRad IS noiseStdDevRad + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto new file mode 100644 index 00000000..0c962dc7 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a CIM motor. +PROTO CIMMotor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 2.42 + stallCurrentAmps 133 + freeCurrentAmps 2.7 + freeSpeedRPM 5310 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto new file mode 100644 index 00000000..7a6af5d2 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a Falcon 500 motor. +PROTO Falcon500Motor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 4.69 + stallCurrentAmps 257 + freeCurrentAmps 1.5 + freeSpeedRPM 6380 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto new file mode 100644 index 00000000..849416b0 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a Mini CIM motor. +PROTO MiniCIMMotor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 1.41 + stallCurrentAmps 89 + freeCurrentAmps 3 + freeSpeedRPM 5840 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto new file mode 100644 index 00000000..d26ceb5d --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto @@ -0,0 +1,28 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a NEO 550 motor. +PROTO NEO550Motor [ + field SFInt32 id 0 + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS id + controllerType "Spark Max" + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 0.97 + stallCurrentAmps 100 + freeCurrentAmps 1.4 + freeSpeedRPM 11000 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto new file mode 100644 index 00000000..b75277a3 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto @@ -0,0 +1,28 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a NEO motor. +PROTO NEOMotor [ + field SFInt32 id 0 + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS id + controllerType "Spark Max" + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 2.6 + stallCurrentAmps 105 + freeCurrentAmps 1.8 + freeSpeedRPM 5676 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto new file mode 100644 index 00000000..d04932bd --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto @@ -0,0 +1,28 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a Romi Bultin Motor +PROTO RomiBuiltinMotor [ + field SFInt32 port 0 + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType "PWM" + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 4.5 + stallTorqueNewtonMeters 0.1765 + stallCurrentAmps 1.25 + freeCurrentAmps 0.13 + freeSpeedRPM 150 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto new file mode 100644 index 00000000..3f1ce833 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto @@ -0,0 +1,22 @@ +#VRML_SIM R2023b utf8 + +EXTERNPROTO "../protos/WPIEncoderBase.proto" + +# A WPIEncoderBase implementation for an absolute encoder for a Spark Max. +PROTO SparkMaxAbsoluteEncoder [ + field SFString{"Motor Shaft", "Output Shaft"} location "Motor Shaft" + field SFFloat absoluteOffsetDeg 0 + field SFBool inverted FALSE + field SFInt32 CPR 4096 + field SFFloat noiseStdDevRad 0 +] +{ + WPIEncoderBase { + location IS location + absolute TRUE + absoluteOffsetDeg IS absoluteOffsetDeg + inverted IS inverted + CPR IS CPR + noiseStdDevRad IS noiseStdDevRad + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto new file mode 100644 index 00000000..d92b1802 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 + +# license: WPILib BSD +# license url: https://github.com/wpilibsuite/allwpilib/blob/main/LICENSE.md + +EXTERNPROTO "../protos/WPIMotorBase.proto" + +# A WPIMotorBase with the parameters of a Vex 775 Pro motor. +PROTO Vex775ProMotor [ + field SFInt32 port 0 + field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFFloat gearing 1 + field SFBool inverted FALSE + field SFString sound "default" +] +{ + WPIMotorBase { + port IS port + controllerType IS controllerType + gearing IS gearing + inverted IS inverted + nominalVoltageVolts 12 + stallTorqueNewtonMeters 0.71 + stallCurrentAmps 134 + freeCurrentAmps 0.7 + freeSpeedRPM 18730 + sound IS sound + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto new file mode 100644 index 00000000..d8a0b77f --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto @@ -0,0 +1,16 @@ +#VRML_SIM R2023b utf8 + +# A base proto for robot encoders +PROTO WPIEncoderBase [ + unconnectedField SFString{"Motor Shaft", "Output Shaft"} location "Motor Shaft" + unconnectedField SFBool absolute FALSE + unconnectedField SFFloat absoluteOffsetDeg 0 + unconnectedField SFBool inverted FALSE + unconnectedField SFInt32 CPR 4096 + field SFFloat noiseStdDevRad 0 +] +{ + PositionSensor { + noise IS noiseStdDevRad + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto new file mode 100644 index 00000000..e6aa14da --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto @@ -0,0 +1,28 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +# A RotationalMotor with the properties necessary to construct a WPILib DCMotor +PROTO WPIMotorBase [ + unconnectedField SFInt32 port 0 + unconnectedField SFString{"Spark Max", "Talon SRX", "Victor SPX", "PWM"} controllerType "Spark Max" + field SFFloat gearing 1 + unconnectedField SFBool inverted FALSE + unconnectedField SFFloat nominalVoltageVolts 12 + field SFFloat stallTorqueNewtonMeters 5 + unconnectedField SFFloat stallCurrentAmps 100 + unconnectedField SFFloat freeCurrentAmps 1 + field SFFloat freeSpeedRPM 5000 + field SFString sound "default" +] +{ + RotationalMotor { + maxTorque %<= fields.stallTorqueNewtonMeters.value * fields.gearing.value >% + maxVelocity %<= fields.freeSpeedRPM.value / fields.gearing.value >% + # The documentation about the multiplier field is unclear as to how it applies differently to differnt fields/functions. + # I think it's best just to implement it ourselves for now. + # multiplier %<= 1 / fields.gearing.value >% + %< if (fields.sound.value !== "default") { >% + soundUrl IS sound + %< } >% + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIQuadratureEncoder.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIQuadratureEncoder.proto new file mode 100644 index 00000000..8d1bb637 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIQuadratureEncoder.proto @@ -0,0 +1,22 @@ +#VRML_SIM R2023b utf8 + +EXTERNPROTO "../protos/WPIEncoderBase.proto" + +# A WPIEncoderBase implementation for a Quadrature Encoder. +PROTO WPIQuadratureEncoder [ + unconnectedField SFInt32 channelA 0 + unconnectedField SFInt32 channelB 1 + field SFString{"Motor Shaft", "Output Shaft"} location "Motor Shaft" + field SFBool inverted FALSE + field SFInt32 CPR 4096 + field SFFloat noiseStdDevRad 0 +] +{ + WPIEncoderBase { + location IS location + absolute FALSE + inverted IS inverted + CPR IS CPR + noiseStdDevRad IS noiseStdDevRad + } +} From a3200819549f0a7e08bbf4e0120e7cc882924c4e Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 01:32:32 -0700 Subject: [PATCH 32/60] bump WPIWebSockets --- WPIWebSockets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WPIWebSockets b/WPIWebSockets index 6ad03be6..e7297b68 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit 6ad03be6cc92503d6bd1e4022f9369c8aef47b10 +Subproject commit e7297b682e3ed6fcb3f24d27948b284b0f4b96f5 From 624dfb61704f466d02a1d4bb21cebefc08bbdbe9 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 01:34:17 -0700 Subject: [PATCH 33/60] bump WPIWebSockets --- WPIWebSockets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WPIWebSockets b/WPIWebSockets index e7297b68..b303fe46 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit e7297b682e3ed6fcb3f24d27948b284b0f4b96f5 +Subproject commit b303fe46a0b28a8b46f9849ef8615ab0f48e2e71 From 1bd888ec7ed0d14827b554202351bb25944051ac Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 02:30:32 -0700 Subject: [PATCH 34/60] implement name-mangling and device linking --- .../controller/src/main/java/DeepBlueSim.java | 2 + .../team199/deepbluesim/SimRegisterer.java | 168 ++++++++++++++++++ .../deepbluesim/mediators/MotorMediator.java | 21 ++- .../mediators/SimDeviceEncoderMediator.java | 5 - .../mediators/WPILibEncoderMediator.java | 9 +- .../dist/protos/AndyMark9015Motor.proto | 4 +- .../dist/protos/AndyMarkRs775_125Motor.proto | 4 +- .../webotsFolder/dist/protos/BagMotor.proto | 4 +- .../dist/protos/BanebotsRs550Motor.proto | 6 +- .../dist/protos/BanebotsRs775Motor.proto | 4 +- .../webotsFolder/dist/protos/CIMMotor.proto | 4 +- .../dist/protos/Falcon500Motor.proto | 4 +- .../dist/protos/MiniCIMMotor.proto | 4 +- .../dist/protos/NEO550Motor.proto | 2 +- .../webotsFolder/dist/protos/NEOMotor.proto | 2 +- .../dist/protos/RomiBuiltinMotor.proto | 2 +- .../dist/protos/SparkMaxAbsoluteEncoder.proto | 2 +- .../protos/SparkMaxAlternateEncoder.proto | 21 +++ .../dist/protos/SparkMaxAnalogSensor.proto | 22 +++ .../dist/protos/Vex775ProMotor.proto | 4 +- .../dist/protos/WPIEncoderBase.proto | 2 + .../dist/protos/WPIMotorBase.proto | 3 +- 22 files changed, 256 insertions(+), 43 deletions(-) create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java create mode 100644 plugin/controller/src/webotsFolder/dist/protos/SparkMaxAlternateEncoder.proto create mode 100644 plugin/controller/src/webotsFolder/dist/protos/SparkMaxAnalogSensor.proto diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index afa3831b..7fa84312 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -94,6 +94,8 @@ public void callback(String name, String value) { throw new RuntimeException("Couldn't even do one timestep!"); } + // SimRegisterer.connectDevices(); + // Connect to the robot code try { System.out.println("Trying to connect to robot..."); diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java new file mode 100644 index 00000000..d84c7154 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java @@ -0,0 +1,168 @@ +package org.team199.deepbluesim; + +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.team199.deepbluesim.mediators.GyroMediator; +import org.team199.deepbluesim.mediators.MotorMediator; +import org.team199.deepbluesim.mediators.SimDeviceEncoderMediator; +import org.team199.deepbluesim.mediators.WPILibEncoderMediator; +import org.team199.wpiws.ScopedObject; +import org.team199.wpiws.devices.EncoderSim; +import org.team199.wpiws.devices.SimDeviceSim; + +import com.cyberbotics.webots.controller.Device; +import com.cyberbotics.webots.controller.Gyro; +import com.cyberbotics.webots.controller.Motor; +import com.cyberbotics.webots.controller.Node; +import com.cyberbotics.webots.controller.PositionSensor; +import com.cyberbotics.webots.controller.Supervisor; + +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.math.util.Units; + +public class SimRegisterer { + + private static final CopyOnWriteArraySet unboundEncoders = new CopyOnWriteArraySet<>(); + private static final CopyOnWriteArraySet> CALLBACKS = new CopyOnWriteArraySet<>(); + + public static void connectDevices() { + Supervisor robot = Simulation.getSupervisor(); + + boolean hasGyro = false; + + for (int i = 0; i < robot.getNumberOfDevices(); i++) { + Device device = robot.getDeviceByIndex(i); + String name = device.getName(); + if (name.startsWith("DBSim_")) { + try { + String type = name.split("_")[1]; + switch (type) { + case "Encoder": + connectEncoder((PositionSensor) device, robot); + break; + case "Motor": + connectMotor((Motor) device, robot); + break; + } + } catch (Exception e) { + System.err.println("Error occurred connecting to device " + device.getName() + ":"); + e.printStackTrace(System.err); + System.err.flush(); + } + } + + if (device instanceof Gyro) { + if (hasGyro) { + System.err.println("Warning: multiple gyros detected! Only one will be used."); + } else { + new GyroMediator((Gyro) device); + hasGyro = true; + } + } + + if(!unboundEncoders.isEmpty()) { + Simulation.registerPeriodicMethod(SimRegisterer::tryBindEncoders); + } + } + } + + public static void tryBindEncoders() { + if(unboundEncoders.isEmpty()) return; + + Supervisor robot = Simulation.getSupervisor(); + + String[] unboundEncodersCopy = unboundEncoders.toArray(new String[0]); + unboundEncoders.clear(); + + for (String encoderName : unboundEncodersCopy) { + try { + connectEncoder((PositionSensor) robot.getDevice(encoderName), robot); + } catch (Exception e) { + System.err.println("Error occurred connecting to device " + encoderName + ":"); + e.printStackTrace(System.err); + System.err.flush(); + } + } + } + + public static void connectEncoder(PositionSensor device, Supervisor robot) { + Node node = robot.getFromDevice(device); + + String[] nameParts = device.getName().split("_"); + boolean isOnMotorShaft = Boolean.parseBoolean(nameParts[2]); + boolean isAbsolute = Boolean.parseBoolean(nameParts[3]); + double absoluteOffsetDeg = Double.parseDouble(nameParts[4]); + boolean isInverted = Boolean.parseBoolean(nameParts[5]); + int countsPerRevolution = Integer.parseInt(nameParts[6]); + + double gearing; + try { + String motorName = device.getMotor().getName(); + if(motorName.startsWith("DBSim_Motor")) { + gearing = Double.parseDouble(motorName.split("_")[4]); + } else { + throw new IllegalArgumentException(); + } + } catch(Exception e) { + System.err.println("Warning: No valid motor found for encoder \"" + device.getName() + "\"! Assuming 1:1 gearing..."); + gearing = 1; + } + + if (node.getField("channelA") != null) { // WPILib Encoder + int channelA = node.getField("channelA").getSFInt32(); + int channelB = node.getField("channelB").getSFInt32(); + + // WPILib encoders no longer have deterministic names (based on channel numbers), so we have to search for the encoder + // This was the best way I could think of to do it + Optional simDevice = Arrays.stream(EncoderSim.enumerateDevices()).map(EncoderSim::new) + .filter(encoder -> encoder.getChannelA() == channelA && encoder.getChannelB() == channelB) + .findAny(); + + if(simDevice.isPresent()) { + new WPILibEncoderMediator(device, simDevice.get(), isOnMotorShaft, isInverted, countsPerRevolution, gearing); + } else { + unboundEncoders.add(device.getName()); + } + } else if(node.getField("id") != null) { // CANCoder + new SimDeviceEncoderMediator(device, new SimDeviceSim("CANCoder[" + node.getField("id").getSFInt32() + "]"), isOnMotorShaft, isAbsolute, absoluteOffsetDeg, isInverted, countsPerRevolution, gearing); + } else if(node.getTypeName().startsWith("SparkMax")) { // One of the SparkMax encoder types + Motor motor = device.getMotor(); + + String motorName; + if(motor == null || !(motorName = motor.getName()).startsWith("DBSim_Motor_Spark Max")) { + System.err.println("Warning: Spark Max Encoder \"" + device.getName() + "\" is not attached to a Spark Max motor!"); + return; + } + + String[] motorNameParts = motorName.split("_"); + int motorId = Integer.parseInt(motorNameParts[3]); + + String simDeviceName = "SparkMax[" + motorId + "]_" + node.getTypeName().substring("SparkMax".length()); + + new SimDeviceEncoderMediator(device, new SimDeviceSim(simDeviceName), isOnMotorShaft, isAbsolute, absoluteOffsetDeg, isInverted, countsPerRevolution, gearing); + } else { + System.err.println("Warning: Ignoring invalid encoder: " + device.getName() + "!"); + } + } + + public static void connectMotor(Motor device, Supervisor robot) { + String[] nameParts = device.getName().split("_"); + String controllerType = nameParts[2]; + int port = Integer.parseInt(nameParts[3]); + double gearing = Double.parseDouble(nameParts[4]); + boolean inverted = Boolean.parseBoolean(nameParts[5]); + double nominalVoltageVolts = Double.parseDouble(nameParts[6]); + double stallTorqueNewtonMeters = Double.parseDouble(nameParts[7]); + double stallCurrentAmps = Double.parseDouble(nameParts[8]); + double freeCurrentAmps = Double.parseDouble(nameParts[9]); + double freeSpeedRPM = Double.parseDouble(nameParts[10]); + + String simDeviceName = controllerType.replaceAll("\\s", "") + "[" + port + "]"; + DCMotor motorConstants = new DCMotor(nominalVoltageVolts, stallTorqueNewtonMeters, stallCurrentAmps, freeCurrentAmps, Units.rotationsPerMinuteToRadiansPerSecond(freeSpeedRPM), 1); + + new MotorMediator(device, new SimDeviceSim(simDeviceName), motorConstants, gearing, inverted, CALLBACKS); + } + +} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java index 2fdfc677..7f5ba0cf 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java @@ -18,8 +18,11 @@ */ public class MotorMediator implements Runnable { + public static final int NEO_BUILTIN_ENCODER_CPR = 42; + public final Motor motor; public final double gearing; + public final boolean inverted; public final DCMotor motorConstants; public final SimDeviceSim motorDevice; public final Brake brake; @@ -31,21 +34,25 @@ public class MotorMediator implements Runnable { /** * Creates a new MotorMediator * @param motor the Webots motor to link to + * @param simDevice the SimDeviceSim to use + * @param motorConstants the motor constants to use + * @param gearing the gear ratio to use * @param callbackStore a collection to store callbacks in * @throws IllegalArgumentException if {@code motor} is not a WPIMotorBase */ - public MotorMediator(Motor motor, Collection> callbackStore) throws IllegalArgumentException { + public MotorMediator(Motor motor, SimDeviceSim simDevice, DCMotor motorConstants, double gearing, boolean inverted, Collection> callbackStore) throws IllegalArgumentException { this.motor = motor; - gearing = 1; - motorConstants = new DCMotor(0, 0, 0, 0, 0, 1); - motorDevice = new SimDeviceSim(String.format("%s[%d]", motor.getName(), 0 /* motor.getPort() */)); + motorDevice = simDevice; + this.motorConstants = motorConstants; + this.gearing = gearing; + this.inverted = inverted; - if(motor.getName().equals("Spark Max")) { + if(motor.getName().startsWith("DBSim_Motor_Spark Max")) { PositionSensor encoder = motor.getPositionSensor(); if(encoder == null) { System.err.println(String.format("WARNING: Spark Max encoder not found for motor: \"%s\", no position data will be reported!", motor.getName())); } else { - new SimDeviceEncoderMediator(encoder, new SimDeviceSim(String.format("%s[%d]_RelativeEncoder", motor.getName(), 0 /* motor.getPort() */))); + new SimDeviceEncoderMediator(encoder, new SimDeviceSim(motorDevice.id + "_RelativeEncoder"), true, false, 0, inverted, NEO_BUILTIN_ENCODER_CPR, gearing); } } @@ -84,7 +91,7 @@ public void run() { } double velocity = currentOutput * motorConstants.freeSpeedRadPerSec; - motor.setVelocity(velocity / gearing); + motor.setVelocity((inverted ? -1 : 1) * velocity / gearing); double currentDraw = motorConstants.getCurrent(velocity, currentOutput * motorConstants.nominalVoltageVolts); motor.setAvailableTorque(motorConstants.getTorque(currentDraw) * gearing); diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java index dd03cadf..ac51f97e 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java @@ -8,11 +8,6 @@ public class SimDeviceEncoderMediator extends EncoderMediatorBase { public final SimDeviceSim device; - public SimDeviceEncoderMediator(PositionSensor encoder, SimDeviceSim device) { - super(encoder); - this.device = device; - } - public SimDeviceEncoderMediator(PositionSensor encoder, SimDeviceSim device, boolean isOnMotorShaft, boolean isAbsolute, double absoluteOffsetDeg, boolean isInverted, int countsPerRevolution, double gearing) { super(encoder, isOnMotorShaft, isAbsolute, absoluteOffsetDeg, isInverted, countsPerRevolution, gearing); this.device = device; diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java index c8918c01..cd7c8cb4 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java @@ -8,13 +8,8 @@ public class WPILibEncoderMediator extends EncoderMediatorBase { public final EncoderSim device; - public WPILibEncoderMediator(PositionSensor encoder, EncoderSim device) { - super(encoder); - this.device = device; - } - - public WPILibEncoderMediator(PositionSensor encoder, EncoderSim device, boolean isOnMotorShaft, boolean isAbsolute, double absoluteOffsetDeg, boolean isInverted, int countsPerRevolution, double gearing) { - super(encoder, isOnMotorShaft, isAbsolute, absoluteOffsetDeg, isInverted, countsPerRevolution, gearing); + public WPILibEncoderMediator(PositionSensor encoder, EncoderSim device, boolean isOnMotorShaft, boolean isInverted, int countsPerRevolution, double gearing) { + super(encoder, isOnMotorShaft, false, 0, isInverted, countsPerRevolution, gearing); this.device = device; } diff --git a/plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto index 98489251..6cf3566a 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/AndyMark9015Motor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of an AndyMark 9015 motor. PROTO AndyMark9015Motor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto index 96965b66..514d11f1 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/AndyMarkRs775_125Motor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of an AndyMark Rs 775_125 motor. PROTO AndyMarkRs775_125Motor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto index 7f4ae11a..b54fd6df 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/BagMotor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of a Bag Motor PROTO BagMotor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto index e3ff8d26..f296caec 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs550Motor.proto @@ -7,18 +7,18 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of a Banebots Rs 550 motor. PROTO BanebotsRs550Motor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port - inverted IS inverted controllerType IS controllerType + port IS port gearing IS gearing + inverted IS inverted nominalVoltageVolts 12 stallTorqueNewtonMeters 0.38 stallCurrentAmps 84 diff --git a/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto index 005f592a..5bf455c2 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/BanebotsRs775Motor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of a Banebots Rs 775 motor. PROTO BanebotsRs775Motor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto index 0c962dc7..b760a311 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/CIMMotor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of a CIM motor. PROTO CIMMotor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto index 7a6af5d2..8a584183 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/Falcon500Motor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of a Falcon 500 motor. PROTO Falcon500Motor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto index 849416b0..517b6b13 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/MiniCIMMotor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of a Mini CIM motor. PROTO MiniCIMMotor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto b/plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto index d26ceb5d..01fd48e6 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/NEO550Motor.proto @@ -14,8 +14,8 @@ PROTO NEO550Motor [ ] { WPIMotorBase { - port IS id controllerType "Spark Max" + port IS id gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto index b75277a3..1eff589a 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/NEOMotor.proto @@ -14,8 +14,8 @@ PROTO NEOMotor [ ] { WPIMotorBase { - port IS id controllerType "Spark Max" + port IS id gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto index d04932bd..370a738c 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/RomiBuiltinMotor.proto @@ -14,8 +14,8 @@ PROTO RomiBuiltinMotor [ ] { WPIMotorBase { - port IS port controllerType "PWM" + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 4.5 diff --git a/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto index 3f1ce833..5b5f428f 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAbsoluteEncoder.proto @@ -7,7 +7,7 @@ PROTO SparkMaxAbsoluteEncoder [ field SFString{"Motor Shaft", "Output Shaft"} location "Motor Shaft" field SFFloat absoluteOffsetDeg 0 field SFBool inverted FALSE - field SFInt32 CPR 4096 + field SFInt32 CPR 8192 field SFFloat noiseStdDevRad 0 ] { diff --git a/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAlternateEncoder.proto b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAlternateEncoder.proto new file mode 100644 index 00000000..131c5d33 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAlternateEncoder.proto @@ -0,0 +1,21 @@ +#VRML_SIM R2023b utf8 + +EXTERNPROTO "../protos/WPIEncoderBase.proto" + +# A WPIEncoderBase implementation for an alternate encoder for a Spark Max. +PROTO SparkMaxAlternateEncoder [ + field SFString{"Motor Shaft", "Output Shaft"} location "Motor Shaft" + field SFBool inverted FALSE + field SFInt32 CPR 8192 + field SFFloat noiseStdDevRad 0 +] +{ + WPIEncoderBase { + location IS location + absolute FALSE + absoluteOffsetDeg 0 + inverted IS inverted + CPR IS CPR + noiseStdDevRad IS noiseStdDevRad + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAnalogSensor.proto b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAnalogSensor.proto new file mode 100644 index 00000000..a6167270 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/SparkMaxAnalogSensor.proto @@ -0,0 +1,22 @@ +#VRML_SIM R2023b utf8 + +EXTERNPROTO "../protos/WPIEncoderBase.proto" + +# A WPIEncoderBase implementation for an analog sensor for a Spark Max. +PROTO SparkMaxAnalogSensor [ + field SFString{"Motor Shaft", "Output Shaft"} location "Motor Shaft" + field SFFloat absoluteOffsetDeg 0 + field SFBool inverted FALSE + field SFInt32 CPR 4096 + field SFFloat noiseStdDevRad 0 +] +{ + WPIEncoderBase { + location IS location + absolute TRUE + absoluteOffsetDeg IS absoluteOffsetDeg + inverted IS inverted + CPR IS CPR + noiseStdDevRad IS noiseStdDevRad + } +} diff --git a/plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto b/plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto index d92b1802..29817ff5 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/Vex775ProMotor.proto @@ -7,16 +7,16 @@ EXTERNPROTO "../protos/WPIMotorBase.proto" # A WPIMotorBase with the parameters of a Vex 775 Pro motor. PROTO Vex775ProMotor [ - field SFInt32 port 0 field SFString{"Talon SRX", "Victor SPX", "PWM"} controllerType "Talon SRX" + field SFInt32 port 0 field SFFloat gearing 1 field SFBool inverted FALSE field SFString sound "default" ] { WPIMotorBase { - port IS port controllerType IS controllerType + port IS port gearing IS gearing inverted IS inverted nominalVoltageVolts 12 diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto index d8a0b77f..534271a1 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto @@ -1,4 +1,5 @@ #VRML_SIM R2023b utf8 +# template language: javascript # A base proto for robot encoders PROTO WPIEncoderBase [ @@ -11,6 +12,7 @@ PROTO WPIEncoderBase [ ] { PositionSensor { + name %<= ["DBSim_Encoder", location === "Motor Shaft", absolute.value, absoluteOffsetDeg.value, inverted.value, CPR.value].join('_') %> noise IS noiseStdDevRad } } diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto index e6aa14da..2c0e2d0e 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto @@ -3,8 +3,8 @@ # A RotationalMotor with the properties necessary to construct a WPILib DCMotor PROTO WPIMotorBase [ - unconnectedField SFInt32 port 0 unconnectedField SFString{"Spark Max", "Talon SRX", "Victor SPX", "PWM"} controllerType "Spark Max" + unconnectedField SFInt32 port 0 field SFFloat gearing 1 unconnectedField SFBool inverted FALSE unconnectedField SFFloat nominalVoltageVolts 12 @@ -16,6 +16,7 @@ PROTO WPIMotorBase [ ] { RotationalMotor { + name %<= ["DBSim_Motor", controllerType.value, port.value, gearing.value, inverted.value, nominalVoltageVolts.value, stallTorqueNewtonMeters.value, stallCurrentAmps.value, freeCurrentAmps.value, freeSpeedRPM.value].join('_') >% maxTorque %<= fields.stallTorqueNewtonMeters.value * fields.gearing.value >% maxVelocity %<= fields.freeSpeedRPM.value / fields.gearing.value >% # The documentation about the multiplier field is unclear as to how it applies differently to differnt fields/functions. From 646b7120dd22db5f672731bcdb59037e1b97c5bd Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 02:51:20 -0700 Subject: [PATCH 35/60] update system test --- plugin/controller/.gitignore | 1 + .../dist/protos/WPIEncoderBase.proto | 2 +- .../dist/protos/WPIMotorBase.proto | 2 +- .../webotsFolder/dist/worlds/DBSExample.wbt | 38 +++++++++---------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/plugin/controller/.gitignore b/plugin/controller/.gitignore index 10261e49..ff57e321 100644 --- a/plugin/controller/.gitignore +++ b/plugin/controller/.gitignore @@ -162,3 +162,4 @@ imgui.ini *.dylib *.jnilib *.so +.DBSExample.jpg diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto index 534271a1..3f414008 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto @@ -12,7 +12,7 @@ PROTO WPIEncoderBase [ ] { PositionSensor { - name %<= ["DBSim_Encoder", location === "Motor Shaft", absolute.value, absoluteOffsetDeg.value, inverted.value, CPR.value].join('_') %> + name %<= '"' + ["DBSim_Encoder", fields.location.value === "Motor Shaft", fields.absolute.value, fields.absoluteOffsetDeg.value, fields.inverted.value, fields.CPR.value].join('_') + '"' %> noise IS noiseStdDevRad } } diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto index 2c0e2d0e..8ff8eb59 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto @@ -16,7 +16,7 @@ PROTO WPIMotorBase [ ] { RotationalMotor { - name %<= ["DBSim_Motor", controllerType.value, port.value, gearing.value, inverted.value, nominalVoltageVolts.value, stallTorqueNewtonMeters.value, stallCurrentAmps.value, freeCurrentAmps.value, freeSpeedRPM.value].join('_') >% + name %<= '"' + ["DBSim_Motor", fields.controllerType.value, fields.port.value, fields.gearing.value, fields.inverted.value, fields.nominalVoltageVolts.value, fields.stallTorqueNewtonMeters.value, fields.stallCurrentAmps.value, fields.freeCurrentAmps.value, fields.freeSpeedRPM.value].join('_') + '"' >% maxTorque %<= fields.stallTorqueNewtonMeters.value * fields.gearing.value >% maxVelocity %<= fields.freeSpeedRPM.value / fields.gearing.value >% # The documentation about the multiplier field is unclear as to how it applies differently to differnt fields/functions. diff --git a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt index 6308a587..f5c84221 100644 --- a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt +++ b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt @@ -3,6 +3,7 @@ EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/floors/protos/RectangleArena.proto" EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/backgrounds/protos/TexturedBackground.proto" EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/backgrounds/protos/TexturedBackgroundLight.proto" +EXTERNPROTO "../protos/MiniCIMMotor.proto" WorldInfo { coordinateSystem "NUE" @@ -41,13 +42,13 @@ Robot { anchor -0.167 0 -0.232 } device [ + MiniCIMMotor { + controllerType "PWM" + gearing 66.8192 + } PositionSensor { name "Back Left Encoder" } - RotationalMotor { - name "PWM[0]" - maxVelocity 87.4102 - } ] endPoint Solid { translation -0.1670000000000002 -2.473071410269423e-06 -0.23199999999746604 @@ -67,8 +68,6 @@ Robot { boundingObject USE Wheel physics Physics { } - linearVelocity 1.822206059407856e-11 4.9538582186142945e-06 2.6858087780223737e-10 - angularVelocity 1.0140593873494673e-11 8.05902970923126e-16 -2.879076630795684e-10 } } HingeJoint { @@ -78,9 +77,10 @@ Robot { anchor 0.167 0 -0.232 } device [ - RotationalMotor { - name "PWM[1]" - maxVelocity 87.4102 + MiniCIMMotor { + controllerType "PWM" + port 1 + gearing 66.8192 } PositionSensor { name "Front Left Encoder" @@ -101,8 +101,6 @@ Robot { boundingObject USE Wheel physics Physics { } - linearVelocity 1.8087846897432735e-11 4.9538604909707546e-06 2.685812324034266e-10 - angularVelocity 1.0168060270765932e-11 8.027475063039239e-16 -2.848409068159918e-10 } } HingeJoint { @@ -115,9 +113,10 @@ Robot { PositionSensor { name "Back Right Encoder" } - RotationalMotor { - name "PWM[2]" - maxVelocity 87.4102 + MiniCIMMotor { + controllerType "PWM" + port 2 + gearing 66.8192 } ] endPoint Solid { @@ -135,8 +134,6 @@ Robot { boundingObject USE Wheel physics Physics { } - linearVelocity 1.8221518892736977e-11 4.953855589429738e-06 2.6858193886255167e-10 - angularVelocity 1.0168060270765977e-11 8.059010421989807e-16 -2.878916574611155e-10 } } HingeJoint { @@ -146,9 +143,10 @@ Robot { anchor 0.167 0 0.232 } device [ - RotationalMotor { - name "PWM[3]" - maxVelocity 87.4102 + MiniCIMMotor { + controllerType "PWM" + port 3 + gearing 66.8192 } PositionSensor { name "Front Right Encoder" @@ -168,8 +166,6 @@ Robot { boundingObject USE Wheel physics Physics { } - linearVelocity 1.8087740188949048e-11 4.95385786537571e-06 2.6857994638416105e-10 - angularVelocity 1.0113127476216427e-11 8.027802005714066e-16 -2.848392744175236e-10 } } Shape { From a355d0ca803df03b2bbcf34516e90aaf8fc9990e Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 03:02:26 -0700 Subject: [PATCH 36/60] update build/ci toolchain --- .github/workflows/ci.yml | 2 +- plugin/controller/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57df669f..a8543dc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Webots id: setupWebots - uses: DeepBlueRobotics/setup-webots@versioning-and-caching + uses: DeepBlueRobotics/setup-webots@v2 with: webotsVersion: R2023b diff --git a/plugin/controller/build.gradle b/plugin/controller/build.gradle index 492a929a..104c2dd6 100644 --- a/plugin/controller/build.gradle +++ b/plugin/controller/build.gradle @@ -19,7 +19,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' // Support finding the local webots installation and adding it as a dependency - id "org.carlmontrobotics.webots" version "0.13.0" + id "org.carlmontrobotics.webots" version "1.0.0" // Support creating a license report of all dependencies id 'com.jaredsburrows.license' version '0.8.80' From 41f89e30ba114dfa62f805c0131bc28781e7b9d8 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 03:39:30 -0700 Subject: [PATCH 37/60] implement pwm --- .../controller/src/main/java/DeepBlueSim.java | 3 +- .../team199/deepbluesim/SimRegisterer.java | 12 ++++-- .../mediators/PWMMotorMediator.java | 40 +++++++++++++++++++ ...iator.java => SimDeviceMotorMediator.java} | 11 ++--- 4 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java rename plugin/controller/src/main/java/org/team199/deepbluesim/mediators/{MotorMediator.java => SimDeviceMotorMediator.java} (89%) diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 7fa84312..30af16b9 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -13,6 +13,7 @@ import org.team199.wpiws.devices.SimDeviceSim; import org.team199.wpiws.interfaces.StringCallback; import org.java_websocket.client.WebSocketClient; +import org.team199.deepbluesim.SimRegisterer; import org.team199.deepbluesim.Simulation; // NOTE: Webots expects the controller class to *not* be in a package and have a name that matches the @@ -94,7 +95,7 @@ public void callback(String name, String value) { throw new RuntimeException("Couldn't even do one timestep!"); } - // SimRegisterer.connectDevices(); + SimRegisterer.connectDevices(); // Connect to the robot code try { diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java index d84c7154..a898fb51 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java @@ -5,11 +5,13 @@ import java.util.concurrent.CopyOnWriteArraySet; import org.team199.deepbluesim.mediators.GyroMediator; -import org.team199.deepbluesim.mediators.MotorMediator; +import org.team199.deepbluesim.mediators.PWMMotorMediator; +import org.team199.deepbluesim.mediators.SimDeviceMotorMediator; import org.team199.deepbluesim.mediators.SimDeviceEncoderMediator; import org.team199.deepbluesim.mediators.WPILibEncoderMediator; import org.team199.wpiws.ScopedObject; import org.team199.wpiws.devices.EncoderSim; +import org.team199.wpiws.devices.PWMSim; import org.team199.wpiws.devices.SimDeviceSim; import com.cyberbotics.webots.controller.Device; @@ -159,10 +161,14 @@ public static void connectMotor(Motor device, Supervisor robot) { double freeCurrentAmps = Double.parseDouble(nameParts[9]); double freeSpeedRPM = Double.parseDouble(nameParts[10]); - String simDeviceName = controllerType.replaceAll("\\s", "") + "[" + port + "]"; DCMotor motorConstants = new DCMotor(nominalVoltageVolts, stallTorqueNewtonMeters, stallCurrentAmps, freeCurrentAmps, Units.rotationsPerMinuteToRadiansPerSecond(freeSpeedRPM), 1); - new MotorMediator(device, new SimDeviceSim(simDeviceName), motorConstants, gearing, inverted, CALLBACKS); + if(controllerType.equals("PWM")) { + new PWMMotorMediator(device, new PWMSim(Integer.toString(port)), motorConstants, gearing, inverted, CALLBACKS); + } else { + String simDeviceName = controllerType.replaceAll("\\s", "") + "[" + port + "]"; + new SimDeviceMotorMediator(device, new SimDeviceSim(simDeviceName), motorConstants, gearing, inverted, CALLBACKS); + } } } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java new file mode 100644 index 00000000..16373869 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java @@ -0,0 +1,40 @@ +package org.team199.deepbluesim.mediators; + +import java.util.Collection; + +import org.team199.wpiws.ScopedObject; +import org.team199.wpiws.devices.PWMSim; + +import com.cyberbotics.webots.controller.Motor; + +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.math.util.Units; + +public class PWMMotorMediator { + + public final Motor motor; + public final double gearing; + public final boolean inverted; + public final DCMotor motorConstants; + public final PWMSim motorDevice; + + public PWMMotorMediator(Motor motor, PWMSim simDevice, DCMotor motorConstants, double gearing, boolean inverted, Collection> callbackStore) { + this.motor = motor; + this.motorDevice = simDevice; + this.motorConstants = motorConstants; + this.gearing = gearing; + this.inverted = inverted; + + // Use velocity control + motor.setPosition(Double.POSITIVE_INFINITY); + + // Disable braking + if(motor.getBrake() != null) motor.getBrake().setDampingConstant(0); + + callbackStore.add(motorDevice.registerSpeedCallback((deviceName, speed) -> { + double velocity = speed * Units.radiansPerSecondToRotationsPerMinute(motorConstants.freeSpeedRadPerSec); + motor.setVelocity((inverted ? -1 : 1) * velocity / gearing); + }, true)); + } + +} diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java similarity index 89% rename from plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java rename to plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java index 7f5ba0cf..20cb8d37 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MotorMediator.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java @@ -12,11 +12,12 @@ import com.cyberbotics.webots.controller.PositionSensor; import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.math.util.Units; /** * Links WPILib motor controllers to Webots */ -public class MotorMediator implements Runnable { +public class SimDeviceMotorMediator implements Runnable { public static final int NEO_BUILTIN_ENCODER_CPR = 42; @@ -38,9 +39,8 @@ public class MotorMediator implements Runnable { * @param motorConstants the motor constants to use * @param gearing the gear ratio to use * @param callbackStore a collection to store callbacks in - * @throws IllegalArgumentException if {@code motor} is not a WPIMotorBase */ - public MotorMediator(Motor motor, SimDeviceSim simDevice, DCMotor motorConstants, double gearing, boolean inverted, Collection> callbackStore) throws IllegalArgumentException { + public SimDeviceMotorMediator(Motor motor, SimDeviceSim simDevice, DCMotor motorConstants, double gearing, boolean inverted, Collection> callbackStore) { this.motor = motor; motorDevice = simDevice; this.motorConstants = motorConstants; @@ -63,6 +63,7 @@ public MotorMediator(Motor motor, SimDeviceSim simDevice, DCMotor motorConstants // Use velocity control motor.setPosition(Double.POSITIVE_INFINITY); + brake.setDampingConstant(motorConstants.stallTorqueNewtonMeters * gearing); callbackStore.add(motorDevice.registerValueChangedCallback("Brake Mode", (name, enabled) -> { @@ -71,7 +72,7 @@ public MotorMediator(Motor motor, SimDeviceSim simDevice, DCMotor motorConstants callbackStore.add(motorDevice.registerValueChangedCallback("Neutral Deadband", (name, deadband) -> { neutralDeadband = Math.abs(ParseUtils.parseDoubleOrDefault(deadband, neutralDeadband)); }, true)); - callbackStore.add(motorDevice.registerValueChangedCallback("Current Speed", (name, speed) -> { + callbackStore.add(motorDevice.registerValueChangedCallback("Speed", (name, speed) -> { requestedOutput = ParseUtils.parseDoubleOrDefault(speed, requestedOutput); }, true)); @@ -90,7 +91,7 @@ public void run() { brake.setDampingConstant(0); } - double velocity = currentOutput * motorConstants.freeSpeedRadPerSec; + double velocity = currentOutput * Units.radiansPerSecondToRotationsPerMinute(motorConstants.freeSpeedRadPerSec); motor.setVelocity((inverted ? -1 : 1) * velocity / gearing); double currentDraw = motorConstants.getCurrent(velocity, currentOutput * motorConstants.nominalVoltageVolts); From 7993981e80a87462b6ed905f6389e9a166232bbc Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 10:22:15 -0700 Subject: [PATCH 38/60] update systemTest library path --- example/build.gradle | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/example/build.gradle b/example/build.gradle index a249383b..99e55262 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -114,18 +114,17 @@ task('systemTest', type: JavaExec) { String pathSeparator = File.pathSeparator // Load native libraries + // See https://github.com/wpilibsuite/GradleRIO/blob/88f3420b1fe554d78b7b682e2adf2080c124ba42/src/main/java/edu/wpi/first/gradlerio/wpi/java/TestTaskDoFirstAction.java#L28C14-L28C14 dependsOn 'extractReleaseNative' String nativeDir = tasks.extractReleaseNative.getDestinationDirectory().getAsFile().get().getAbsolutePath() - String defaultLibraryPath; - if(OperatingSystem.current().isWindows()) { - defaultLibraryPath = System.getenv('PATH') - } else if(OperatingSystem.current().isMacOsX()) { - defaultLibraryPath = System.getenv('DYLD_LIBRARY_PATH') - } else { - defaultLibraryPath = System.getenv('LD_LIBRARY_PATH') + if(OperatingSystem.current().isUnix() || OperatingSystem.current().isMacOsX() || OperatingSystem.current().isLinux()) { + environment "LD_LIBRARY_PATH", nativeDir + environment "DYLD_FALLBACK_LIBRARY_PATH", nativeDir + environment "DYLD_LIBRARY_PATH", nativeDir + } else if(OperatingSystem.current().isWindows()) { + environment 'PATH', System.getenv('PATH') + pathSeparator + nativeDir } - jvmArgs '-Djava.library.path=' + defaultLibraryPath + pathSeparator + nativeDir - environment 'PATH', System.getenv('PATH') + pathSeparator + nativeDir + systemProperty "java.library.path", systemProperties["java.library.path"] + pathSeparator + nativeDir afterEvaluate { // We have to wait until after the project is evaluated so WPILibPlugin can setup the repositories to load the HALSim extensions from // Load HALSim extensions (See https://github.com/wpilibsuite/GradleRIO/blob/88f3420b1fe554d78b7b682e2adf2080c124ba42/src/main/java/edu/wpi/first/gradlerio/wpi/java/WPIJavaExtension.java#L139) From 414af0a1a7c3d9cdfd92084879c207e035e7baef Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 10:33:01 -0700 Subject: [PATCH 39/60] fix units --- .../team199/deepbluesim/mediators/PWMMotorMediator.java | 3 +-- .../deepbluesim/mediators/SimDeviceMotorMediator.java | 3 +-- .../src/webotsFolder/dist/protos/WPIMotorBase.proto | 2 +- .../src/webotsFolder/dist/worlds/DBSExample.wbt | 8 ++++---- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java index 16373869..58828e24 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java @@ -8,7 +8,6 @@ import com.cyberbotics.webots.controller.Motor; import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.math.util.Units; public class PWMMotorMediator { @@ -32,7 +31,7 @@ public PWMMotorMediator(Motor motor, PWMSim simDevice, DCMotor motorConstants, d if(motor.getBrake() != null) motor.getBrake().setDampingConstant(0); callbackStore.add(motorDevice.registerSpeedCallback((deviceName, speed) -> { - double velocity = speed * Units.radiansPerSecondToRotationsPerMinute(motorConstants.freeSpeedRadPerSec); + double velocity = speed * motorConstants.freeSpeedRadPerSec; motor.setVelocity((inverted ? -1 : 1) * velocity / gearing); }, true)); } diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java index 20cb8d37..c44a3303 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java @@ -12,7 +12,6 @@ import com.cyberbotics.webots.controller.PositionSensor; import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.math.util.Units; /** * Links WPILib motor controllers to Webots @@ -91,7 +90,7 @@ public void run() { brake.setDampingConstant(0); } - double velocity = currentOutput * Units.radiansPerSecondToRotationsPerMinute(motorConstants.freeSpeedRadPerSec); + double velocity = currentOutput * motorConstants.freeSpeedRadPerSec; motor.setVelocity((inverted ? -1 : 1) * velocity / gearing); double currentDraw = motorConstants.getCurrent(velocity, currentOutput * motorConstants.nominalVoltageVolts); diff --git a/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto index 8ff8eb59..61484f90 100644 --- a/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto @@ -18,7 +18,7 @@ PROTO WPIMotorBase [ RotationalMotor { name %<= '"' + ["DBSim_Motor", fields.controllerType.value, fields.port.value, fields.gearing.value, fields.inverted.value, fields.nominalVoltageVolts.value, fields.stallTorqueNewtonMeters.value, fields.stallCurrentAmps.value, fields.freeCurrentAmps.value, fields.freeSpeedRPM.value].join('_') + '"' >% maxTorque %<= fields.stallTorqueNewtonMeters.value * fields.gearing.value >% - maxVelocity %<= fields.freeSpeedRPM.value / fields.gearing.value >% + maxVelocity %<= (2 * Math.PI / 60) * fields.freeSpeedRPM.value / fields.gearing.value >% # The documentation about the multiplier field is unclear as to how it applies differently to differnt fields/functions. # I think it's best just to implement it ourselves for now. # multiplier %<= 1 / fields.gearing.value >% diff --git a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt index f5c84221..e1086869 100644 --- a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt +++ b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt @@ -44,7 +44,7 @@ Robot { device [ MiniCIMMotor { controllerType "PWM" - gearing 66.8192 + gearing 6.9973 } PositionSensor { name "Back Left Encoder" @@ -80,7 +80,7 @@ Robot { MiniCIMMotor { controllerType "PWM" port 1 - gearing 66.8192 + gearing 6.9973 } PositionSensor { name "Front Left Encoder" @@ -116,7 +116,7 @@ Robot { MiniCIMMotor { controllerType "PWM" port 2 - gearing 66.8192 + gearing 6.9973 } ] endPoint Solid { @@ -146,7 +146,7 @@ Robot { MiniCIMMotor { controllerType "PWM" port 3 - gearing 66.8192 + gearing 6.9973 } PositionSensor { name "Front Right Encoder" From def238dc38b245017198ff3f426eb754b14b4eea Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 10:34:50 -0700 Subject: [PATCH 40/60] update upload-artifact --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8543dc4..6bafd8fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,14 +37,14 @@ jobs: uses: ./.github/actions/run-system-test - name: Archive the example folder - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: Resulting example project for ${{ runner.os }} path: example/ - name: Archive Webots log - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: Webots log for ${{ runner.os }} From 0ea8bf707c4836383151b9552a1202040dfc2451 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 10:45:01 -0700 Subject: [PATCH 41/60] (TMP) wait 1hr for webots to start --- example/src/systemTest/java/frc/robot/SystemTestRobot.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 98c632e1..403c2a22 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -80,10 +80,10 @@ public void callback(String name, int handle, int direction, HALValue value) { var startedWaitingTimeMs = System.currentTimeMillis(); var isReady = false; System.err.println("Waiting for WebotsSupervisor to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); - while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 600000) { + while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 36000000) { try { long elapsedTime = System.currentTimeMillis() - startedWaitingTimeMs; - long remainingTime = 600000 - elapsedTime; + long remainingTime = 36000000 - elapsedTime; if(remainingTime > 0) isReady = future.get(remainingTime, TimeUnit.MILLISECONDS); else isReady = true; } catch (TimeoutException ex) { From 4b49dfe7bd8261f476f99906b4a223b4b17141da Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 15 Aug 2023 15:03:54 -0700 Subject: [PATCH 42/60] wait 15 mins for webots to start --- example/src/systemTest/java/frc/robot/SystemTestRobot.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 403c2a22..3c271b49 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -75,15 +75,15 @@ public void callback(String name, int handle, int direction, HALValue value) { System.out.println("WebotsSupervisor is ready"); future.complete(true); } - // Wait up to 10 minutes for Webots to respond. On GitHub's MacOS Continuous + // Wait up to 15 minutes for Webots to respond. On GitHub's MacOS Continuous // Integration servers, it can take over 8 minutes for Webots to start. var startedWaitingTimeMs = System.currentTimeMillis(); var isReady = false; System.err.println("Waiting for WebotsSupervisor to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); - while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 36000000) { + while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 900000) { try { long elapsedTime = System.currentTimeMillis() - startedWaitingTimeMs; - long remainingTime = 36000000 - elapsedTime; + long remainingTime = 900000 - elapsedTime; if(remainingTime > 0) isReady = future.get(remainingTime, TimeUnit.MILLISECONDS); else isReady = true; } catch (TimeoutException ex) { From eec0aa29efd7081c642bf5674bcd76df29c13313 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 3 May 2024 14:24:08 -0700 Subject: [PATCH 43/60] add newlines at end of files --- .../src/main/java/org/team199/deepbluesim/Simulation.java | 3 ++- .../java/org/team199/deepbluesim/mediators/GyroMediator.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java b/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java index 6753512f..fce0dec1 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/Simulation.java @@ -76,4 +76,5 @@ public static Node getPROTOBase(Node node, String baseName) { private Simulation() {} -} \ No newline at end of file +} + diff --git a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java index 98873575..287294b5 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java @@ -43,4 +43,5 @@ public void run() { device.set("Yaw", angle + ""); } -} \ No newline at end of file +} + From 98e57ca3db7e31b6109654feab6fee323df223fa Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Fri, 3 May 2024 14:45:00 -0700 Subject: [PATCH 44/60] bump WPIWebSockets --- WPIWebSockets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WPIWebSockets b/WPIWebSockets index b303fe46..2c3990fc 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit b303fe46a0b28a8b46f9849ef8615ab0f48e2e71 +Subproject commit 2c3990fca597e1cd6009f26b3dd577172616dc66 From e6abe6be23bc951375b0469a23b8c20171448321 Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Sat, 4 May 2024 16:15:16 -0700 Subject: [PATCH 45/60] Prevent robot code from outrunning simulator. --- .../java/frc/robot/SystemTestRobot.java | 23 +++++++++++++++---- .../controller/src/main/java/DeepBlueSim.java | 6 +++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 3c271b49..3e2a1ec5 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -16,6 +16,7 @@ import edu.wpi.first.math.numbers.N3; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.simulation.DriverStationSim; import edu.wpi.first.wpilibj.simulation.SimDeviceSim; import edu.wpi.first.wpilibj.simulation.SimHooks; @@ -47,11 +48,13 @@ public void startCompetition() { SimDevice webotsSupervisor = null; SimDouble positionX = null, positionY = null, positionZ = null; + SimDouble simTimeSec = null; @Override public void simulationInit() { webotsSupervisor = SimDevice.create("WebotsSupervisor"); SimDouble simStartMs = webotsSupervisor.createDouble("simStartMs", SimDevice.Direction.kInput, 0.0); + simTimeSec = webotsSupervisor.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); positionX = webotsSupervisor.createDouble("self.position.x", SimDevice.Direction.kInput, 0.0); positionY = webotsSupervisor.createDouble("self.position.y", SimDevice.Direction.kInput, 0.0); positionZ = webotsSupervisor.createDouble("self.position.z", SimDevice.Direction.kInput, 0.0); @@ -99,6 +102,20 @@ public void callback(String name, int handle, int direction, HALValue value) { // occurred while we waited, will be considered behind schedule and // will all happen at once. SimHooks.restartTiming(); + + // Pause the clock so that we can step it in sync with the simulator + SimHooks.pauseTiming(); + + webotsSupervisorSim.registerValueChangedCallback(simTimeSec, new SimValueCallback() { + @Override + public void callback(String name, int handle, int direction, HALValue value) { + double deltaSecs = value.getDouble() - Timer.getFPGATimestamp(); + if (deltaSecs > 0.0) { + SimHooks.stepTimingAsync(deltaSecs); + } + } + }, true); + // Simulate starting autonomous DriverStationSim.setAutonomous(true); DriverStationSim.setEnabled(true); @@ -107,14 +124,12 @@ public void callback(String name, int handle, int direction, HALValue value) { super.simulationInit(); } - private int count = 0; - @Override public void simulationPeriodic() { super.simulationPeriodic(); - count++; - if (count > 50*10) { + // The motors are on for 2 secs. We wait an extra second to give the robot time to stop. + if (Timer.getFPGATimestamp() > 3.0) { // Simulate disabling the robot DriverStationSim.setEnabled(false); DriverStationSim.notifyNewData(); diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 30af16b9..c3dfa03d 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -52,9 +52,9 @@ public void uncaughtException(Thread arg0, Throwable arg1) { } Simulation.init(robot, robot.getBasicTimeStep()); - // Use a SimDeviceSim to coordinate with robot code tests + // Use a SimDeviceSim to coordinate with robot code + final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); { - final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); // Regular report the simulated robot's position Simulation.registerPeriodicMethod(new Runnable() { public void run() { @@ -94,6 +94,7 @@ public void callback(String name, String value) { if (robot.step(basicTimeStep) == -1) { throw new RuntimeException("Couldn't even do one timestep!"); } + webotsSupervisorSim.set("simTimeSec", robot.getTime()); SimRegisterer.connectDevices(); @@ -116,6 +117,7 @@ public void callback(String name, String value) { })); while(robot.step(basicTimeStep) != -1) { + webotsSupervisorSim.set("simTimeSec", robot.getTime()); queuedMessages.forEach(Runnable::run); queuedMessages.clear(); Simulation.runPeriodicMethods(); From 4a05889f3aa313883f40392b81efbee21ea903f5 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Sat, 4 May 2024 16:59:29 -0700 Subject: [PATCH 46/60] bump WPIWebSockets --- WPIWebSockets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WPIWebSockets b/WPIWebSockets index 2c3990fc..fe44c974 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit 2c3990fca597e1cd6009f26b3dd577172616dc66 +Subproject commit fe44c9742fe5992f3f0bd9302e5c6f5dc98d3752 From 1b75f5b0bfeeaa60a98c4b92392d0f6ce354db83 Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Sat, 4 May 2024 21:49:29 -0700 Subject: [PATCH 47/60] Prevent sim from starting until robot is ready. --- .../java/frc/robot/SystemTestRobot.java | 43 ++++++++++--------- .../controller/src/main/java/DeepBlueSim.java | 28 ++++++++---- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 3e2a1ec5..5c26f7cc 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -54,6 +54,7 @@ public void startCompetition() { public void simulationInit() { webotsSupervisor = SimDevice.create("WebotsSupervisor"); SimDouble simStartMs = webotsSupervisor.createDouble("simStartMs", SimDevice.Direction.kInput, 0.0); + SimDouble robotStartMs = webotsSupervisor.createDouble("robotStartMs", SimDevice.Direction.kOutput, 0.0); simTimeSec = webotsSupervisor.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); positionX = webotsSupervisor.createDouble("self.position.x", SimDevice.Direction.kInput, 0.0); positionY = webotsSupervisor.createDouble("self.position.y", SimDevice.Direction.kInput, 0.0); @@ -66,18 +67,37 @@ public void simulationInit() { @Override public void callback(String name, int handle, int direction, HALValue value) { if (value.getDouble() > 0.0) { - System.out.println("WebotsSupervisor is ready"); + System.out.println("WebotsSupervisor is ready."); + System.out.println("Telling WebotsSupervisor that we're ready"); + robotStartMs.set(System.currentTimeMillis()); future.complete(true); } } }, true)) { System.out.println("Telling WebotsSupervisor that we're ready"); - SimDouble robotStartMs = webotsSupervisor.createDouble("robotStartMs", SimDevice.Direction.kOutput, 0.0); robotStartMs.set(System.currentTimeMillis()); if (simStartMs.get() > 0.0) { System.out.println("WebotsSupervisor is ready"); future.complete(true); } + // Reset the clock. Without this, *Periodic calls that should have + // occurred while we waited, will be considered behind schedule and + // will all happen at once. + SimHooks.restartTiming(); + + // Pause the clock so that we can step it in sync with the simulator + SimHooks.pauseTiming(); + + webotsSupervisorSim.registerValueChangedCallback(simTimeSec, new SimValueCallback() { + @Override + public void callback(String name, int handle, int direction, HALValue value) { + double deltaSecs = value.getDouble() - Timer.getFPGATimestamp(); + if (deltaSecs > 0.0) { + SimHooks.stepTimingAsync(deltaSecs); + } + } + }, true); + // Wait up to 15 minutes for Webots to respond. On GitHub's MacOS Continuous // Integration servers, it can take over 8 minutes for Webots to start. var startedWaitingTimeMs = System.currentTimeMillis(); @@ -98,24 +118,7 @@ public void callback(String name, int handle, int direction, HALValue value) { assertTrue("Webots ready in time", isReady); } - // Reset the clock. Without this, *Periodic calls that should have - // occurred while we waited, will be considered behind schedule and - // will all happen at once. - SimHooks.restartTiming(); - - // Pause the clock so that we can step it in sync with the simulator - SimHooks.pauseTiming(); - - webotsSupervisorSim.registerValueChangedCallback(simTimeSec, new SimValueCallback() { - @Override - public void callback(String name, int handle, int direction, HALValue value) { - double deltaSecs = value.getDouble() - Timer.getFPGATimestamp(); - if (deltaSecs > 0.0) { - SimHooks.stepTimingAsync(deltaSecs); - } - } - }, true); - + System.out.println("WebotsSupervisor is ready. Enabling in autonomous."); // Simulate starting autonomous DriverStationSim.setAutonomous(true); DriverStationSim.setEnabled(true); diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index c3dfa03d..1b626237 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -1,6 +1,7 @@ import java.lang.Thread.UncaughtExceptionHandler; import java.net.URISyntaxException; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CompletableFuture; import com.cyberbotics.webots.controller.Node; import com.cyberbotics.webots.controller.Supervisor; @@ -53,6 +54,7 @@ public void uncaughtException(Thread arg0, Throwable arg1) { Simulation.init(robot, robot.getBasicTimeStep()); // Use a SimDeviceSim to coordinate with robot code + final CompletableFuture future = new CompletableFuture(); final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); { // Regular report the simulated robot's position @@ -66,15 +68,17 @@ public void run() { } }); - // If the robot code starts after us, we expect it to tell us it's ready, and we respond // that we're ready. callbackStore = webotsSupervisorSim.registerValueChangedCallback("robotStartMs", new StringCallback() { @Override public void callback(String name, String value) { - System.out.println("Telling the robot we're ready"); - webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); - } + if (value != null) { + System.out.println("Telling the robot we're ready"); + webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); + future.complete(true); + } + } }, true); // If the robot code starts before we us, then it might have already tried to tell @@ -83,6 +87,7 @@ public void callback(String name, String value) { ConnectionProcessor.addOpenListener(() -> { System.out.println("Telling the robot we're ready"); webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); + future.complete(true); }); } @@ -94,13 +99,12 @@ public void callback(String name, String value) { if (robot.step(basicTimeStep) == -1) { throw new RuntimeException("Couldn't even do one timestep!"); } - webotsSupervisorSim.set("simTimeSec", robot.getTime()); - SimRegisterer.connectDevices(); // Connect to the robot code try { System.out.println("Trying to connect to robot..."); + System.out.flush(); wsConnection = WSConnection.connectHALSim(true); } catch(URISyntaxException e) { System.err.println("Error occurred connecting to server:"); @@ -116,12 +120,20 @@ public void callback(String name, String value) { } catch(InterruptedException e) {} })); - while(robot.step(basicTimeStep) != -1) { + try { + future.get(); + } catch (Exception ex) { + throw new RuntimeException("Exception while waiting for robot to be ready"); + } + + System.out.println("Starting simulation"); + System.out.flush(); + do { webotsSupervisorSim.set("simTimeSec", robot.getTime()); queuedMessages.forEach(Runnable::run); queuedMessages.clear(); Simulation.runPeriodicMethods(); - } + } while(robot.step(basicTimeStep) != -1); System.out.println("Shutting down DeepBlueSim..."); System.out.flush(); From 3ce638b1890bb31143e699ff70acc6322752848f Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Mon, 6 May 2024 20:49:48 -0700 Subject: [PATCH 48/60] Keep robot and sim in sync during simulation. This works but still needs be cleaned up. --- .../java/frc/robot/SystemTestRobot.java | 161 ++++++++++++++---- .../controller/src/main/java/DeepBlueSim.java | 115 +++++++++---- 2 files changed, 207 insertions(+), 69 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 5c26f7cc..028fdf18 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -7,6 +7,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.opencv.aruco.DetectorParameters; + import edu.wpi.first.hal.HAL; import edu.wpi.first.hal.HALValue; import edu.wpi.first.hal.SimDevice; @@ -15,6 +17,7 @@ import edu.wpi.first.math.Vector; import edu.wpi.first.math.numbers.N3; import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Notifier; import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.simulation.DriverStationSim; @@ -23,6 +26,8 @@ public class SystemTestRobot extends Robot { + protected static final double MAX_TIME_DIFF_SECS = 0.0; + public static void main(String... args) { RobotBase.startRobot(SystemTestRobot::new); } @@ -47,39 +52,105 @@ public void startCompetition() { } SimDevice webotsSupervisor = null; + SimDevice timeSynchronizer = null; SimDouble positionX = null, positionY = null, positionZ = null; - SimDouble simTimeSec = null; + SimDouble simTimeSecSim = null; + SimDouble robotTimeSecSim = null; + CompletableFuture isDoneFuture = new CompletableFuture(); @Override public void simulationInit() { webotsSupervisor = SimDevice.create("WebotsSupervisor"); - SimDouble simStartMs = webotsSupervisor.createDouble("simStartMs", SimDevice.Direction.kInput, 0.0); - SimDouble robotStartMs = webotsSupervisor.createDouble("robotStartMs", SimDevice.Direction.kOutput, 0.0); - simTimeSec = webotsSupervisor.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); + timeSynchronizer = SimDevice.create("TimeSynchronizer"); + // SimDouble simStartMs = webotsSupervisor.createDouble("simStartMs", SimDevice.Direction.kInput, 0.0); + // SimDouble robotStartMs = webotsSupervisor.createDouble("robotStartMs", SimDevice.Direction.kOutput, 0.0); + simTimeSecSim = timeSynchronizer.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); + robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); positionX = webotsSupervisor.createDouble("self.position.x", SimDevice.Direction.kInput, 0.0); positionY = webotsSupervisor.createDouble("self.position.y", SimDevice.Direction.kInput, 0.0); positionZ = webotsSupervisor.createDouble("self.position.z", SimDevice.Direction.kInput, 0.0); - SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); + // SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); + SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); // Wait for the Webots supervisor to be ready - final var future = new CompletableFuture(); - try (var callback = webotsSupervisorSim.registerValueChangedCallback(simStartMs, new SimValueCallback() { - @Override - public void callback(String name, int handle, int direction, HALValue value) { - if (value.getDouble() > 0.0) { - System.out.println("WebotsSupervisor is ready."); - System.out.println("Telling WebotsSupervisor that we're ready"); - robotStartMs.set(System.currentTimeMillis()); - future.complete(true); + final var isReadyFuture = new CompletableFuture(); + // try (var callback = webotsSupervisorSim.registerValueChangedCallback(simStartMs, new SimValueCallback() { + // @Override + // public void callback(String name, int handle, int direction, HALValue value) { + // if (value.getDouble() > 0.0) { + // System.out.println("WebotsSupervisor is ready."); + // System.out.println("Telling WebotsSupervisor that we're ready"); + // robotStartMs.set(System.currentTimeMillis()); + // isReadyFuture.complete(true); + // } + // } + // }, true)) + { + // System.out.println("Telling WebotsSupervisor that we're ready"); + // robotStartMs.set(System.currentTimeMillis()); + // if (simStartMs.get() > 0.0) { + // System.out.println("WebotsSupervisor is ready"); + // isReadyFuture.complete(true); + // } + + final Notifier pauser = new Notifier(SimHooks::pauseTiming); + + timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, new SimValueCallback() { + @Override + public synchronized void callback(String name, int handle, int direction, HALValue value) { + double simTimeSec = value.getDouble(); + System.out.println("simTime valueChangedCallback called with " + simTimeSec); + double robotTimeSec = Timer.getFPGATimestamp(); + System.out.println("robotTimeSec = " + robotTimeSec); + double deltaSecs = simTimeSec + MAX_TIME_DIFF_SECS - robotTimeSec; + if (simTimeSec == -1.0) { + return; + } + if (robotTimeSecSim.get() == -2.0) { + // Waiting for sim to start + if (simTimeSec == -2.0) { + // Sim just started so restart robot timing + + if (!isReadyFuture.isDone()) { + System.out.println("Calling isReadyFuture.complete(true)"); + isReadyFuture.complete(true); + } + } + } else if (deltaSecs >= 0.0) { + // Tell the sim what the new robot time is. Strictly speaking it won't be that time + // until stepTiming() returns but this allows the sim to run in parrallel with the robot code. + // It's less deterministic but faster. + // System.out.println(String.format("calling robotTimeSec.set(%g)", Timer.getFPGATimestamp() + deltaSecs)); + if (isDoneFuture.getNow(false).booleanValue()) { + // Call endCompetition() to end the test and report success. + // NOTE: throwing an exception will end the test and report failure. + System.out.println("Calling endCompetition()"); + endCompetition(); + System.out.println("endCompetition() returned"); + } else { + robotTimeSec += deltaSecs; + // Let robot code run for deltaSecs. We use a Notifier instead of SimHooks.stepTiming() because + // using SimHooks.stepTiming() causes accesses to sim data to block. + System.out.println("Calling pauser.stop()"); + // Make any awaiting pauser run doesn't run before our new one. + pauser.stop(); + System.out.println("Calling pauser.startSingle()"); + pauser.startSingle(deltaSecs); + System.out.println("Calling resumeTiming()"); + SimHooks.resumeTiming(); + System.out.println("resumeTiming() returned"); + // System.out.println(String.format("Calling stepTiming(%g)", deltaSecs)); + // SimHooks.stepTiming(deltaSecs); + // System.out.println("stepTiming() returned"); + } + } + if (robotTimeSec != robotTimeSecSim.get()) { + System.out.println("Calling robotTimeSecSim() with " + robotTimeSec); + robotTimeSecSim.set(robotTimeSec); + } } - } - }, true)) { - System.out.println("Telling WebotsSupervisor that we're ready"); - robotStartMs.set(System.currentTimeMillis()); - if (simStartMs.get() > 0.0) { - System.out.println("WebotsSupervisor is ready"); - future.complete(true); - } + }, true); + // Reset the clock. Without this, *Periodic calls that should have // occurred while we waited, will be considered behind schedule and // will all happen at once. @@ -88,15 +159,11 @@ public void callback(String name, int handle, int direction, HALValue value) { // Pause the clock so that we can step it in sync with the simulator SimHooks.pauseTiming(); - webotsSupervisorSim.registerValueChangedCallback(simTimeSec, new SimValueCallback() { - @Override - public void callback(String name, int handle, int direction, HALValue value) { - double deltaSecs = value.getDouble() - Timer.getFPGATimestamp(); - if (deltaSecs > 0.0) { - SimHooks.stepTimingAsync(deltaSecs); - } - } - }, true); + // Tell sim to start + robotTimeSecSim.set(-2.0); + + // Tell sim that we're ready + // robotTimeSecSim.set(Timer.getFPGATimestamp()); // Wait up to 15 minutes for Webots to respond. On GitHub's MacOS Continuous // Integration servers, it can take over 8 minutes for Webots to start. @@ -107,7 +174,12 @@ public void callback(String name, int handle, int direction, HALValue value) { try { long elapsedTime = System.currentTimeMillis() - startedWaitingTimeMs; long remainingTime = 900000 - elapsedTime; - if(remainingTime > 0) isReady = future.get(remainingTime, TimeUnit.MILLISECONDS); + System.out.println("Waiting for isReadyFuture"); + if(remainingTime > 0) { + System.out.println("Waiting for isReadyFuture"); + isReady = isReadyFuture.get(remainingTime, TimeUnit.MILLISECONDS); + System.out.println("isReadyFuture.get() returned " + isReady); + } else isReady = true; } catch (TimeoutException ex) { System.err.println("Waiting for WebotsSupervisor to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); @@ -115,33 +187,50 @@ public void callback(String name, int handle, int direction, HALValue value) { throw new RuntimeException("Error while waiting for WebotsSupervisor to be ready", e); } } + System.out.println("Done waiting"); assertTrue("Webots ready in time", isReady); + System.out.println("Done asserting"); } - System.out.println("WebotsSupervisor is ready. Enabling in autonomous."); + System.out.println("WebotsSupervisor is ready. Enabling in autonomous."); System.out.flush(); + // Simulate starting autonomous DriverStationSim.setAutonomous(true); DriverStationSim.setEnabled(true); DriverStationSim.notifyNewData(); super.simulationInit(); + System.out.println("Time at end of simulationInit="+Timer.getFPGATimestamp()); } @Override public void simulationPeriodic() { + System.out.println("Time at start of simulationPeriodic="+Timer.getFPGATimestamp()); + // if (Timer.getFPGATimestamp() > simTimeSecSim.get()) { + // SimHooks.pauseTiming(); + // System.out.println(String.format("calling robotTimeSec.set(%g)", Timer.getFPGATimestamp())); + // robotTimeSec.set(Timer.getFPGATimestamp()); + // } + super.simulationPeriodic(); + System.out.println("In simulationPeriodic()"); + System.out.println("Calling positionX.get()"); + System.out.println("self.position.x =" + positionX.get()); // The motors are on for 2 secs. We wait an extra second to give the robot time to stop. if (Timer.getFPGATimestamp() > 3.0) { // Simulate disabling the robot + System.out.println("Disabling robot"); DriverStationSim.setEnabled(false); DriverStationSim.notifyNewData(); + System.out.println("Done disabling"); Vector expectedPos = new Vector<>(N3.instance); expectedPos.set(0, 0, -2.6); expectedPos.set(1, 0, 0.0); expectedPos.set(2, 0, 0.0); + System.out.println("Calling positionX.get()"); System.out.println("self.position.x =" + positionX.get()); System.out.println("self.position.y =" + positionY.get()); System.out.println("self.position.z =" + positionZ.get()); @@ -154,11 +243,11 @@ public void simulationPeriodic() { var diff = new Vector(expectedPos.minus(actualPos)); var distance = Math.sqrt(diff.elementTimes(diff).elementSum()); + System.out.println("Asserting robot near targe position"); assertEquals("Robot close to target position", 0.0, distance, 1.0); - // Call endCompetition() to end the test and report success. - // NOTE: throwing an exception will end the test and report failure. - endCompetition(); + System.out.println("Calling isDoneFuture.complete(true)"); + isDoneFuture.complete(true); } } } diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 1b626237..6e4f4438 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -1,6 +1,7 @@ import java.lang.Thread.UncaughtExceptionHandler; import java.net.URISyntaxException; -import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.CompletableFuture; import com.cyberbotics.webots.controller.Node; @@ -21,10 +22,13 @@ // the name of the jar. public class DeepBlueSim { - private static final ConcurrentLinkedDeque queuedMessages = new ConcurrentLinkedDeque<>(); + private static final BlockingDeque queuedMessages = new LinkedBlockingDeque<>(); + + protected static final double MAX_TIME_DIFF_SECS = 0.0; @SuppressWarnings("unused") - private static ScopedObject> callbackStore = null; + private static ScopedObject> robotStartMsCallbackStore = null; + private static ScopedObject> robotTimeSecCallbackStore = null; private static RunningObject wsConnection = null; public static void main(String[] args) { @@ -51,11 +55,16 @@ public void uncaughtException(Thread arg0, Throwable arg1) { System.err.println("The robot does not have supervisor=true. This is required to detect devices."); System.exit(1); } + // Get the basic timestep to use for calls to robot.step() + final int basicTimeStep = (int)Math.round(robot.getBasicTimeStep()); + Simulation.init(robot, robot.getBasicTimeStep()); // Use a SimDeviceSim to coordinate with robot code - final CompletableFuture future = new CompletableFuture(); + final CompletableFuture isDoneFuture = new CompletableFuture(); final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); + final SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); + { // Regular report the simulated robot's position Simulation.registerPeriodicMethod(new Runnable() { @@ -68,39 +77,78 @@ public void run() { } }); - // If the robot code starts after us, we expect it to tell us it's ready, and we respond - // that we're ready. - callbackStore = webotsSupervisorSim.registerValueChangedCallback("robotStartMs", new StringCallback() { + // Whenever the robot time changes, step the simulation until just past that time + robotTimeSecCallbackStore = timeSynchronizerSim.registerValueChangedCallback("robotTimeSec", new StringCallback() { @Override - public void callback(String name, String value) { + public synchronized void callback(String name, String value) { + System.out.println("In robotTimeSec callback with value = " + value); System.out.flush(); + if (value != null) { - System.out.println("Telling the robot we're ready"); - webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); - future.complete(true); + double robotTimeSec = Double.parseDouble(value); + + if (robotTimeSec == -2.0) { + System.out.println("Reloading world"); + robot.worldReload(); + return; + } + // Keep stepping the simulation forward until + // the sim time is more than MAX_TIME_DIFF_SECS ahead of the robot time + // or the simulation ends. + while(true) { + double simTimeSec = robot.getTime(); + if (simTimeSec > robotTimeSec + MAX_TIME_DIFF_SECS) { + break; + } + System.out.println("Calling robot.step(basicTimeStep) in robotTimeSec callback"); System.out.flush(); + boolean isDone = (robot.step(basicTimeStep) == -1); + System.out.println("robot.step(basicTimeStep) returned in robotTimeSec callback"); System.out.flush(); + timeSynchronizerSim.set("simTimeSec", robot.getTime()); + if (isDone) { + isDoneFuture.complete(true); + break; + } + Simulation.runPeriodicMethods(); + } } - } + System.out.println("exiting robotTimeSec callback"); System.out.flush(); + } }, true); + + // If the robot code starts after us, we expect it to tell us it's ready, and we respond + // that we're ready. + // robotStartMsCallbackStore = webotsSupervisorSim.registerValueChangedCallback("robotStartMs", new StringCallback() { + // @Override + // public void callback(String name, String value) { + // if (value != null) { + // System.out.println("Telling the robot we're ready"); + // webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); + // isReadyFuture.complete(true); + // } + // } + // }, true); + // If the robot code starts before we us, then it might have already tried to tell // us it was ready and we would have missed it. So, we tell it we're ready when we // connect to it. ConnectionProcessor.addOpenListener(() -> { - System.out.println("Telling the robot we're ready"); - webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); - future.complete(true); + System.out.println("Telling the robot we're ready"); System.out.flush(); + // webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); + timeSynchronizerSim.set("simTimeSec", -2.0); }); } - - // Get the basic timestamp to use for calls to robot.step() - int basicTimeStep = (int)Math.round(robot.getBasicTimeStep()); - - // Wait until one timestep has completed to ensure that the Webots simulator is + // Wait until startup has completed to ensure that the Webots simulator is // not still starting up. - if (robot.step(basicTimeStep) == -1) { - throw new RuntimeException("Couldn't even do one timestep!"); + System.out.println(String.format("Calling robot.step(%d)...", 0)); + System.out.flush(); + if (robot.step(0) == -1) { + throw new RuntimeException("Couldn't even start up!"); } + System.out.println("Calling connectDevices()..."); + System.out.flush(); SimRegisterer.connectDevices(); + // Connect to the robot code try { System.out.println("Trying to connect to robot..."); @@ -114,27 +162,28 @@ public void callback(String name, String value) { return; } + System.out.println("connectHALSim() returned"); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { wsConnection.object.closeBlocking(); } catch(InterruptedException e) {} })); + // Tell robot we're ready + System.out.println("telling robot we're ready. robot.getTime()=" + robot.getTime()); + timeSynchronizerSim.set("simTimeSec", -2.0); + + System.out.println("Waiting for simulation to finish..."); + System.out.flush(); try { - future.get(); + while (isDoneFuture.getNow(false).booleanValue() == false) { + queuedMessages.takeFirst().run(); + } } catch (Exception ex) { - throw new RuntimeException("Exception while waiting for robot to be ready"); + throw new RuntimeException("Exception while waiting for simulation to be done"); } - System.out.println("Starting simulation"); - System.out.flush(); - do { - webotsSupervisorSim.set("simTimeSec", robot.getTime()); - queuedMessages.forEach(Runnable::run); - queuedMessages.clear(); - Simulation.runPeriodicMethods(); - } while(robot.step(basicTimeStep) != -1); - System.out.println("Shutting down DeepBlueSim..."); System.out.flush(); From 442cac9c8e54144bc1d98d87dcc5f945d96f7bf0 Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Mon, 6 May 2024 21:55:29 -0700 Subject: [PATCH 49/60] Cleaned up and removed debug output. --- .../java/frc/robot/SystemTestRobot.java | 208 ++++++------------ .../controller/src/main/java/DeepBlueSim.java | 138 +++++------- 2 files changed, 121 insertions(+), 225 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 028fdf18..59708d89 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -7,8 +7,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.opencv.aruco.DetectorParameters; - import edu.wpi.first.hal.HAL; import edu.wpi.first.hal.HALValue; import edu.wpi.first.hal.SimDevice; @@ -26,7 +24,7 @@ public class SystemTestRobot extends Robot { - protected static final double MAX_TIME_DIFF_SECS = 0.0; + private static final double START_SIMULATION = -2.0; public static void main(String... args) { RobotBase.startRobot(SystemTestRobot::new); @@ -56,181 +54,112 @@ public void startCompetition() { SimDouble positionX = null, positionY = null, positionZ = null; SimDouble simTimeSecSim = null; SimDouble robotTimeSecSim = null; - CompletableFuture isDoneFuture = new CompletableFuture(); @Override public void simulationInit() { - webotsSupervisor = SimDevice.create("WebotsSupervisor"); timeSynchronizer = SimDevice.create("TimeSynchronizer"); - // SimDouble simStartMs = webotsSupervisor.createDouble("simStartMs", SimDevice.Direction.kInput, 0.0); - // SimDouble robotStartMs = webotsSupervisor.createDouble("robotStartMs", SimDevice.Direction.kOutput, 0.0); simTimeSecSim = timeSynchronizer.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); + webotsSupervisor = SimDevice.create("WebotsSupervisor"); positionX = webotsSupervisor.createDouble("self.position.x", SimDevice.Direction.kInput, 0.0); positionY = webotsSupervisor.createDouble("self.position.y", SimDevice.Direction.kInput, 0.0); positionZ = webotsSupervisor.createDouble("self.position.z", SimDevice.Direction.kInput, 0.0); - // SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); - // Wait for the Webots supervisor to be ready final var isReadyFuture = new CompletableFuture(); - // try (var callback = webotsSupervisorSim.registerValueChangedCallback(simStartMs, new SimValueCallback() { - // @Override - // public void callback(String name, int handle, int direction, HALValue value) { - // if (value.getDouble() > 0.0) { - // System.out.println("WebotsSupervisor is ready."); - // System.out.println("Telling WebotsSupervisor that we're ready"); - // robotStartMs.set(System.currentTimeMillis()); - // isReadyFuture.complete(true); - // } - // } - // }, true)) - { - // System.out.println("Telling WebotsSupervisor that we're ready"); - // robotStartMs.set(System.currentTimeMillis()); - // if (simStartMs.get() > 0.0) { - // System.out.println("WebotsSupervisor is ready"); - // isReadyFuture.complete(true); - // } - - final Notifier pauser = new Notifier(SimHooks::pauseTiming); - - timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, new SimValueCallback() { - @Override - public synchronized void callback(String name, int handle, int direction, HALValue value) { - double simTimeSec = value.getDouble(); - System.out.println("simTime valueChangedCallback called with " + simTimeSec); - double robotTimeSec = Timer.getFPGATimestamp(); - System.out.println("robotTimeSec = " + robotTimeSec); - double deltaSecs = simTimeSec + MAX_TIME_DIFF_SECS - robotTimeSec; - if (simTimeSec == -1.0) { - return; - } - if (robotTimeSecSim.get() == -2.0) { - // Waiting for sim to start - if (simTimeSec == -2.0) { - // Sim just started so restart robot timing - - if (!isReadyFuture.isDone()) { - System.out.println("Calling isReadyFuture.complete(true)"); - isReadyFuture.complete(true); - } - } - } else if (deltaSecs >= 0.0) { - // Tell the sim what the new robot time is. Strictly speaking it won't be that time - // until stepTiming() returns but this allows the sim to run in parrallel with the robot code. - // It's less deterministic but faster. - // System.out.println(String.format("calling robotTimeSec.set(%g)", Timer.getFPGATimestamp() + deltaSecs)); - if (isDoneFuture.getNow(false).booleanValue()) { - // Call endCompetition() to end the test and report success. - // NOTE: throwing an exception will end the test and report failure. - System.out.println("Calling endCompetition()"); - endCompetition(); - System.out.println("endCompetition() returned"); - } else { - robotTimeSec += deltaSecs; - // Let robot code run for deltaSecs. We use a Notifier instead of SimHooks.stepTiming() because - // using SimHooks.stepTiming() causes accesses to sim data to block. - System.out.println("Calling pauser.stop()"); - // Make any awaiting pauser run doesn't run before our new one. - pauser.stop(); - System.out.println("Calling pauser.startSingle()"); - pauser.startSingle(deltaSecs); - System.out.println("Calling resumeTiming()"); - SimHooks.resumeTiming(); - System.out.println("resumeTiming() returned"); - // System.out.println(String.format("Calling stepTiming(%g)", deltaSecs)); - // SimHooks.stepTiming(deltaSecs); - // System.out.println("stepTiming() returned"); - } - } - if (robotTimeSec != robotTimeSecSim.get()) { - System.out.println("Calling robotTimeSecSim() with " + robotTimeSec); - robotTimeSecSim.set(robotTimeSec); - } + + final Notifier pauser = new Notifier(SimHooks::pauseTiming); + timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, new SimValueCallback() { + @Override + public synchronized void callback(String name, int handle, int direction, HALValue value) { + double simTimeSec = value.getDouble(); + double robotTimeSec = Timer.getFPGATimestamp(); + double deltaSecs = simTimeSec - robotTimeSec; + + // Ignore the default initial value + if (simTimeSec == -1.0) { + return; } - }, true); - - // Reset the clock. Without this, *Periodic calls that should have - // occurred while we waited, will be considered behind schedule and - // will all happen at once. - SimHooks.restartTiming(); - - // Pause the clock so that we can step it in sync with the simulator - SimHooks.pauseTiming(); - - // Tell sim to start - robotTimeSecSim.set(-2.0); - - // Tell sim that we're ready - // robotTimeSecSim.set(Timer.getFPGATimestamp()); - - // Wait up to 15 minutes for Webots to respond. On GitHub's MacOS Continuous - // Integration servers, it can take over 8 minutes for Webots to start. - var startedWaitingTimeMs = System.currentTimeMillis(); - var isReady = false; - System.err.println("Waiting for WebotsSupervisor to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); - while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 900000) { - try { - long elapsedTime = System.currentTimeMillis() - startedWaitingTimeMs; - long remainingTime = 900000 - elapsedTime; - System.out.println("Waiting for isReadyFuture"); - if(remainingTime > 0) { - System.out.println("Waiting for isReadyFuture"); - isReady = isReadyFuture.get(remainingTime, TimeUnit.MILLISECONDS); - System.out.println("isReadyFuture.get() returned " + isReady); - } - else isReady = true; - } catch (TimeoutException ex) { - System.err.println("Waiting for WebotsSupervisor to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); - } catch (InterruptedException|ExecutionException e) { - throw new RuntimeException("Error while waiting for WebotsSupervisor to be ready", e); + // If we asked for the simulation to start and it has started, say that we're ready, + // Otherwise, let robot code run for deltaSecs. + if (robotTimeSecSim.get() == START_SIMULATION && simTimeSec == START_SIMULATION) { + isReadyFuture.complete(true); + } else if (deltaSecs >= 0.0) { + // We use a Notifier instead of SimHooks.stepTiming() because + // using SimHooks.stepTiming() causes accesses to sim data to block. + pauser.stop(); + pauser.startSingle(deltaSecs); + SimHooks.resumeTiming(); + // Increment the robot time we'll report to the sim. Strictly speaking it won't be that time + // until the pauser notification runs deltaSecs from now but this allows the sim to run in parrallel + // with the robot code. It's less deterministic but faster and arguably more realistic. + robotTimeSec += deltaSecs; } + + // Tell the sim what the robot time is if it has changed. + if (robotTimeSec != robotTimeSecSim.get()) { + robotTimeSecSim.set(robotTimeSec); + } + } + }, true); + + // Reset the clock. Without this, *Periodic calls that should have + // occurred while we waited, will be considered behind schedule and + // will all happen at once. + SimHooks.restartTiming(); + + // Pause the clock so that we can step it in sync with the simulator + SimHooks.pauseTiming(); + + // Tell sim to start + robotTimeSecSim.set(START_SIMULATION); + + // Wait up to 15 minutes for Webots to respond. On GitHub's MacOS Continuous + // Integration servers, it can take over 8 minutes for Webots to start. + var startedWaitingTimeMs = System.currentTimeMillis(); + var isReady = false; + System.err.println("Waiting for Webots to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); + while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 900000) { + try { + long elapsedTime = System.currentTimeMillis() - startedWaitingTimeMs; + long remainingTime = 900000 - elapsedTime; + if(remainingTime > 0) { + isReady = isReadyFuture.get(remainingTime, TimeUnit.MILLISECONDS); + } + else isReady = true; + } catch (TimeoutException ex) { + System.err.println("Waiting for Webots to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); + } catch (InterruptedException|ExecutionException e) { + throw new RuntimeException("Error while waiting for Webots to be ready", e); } - System.out.println("Done waiting"); - assertTrue("Webots ready in time", isReady); - System.out.println("Done asserting"); } + assertTrue("Webots ready in time", isReady); - System.out.println("WebotsSupervisor is ready. Enabling in autonomous."); System.out.flush(); + System.out.println("Webots. Enabling in autonomous."); System.out.flush(); // Simulate starting autonomous DriverStationSim.setAutonomous(true); DriverStationSim.setEnabled(true); DriverStationSim.notifyNewData(); super.simulationInit(); - System.out.println("Time at end of simulationInit="+Timer.getFPGATimestamp()); } @Override public void simulationPeriodic() { - System.out.println("Time at start of simulationPeriodic="+Timer.getFPGATimestamp()); - // if (Timer.getFPGATimestamp() > simTimeSecSim.get()) { - // SimHooks.pauseTiming(); - // System.out.println(String.format("calling robotTimeSec.set(%g)", Timer.getFPGATimestamp())); - // robotTimeSec.set(Timer.getFPGATimestamp()); - // } - super.simulationPeriodic(); - System.out.println("In simulationPeriodic()"); - System.out.println("Calling positionX.get()"); - System.out.println("self.position.x =" + positionX.get()); // The motors are on for 2 secs. We wait an extra second to give the robot time to stop. if (Timer.getFPGATimestamp() > 3.0) { // Simulate disabling the robot - System.out.println("Disabling robot"); DriverStationSim.setEnabled(false); DriverStationSim.notifyNewData(); - System.out.println("Done disabling"); Vector expectedPos = new Vector<>(N3.instance); expectedPos.set(0, 0, -2.6); expectedPos.set(1, 0, 0.0); expectedPos.set(2, 0, 0.0); - System.out.println("Calling positionX.get()"); System.out.println("self.position.x =" + positionX.get()); System.out.println("self.position.y =" + positionY.get()); System.out.println("self.position.z =" + positionZ.get()); @@ -243,11 +172,10 @@ public void simulationPeriodic() { var diff = new Vector(expectedPos.minus(actualPos)); var distance = Math.sqrt(diff.elementTimes(diff).elementSum()); - System.out.println("Asserting robot near targe position"); assertEquals("Robot close to target position", 0.0, distance, 1.0); - System.out.println("Calling isDoneFuture.complete(true)"); - isDoneFuture.complete(true); + // If the assert didn't throw, then just end normally. + endCompetition(); } } } diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 6e4f4438..46539994 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -24,13 +24,12 @@ public class DeepBlueSim { private static final BlockingDeque queuedMessages = new LinkedBlockingDeque<>(); - protected static final double MAX_TIME_DIFF_SECS = 0.0; - @SuppressWarnings("unused") - private static ScopedObject> robotStartMsCallbackStore = null; private static ScopedObject> robotTimeSecCallbackStore = null; private static RunningObject wsConnection = null; + private static final double START_SIMULATION = -2.0; + public static void main(String[] args) { // Set up exception handling to log to stderr and exit { @@ -65,94 +64,70 @@ public void uncaughtException(Thread arg0, Throwable arg1) { final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); final SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); - { - // Regular report the simulated robot's position - Simulation.registerPeriodicMethod(new Runnable() { - public void run() { - Node self = robot.getSelf(); - double[] pos = self.getPosition(); - webotsSupervisorSim.set("self.position.x", pos[0]); - webotsSupervisorSim.set("self.position.y", pos[1]); - webotsSupervisorSim.set("self.position.z", pos[2]); + // Regularly report the simulated robot's position + Simulation.registerPeriodicMethod(new Runnable() { + public void run() { + Node self = robot.getSelf(); + double[] pos = self.getPosition(); + webotsSupervisorSim.set("self.position.x", pos[0]); + webotsSupervisorSim.set("self.position.y", pos[1]); + webotsSupervisorSim.set("self.position.z", pos[2]); + } + }); + + // Whenever the robot time changes, step the simulation until just past that time + robotTimeSecCallbackStore = timeSynchronizerSim.registerValueChangedCallback("robotTimeSec", new StringCallback() { + @Override + public synchronized void callback(String name, String value) { + // Ignore null default initial value + if (value == null) + return; + + double robotTimeSec = Double.parseDouble(value); + + // If we are asked to start the simulation, reload the world. + // this will restart this controller process so that we are running the most recent controller. + if (robotTimeSec == START_SIMULATION) { + robot.worldReload(); + return; } - }); - // Whenever the robot time changes, step the simulation until just past that time - robotTimeSecCallbackStore = timeSynchronizerSim.registerValueChangedCallback("robotTimeSec", new StringCallback() { - @Override - public synchronized void callback(String name, String value) { - System.out.println("In robotTimeSec callback with value = " + value); System.out.flush(); - - if (value != null) { - double robotTimeSec = Double.parseDouble(value); - - if (robotTimeSec == -2.0) { - System.out.println("Reloading world"); - robot.worldReload(); - return; - } - // Keep stepping the simulation forward until - // the sim time is more than MAX_TIME_DIFF_SECS ahead of the robot time - // or the simulation ends. - while(true) { - double simTimeSec = robot.getTime(); - if (simTimeSec > robotTimeSec + MAX_TIME_DIFF_SECS) { - break; - } - System.out.println("Calling robot.step(basicTimeStep) in robotTimeSec callback"); System.out.flush(); - boolean isDone = (robot.step(basicTimeStep) == -1); - System.out.println("robot.step(basicTimeStep) returned in robotTimeSec callback"); System.out.flush(); - timeSynchronizerSim.set("simTimeSec", robot.getTime()); - if (isDone) { - isDoneFuture.complete(true); - break; - } - Simulation.runPeriodicMethods(); - } + // Keep stepping the simulation forward until the sim time is more than the robot time + // or the simulation ends. + while(true) { + double simTimeSec = robot.getTime(); + if (simTimeSec > robotTimeSec) { + break; + } + boolean isDone = (robot.step(basicTimeStep) == -1); + timeSynchronizerSim.set("simTimeSec", robot.getTime()); + if (isDone) { + isDoneFuture.complete(true); + break; } - System.out.println("exiting robotTimeSec callback"); System.out.flush(); + Simulation.runPeriodicMethods(); } - }, true); - - - // If the robot code starts after us, we expect it to tell us it's ready, and we respond - // that we're ready. - // robotStartMsCallbackStore = webotsSupervisorSim.registerValueChangedCallback("robotStartMs", new StringCallback() { - // @Override - // public void callback(String name, String value) { - // if (value != null) { - // System.out.println("Telling the robot we're ready"); - // webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); - // isReadyFuture.complete(true); - // } - // } - // }, true); - - // If the robot code starts before we us, then it might have already tried to tell - // us it was ready and we would have missed it. So, we tell it we're ready when we - // connect to it. - ConnectionProcessor.addOpenListener(() -> { - System.out.println("Telling the robot we're ready"); System.out.flush(); - // webotsSupervisorSim.set("simStartMs", System.currentTimeMillis()); - timeSynchronizerSim.set("simTimeSec", -2.0); - }); - } + } + }, true); + + + // If the robot code starts before we us, then it might have already tried to tell + // us it was ready and we would have missed it. So, we tell it we're ready when we + // connect to it. + ConnectionProcessor.addOpenListener(() -> { + timeSynchronizerSim.set("simTimeSec", START_SIMULATION); + }); + // Wait until startup has completed to ensure that the Webots simulator is // not still starting up. - System.out.println(String.format("Calling robot.step(%d)...", 0)); - System.out.flush(); if (robot.step(0) == -1) { throw new RuntimeException("Couldn't even start up!"); } - System.out.println("Calling connectDevices()..."); - System.out.flush(); SimRegisterer.connectDevices(); // Connect to the robot code try { - System.out.println("Trying to connect to robot..."); - System.out.flush(); wsConnection = WSConnection.connectHALSim(true); } catch(URISyntaxException e) { System.err.println("Error occurred connecting to server:"); @@ -162,20 +137,13 @@ public synchronized void callback(String name, String value) { return; } - System.out.println("connectHALSim() returned"); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { wsConnection.object.closeBlocking(); } catch(InterruptedException e) {} })); - // Tell robot we're ready - System.out.println("telling robot we're ready. robot.getTime()=" + robot.getTime()); - timeSynchronizerSim.set("simTimeSec", -2.0); - - System.out.println("Waiting for simulation to finish..."); - System.out.flush(); + // Process incoming messages until simulation finishes try { while (isDoneFuture.getNow(false).booleanValue() == false) { queuedMessages.takeFirst().run(); From 97760d06c8186960b6f831ba926938d2bba2379c Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Mon, 6 May 2024 22:03:29 -0700 Subject: [PATCH 50/60] Separate time sync code from robot test code. --- .../java/frc/robot/SystemTestRobot.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 59708d89..cc89c5b7 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -50,20 +50,29 @@ public void startCompetition() { } SimDevice webotsSupervisor = null; - SimDevice timeSynchronizer = null; SimDouble positionX = null, positionY = null, positionZ = null; - SimDouble simTimeSecSim = null; - SimDouble robotTimeSecSim = null; @Override public void simulationInit() { - timeSynchronizer = SimDevice.create("TimeSynchronizer"); - simTimeSecSim = timeSynchronizer.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); - robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); webotsSupervisor = SimDevice.create("WebotsSupervisor"); positionX = webotsSupervisor.createDouble("self.position.x", SimDevice.Direction.kInput, 0.0); positionY = webotsSupervisor.createDouble("self.position.y", SimDevice.Direction.kInput, 0.0); positionZ = webotsSupervisor.createDouble("self.position.z", SimDevice.Direction.kInput, 0.0); + webotsInit(); + + System.out.println("Webots. Enabling in autonomous."); System.out.flush(); + // Simulate starting autonomous + DriverStationSim.setAutonomous(true); + DriverStationSim.setEnabled(true); + DriverStationSim.notifyNewData(); + + super.simulationInit(); + } + + private void webotsInit() { + SimDevice timeSynchronizer = SimDevice.create("TimeSynchronizer"); + SimDouble simTimeSecSim = timeSynchronizer.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); + SimDouble robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); final var isReadyFuture = new CompletableFuture(); @@ -134,15 +143,6 @@ public synchronized void callback(String name, int handle, int direction, HALVal } } assertTrue("Webots ready in time", isReady); - - - System.out.println("Webots. Enabling in autonomous."); System.out.flush(); - // Simulate starting autonomous - DriverStationSim.setAutonomous(true); - DriverStationSim.setEnabled(true); - DriverStationSim.notifyNewData(); - - super.simulationInit(); } @Override From d10237200b672148438f937334325d581c9ad932 Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Tue, 7 May 2024 12:14:10 -0700 Subject: [PATCH 51/60] Fix test and sim time sync. Define robot time to start when the first periodic method is called and keep that time in sync with the sim time. --- .../java/frc/robot/SystemTestRobot.java | 75 +++++++++++++------ 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index cc89c5b7..a510f17b 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -69,46 +69,77 @@ public void simulationInit() { super.simulationInit(); } + private final Timer robotTime = new Timer(); + private void webotsInit() { + // Start robot time right before the first periodic call is made so that we ignore + // startup time. + robotTime.stop(); + robotTime.reset(); + addPeriodic(() -> { + robotTime.start(); + }, getPeriod(), -getPeriod()); SimDevice timeSynchronizer = SimDevice.create("TimeSynchronizer"); SimDouble simTimeSecSim = timeSynchronizer.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); - SimDouble robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); + final SimDouble robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); + final Notifier pauser = new Notifier(() -> { + // This is replaced on the next line + }); + pauser.setHandler(() -> { + double simTimeSec = simTimeSecSim.get(); + double robotTimeSec = robotTime.get(); + double deltaSecs = simTimeSec - robotTimeSec; + // If we still haven't caught up to the simulator, then wait longer. + // This would typically happen when robot time hasn't yet started. + if (deltaSecs > 0) { + pauser.stop(); + pauser.startSingle(deltaSecs); + return; + } + // We're caught up, so pause and tell the sim what our new time is so that it can continue. + SimHooks.pauseTiming(); + robotTimeSecSim.set(robotTimeSec); + }); + final var isReadyFuture = new CompletableFuture(); - final Notifier pauser = new Notifier(SimHooks::pauseTiming); timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, new SimValueCallback() { @Override public synchronized void callback(String name, int handle, int direction, HALValue value) { double simTimeSec = value.getDouble(); - double robotTimeSec = Timer.getFPGATimestamp(); - double deltaSecs = simTimeSec - robotTimeSec; + double robotTimeSec = robotTime.get(); // Ignore the default initial value if (simTimeSec == -1.0) { return; } - // If we asked for the simulation to start and it has started, say that we're ready, - // Otherwise, let robot code run for deltaSecs. - if (robotTimeSecSim.get() == START_SIMULATION && simTimeSec == START_SIMULATION) { - isReadyFuture.complete(true); - } else if (deltaSecs >= 0.0) { - // We use a Notifier instead of SimHooks.stepTiming() because - // using SimHooks.stepTiming() causes accesses to sim data to block. - pauser.stop(); - pauser.startSingle(deltaSecs); - SimHooks.resumeTiming(); - // Increment the robot time we'll report to the sim. Strictly speaking it won't be that time - // until the pauser notification runs deltaSecs from now but this allows the sim to run in parrallel - // with the robot code. It's less deterministic but faster and arguably more realistic. - robotTimeSec += deltaSecs; + // If we asked for the simulation to start and it has started, say that we're ready. + if (robotTimeSecSim.get() == START_SIMULATION) { + if (simTimeSec == START_SIMULATION) { + isReadyFuture.complete(true); + robotTimeSecSim.set(robotTimeSec); + } + return; + } + // Otherwise, ignore notifications that the sim has started. + if (simTimeSec == START_SIMULATION) { + return; } - // Tell the sim what the robot time is if it has changed. - if (robotTimeSec != robotTimeSecSim.get()) { - robotTimeSecSim.set(robotTimeSec); + // If we're not behind the sim time, there is nothing to do. + double deltaSecs = simTimeSec - robotTimeSec; + if (deltaSecs < 0.0) { + return; } + + // We are behind the sim time, so run until we've caught up. + // We use a Notifier instead of SimHooks.stepTiming() because + // using SimHooks.stepTiming() causes accesses to sim data to block. + pauser.stop(); + pauser.startSingle(deltaSecs); + SimHooks.resumeTiming(); } }, true); @@ -150,7 +181,7 @@ public void simulationPeriodic() { super.simulationPeriodic(); // The motors are on for 2 secs. We wait an extra second to give the robot time to stop. - if (Timer.getFPGATimestamp() > 3.0) { + if (robotTime.get() > 3.0) { // Simulate disabling the robot DriverStationSim.setEnabled(false); DriverStationSim.notifyNewData(); From 6c16dd41118e0b62daf3ff8b6322c640147afe3a Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Tue, 7 May 2024 16:47:59 -0700 Subject: [PATCH 52/60] Automatically pause simulator if it's been idle for 1-2 seconds. --- .../controller/src/main/java/DeepBlueSim.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 46539994..bb502a7b 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -3,6 +3,8 @@ import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.CompletableFuture; +import java.util.Timer; +import java.util.TimerTask; import com.cyberbotics.webots.controller.Node; import com.cyberbotics.webots.controller.Supervisor; @@ -30,6 +32,8 @@ public class DeepBlueSim { private static final double START_SIMULATION = -2.0; + private static volatile long lastStepMillis = 0; + public static void main(String[] args) { // Set up exception handling to log to stderr and exit { @@ -59,6 +63,9 @@ public void uncaughtException(Thread arg0, Throwable arg1) { Simulation.init(robot, robot.getBasicTimeStep()); + // Remember the current simulation speed (default to real time if paused) + final int originalSimulationSpeed = robot.simulationGetMode() == Supervisor.SIMULATION_MODE_PAUSE ? Supervisor.SIMULATION_MODE_REAL_TIME : robot.simulationGetMode(); + // Use a SimDeviceSim to coordinate with robot code final CompletableFuture isDoneFuture = new CompletableFuture(); final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); @@ -75,6 +82,20 @@ public void run() { } }); + // Pause the simulator if it hasn't taken any steps in the last 1-2 seconds + // so it doesn't suck up CPU. + Timer simPauseTimer = new Timer(); + simPauseTimer.schedule(new TimerTask() { + @Override + public void run() { + if (System.currentTimeMillis() - lastStepMillis > 1000) { + queuedMessages.add(() -> { + robot.simulationSetMode(Supervisor.SIMULATION_MODE_PAUSE); + }); + } + } + }, 1000, 1000); + // Whenever the robot time changes, step the simulation until just past that time robotTimeSecCallbackStore = timeSynchronizerSim.registerValueChangedCallback("robotTimeSec", new StringCallback() { @Override @@ -88,6 +109,8 @@ public synchronized void callback(String name, String value) { // If we are asked to start the simulation, reload the world. // this will restart this controller process so that we are running the most recent controller. if (robotTimeSec == START_SIMULATION) { + // Unpause before reloading so that the new controller can take it's first step. + robot.simulationSetMode(originalSimulationSpeed); robot.worldReload(); return; } @@ -99,7 +122,10 @@ public synchronized void callback(String name, String value) { if (simTimeSec > robotTimeSec) { break; } + // Unpause if necessary + robot.simulationSetMode(originalSimulationSpeed); boolean isDone = (robot.step(basicTimeStep) == -1); + lastStepMillis = System.currentTimeMillis(); timeSynchronizerSim.set("simTimeSec", robot.getTime()); if (isDone) { isDoneFuture.complete(true); From 64d45a98f08c17dee8e1fe978a3f3af7f3e63b93 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 14 May 2024 23:25:23 -0700 Subject: [PATCH 53/60] fix installDeepBlueSim .wbproj access denied error on Windows --- .../gradle/DeepBlueSimPlugin.groovy | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy b/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy index 7a941c25..97493ecc 100644 --- a/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy +++ b/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy @@ -5,10 +5,14 @@ package org.team199.deepbluesim.gradle import org.gradle.api.Project import org.gradle.api.Plugin +import org.gradle.internal.os.OperatingSystem import org.apache.commons.io.FileUtils import org.apache.commons.io.IOUtils +import java.nio.file.Files +import java.nio.file.Paths + /** * A simple 'hello world' plugin. */ @@ -20,16 +24,27 @@ class DeepBlueSimPlugin implements Plugin { if (resourceStream == null) throw new RuntimeException("resourceStream is null") def dbsDir = new File(project.buildDir, "tmp/deepbluesim") dbsDir.mkdirs() - FileUtils.copyInputStreamToFile(resourceStream, new File(dbsDir,"Webots.zip")) + + // Java IO cannot open files with the hidden attribute set on Windows (JDK-8047342) + // This capability is needed to overwrite the files loaded from the zip (if they exist) + // Webots automatically sets the hidden attribute on the .wbproj file + // so the task will fail unless we remove it + if(OperatingSystem.current().isWindows()) { + def wbprojPath = Paths.get(project.projectDir.getAbsolutePath(), "Webots", "worlds", ".DBSExample.wbproj") + Files.setAttribute(wbprojPath, "dos:hidden", false) + } + + def extractedZipFile = new File(dbsDir, "Webots.zip") + FileUtils.copyInputStreamToFile(resourceStream, extractedZipFile) project.copy { - from project.zipTree(new File(dbsDir,"Webots.zip")) + from project.zipTree(extractedZipFile) into project.projectDir } } } - project.tasks.matching({ task -> + project.tasks.matching({ task -> (task.name.toLowerCase().contains("simulate")) - }).all { GroovyObject t -> + }).all { GroovyObject t -> t.dependsOn(installDeepBlueSim) } } From 4f5f4a34eac491921412a410de25a720b2062d15 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 14 May 2024 23:25:44 -0700 Subject: [PATCH 54/60] cleanup and minor fixes --- .../java/frc/robot/SystemTestRobot.java | 80 +++++++++---------- .../controller/src/main/java/DeepBlueSim.java | 26 +++--- 2 files changed, 52 insertions(+), 54 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index a510f17b..8bb2ab71 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -8,10 +8,8 @@ import java.util.concurrent.TimeoutException; import edu.wpi.first.hal.HAL; -import edu.wpi.first.hal.HALValue; import edu.wpi.first.hal.SimDevice; import edu.wpi.first.hal.SimDouble; -import edu.wpi.first.hal.simulation.SimValueCallback; import edu.wpi.first.math.Vector; import edu.wpi.first.math.numbers.N3; import edu.wpi.first.wpilibj.DriverStation; @@ -24,7 +22,11 @@ public class SystemTestRobot extends Robot { + /** + * Time value set by the simulator and the robot to indicate that the simulation should start. + */ private static final double START_SIMULATION = -2.0; + private static final Notifier pauser = new Notifier(() -> { /* This is replaced in webotsInit */ }); public static void main(String... args) { RobotBase.startRobot(SystemTestRobot::new); @@ -60,7 +62,7 @@ public void simulationInit() { positionZ = webotsSupervisor.createDouble("self.position.z", SimDevice.Direction.kInput, 0.0); webotsInit(); - System.out.println("Webots. Enabling in autonomous."); System.out.flush(); + System.out.println("Webots has started. Enabling in autonomous."); System.out.flush(); // Simulate starting autonomous DriverStationSim.setAutonomous(true); DriverStationSim.setEnabled(true); @@ -76,17 +78,12 @@ private void webotsInit() { // startup time. robotTime.stop(); robotTime.reset(); - addPeriodic(() -> { - robotTime.start(); - }, getPeriod(), -getPeriod()); + addPeriodic(robotTime::start, getPeriod(), -getPeriod()); SimDevice timeSynchronizer = SimDevice.create("TimeSynchronizer"); SimDouble simTimeSecSim = timeSynchronizer.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); final SimDouble robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); - final Notifier pauser = new Notifier(() -> { - // This is replaced on the next line - }); pauser.setHandler(() -> { double simTimeSec = simTimeSecSim.get(); double robotTimeSec = robotTime.get(); @@ -105,45 +102,42 @@ private void webotsInit() { final var isReadyFuture = new CompletableFuture(); - timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, new SimValueCallback() { - @Override - public synchronized void callback(String name, int handle, int direction, HALValue value) { - double simTimeSec = value.getDouble(); - double robotTimeSec = robotTime.get(); + timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, (name, handle, direction, value) -> { + double simTimeSec = value.getDouble(); + double robotTimeSec = robotTime.get(); - // Ignore the default initial value - if (simTimeSec == -1.0) { - return; - } - // If we asked for the simulation to start and it has started, say that we're ready. - if (robotTimeSecSim.get() == START_SIMULATION) { - if (simTimeSec == START_SIMULATION) { - isReadyFuture.complete(true); - robotTimeSecSim.set(robotTimeSec); - } - return; - } - // Otherwise, ignore notifications that the sim has started. + // Ignore the default initial value + if (simTimeSec == -1.0) { + return; + } + // If we asked for the simulation to start and it has started, say that we're ready. + if (robotTimeSecSim.get() == START_SIMULATION) { if (simTimeSec == START_SIMULATION) { - return; - } - - // If we're not behind the sim time, there is nothing to do. - double deltaSecs = simTimeSec - robotTimeSec; - if (deltaSecs < 0.0) { - return; + isReadyFuture.complete(true); + robotTimeSecSim.set(robotTimeSec); } + return; + } + // Otherwise, ignore notifications that the sim has started. + if (simTimeSec == START_SIMULATION) { + return; + } - // We are behind the sim time, so run until we've caught up. - // We use a Notifier instead of SimHooks.stepTiming() because - // using SimHooks.stepTiming() causes accesses to sim data to block. - pauser.stop(); - pauser.startSingle(deltaSecs); - SimHooks.resumeTiming(); + // If we're not behind the sim time, there is nothing to do. + double deltaSecs = simTimeSec - robotTimeSec; + if (deltaSecs <= 0.0) { + return; } + + // We are behind the sim time, so run until we've caught up. + // We use a Notifier instead of SimHooks.stepTiming() because + // using SimHooks.stepTiming() causes accesses to sim data to block. + pauser.stop(); + pauser.startSingle(deltaSecs); + SimHooks.resumeTiming(); }, true); - // Reset the clock. Without this, *Periodic calls that should have + // Reset the clock. Without this, *Periodic calls that should have // occurred while we waited, will be considered behind schedule and // will all happen at once. SimHooks.restartTiming(); @@ -153,7 +147,7 @@ public synchronized void callback(String name, int handle, int direction, HALVal // Tell sim to start robotTimeSecSim.set(START_SIMULATION); - + // Wait up to 15 minutes for Webots to respond. On GitHub's MacOS Continuous // Integration servers, it can take over 8 minutes for Webots to start. var startedWaitingTimeMs = System.currentTimeMillis(); @@ -166,7 +160,7 @@ public synchronized void callback(String name, int handle, int direction, HALVal if(remainingTime > 0) { isReady = isReadyFuture.get(remainingTime, TimeUnit.MILLISECONDS); } - else isReady = true; + else break; } catch (TimeoutException ex) { System.err.println("Waiting for Webots to be ready. Please open example/Webots/worlds/DBSExample.wbt in Webots."); } catch (InterruptedException|ExecutionException e) { diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index bb502a7b..be9fbdb3 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -30,8 +30,14 @@ public class DeepBlueSim { private static ScopedObject> robotTimeSecCallbackStore = null; private static RunningObject wsConnection = null; + /** + * Time value set by the simulator and the robot to indicate that the simulation should start. + */ private static final double START_SIMULATION = -2.0; + /** + * The time in milliseconds since the last timestep update from the robot. + */ private static volatile long lastStepMillis = 0; public static void main(String[] args) { @@ -72,14 +78,12 @@ public void uncaughtException(Thread arg0, Throwable arg1) { final SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); // Regularly report the simulated robot's position - Simulation.registerPeriodicMethod(new Runnable() { - public void run() { - Node self = robot.getSelf(); - double[] pos = self.getPosition(); - webotsSupervisorSim.set("self.position.x", pos[0]); - webotsSupervisorSim.set("self.position.y", pos[1]); - webotsSupervisorSim.set("self.position.z", pos[2]); - } + Simulation.registerPeriodicMethod(() -> { + Node self = robot.getSelf(); + double[] pos = self.getPosition(); + webotsSupervisorSim.set("self.position.x", pos[0]); + webotsSupervisorSim.set("self.position.y", pos[1]); + webotsSupervisorSim.set("self.position.z", pos[2]); }); // Pause the simulator if it hasn't taken any steps in the last 1-2 seconds @@ -101,7 +105,7 @@ public void run() { @Override public synchronized void callback(String name, String value) { // Ignore null default initial value - if (value == null) + if (value == null) return; double robotTimeSec = Double.parseDouble(value); @@ -117,7 +121,7 @@ public synchronized void callback(String name, String value) { // Keep stepping the simulation forward until the sim time is more than the robot time // or the simulation ends. - while(true) { + for(;;) { double simTimeSec = robot.getTime(); if (simTimeSec > robotTimeSec) { break; @@ -175,7 +179,7 @@ public synchronized void callback(String name, String value) { queuedMessages.takeFirst().run(); } } catch (Exception ex) { - throw new RuntimeException("Exception while waiting for simulation to be done"); + throw new RuntimeException("Exception while waiting for simulation to be done", ex); } System.out.println("Shutting down DeepBlueSim..."); From 88ed7d05f1a8db6f651f6c43b4314329f567d114 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Tue, 14 May 2024 23:50:50 -0700 Subject: [PATCH 55/60] add missing Files.exists check --- .../org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy b/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy index 97493ecc..bc2525b1 100644 --- a/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy +++ b/plugin/src/main/groovy/org/team199/deepbluesim/gradle/DeepBlueSimPlugin.groovy @@ -31,7 +31,9 @@ class DeepBlueSimPlugin implements Plugin { // so the task will fail unless we remove it if(OperatingSystem.current().isWindows()) { def wbprojPath = Paths.get(project.projectDir.getAbsolutePath(), "Webots", "worlds", ".DBSExample.wbproj") - Files.setAttribute(wbprojPath, "dos:hidden", false) + if(Files.exists(wbprojPath)) { + Files.setAttribute(wbprojPath, "dos:hidden", false) + } } def extractedZipFile = new File(dbsDir, "Webots.zip") From e9dcb67f9e8df8a4ed7b13e01343c115e2243895 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Wed, 15 May 2024 00:45:59 -0700 Subject: [PATCH 56/60] revert parts of 4f5f4a34eac491921412a410de25a720b2062d15 --- .../java/frc/robot/SystemTestRobot.java | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 8bb2ab71..62e155f9 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -8,8 +8,10 @@ import java.util.concurrent.TimeoutException; import edu.wpi.first.hal.HAL; +import edu.wpi.first.hal.HALValue; import edu.wpi.first.hal.SimDevice; import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.simulation.SimValueCallback; import edu.wpi.first.math.Vector; import edu.wpi.first.math.numbers.N3; import edu.wpi.first.wpilibj.DriverStation; @@ -26,7 +28,7 @@ public class SystemTestRobot extends Robot { * Time value set by the simulator and the robot to indicate that the simulation should start. */ private static final double START_SIMULATION = -2.0; - private static final Notifier pauser = new Notifier(() -> { /* This is replaced in webotsInit */ }); + private static Notifier pauser; public static void main(String... args) { RobotBase.startRobot(SystemTestRobot::new); @@ -84,6 +86,9 @@ private void webotsInit() { final SimDouble robotTimeSecSim = timeSynchronizer.createDouble("robotTimeSec", SimDevice.Direction.kOutput, -1.0); SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); + pauser = new Notifier(() -> { + // This is replaced on the next line + }); pauser.setHandler(() -> { double simTimeSec = simTimeSecSim.get(); double robotTimeSec = robotTime.get(); @@ -102,39 +107,42 @@ private void webotsInit() { final var isReadyFuture = new CompletableFuture(); - timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, (name, handle, direction, value) -> { - double simTimeSec = value.getDouble(); - double robotTimeSec = robotTime.get(); + timeSynchronizerSim.registerValueChangedCallback(simTimeSecSim, new SimValueCallback() { + @Override + public synchronized void callback(String name, int handle, int direction, HALValue value) { + double simTimeSec = value.getDouble(); + double robotTimeSec = robotTime.get(); - // Ignore the default initial value - if (simTimeSec == -1.0) { - return; - } - // If we asked for the simulation to start and it has started, say that we're ready. - if (robotTimeSecSim.get() == START_SIMULATION) { + // Ignore the default initial value + if (simTimeSec == -1.0) { + return; + } + // If we asked for the simulation to start and it has started, say that we're ready. + if (robotTimeSecSim.get() == START_SIMULATION) { + if (simTimeSec == START_SIMULATION) { + isReadyFuture.complete(true); + robotTimeSecSim.set(robotTimeSec); + } + return; + } + // Otherwise, ignore notifications that the sim has started. if (simTimeSec == START_SIMULATION) { - isReadyFuture.complete(true); - robotTimeSecSim.set(robotTimeSec); + return; } - return; - } - // Otherwise, ignore notifications that the sim has started. - if (simTimeSec == START_SIMULATION) { - return; - } - // If we're not behind the sim time, there is nothing to do. - double deltaSecs = simTimeSec - robotTimeSec; - if (deltaSecs <= 0.0) { - return; - } + // If we're not behind the sim time, there is nothing to do. + double deltaSecs = simTimeSec - robotTimeSec; + if (deltaSecs < 0.0) { + return; + } - // We are behind the sim time, so run until we've caught up. - // We use a Notifier instead of SimHooks.stepTiming() because - // using SimHooks.stepTiming() causes accesses to sim data to block. - pauser.stop(); - pauser.startSingle(deltaSecs); - SimHooks.resumeTiming(); + // We are behind the sim time, so run until we've caught up. + // We use a Notifier instead of SimHooks.stepTiming() because + // using SimHooks.stepTiming() causes accesses to sim data to block. + pauser.stop(); + pauser.startSingle(deltaSecs); + SimHooks.resumeTiming(); + } }, true); // Reset the clock. Without this, *Periodic calls that should have From 283053fc5e819982ed0aa15f2abbf5ccc288e546 Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Thu, 16 May 2024 09:29:06 -0700 Subject: [PATCH 57/60] Remember user changes to simulation speed. --- plugin/controller/src/main/java/DeepBlueSim.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index be9fbdb3..8288429f 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -40,6 +40,13 @@ public class DeepBlueSim { */ private static volatile long lastStepMillis = 0; + private static int usersSimulationSpeed = 0; + + // Remember the current simulation speed (default to real time if paused) + private static void updateUsersSimulationSpeed(Supervisor robot) { + usersSimulationSpeed = robot.simulationGetMode() == Supervisor.SIMULATION_MODE_PAUSE ? Supervisor.SIMULATION_MODE_REAL_TIME : robot.simulationGetMode(); + } + public static void main(String[] args) { // Set up exception handling to log to stderr and exit { @@ -69,9 +76,7 @@ public void uncaughtException(Thread arg0, Throwable arg1) { Simulation.init(robot, robot.getBasicTimeStep()); - // Remember the current simulation speed (default to real time if paused) - final int originalSimulationSpeed = robot.simulationGetMode() == Supervisor.SIMULATION_MODE_PAUSE ? Supervisor.SIMULATION_MODE_REAL_TIME : robot.simulationGetMode(); - + updateUsersSimulationSpeed(robot); // Use a SimDeviceSim to coordinate with robot code final CompletableFuture isDoneFuture = new CompletableFuture(); final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); @@ -94,6 +99,7 @@ public void uncaughtException(Thread arg0, Throwable arg1) { public void run() { if (System.currentTimeMillis() - lastStepMillis > 1000) { queuedMessages.add(() -> { + updateUsersSimulationSpeed(robot); robot.simulationSetMode(Supervisor.SIMULATION_MODE_PAUSE); }); } @@ -114,7 +120,7 @@ public synchronized void callback(String name, String value) { // this will restart this controller process so that we are running the most recent controller. if (robotTimeSec == START_SIMULATION) { // Unpause before reloading so that the new controller can take it's first step. - robot.simulationSetMode(originalSimulationSpeed); + robot.simulationSetMode(usersSimulationSpeed); robot.worldReload(); return; } @@ -127,7 +133,7 @@ public synchronized void callback(String name, String value) { break; } // Unpause if necessary - robot.simulationSetMode(originalSimulationSpeed); + robot.simulationSetMode(usersSimulationSpeed); boolean isDone = (robot.step(basicTimeStep) == -1); lastStepMillis = System.currentTimeMillis(); timeSynchronizerSim.set("simTimeSec", robot.getTime()); From 7bc91976647a5e8ee6365912d157f618d998eb67 Mon Sep 17 00:00:00 2001 From: CoolSpy3 Date: Thu, 16 May 2024 10:15:09 -0700 Subject: [PATCH 58/60] clarify usage of -getPeriod() in SystemTestRobot --- example/src/systemTest/java/frc/robot/SystemTestRobot.java | 1 + 1 file changed, 1 insertion(+) diff --git a/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 62e155f9..641ca176 100644 --- a/example/src/systemTest/java/frc/robot/SystemTestRobot.java +++ b/example/src/systemTest/java/frc/robot/SystemTestRobot.java @@ -80,6 +80,7 @@ private void webotsInit() { // startup time. robotTime.stop(); robotTime.reset(); + // Set the offset to -period to run before other WPILib periodic methods addPeriodic(robotTime::start, getPeriod(), -getPeriod()); SimDevice timeSynchronizer = SimDevice.create("TimeSynchronizer"); SimDouble simTimeSecSim = timeSynchronizer.createDouble("simTimeSec", SimDevice.Direction.kInput, -1.0); From 0676f135d0a588a1ef5fe237be95d46d8782a001 Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Thu, 16 May 2024 12:50:33 -0700 Subject: [PATCH 59/60] Support running without time synchronization. --- .../controller/src/main/java/DeepBlueSim.java | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/plugin/controller/src/main/java/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 8288429f..0dc8e010 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -91,20 +91,7 @@ public void uncaughtException(Thread arg0, Throwable arg1) { webotsSupervisorSim.set("self.position.z", pos[2]); }); - // Pause the simulator if it hasn't taken any steps in the last 1-2 seconds - // so it doesn't suck up CPU. Timer simPauseTimer = new Timer(); - simPauseTimer.schedule(new TimerTask() { - @Override - public void run() { - if (System.currentTimeMillis() - lastStepMillis > 1000) { - queuedMessages.add(() -> { - updateUsersSimulationSpeed(robot); - robot.simulationSetMode(Supervisor.SIMULATION_MODE_PAUSE); - }); - } - } - }, 1000, 1000); // Whenever the robot time changes, step the simulation until just past that time robotTimeSecCallbackStore = timeSynchronizerSim.registerValueChangedCallback("robotTimeSec", new StringCallback() { @@ -135,6 +122,23 @@ public synchronized void callback(String name, String value) { // Unpause if necessary robot.simulationSetMode(usersSimulationSpeed); boolean isDone = (robot.step(basicTimeStep) == -1); + + // If that was our first step, schedule a task to pause the simulator if it + // doesn't taken any steps for 1-2 seconds so it doesn't suck up CPU. + if (lastStepMillis == 0) { + simPauseTimer.schedule(new TimerTask() { + @Override + public void run() { + if (System.currentTimeMillis() - lastStepMillis > 1000) { + queuedMessages.add(() -> { + updateUsersSimulationSpeed(robot); + robot.simulationSetMode(Supervisor.SIMULATION_MODE_PAUSE); + }); + } + } + }, 1000, 1000); + } + lastStepMillis = System.currentTimeMillis(); timeSynchronizerSim.set("simTimeSec", robot.getTime()); if (isDone) { @@ -161,6 +165,9 @@ public synchronized void callback(String name, String value) { } SimRegisterer.connectDevices(); + // Pause the simulation until either the robot code tells us to proceed or the + // user does. + robot.simulationSetMode(Supervisor.SIMULATION_MODE_PAUSE); // Connect to the robot code try { @@ -182,7 +189,27 @@ public synchronized void callback(String name, String value) { // Process incoming messages until simulation finishes try { while (isDoneFuture.getNow(false).booleanValue() == false) { - queuedMessages.takeFirst().run(); + if (timeSynchronizerSim.get("robotTimeSec") != null || !queuedMessages.isEmpty()) { + // Either there is a message waiting or it is ok to wait for it because the + // robot code will tell us when to step the simulation. + queuedMessages.takeFirst().run(); + } else if (timeSynchronizerSim.get("robotTimeSec") == null + && robot.simulationGetMode() != Supervisor.SIMULATION_MODE_PAUSE) { + // The robot code isn't going to tell us when to step the simulation and the + // user has unpaused it. + Simulation.runPeriodicMethods(); + if (robot.step(basicTimeStep) == -1) { + break; + } + } else { + // The simulation is paused and robot code isn't in control so wait a beat + // before checking again (so we don't suck up all the CPU) + Thread.sleep(20); + // Process any pending user interface events. + if (robot.step(0) == -1) { + break; + } + } } } catch (Exception ex) { throw new RuntimeException("Exception while waiting for simulation to be done", ex); From a11cdea959c82ce3ca0ba491e083af5e0019a80e Mon Sep 17 00:00:00 2001 From: Dean Brettle Date: Thu, 16 May 2024 13:30:27 -0700 Subject: [PATCH 60/60] Document TimeSynchronizer. --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a126d566..48987188 100644 --- a/README.md +++ b/README.md @@ -39,5 +39,25 @@ advantage of the WPILib's WebSockets server desktop simulation extension. 1. In the HALSim GUI, select `Autonomous` to see the robot drive forward for 2 seconds, or select `Teleop` and use the keyboard on joystick to drive the robot around. - +## Details + +### Time synchronization + +By default, the robot code and Webots run at their own speeds so their clocks +will not necessarily match. This can be particularly problematic when trying to +write tests which should be as deterministic as possible. To synchronize the +clocks, the robot code can create a `SimDeviceSim` named `TimeSynchronizer` with +2 `double` values: `robotTimeSec` (output direction) and `simTimeSec` (input +direction). To start the synchronization and force Webots to reload the world +(so that it is in a known state with time = 0), the robot code should set +`robotTimeSec` to -2. The simulator will respond by setting `simTimeSec` to -2. +At that point the robot code can set `robotTimeSec` to the current robot time +and the simulator will run until it's time is greater than that. As it runs, it +will also updae `simTimeSec`. The robot code can ensure that it doesn't get +ahead of the simulator by using `SimHooks.pauseTiming()` to pause the robot code +when the robot time is ahead of the simulation time and +`SimHooks.resumeTiming()` when it is not. See the `webotsInit()` method of the +example's +[`SystemTestRobot.java`](example/src/systemTest/java/frc/robot/SystemTestRobot.java) +for an example implementation.