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..6bafd8fc 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 ] @@ -16,45 +16,35 @@ 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@v2 + with: + webotsVersion: R2023b - name: Do the system test 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 }} @@ -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" @@ -108,10 +89,12 @@ 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 - + 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 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", 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. diff --git a/WPIWebSockets b/WPIWebSockets index e131336b..fe44c974 160000 --- a/WPIWebSockets +++ b/WPIWebSockets @@ -1 +1 @@ -Subproject commit e131336bfa9dd2333dad0237f03315c0d80fc4ff +Subproject commit fe44c9742fe5992f3f0bd9302e5c6f5dc98d3752 diff --git a/example/.gitignore b/example/.gitignore index 5f50cd6b..18aed26e 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -161,5 +161,9 @@ 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/* + +# 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/build.gradle b/example/build.gradle index 990b20e4..99e55262 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 "2023.4.3" 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() + + testImplementation 'junit:junit:4.12' +} - // 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) +// 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 @@ -86,7 +93,7 @@ sourceSets { configurations { systemTestImplementation.extendsFrom testImplementation - systemTestRuntimeOnly.extendsFrom runtimeOnly + systemTestRuntimeOnly.extendsFrom runtimeOnly } task('systemTestJar', type: Jar) { @@ -95,66 +102,39 @@ 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) { +import org.gradle.internal.os.OperatingSystem +task('systemTest', type: JavaExec) { + // Run the jar file dependsOn 'systemTestJar' - dependsOn 'extractTestJNI' + classpath = files(tasks.systemTestJar) + + 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() + 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 + } + 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) + 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 }) + } } 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 - } -} - diff --git a/example/gradle/wrapper/gradle-wrapper.jar b/example/gradle/wrapper/gradle-wrapper.jar index cc4fdc29..7454180f 100644 Binary files a/example/gradle/wrapper/gradle-wrapper.jar and b/example/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example/gradle/wrapper/gradle-wrapper.properties b/example/gradle/wrapper/gradle-wrapper.properties index d050f177..a0450b88 100644 --- a/example/gradle/wrapper/gradle-wrapper.properties +++ b/example/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=permwrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=permwrapper/dists diff --git a/example/gradlew b/example/gradlew index 2fe81a7d..c53aefaa 100755 --- a/example/gradlew +++ b/example/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,78 +17,113 @@ # ############################################################################## -## -## 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 + # 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 940229fd..6ccf00e3 100644 --- a/example/src/main/java/frc/robot/Robot.java +++ b/example/src/main/java/frc/robot/Robot.java @@ -8,10 +8,11 @@ package frc.robot; import edu.wpi.first.wpilibj.Joystick; -import edu.wpi.first.wpilibj.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.MotorControllerGroup; +import edu.wpi.first.wpilibj.motorcontrol.PWMVictorSPX; /** * The VM is configured to automatically run this class, and to call the @@ -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/example/src/systemTest/java/frc/robot/SystemTestRobot.java b/example/src/systemTest/java/frc/robot/SystemTestRobot.java index 76e14e62..641ca176 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,15 +12,24 @@ 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.Notifier; +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; -import edu.wpi.first.wpiutil.math.Vector; -import edu.wpi.first.wpiutil.math.numbers.N3; 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 Notifier pauser; + public static void main(String... args) { RobotBase.startRobot(SystemTestRobot::new); } @@ -53,49 +59,12 @@ public void startCompetition() { @Override public void simulationInit() { webotsSupervisor = SimDevice.create("WebotsSupervisor"); - SimDouble simStartMs = webotsSupervisor.createDouble("simStartMs", SimDevice.Direction.kInput, 0.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"); - - // 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) { - if (value.getDouble() > 0.0) { - System.out.println("WebotsSupervisor is ready"); - 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); - } - // Wait up to 10 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; - while (!isReady && System.currentTimeMillis() - startedWaitingTimeMs < 600000) { - try { - isReady = future.get(1, TimeUnit.SECONDS); - } 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); - } - } - assertTrue("Webots ready in time", isReady); - } + webotsInit(); - // 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(); + System.out.println("Webots has started. Enabling in autonomous."); System.out.flush(); // Simulate starting autonomous DriverStationSim.setAutonomous(true); DriverStationSim.setEnabled(true); @@ -104,14 +73,118 @@ public void callback(String name, int handle, boolean readonly, HALValue value) super.simulationInit(); } - private int count = 0; + 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(); + // 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); + 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(); + 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(); + + 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) { + if (simTimeSec == START_SIMULATION) { + isReadyFuture.complete(true); + robotTimeSecSim.set(robotTimeSec); + } + 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; + } + + // 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 + // 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 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) { + throw new RuntimeException("Error while waiting for Webots to be ready", e); + } + } + assertTrue("Webots ready in time", isReady); + } @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 (robotTime.get() > 3.0) { // Simulate disabling the robot DriverStationSim.setEnabled(false); DriverStationSim.notifyNewData(); @@ -135,8 +208,7 @@ public void simulationPeriodic() { 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. + // If the assert didn't throw, then just end normally. endCompetition(); } } 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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c0..7454180f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da9702f9..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-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.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/build.gradle b/plugin/build.gradle index 3c99101e..b5342636 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:1.3-groovy-2.5' - 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: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') } 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/build.gradle b/plugin/controller/build.gradle index b64b07db..104c2dd6 100644 --- a/plugin/controller/build.gradle +++ b/plugin/controller/build.gradle @@ -16,13 +16,17 @@ 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" + id "org.carlmontrobotics.webots" version "1.0.0" // 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,21 +59,28 @@ 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 +// 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 +112,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/DeepBlueSim.java b/plugin/controller/src/main/java/DeepBlueSim.java index 9f66f9a2..0dc8e010 100644 --- a/plugin/controller/src/main/java/DeepBlueSim.java +++ b/plugin/controller/src/main/java/DeepBlueSim.java @@ -1,5 +1,10 @@ +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 java.util.Timer; +import java.util.TimerTask; import com.cyberbotics.webots.controller.Node; import com.cyberbotics.webots.controller.Supervisor; @@ -7,87 +12,213 @@ 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.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 // the name of the jar. public class DeepBlueSim { - private static final ConcurrentLinkedDeque queuedMessages = new ConcurrentLinkedDeque<>(); + private static final BlockingDeque queuedMessages = new LinkedBlockingDeque<>(); + + @SuppressWarnings("unused") + 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; + + 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(); + } - private static ScopedObject> callbackStore = null; public static void main(String[] args) { + // 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()); - - SimConfig.initConfig(); + + if (!robot.getSupervisor()) { + 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 tests + updateUsersSimulationSpeed(robot); + // Use a SimDeviceSim to coordinate with robot code + final CompletableFuture isDoneFuture = new CompletableFuture(); final SimDeviceSim webotsSupervisorSim = new SimDeviceSim("WebotsSupervisor"); - // Regular report the simulated robot's position - if (robot.getSupervisor()) { - 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]); + final SimDeviceSim timeSynchronizerSim = new SimDeviceSim("TimeSynchronizer"); + + // Regularly report the simulated robot's position + 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]); + }); + + Timer simPauseTimer = new Timer(); + + // 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) { + // Unpause before reloading so that the new controller can take it's first step. + robot.simulationSetMode(usersSimulationSpeed); + robot.worldReload(); + return; } - }); - } else { - System.err.println("The robot does not have supervisor=true. Reporting is limited."); - } + // Keep stepping the simulation forward until the sim time is more than the robot time + // or the simulation ends. + for(;;) { + double simTimeSec = robot.getTime(); + if (simTimeSec > robotTimeSec) { + break; + } + // Unpause if necessary + robot.simulationSetMode(usersSimulationSpeed); + boolean isDone = (robot.step(basicTimeStep) == -1); - // 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 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) { + isDoneFuture.complete(true); + break; + } + Simulation.runPeriodicMethods(); + } + } }, 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()); + timeSynchronizerSim.set("simTimeSec", START_SIMULATION); }); - // 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!"); + if (robot.step(0) == -1) { + throw new RuntimeException("Couldn't even start up!"); } + 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 { - 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:"); + System.err.println("Error occurred connecting to server:"); e.printStackTrace(System.err); System.err.flush(); System.exit(1); return; } - while(robot.step(basicTimeStep) != -1) { - queuedMessages.forEach(Runnable::run); - queuedMessages.clear(); - Simulation.runPeriodicMethods(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + wsConnection.object.closeBlocking(); + } catch(InterruptedException e) {} + })); + + // Process incoming messages until simulation finishes + try { + while (isDoneFuture.getNow(false).booleanValue() == false) { + 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); } + + 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/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/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..a898fb51 100644 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/SimRegisterer.java @@ -1,86 +1,174 @@ package org.team199.deepbluesim; -import org.team199.deepbluesim.mediators.*; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArraySet; +import org.team199.deepbluesim.mediators.GyroMediator; +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.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; -// 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 +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 SimDeviceCallback MISC_DEVICE_CALLBACK = SimRegisterer::callback; - private static final UniqueArrayList> CALLBACKS = new UniqueArrayList<>(); - - static { - // Register Initalized Callbacks for Misc Devices - CALLBACKS.add(SimDeviceSim.registerDeviceCreatedCallback("", MISC_DEVICE_CALLBACK, true)); - // Register Initalized Callbacks for PWM Devices - CALLBACKS.add(PWMSim.registerStaticInitializedCallback((name, isInitialized) -> { - if(isInitialized) { - callback("PWM", name, 0); + + 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; + } } - }, true)); - // Register Initalized Callbacks for Encoder Devices - CALLBACKS.add(EncoderSim.registerStaticInitializedCallback((name, isInitialized) -> { - if(isInitialized) { - callback("Encoder", name, 0); + + if(!unboundEncoders.isEmpty()) { + Simulation.registerPeriodicMethod(SimRegisterer::tryBindEncoders); } - }, true)); + } } - // Initalize 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 - - // 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 - // 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 - true)); + 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(); + } } - if(deviceName.startsWith("navX")) { - // If a navX is registered, try to link its SimDevice to the Webots robot - MockGyro.linkGyro(); + } + + 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(deviceName.startsWith("CANEncoder_")) { - //deviceName should be CANEncoder_ - new MockedSparkEncoder(new SimDeviceSim(deviceName), deviceName.substring(11)); + + 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() + "!"); } } - // Callback for when a known device type is registered on a Non-Can port - private static void callback(String type, String port, int storePos) { - if(type.equals("PWM")) { - // 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 - new WebotsMotorForwarder(Simulation.getRobot(), "PWM[" + port + "]"), - // Initalize with current speed - true)); - } - else if(type.equals("Encoder")) { - // If a new PWM device has been initalized, attempt to link it to a Webots Motor - // Register a speed callback on this device - EncoderSim sim = new EncoderSim(port); - new MockedEncoder(CALLBACKS, sim, port); + 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]); + + DCMotor motorConstants = new DCMotor(nominalVoltageVolts, stallTorqueNewtonMeters, stallCurrentAmps, freeCurrentAmps, Units.rotationsPerMinuteToRadiansPerSecond(freeSpeedRPM), 1); + + 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); } } -} \ 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 01111427..fce0dec1 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() @@ -23,16 +25,10 @@ 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 - private static final CopyOnWriteArrayList periodicMethods; + // Use a CopyOnWriteArrayList to prevent synchronization errors + 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,15 @@ 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/GyroMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java new file mode 100644 index 00000000..287294b5 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/GyroMediator.java @@ -0,0 +1,47 @@ +package org.team199.deepbluesim.mediators; + +import com.cyberbotics.webots.controller.Gyro; + +import org.team199.deepbluesim.Constants; +import org.team199.deepbluesim.Simulation; +import org.team199.wpiws.devices.SimDeviceSim; + +/** + * Handles the linking of the simulated AHRS gyro to Webots + * @see com.kauailabs.navx.frc.AHRS + */ +public class GyroMediator implements Runnable { + + public final Gyro gyro; + public final SimDeviceSim device; + private double angle = 0; + + /** + * Links the simulated AHRS gyro to Webots + * @param gyro the Webots gyro to link to + */ + public GyroMediator(Gyro gyro) { + this.gyro = gyro; + gyro.enable(Constants.sensorTimestep); + device = new SimDeviceSim("navX-Sensor[0]"); + Simulation.registerPeriodicMethod(this); + } + + @Override + 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 Webots's coordinate system (counter-clockwise = positive) to WPILib's coordinate system (counter-clockwise = negative). + */ + 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 + device.set("Yaw", angle + ""); + } + +} + 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 deleted file mode 100644 index 009dc896..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockGyro.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.team199.deepbluesim.mediators; - -import com.cyberbotics.webots.controller.Gyro; - -import org.team199.deepbluesim.BaseSimConfig; -import org.team199.deepbluesim.Simulation; -import org.team199.wpiws.devices.SimDeviceSim; - -/** - * Handles the linking of the simulated AHRS gyro to Webots - * @see com.kauailabs.navx.frc.AHRS - */ -public final class MockGyro implements Runnable { - - private static boolean gyroCreated = false; - private static SimDeviceSim gyroSim; - private static Gyro webotsGyro; - private static double angle = 0; - - /** - * Links the simulated AHRS gyro to Webots if it has not been already - */ - public static void linkGyro() { - if(gyroCreated) { - return; - } - gyroCreated = true; - // Create Sims - gyroSim = new SimDeviceSim("navX-Sensor[0]"); - webotsGyro = Simulation.getRobot().getGyro("gyro"); - webotsGyro.enable(BaseSimConfig.getSensorTimestep()); - 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). - */ - double reading = -webotsGyro.getValues()[1] * 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 + ""); - } - - private MockGyro() {} - -} \ No newline at end of file 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 6da2839d..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.BaseSimConfig; -import org.team199.deepbluesim.Simulation; -import org.team199.wpiws.ScopedObject; -import org.team199.wpiws.UniqueArrayList; -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(UniqueArrayList> 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(BaseSimConfig.getSensorTimestep()); - 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 - // 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 - // 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 ad75b8a6..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/MockedSparkEncoder.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.team199.deepbluesim.mediators; - -import com.cyberbotics.webots.controller.PositionSensor; - -import org.team199.deepbluesim.BaseSimConfig; -import org.team199.deepbluesim.Simulation; -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 - private final int countsPerRevolution = 4096; - private double position; - - 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(); - if(webotsEncoder != null) { - webotsEncoder.enable(BaseSimConfig.getSensorTimestep()); - Simulation.registerPeriodicMethod(this); - } - } - - public double getPosition() { - return position; - } - - @Override - 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); - 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/PWMMotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java new file mode 100644 index 00000000..58828e24 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/PWMMotorMediator.java @@ -0,0 +1,39 @@ +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; + +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 * motorConstants.freeSpeedRadPerSec; + motor.setVelocity((inverted ? -1 : 1) * velocity / gearing); + }, true)); + } + +} 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..ac51f97e --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceEncoderMediator.java @@ -0,0 +1,26 @@ +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, 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/SimDeviceMotorMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java new file mode 100644 index 00000000..c44a3303 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/SimDeviceMotorMediator.java @@ -0,0 +1,102 @@ +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 SimDeviceMotorMediator 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; + + 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 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 + */ + public SimDeviceMotorMediator(Motor motor, SimDeviceSim simDevice, DCMotor motorConstants, double gearing, boolean inverted, Collection> callbackStore) { + this.motor = motor; + motorDevice = simDevice; + this.motorConstants = motorConstants; + this.gearing = gearing; + this.inverted = inverted; + + 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(motorDevice.id + "_RelativeEncoder"), true, false, 0, inverted, NEO_BUILTIN_ENCODER_CPR, gearing); + } + } + + 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("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((inverted ? -1 : 1) * 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/WPILibEncoderMediator.java b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java new file mode 100644 index 00000000..cd7c8cb4 --- /dev/null +++ b/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WPILibEncoderMediator.java @@ -0,0 +1,26 @@ +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, boolean isOnMotorShaft, boolean isInverted, int countsPerRevolution, double gearing) { + super(encoder, isOnMotorShaft, false, 0, 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 c190be0a..00000000 --- a/plugin/controller/src/main/java/org/team199/deepbluesim/mediators/WebotsMotorForwarder.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.team199.deepbluesim.mediators; - -import com.cyberbotics.webots.controller.Motor; -import com.cyberbotics.webots.controller.Robot; - -import org.team199.deepbluesim.Simulation; -import org.team199.wpiws.interfaces.DoubleCallback; -import org.team199.wpiws.interfaces.StringCallback; - -/** - * Forwards motor calls from WPILib motor controllers to Webots - */ -public class WebotsMotorForwarder implements DoubleCallback, Runnable, StringCallback { - - private double currentOutput; - 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); - currentOutput = 0; - // Make sure that the motor can rotate any number of times - motor.setPosition(Double.POSITIVE_INFINITY); - motor.setVelocity(0); - Simulation.registerPeriodicMethod(this); - } - - @Override - public void callback(String name, String value) { - try { - callback(name, Double.parseDouble(value)); - } catch(NullPointerException | NumberFormatException e) {} - } - - @Override - public void callback(String name, double value) { - currentOutput = value; - } - - @Override - public void run() { - motor.setVelocity(motor.getMaxVelocity() * currentOutput); - } - -} \ 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..6cf3566a --- /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 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 { + controllerType IS controllerType + port IS port + 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..514d11f1 --- /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 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 { + controllerType IS controllerType + port IS port + 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..b54fd6df --- /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 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 { + controllerType IS controllerType + port IS port + 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..f296caec --- /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 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 { + controllerType IS controllerType + port IS port + gearing IS gearing + inverted IS inverted + 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..5bf455c2 --- /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 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 { + controllerType IS controllerType + port IS port + 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..b760a311 --- /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 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 { + controllerType IS controllerType + port IS port + 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..8a584183 --- /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 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 { + controllerType IS controllerType + port IS port + 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..517b6b13 --- /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 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 { + controllerType IS controllerType + port IS port + 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..01fd48e6 --- /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 { + controllerType "Spark Max" + port IS id + 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..1eff589a --- /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 { + controllerType "Spark Max" + port IS id + 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..370a738c --- /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 { + controllerType "PWM" + port IS port + 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..5b5f428f --- /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 8192 + 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/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 new file mode 100644 index 00000000..29817ff5 --- /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 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 { + controllerType IS controllerType + port IS port + 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..3f414008 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIEncoderBase.proto @@ -0,0 +1,18 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +# 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 { + 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 new file mode 100644 index 00000000..61484f90 --- /dev/null +++ b/plugin/controller/src/webotsFolder/dist/protos/WPIMotorBase.proto @@ -0,0 +1,29 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +# A RotationalMotor with the properties necessary to construct a WPILib DCMotor +PROTO WPIMotorBase [ + 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 + field SFFloat stallTorqueNewtonMeters 5 + unconnectedField SFFloat stallCurrentAmps 100 + unconnectedField SFFloat freeCurrentAmps 1 + field SFFloat freeSpeedRPM 5000 + field SFString sound "default" +] +{ + 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 %<= (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 >% + %< 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 + } +} diff --git a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt index 5036272c..e1086869 100644 --- a/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt +++ b/plugin/controller/src/webotsFolder/dist/worlds/DBSExample.wbt @@ -1,15 +1,22 @@ -#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" +EXTERNPROTO "../protos/MiniCIMMotor.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 +28,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,22 +37,22 @@ Robot { } HingeJoint { jointParameters HingeJointParameters { - position 1.667027211877997e-11 + position -4.1511655620378363e-11 axis 0 0 1 anchor -0.167 0 -0.232 } device [ + MiniCIMMotor { + controllerType "PWM" + gearing 6.9973 + } PositionSensor { name "Back Left Encoder" } - RotationalMotor { - name "PWM[0]" - maxVelocity 87.4102 - } ] 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 { @@ -65,18 +72,23 @@ Robot { } HingeJoint { jointParameters HingeJointParameters { - position 1.6672976088524906e-11 + position 9.225294324560214e-11 axis 0 0 1 anchor 0.167 0 -0.232 } device [ + MiniCIMMotor { + controllerType "PWM" + port 1 + gearing 6.9973 + } 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 { @@ -93,7 +105,7 @@ Robot { } HingeJoint { jointParameters HingeJointParameters { - position -1.6676778847781958e-11 + position 4.150378260475315e-11 axis 0 0 -1 anchor -0.167 0 0.232 } @@ -101,14 +113,15 @@ Robot { PositionSensor { name "Back Right Encoder" } - RotationalMotor { - name "PWM[1]" - maxVelocity 87.4102 + MiniCIMMotor { + controllerType "PWM" + port 2 + gearing 6.9973 } ] 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 { @@ -125,18 +138,23 @@ Robot { } HingeJoint { jointParameters HingeJointParameters { - position -1.6676110401423026e-11 + position 8.919282517793818e-11 axis 0 0 -1 anchor 0.167 0 0.232 } device [ + MiniCIMMotor { + controllerType "PWM" + port 3 + gearing 6.9973 + } 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 { @@ -164,6 +182,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 +195,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 } diff --git a/plugin/gradle/wrapper/gradle-wrapper.jar b/plugin/gradle/wrapper/gradle-wrapper.jar index e708b1c0..7454180f 100644 Binary files a/plugin/gradle/wrapper/gradle-wrapper.jar and b/plugin/gradle/wrapper/gradle-wrapper.jar differ 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" "$@" 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..bc2525b1 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,29 @@ 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") + if(Files.exists(wbprojPath)) { + 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) } }